Adrian Tappe
11/01/2021, 5:03 PMSwipeToDismiss
state when navigating back and forth between screens.
I would like to reset
(suspend function) the dismissState
as part of a cleanup. (Or “forget” that state when navigating back to this screen)
Idea:
When the user swipes out a Card in a LazyList the card stays in the swiped out state while a SnackBar appears.
If the snackbar gets dismissed, the element is deleted via the viewModel.
If the snackbar action (Undo) gets pressed, the dismissState gets reset.
Problem:
If I navigate to another screen, the coroutineScope the snackbar is opened gets cancelled.
The dismissState will never be reset. When I navigate back to this screen, the swiped card will stay swiped.
I tried but failed:
A) Have a finally
block in the coroutine to do the cleanup.
B) Having a DisposableEffect
seems to not work as I cannot access the compose state and have no coroutine scope.
Sourcecode:
edit: in thread
tad
11/01/2021, 5:12 PMtad
11/01/2021, 5:17 PMSaveableStateHolder
is one solution to this, as you can remove the remembered dismissed state in a DisposableEffect
. See https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/SaveableStateHolderAdrian Tappe
11/01/2021, 5:55 PM@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ProductListScreen(snackbarHostState: SnackbarHostState) {
val viewModel: ProductListViewModel = hiltViewModel()
val products by viewModel.productsLiveData.observeAsState(listOf())
val scope = rememberCoroutineScope()
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(products, key = { _, item -> item.id }) { index, item ->
var dismissState: DismissState? = null
fun dismissed() {
scope.launch {
val snackBarResult = snackbarHostState.showSnackbar(
"About to be deleted: ${item.name}",
actionLabel = "UNDO",
duration = SnackbarDuration.Short
)
when (snackBarResult) {
SnackbarResult.Dismissed -> viewModel.delete(item)
SnackbarResult.ActionPerformed -> dismissState?.reset()
}
}
}
dismissState = rememberDismissState(confirmStateChange = {
when (it) {
DismissValue.DismissedToEnd -> false
DismissValue.DismissedToStart -> {
dismissed()
true
}
else -> true
}
})
SwipeToDismiss(
state = dismissState,
background = SwipeDismissBackground(dismissState, item),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(0.75f) },
) {
ProductListCard(index, item, today())
}
}
}
}
Adrian Tappe
11/01/2021, 7:17 PM@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ProductListScreen(snackbarHostState: SnackbarHostState) {
val viewModel: ProductListViewModel = hiltViewModel()
val products by viewModel.productsLiveData.observeAsState(listOf())
val scope = rememberCoroutineScope()
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(products, key = { _, item -> item.id }) { index, item ->
val saveableStateHolder = rememberSaveableStateHolder()
saveableStateHolder.SaveableStateProvider(key = item.id) {
var dismissState: DismissState? = null
fun dismissed() {
scope.launch {
val snackBarResult = snackbarHostState.showSnackbar(
"About to be deleted: ${item.name}",
actionLabel = "UNDO",
duration = SnackbarDuration.Short
)
when (snackBarResult) {
SnackbarResult.Dismissed -> viewModel.delete(item)
SnackbarResult.ActionPerformed -> dismissState?.reset()
}
}
}
dismissState = rememberDismissState(confirmStateChange = {
when (it) {
DismissValue.DismissedToEnd -> false
DismissValue.DismissedToStart -> {
dismissed()
true
}
else -> true
}
})
SwipeToDismiss(
state = dismissState,
background = SwipeDismissBackground(dismissState, item),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(0.75f) },
) {
ProductListCard(index, item, today())
}
}
DisposableEffect(key1 = item.id){
onDispose {
saveableStateHolder.removeState(item.id)
}
}
}
}
}
tad
11/01/2021, 7:18 PMkey1 = item.id
in the DisposableEffect with Unit
tad
11/01/2021, 7:18 PMtad
11/01/2021, 7:18 PMAdrian Tappe
11/01/2021, 7:20 PMtad
11/01/2021, 7:23 PMdismissed: Boolean, onDismiss: () -> Unit
parameters, and remember your own boolean value.Adrian Tappe
11/01/2021, 7:32 PMAdrian Tappe
11/01/2021, 7:33 PMdimissed: Boolean
affect the dismissState
within the new Item-Composable?tad
11/01/2021, 7:35 PMinitialValue
of rememberDismissState
Adrian Tappe
11/01/2021, 7:37 PMinitialValue
is always DEFAULT
(basically what I would like after a reset), no matter any parameter. So what would the initialValue
be changed to?tad
11/01/2021, 7:37 PMif (dismissed) DismissValue.DismissedToStart else DissmissValue.Default
Adrian Tappe
11/01/2021, 7:40 PMinitialValue
is hard-coded to DissmissValue.Default
- I therefore still miss how that would help resetting it to DissmissValue.Default
?tad
11/01/2021, 7:40 PMdismissed
boolean that's passed intad
11/01/2021, 7:41 PMrememberSaveable
, it will be reset when restoring the compositiontad
11/01/2021, 7:43 PMremember { DismissState(...) }
instead of rememberDismissState
, if you don't mind the dismiss state resetting if the process dies.tad
11/01/2021, 7:46 PMrememberDismissState
uses rememberSaveable
under the hood, which causes it to save and restore itself when navigating away and back.Adrian Tappe
11/01/2021, 7:47 PMDisposableEffect(key1 = Unit){
onDispose {
saveableStateHolder.removeState(item.id)
}
}
as all was wrapped with saveableStateHolder.SaveableStateProvider(key = item.id) {
according to your suggestion.tad
11/01/2021, 7:48 PMLazyColumn
theretad
11/01/2021, 7:50 PMNavHost
behavior on top of that, which implements its own saveable state registry to maintain state for nav composablesAdrian Tappe
11/01/2021, 7:55 PMremember{ DismissState(...) }
worked. That’s a small win.
The big win would have been to fully understand state management of Compose in on one long weekend 😉 But well you can’t have everything. Have a nice rest of the holiday or Monday depending on where you are in the world!tad
11/01/2021, 7:56 PM