https://kotlinlang.org logo
#compose
Title
# compose
l

Lokik Soni

06/20/2022, 4:48 PM
I am showing Snackbar on NoteScreen but when the Snackbar is showing and I navigate to next screen and again go to back i.e NoteScreen Snackbar shows again . What is the best way to solve this.
Copy code
fun NotesScreen(
   navController: NavController,
   viewModel: NoteViewModel
) {
   val noteState by viewModel.noteState.collectAsState()
    val scaffoldState = rememberScaffoldState()

   noteState.errorMsg?.let { message ->
      val undoMessageText = stringResource(R.string.undo)

      LaunchedEffect(scaffoldState, undoMessageText, message) {
         val result = scaffoldState.snackbarHostState.showSnackbar(
            message = message,
            actionLabel = undoMessageText
         )

         if (result == SnackbarResult.ActionPerformed) {
            viewModel.onEvent(NotesEvent.RestoreNote)
         }
         viewModel.onEvent(NotesEvent.MessageShown)
      }
   }

   Scaffold(
      modifier = Modifier
         .fillMaxSize(),
      scaffoldState = scaffoldState,
      floatingActionButton = {
         FloatingActionButton(
            onClick = {
               // TODO dismiss snackbar before go to new screen if shown
               navController.navigate(Screen.AddEditNote.route)
            },
            backgroundColor = MaterialTheme.colors.primary
         ) {
            Icon(
               imageVector = Icons.Rounded.Add,
               contentDescription = stringResource(R.string.add_note)
            )
         }
      },
   ) {
      Column(
         modifier = Modifier
            .padding(16.dp)
      ) {
         Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
         ) {
            Text(
               text = stringResource(R.string.your_note),
               style = MaterialTheme.typography.h4
            )
            IconButton(
               onClick = { viewModel.onEvent(NotesEvent.ToggleOrderSection) }
            ) {
               Icon(
                  imageVector = Icons.Rounded.Sort,
                  contentDescription = stringResource(R.string.sort)
               )
            }
         }
         AnimatedVisibility(
            visible = noteState.isOrderSectionVisible
         ) {
            OrderSection(
               modifier = Modifier
                  .fillMaxWidth()
                  .padding(16.dp),
               noteOrder = noteState.noteOrder,
               onOrderChange = {
                  viewModel.onEvent(NotesEvent.Order(it))
               }
            )
         }
         Spacer(modifier = Modifier.height(16.dp))
         LazyColumn {
            items(noteState.notes) { note ->
               NoteItem(
                  modifier = Modifier
                     .fillMaxWidth()
                     .clickable {
                        // TODO dismiss snackbar before go to new screen if shown
                        navController.navigate(
                           Screen.AddEditNote.route + "?noteId=${note.id}&colorId=${note.color}"
                        )
                     },
                  note = note,
                  onDeleteClick = {
                     viewModel.onEvent(NotesEvent.DeleteNote(note))
                  }
               )
               Spacer(modifier = Modifier.height(16.dp))
            }
         }
      }
   }
}
s

shaarawy

06/20/2022, 6:06 PM
Could you please share the VM code?
t

tad

06/20/2022, 7:38 PM
Snackbar messages are not state; I like to expose a
SharedFlow
in the model, which ensures the message is collected and displayed only once.
d

dorche

06/20/2022, 7:46 PM
@tad that's against the current guidelines, showing a Snackbar is absolutely fine to be State. Actually using a SharedFlow means you could miss the emission so it really should be a StateFlow.
t

tad

06/20/2022, 7:47 PM
Then I disagree with the current guidelines :) Also, SharedFlow has a replay buffer for that reason.
d

dorche

06/20/2022, 7:50 PM
Disagreeing is fine, stating that StateFlow usage is wrong is not tho ;)
t

tad

06/20/2022, 7:51 PM
StateFlow has the same problem as a State<T> field, namely that it is state that has to be cleared and does not itself guarantee that its values are collected once.
d

dorche

