kotlin - Android Compose - How to prevent Dialog from displaying twice? - Stack Overflow

admin2025-04-25  2

I'm trying to display a dialog as soon as the current view renders. However, the dialog is shown twice (maybe due to how recomposition works) and requires two dismisses to disappear.

    val showGpsDisabledDialog by viewModel.showGpsDisabledDialog.collectAsState()
    
    
    Log.d(TAG, showGpsDisabledDialog.toString())
    if (showGpsDisabledDialog) {
        Log.d(TAG, "dialog")
        val context = LocalContext.current

        BasicAlertDialog(
            onDismissRequest = {
                Log.d(TAG, "dismiss")
                viewModel.closeGpsDisabledDialog()
            },
           
        ) {
            Text("dialog")
        }
    }

ViewModel:

    @HiltViewModel
    class MapViewModel @Inject constructor(): ViewModel() {

    private var _showGpsDisabledDialog: MutableStateFlow<Boolean> = MutableStateFlow(true)
    val showGpsDisabledDialog: StateFlow<Boolean> get() = _showGpsDisabledDialog.asStateFlow()

    fun openGpsDisabledDialog() {
        if(!_showGpsDisabledDialog.value)
            _showGpsDisabledDialog.value = true
    }

    fun closeGpsDisabledDialog() {
        if(_showGpsDisabledDialog.value)
            _showGpsDisabledDialog.value = false
    }
}

I tried to store the state in the composable, then in viewmodel. I tried using LaunchedEffect to set those flags (so they are not true before the composable renders) I tried setting them to true in viewmodel using a delay.

I've figured out that the dialog is displayed twice in all these cases by rendering a simple dialog with a text and noticed that there are two text overlapping

I'm trying to display a dialog as soon as the current view renders. However, the dialog is shown twice (maybe due to how recomposition works) and requires two dismisses to disappear.

    val showGpsDisabledDialog by viewModel.showGpsDisabledDialog.collectAsState()
    
    
    Log.d(TAG, showGpsDisabledDialog.toString())
    if (showGpsDisabledDialog) {
        Log.d(TAG, "dialog")
        val context = LocalContext.current

        BasicAlertDialog(
            onDismissRequest = {
                Log.d(TAG, "dismiss")
                viewModel.closeGpsDisabledDialog()
            },
           
        ) {
            Text("dialog")
        }
    }

ViewModel:

    @HiltViewModel
    class MapViewModel @Inject constructor(): ViewModel() {

    private var _showGpsDisabledDialog: MutableStateFlow<Boolean> = MutableStateFlow(true)
    val showGpsDisabledDialog: StateFlow<Boolean> get() = _showGpsDisabledDialog.asStateFlow()

    fun openGpsDisabledDialog() {
        if(!_showGpsDisabledDialog.value)
            _showGpsDisabledDialog.value = true
    }

    fun closeGpsDisabledDialog() {
        if(_showGpsDisabledDialog.value)
            _showGpsDisabledDialog.value = false
    }
}

I tried to store the state in the composable, then in viewmodel. I tried using LaunchedEffect to set those flags (so they are not true before the composable renders) I tried setting them to true in viewmodel using a delay.

I've figured out that the dialog is displayed twice in all these cases by rendering a simple dialog with a text and noticed that there are two text overlapping

Share Improve this question edited Jan 15 at 9:11 Dumitrescul Valentin Eduard asked Jan 15 at 7:18 Dumitrescul Valentin EduardDumitrescul Valentin Eduard 11 bronze badge 2
  • You could simply a lot of the logic here, if the class that holds the dialog has a local variable that controls if to display/hide the dialog. – tomerpacific Commented Jan 15 at 8:58
  • @tomerpacific I know and I did this. The problem is that because the dialog should be displayed as soon as the view is rendered. it is being displayedtwo times. I need it to be displayed just one time. – Dumitrescul Valentin Eduard Commented Jan 15 at 9:10
Add a comment  | 

2 Answers 2

Reset to default 0

Your code is very complicated and hard to understand. From your above code, I think you want to show a dialog if the device's GPS is disabled.

Here is my simple and reusable approach:

Context extension: You can write the following extension function (good to have it in a separate file under the utils package in your project). It returns a callback flow streaming the real-time changes in the GPS settings.

fun Context.observeGpsStatus(): Flow<Boolean> = callbackFlow {
    val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

    val isGpsEnabled = {
        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
    }

    // Emit initial status
    trySend(isGpsEnabled())

    val gpsStatusReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent?) {
            trySend(isGpsEnabled())
        }
    }

    val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
    registerReceiver(gpsStatusReceiver, filter)

    awaitClose {
        unregisterReceiver(gpsStatusReceiver)
    }
}

GpsStatusObserver composable: This is a simple reusable composable which can be integrated in any screen in any project.

@Composable
fun GpsStatusObserver() {
    val context = LocalContext.current
    val gpsFlow = remember { context.observeGpsStatus() }
    val isGpsEnabled = gpsFlow.collectAsState(initial = true)

    val showDialog = remember { mutableStateOf(true) }

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult()
    ) {}

    if (!isGpsEnabled.value && showDialog.value) {
        TwoActionsDialog(
            onDismissRequest = {
                Log.d(TAG, "dismiss")
                showDialog.value = false
            },
            mainText = "Gps disabled",
            secondaryText = "Please enable location services.\nOtherwise, the app will not know where you took the photos.",
            primaryButtonText = "OK, go to settings",
            secondaryButtonText = "No, continue without location",
            onPrimaryButtonClick = {
                val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                launcher.launch(intent)
            },
            onSecondaryButtonClick = {
                Log.d(TAG, "decline")
                showDialog.value = false
            }
        )
    }
}

Then simply call it in your screen composable:

@Composable
fun ExampleScreen() {
    GpsStatusObserver()
    
    // other content of the screen
}

The answer to your question is pretty simple.

Use 2 variables:

  1. Collects the data incoming from ViewModel
  2. Keeps a check of whether the Dialog is being displayed or not.

Changes should look something like this.

    // use this variable to fetch data from backend or viewmodel
    val showGpsDisabledDialog by viewModel.showGpsDisabledDialog.collectAsState()

    // use this variable to store whether the dialog is being displayed or not.
    var isDialogOpen by remember { mutableStateOf(false) } 

    Log.d(TAG, showGpsDisabledDialog.toString())

    // Show dialog when both the conditions are satisfied.
    if (showGpsDisabledDialog && isDialogOpen) {
        Log.d(TAG, "dialog")
        val context = LocalContext.current

        BasicAlertDialog(
            onDismissRequest = {
                Log.d(TAG, "dismiss")
                isDialogOpen = false
                viewModel.closeGpsDisabledDialog()
            },

            ) {
            Text("dialog")
        }
    }
转载请注明原文地址:http://anycun.com/QandA/1745595643a90946.html