Android Compose - How to prevent Dialog from displaying twice?

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

Upvotes: 0

Views: 71

Answers (2)

Dr. Sa.M.
Dr. Sa.M.

Reputation: 2513

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")
        }
    }

Upvotes: 0

Waseem Abbas
Waseem Abbas

Reputation: 865

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
}

Upvotes: 0

Related Questions