5

My screen has one button and on click of it, I want to show a snackbar for short duration. So here is my composable:

@Composable
fun RegisterLocation(
    navController: NavController,
    locationVm: LocationViewModel = LocationViewModel()
) {
    val uiState = locationVm.locationState.collectAsState(LocationUiState.Init)
    val snackbarHostState = remember { SnackbarHostState() }
    when (uiState.value) {
        is LocationUiState.Init -> RegisterLocation(locationVm, snackbarHostState)
        is LocationUiState.Invalid -> {
            LaunchedEffect(locationVm.locationState.value) {
                snackbarHostState.showSnackbar(
                    "Failed Creating Location. Try Again!!!"
                )
            }
        }
        is LocationUiState.Valid -> navController.popBackStack()
    }
}

@Composable
fun RegisterLocation(locationVm: WLocationViewModel, snackbarHostState: SnackbarHostState) {
    Box(Modifier.fillMaxSize()) {
        Column {
            SubmitLocation(locationVm)
        }

        Column(
            Modifier
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
        ) {
            Text(text = "Hello") // <-- This is a dummy text and it shows in the UI. Moreover, it animates up to give space for snackbar. But the snackbar doesn't show up :(
            RegistrationError(snackbarHostState)
        }
    }
}

@ExperimentalCoroutinesApi
@Composable
fun SubmitLocation(vm: WLocationViewModel) {
    Button(
        onClick = { vm.onCreate(LocationUiState.Invalid) },
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(text = "Submit Location")
    }
}

@Composable
fun RegistrationError(hostState: SnackbarHostState) {
    SnackbarHost(
        hostState = hostState,
        modifier = Modifier.fillMaxWidth(),
        snackbar = {
            Snackbar {
                Text(text = "Hello", color = Color.White)
            }
        }
    )
}

What is wrong in this code? Any help would be appreciated.

Chandra Sekhar
  • 18,914
  • 16
  • 84
  • 125
  • Did you try with a coroutineScope.launch instead of LaunchedEffect? – Gabriele Mariotti May 14 '21 at 20:29
  • Checked with `coroutineScope.launch(Dispatchers.Main) {}` not working. Also directly launching a coroutine is giving the following error. `Calls to launch should happen inside a LaunchedEffect and not composition` @GabrieleMariotti – Santanu Sur May 14 '21 at 20:47

1 Answers1

10

SnackbarHostState shows a snackbar in a Scaffold or can be passed as a param to SnackBar composable.

Why the snackbar host does not show the snackbar ?

Whenever the state is changed the snackBar does NOT have any Scaffold or a SnackbarHost to show the content. ( snackbarHostState can also be passed while composing a SnackBar which u have already done - Check solution 2 )

You are neither using a Scaffold nor composing a SnackBarHost by using the snackBarHostState during the Invalid state.

Solution 1

We need to attach the SnackbarHostState at a Scaffold to make it show up.

when (uiState.value) {
   is LocationUiState.Init -> RegisterLocation(locationVm, snackbarHostState)
   is LocationUiState.Invalid -> {
         LaunchedEffect(locationVm.locationState.value) {
            snackbarHostState.showSnackbar(
                    "Failed Creating Location. Try Again!!!",
                    "label",
                    duration = SnackbarDuration.Long
            )
         }
         Scaffold(
                // attach snackbar host state to the scaffold
             scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
             content = { } // content is not mandatory
         )
   }
}

1

Solution 2:-

You are never composing RegisterLocation(locationVm, snackbarHostState) in the Invalid state. As per the code, if we compose the RegisterLocation composable in the Invalid state, then also a snackbar added as a composable should show up.

when (uiState.value) {
        is LocationUiState.Init -> RegisterLocation(locationVm, snackbarHostState)
        is LocationUiState.Invalid -> {
            RegistrationError(hostState = snackbarHostState)
            LaunchedEffect(locationVm.locationState.value) {
                snackbarHostState.showSnackbar(
                    "Failed Creating Location. Try Again!!!",
                    "label",
                    duration = SnackbarDuration.Long
                )
            }
        }
}

Update ( to elaborate )

Compose maintains a GroupArray under the hood , which contains all the UI elements drawn in the screen based on their position. So during a re-compose it compares and shows only the UI elements that are drawn in during the re-composition and removes the other components which were built during the previous composition that are redundant ( as in the UI components we dont visit during re-composition are removed ).

For your case:- The states are totally different and we are never visiting the Init state during the second composition. So, compose sees during re-composition we are never visiting the Init state so it removes the Init UI from its memory. ( using positional memoisation under the hood ).

Please check compose under the hood

Santanu Sur
  • 10,997
  • 7
  • 33
  • 52
  • It is not totally true. You can use a SnackbarHost without a Scaffold. – Gabriele Mariotti May 14 '21 at 20:28
  • We can also pass the `SnackbarHostState` as a param for `SnackBarHost` ryt which i mentioned in the solution 2. `SnackbarHost( hostState = hostState, ....)` . Then composing the `SnackBarHost` with the host state should also work. So whenever the host state would call `showSnackBar(..)` the `SnackbarHost` should show the snackbar @GabrieleMariotti – Santanu Sur May 14 '21 at 20:31
  • 1
    The `SnackbarHost` can work independently but the `SnackbarHostState` should *need* a composable to show up ( either a `Scaffold` or a `SnackbarHost` ) ryt ? @GabrieleMariotti – Santanu Sur May 14 '21 at 20:55
  • 1
    I have already mentioned that in the answer. – Santanu Sur May 14 '21 at 21:18
  • @SantanuSur, isn't composing RegisterLocation in invalid state redraws the entire screen instead of just drawing the snackbar? What if the composable has large chunk of ui component to be drawn? – Chandra Sekhar May 15 '21 at 03:40
  • @SantanuSur to answer your question, I am neither using Scaffold nor composing SncakBarrHost, here is my thought: When the screen loads for the 1st time, the default state is Init and as part of RegisterLocation composition, SnackBarHost is comppposed. And when it moves from Init to Invalid state, why do we need to recomposee the entire RegisterLocation instead of simply showing the snackbar using the host state? – Chandra Sekhar May 15 '21 at 03:50
  • @ChandraSekhar when we recompose the previously drawn UI are cleared off from the screen ( from the compose `GroupArray` ), in case they are different components. So, when the state is `Invalid` , the snackbar does not exist anymore which was drawn in `Init` state. Please go through this https://www.google.com/url?sa=t&source=web&rct=j&url=https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd&ved=2ahUKEwjmxsO99crwAhXI_XMBHU36AvcQjjgwAHoECAQQAg&usg=AOvVaw3EwEeFE-OMpffLKHfVJzcC – Santanu Sur May 15 '21 at 05:10
  • @ChandraSekhar also if the compose has a `large chunk` of data then use the **solution 1** approach. Totally up to u as mentioned the `SnackbarHostState` either needs a `Scaffold` or a `SnackbarHost` to show the snackbar as i have mentioned in the answer. Its not showing since its not getting either of them to show. – Santanu Sur May 15 '21 at 05:35