06/20/2022, 7:58 PM
Depends what you call a problem - more code to handle explicitly - yes. And yes you'd have to "consume" the state so that the VM can be notified that the user has really seen the Snackbar (or error popup etc). Again in the basic case such StateFlow would only have 1 collector. Then the advantage you actually get is that you're sure this will eventually end up in the (only) collector where with SharedFlow it could be missed
t

tad

06/20/2022, 8:03 PM
Well, here you have an example of how that can go wrong, and having a LaunchedEffect collecting a SharedFlow and immediately calling
SnackbarHostState.showSnackbar
would not exhibit the same issue, even across recompositions.
d

dorche

06/20/2022, 8:04 PM
What’s the example of it going wrong?
t

tad

06/20/2022, 8:04 PM
The question that started this thread
d

dorche

06/20/2022, 8:05 PM
The question that started this thread seems to just not handle it?
Copy code
// TODO dismiss snackbar before go to new screen if shown
t

tad

06/20/2022, 8:05 PM
The only reason this needs to be done is because we're treating a one-off effect as state
And doing so adds a second side effect, namely that updating NoteState in the model will cause a recomposition with a new NoteState just to show a snackbar
d

dorche

06/20/2022, 8:13 PM
All that let’s you actually restore the snackbar (which is not what OP wants, but could be for other people). We’ll have to agree to disagree just. https://developer.android.com/jetpack/compose/state talks about Snackbar actually being State in the very first opening lines. Of course these are just guidelines that everyone is free to follow or not. edit: Also generally recomposition will happen regardless of usage of SharedFlow or StateFlow, the UI has to change anyway..? Unless Snackbar has some magic that I’m not aware of.
t

tad

06/21/2022, 2:53 AM
On the last point, just showing a Snackbar will only recompose the SnackbarHost; Compose can skip everything else if the inputs are stable and haven't changed. Changing NoteState to remove the error value will recompose everything that reads NoteState in the composition. In practice this depends on what NoteState is and how "decomposed" it gets as you traverse the call tree; if NoteState is passed to 100 functions, all 100 of them will be recomposed, but if its fields don't change then passing just the fields shouldn't cause a recomposition.
But I have to say: it makes no sense to develop a suspending, event-based API for
Snackbar
if messages are just state, because the API would be 10x easier if it was just a
MutableState
field. Because with the current API you have to define the clock-edges of that state as events (function calls on SnackbarHostState), instead of just calling
showSnackbar
and not worrying about it. It just screams "event-handling" API to me, and it's definitely a mismatch for hoisted state in compositions.
l

Lokik Soni

06/21/2022, 7:10 AM
ViewModel:
Copy code
class NoteViewModel @Inject constructor(
   private val _getNotes: GetNotesUseCase,
   private val _addNoteUseCase: AddNoteUseCase,
   private val _deleteNoteUseCase: DeleteNoteUseCase
): ViewModel() {

   private val _noteState = MutableStateFlow(NoteState())
   val noteState = _noteState.asStateFlow()

   private var _getNotesJob: Job? = null
   private var _note: Note? = null

   init {
      // Initially loads notes with default order
      getNotes()
   }

   /**
    * Handle events of [NotesScreen].
    */
   fun onEvent(notesEvent: NotesEvent) {

      when(notesEvent) {
         is NotesEvent.DeleteNote -> {
            viewModelScope.launch {
               _deleteNoteUseCase(notesEvent.note)
               _noteState.update { it.copy(errorMsg = "Note ${notesEvent.note.title} deleted.") }
               _note = notesEvent.note
            }
         }
         is NotesEvent.MessageShown -> {
            _noteState.update { it.copy(errorMsg = null) }
         }
      }
   }
}
Should I call viewModel.onEvent(NotesEvent.MessageShown) before navigating to new screen to dismiss snackbar or it is good to restore snackbar when I come back to NoteScreen. Note* Snackbar only shows again only when we navigate to new screen during Snackbar is currently showing once the snackbar is dismissed and then we navigate and then we come back it does not show again.
134 Views