Is there a way to save the UI state of a Composable so that when switching between Composable their UI state is identical to when the view was left ?
I've tried using rememberLazyListState() (which uses rememberSaveable) to save the scroll state of a LazyColumn but it doesn't seems to work when coming back to the Composable.
Any ideas ?
Edit : I am using NavControllerto handle the navigation between the Composable
I just figured out how to do it. The idea is to hoist the LazyListState to the Composable managing the view navigation.
#Composable
fun AppScreenNav(screen: Screen) {
val listState = rememberLazyListState()
when (screen) {
Screen.Home -> Home()
Screen.Favorites -> Favorites(listState)
}
}
#Composable
fun Favorites(listState: LazyListState) {
val favorites: List<String> by rememberSaveable { mutableStateOf(List(1000) { "Favorites $it" }) }
LazyColumn(
state = listState
) {
items(favorites) { item ->
Text(
color = Color.Black,
text = item,
)
}
}
}
Here we are hoisting the list state to the parent component. When switching between the Home() and Favorites() composable the list scrolling state should remains identical.
Related
I have following composable
#Composable
fun ManageCredential(
manageCredentialViewModel: ManageCredentialViewModel
) {
val expandedList by manageCredentialViewModel.expandedList.collectAsState()
val text = "Hello"
Scaffold(
modifier = Modifier.padding(top = 100.dp),
content = { padding ->
Image(
painter = if (expandedList.contains(text)) painterResource(id = R.drawable.drop_down_expand) else painterResource(
id = R.drawable.drop_down_collapse
),
contentDescription = text, modifier = Modifier.padding(padding).clickable {
if (expandedList.contains(text)) {
manageCredentialViewModel.removeFromList(text)
} else {
manageCredentialViewModel.addToList(text)
}
}
)
})
}
The view model is as follows
#HiltViewModel
class ManageCredentialViewModel #Inject constructor(
) : ViewModel() {
private val _expandedList = MutableStateFlow<MutableList<String>>(mutableListOf())
val expandedList = _expandedList.asStateFlow()
fun addToList(value: String) {
_expandedList.value.add(value)
}
fun removeFromList(value: String) {
_expandedList.value.remove(value)
}
}
I just want to toggle the image if a particular text is added to the list but it never happens
For recomposition to be triggered you need to change value of State. What you currently do is adding or removing items from existing list.
You can create SnapshotStateList with mutableStateListOf. Any changes in this list by removing, adding or updating an existing item with a new item, easily can be done with data class copy function, you can trigger recomposition and good thing about it is it only triggers recomposition for the Composable that reads that item such as LazyColumn item.
You can refer this answer also
Jetpack Compose lazy column all items recomposes when a single item update
I'm having a little trouble adding a form inside a Bottom sheet because every time I open the bottomSheet, the previous values continue there. I'm trying to make something like this
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
sheetPeekHeight = 0.dp
) {
Button(onClick = {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.expand()
}
}) {
Text(text = "Expand")
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun Form(
onSaveFoo: (String) -> Unit
) {
var foo by remember { mutableStateOf("") }
Column {
Button(onClick = {
onSaveFoo(foo)
}) {
Text(text = "Save")
}
OutlinedTextField(value = foo, onValueChange = { foo = it })
}
}
There is a way to "clean" my form every time the bottom sheet collapses without manually setting all values to "" again?
Something like the BottomShettFragment. If I close and reopen the BottomSheetFragment, the previous values will not be there.
Firstly, they say that it is better to control your state outside of a composable function (in a viewmodel) and pass it as a parameter.
You may clear the textField value, when you decide to collapse your bottomSheet, for example in onSaveFoo function.
Add a MutableStateFlow to your viewmodel, subscribe to its updates via collectAsState extension in your composable. You can get a viewmodel by a composable function viewModel(ViewModelClass::class.java).
In onSaveFoo function update your state with new string or empty string if that's the behaviour you want to achieve. State updates should happen inside viewmodel. So create a method in your viewmodel to update your state and call it when you want to collapse your bottomsheet to clear the text contained in your state.
And another thing, remember saves the value across recompositions. The value will be lost only if your Composable is removed from the Composition. It will happen if you change the content of your bottomSheet.
Something like this:
sheetContent = {
if(bottomSheetScaffoldState.bottomSheetState.isExpanded){
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}else{
Spacer(modifier=Modifier.height(16.dp).background(Color.White)//or some other composable
}
},
I'm working on a search page made in Compose with LazyColumn, everything works fine except for the wanted behavior of LazyColumn returing to first item when data changes.
This is my actual implementation of lazy column:
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
item: #Composable (DataType) -> Unit
) {
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
listState.scrollToItem(0)
}
}
}
As docs says, SideEffect should be called everytime the function is recomposed,
but it appear to be working only in debug mode with breakpoints in SideEffect, otherwise, it works only when the whole page is first created.
I've already tried with LaunchedEffect instead of SideEffect, using itemsList as key, but nothing happened.
Why my code works only in debug mode ?
Or better, an already made working solution to reset position when new data are set ?
SideEffect doesn't work because Compose is not actually recomposing the whole view when the SnapshotStateList is changed: it sees that only LazyColumn is using this state value so only this function needs to be recomposed.
To make it work you can change itemsList to List<DataType> and pass plain list, like itemsList = mutableStateList.toList() - it'll force whole view recomposition.
LaunchedEffect with passed SnapshotStateList doesn't work for kind of the same reason: it compares the address of the state container, which is not changed. To compare the items itself, you again can convert it to a plain list: in this case it'll be compared by items hash.
LaunchedEffect(itemsList.toList()) {
}
You can achieve the mentioned functionality with SideEffect, remember and with some kind of identificator (listId) of the list items. If this identificator changes, the list will scroll to the top, otherwise not.
I have extended your code. (You can choose any type for listId.)
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
listId: String? = null,
item: #Composable (DataType) -> Unit
) {
var lastListId: String? by remember {
mutableStateOf(null)
}
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
if (lastListId != listId) {
lastListId = listId
listState.scrollToItem(0)
}
}
}
}
What should I do to make my composable remember the list state when navigating back to it?
If I understood correctly, when navigating "down" the composable is replaced by another one, so when going "up"/back it will create a new one and if I want something to persist in that situation I must hoist that value.
The thing is, I don't understand how to maintain a clean architecture if everything will be a property of my parent, in this case the activity which holds the NavHost and the composables for the NavGraph.
I've been looking to the Jetnews sample and they don't hoist anything to the NavGraph level, so how did they make the list to stay in the same position?
Activity
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NavigationScreens.Launcher.route
) {
composable(NavigationScreens.Home.route) {
val viewModel = hiltViewModel<DealsViewModel>(backStackEntry = it)
HomeScreen(viewModel) { game ->
navController.putArgument(NavigationScreens.Pdp.Args.game, game)
navController.navigate(NavigationScreens.Pdp.route)
}
}
}
ViewModel
#HiltViewModel
class DealsViewModel #Inject constructor(
private val fetchGameDealsUseCase: FetchGameDealsUseCase,
private val dispatcherProvider: DispatcherProvider
) : ViewModel() {
private val scope = CoroutineScope(dispatcherProvider.io)
val deals = fetchGameDealsUseCase().cachedIn(scope)
}
HomeScreen
Scaffold(
scaffoldState = scaffoldState
) {
Box(modifier = Modifier.fillMaxSize()) {
SearchField()
DealsScreen(
deals = viewModel.deals,
contentPadding = PaddingValues(top = 84.dp),
onItemClick = onItemClick
)
}
}
DealsScreen
fun DealsScreen(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
deals: Flow<PagingData<Game>>,
onItemClick: (Game) -> Unit,
) {
val nColumns = 3
val content = deals.collectAsLazyPagingItems()
LazyVerticalGrid(
cells = GridCells.Fixed(nColumns),
modifier = Modifier
.fillMaxSize()
.composed { modifier },
contentPadding = contentPadding,
) {
items(content) { game ->
game?.let {
DealsContent(game, onItemClick)
}
}
}
}
The main difference I noted between my code and theirs is that they don't collect the data on the composable, but instead the use a producer which in this case I don't know what to do since I'm using the collectAsLazyPagingItems. Does anyone have a solution for this?
Full code is available at https://github.com/Danil0v3s/wishlisted-android/tree/compose
Edit: I've found that there's actually a bug which makes the composable defaults the list to initial position when the collectAsLazyPagingItems is used inside a NavHost
I can't seem to find much information on touch handling in Compose.
In the specific case I'm looking at I have a list like this:
#Composable
fun MyListComposable(items: List<Item>) {
LazyColumn(
contentPadding = paddingValues(listHorizontalMargin, listVerticalMargin),
) {
// Init items emitted for brevity
}
}
This list is contained in a parent which uses the swipeable modifier, something like this.
Card(
modifier = Modifier.swipeable(
state = state,
anchors = mapOf(
0.dp.value to DrawerState.OFFSCREEN,
50.dp.value to DrawerState.PEEKING,
maxHeight.value to DrawerState.EXPANDED,
),
reverseDirection = true,
thresholds = { _, _ -> FractionalThreshold(0f) },
orientation = Orientation.Vertical
) {
MyListComposable(items)
}
My problem is the list swallows all touches, so the swipable is never invoked. So my question is, is there a way to stop lazy column swallowing touches?