1

I want to prompt the user with either a snackbar or an alert dialog based on the results of some Api calls. My app is a multi-activity one, and the API calls can return when the user is in any of those activities. I have a stateful composable so I can toggle its visibility when its state changes. I keep the state in a view model and the Api calls can simply change that state by accessing the relevant ViewModel setter. I'm not sure where to put the composable though? I don't want to have to include it at every Activity's 'SetContent'. How can I have it visible when the time is right in any context.

@Composable
fun MessageAlertView() {

    val viewModel = viewModel<AlertVM>()
    val isVisible by viewModel.isVisible.observeAsState()
    
        AlertDialog(
            showDialog = isVisible,
            title = "",
            message = "",
            confirmButton = "OK",
            dismissButton = "Cancel",
            onAccept = { dismissDialog() }) {
        }
}

and my viewmodel:

class AlertVM : ViewModel() {
    var isVisible = MutableLiveData(false)

    fun setVisibilityOn() {
        isVisible.value = true
    }

    fun setVisibilityOff() {
        isVisible.value = false
    }
}
Nikos Hidalgo
  • 3,666
  • 9
  • 25
  • 39

1 Answers1

1

I did something similar but instead of a multi-activity app, it was setup for a single activity app. Nevertheless, the concept is still the same for a multi-activity app.

For starters, I got rid of the snackbar that is provided by Compose. Like so many badly designed components from Google, I ditched it because it was tied to the scaffold. What the hell does a snackbar have to do with a scaffold? There are plenty of apps that don't use scaffolds but need snackbars. They would have been better off to created a general purpose snackbar that could be displayed from anywhere in your app and from any activity - if your app happens to use multiple activities.

So I wrote my own custom snackbar, which by the way is hardly even any code. To see this custom snackbar in action and how it is called from anywhere in the app, visit:

https://github.com/JohannBlake/Jetmagic

To test it in the demo, run the app and open the navigation drawer and click on any item. That will take you to a test screen. There is a button on the test screen that lets you return an item to the previous screen. Click on that and after selecting an item and pressing the Back button, you'll see the custom snackbar appear.

The one thing you can't avoid is a small amount of boilerplate code in each activity that is used to render the snackbar. Nothing in an activity is going to get composed if it isn't part of the UI hierarchy that makes up the activity. Here is the minimum boilerplate code I use and it is positioned below the scaffold:

Box {
  Scaffold(
        modifier = modifier,
        drawerGesturesEnabled = drawerGesturesEnabled,
        scaffoldState = scaffoldState,
        drawerBackgroundColor = Color.Transparent,
        drawerElevation = 0.dp,
        drawerContent = {
             NavDrawerHandler(scaffoldState = scaffoldState)
        },
        content = {
             ScreenFactoryHandler()
        }
  )

  Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
        CustomSnackbarHandler(modifier = modifier)
  }
}

The custom snackbar is shown or hidden by observing the state of a variable that is located in my main viewmodel:

@Composable
fun CustomSnackbarHandler(modifier: Modifier = Modifier) {

    val vmMain: MainViewModel = viewModel()
    val visible = vmMain.onSnackbarVisible.observeAsState(false)

    CustomSnackbar(
        visible = visible.value,
        snackbarInfo = vmMain.snackbarInfo,
        modifier = modifier,
        onTimeout = {
            vmMain.hideSnackbar()
        }) {
        vmMain.onSnackbarActionButtonClick()
    }
}

In your app, you just need to place the code that triggers the observable in some global object instead of some viewmodel. If you have a class that inherits from Application, you can place it there.

To show or hide the snackbar, I simply make the call from anywhere like this:

App.mainViewModel.displaySnackbar(SnackbarInfo(message = "You selected: $result"))

I use a SnackbarInfo class (that I wrote) to provide multiple parameters to the snackbar. I perfer doing it that way instead of passing a bunch of parameters to it.

Johann
  • 27,536
  • 39
  • 165
  • 279
  • Maybe I'm missing something but wouldn't this just display a Text on the MainActivity regardless? How can I trigger that composable only when I get a response from an Api call, especially not knowing which activity will be visible when I get that response. – Nikos Hidalgo Dec 13 '21 at 12:56
  • I was in a hurry and wrote some bs. Sorry. I updated my response with a proper solution. – Johann Dec 13 '21 at 13:29
  • No worries at all. I'll be checking that sample now. I will let you know as soon as I get it working! – Nikos Hidalgo Dec 13 '21 at 14:08