Reputation: 4301
I'm trying to save/remember LazyColumn scroll position when I navigate away from one composable screen to another. Even if I pass a rememberLazyListState to a LazyColumn the scroll position is not saved after I get back to my first composable screen. Can someone help me out?
@ExperimentalMaterialApi
@Composable
fun DisplayTasks(
tasks: List<Task>,
navigateToTaskScreen: (Int) -> Unit
) {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { _, task ->
LazyColumnItem(
toDoTask = task,
navigateToTaskScreen = navigateToTaskScreen
)
}
}
}
Upvotes: 28
Views: 34022
Reputation: 61
Faced with same problem, I've tried to save LazyScrollState in viewModell and it looks like it works als desired. Need more tests but worth posting here, because it's very simple:
in your ViewModel:
var llState: LazyListState by mutableStateOf( LazyListState(0,0))
and in Composable:
LazyColumn(
state = myViewModel.llState,
modifier = Modifier
.fillMaxWidth(),
contentPadding = it,
userScrollEnabled = true
) {
//...
}
Upvotes: 3
Reputation: 3738
I took @Константин Казаченко answer and I changed it a little bit to support ScrollState using rememberForeverScrollState
.
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.saveable.rememberSaveable
/**
* Static field, contains all scroll values
*/
private val SaveMap = mutableMapOf<String, ScrollKeyParams>()
private data class ScrollKeyParams(
val value: Int
)
/**
* Save scroll state on all time.
* @param key value for comparing screen
* @param initial see [ScrollState.value]
*/
@Composable
fun rememberForeverScrollState(
key: String,
initial: Int = 0
): ScrollState {
val scrollState = rememberSaveable(saver = ScrollState.Saver) {
val scrollValue: Int = SaveMap[key]?.value ?: initial
SaveMap[key] = ScrollKeyParams(scrollValue)
return@rememberSaveable ScrollState(scrollValue)
}
DisposableEffect(Unit) {
onDispose {
SaveMap[key] = ScrollKeyParams(scrollState.value)
}
}
return scrollState
}
For usage:
val scrollState = rememberForeverScrollState("history_screen")
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
...
}
Upvotes: 0
Reputation: 3187
The LazyColumn
should save scroll position out of the box when navigating to next screen. If it doesn't work, this may be a bug described here (issue tracker). Basically check if the list becomes empty when changing screens, e.g. because you observe a cold Flow or LiveData (so the initial value is used).
Upvotes: 3
Reputation: 180
@Composable
fun persistedLazyScrollState(viewModel: YourViewModel): LazyListState {
val scrollState = rememberLazyListState(viewModel.firstVisibleItemIdx, viewModel.firstVisibleItemOffset)
DisposableEffect(key1 = null) {
onDispose {
viewModel.firstVisibleItemIdx = scrollState.firstVisibleItemIndex
viewModel.firstVisibleItemOffset = scrollState.firstVisibleItemScrollOffset
}
}
return scrollState
}
}
Above I defined a helper function to persist scroll state when a composable is disposed of. All that is needed is a ViewModel with variables for firstVisibleItemIdx and firstVisibleItemOffet.
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(
persistedScrollState(viewModel = viewModel)
) {
//Your content here
}
Upvotes: 4
Reputation: 180
@Composable
fun persistedScrollState(viewModel: ParentViewModel): ScrollState {
val scrollState = rememberScrollState(viewModel.scrollPosition)
DisposableEffect(key1 = null) {
onDispose {
viewModel.scrollPosition = scrollState.value
}
}
return scrollState
}
Above I defined a helper function to persist scroll state when a composable is disposed of. All that is needed is a ViewModel with a scroll position variable.
Hope this helps someone!
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(
persistedScrollState(viewModel = viewModel)
) {
//Your content here
}
Upvotes: 1
Reputation: 159
/**
* Static field, contains all scroll values
*/
private val SaveMap = mutableMapOf<String, KeyParams>()
private data class KeyParams(
val params: String = "",
val index: Int,
val scrollOffset: Int
)
/**
* Save scroll state on all time.
* @param key value for comparing screen
* @param params arguments for find different between equals screen
* @param initialFirstVisibleItemIndex see [LazyListState.firstVisibleItemIndex]
* @param initialFirstVisibleItemScrollOffset see [LazyListState.firstVisibleItemScrollOffset]
*/
@Composable
fun rememberForeverLazyListState(
key: String,
params: String = "",
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
val scrollState = rememberSaveable(saver = LazyListState.Saver) {
var savedValue = SaveMap[key]
if (savedValue?.params != params) savedValue = null
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset
LazyListState(
savedIndex,
savedOffset
)
}
DisposableEffect(Unit) {
onDispose {
val lastIndex = scrollState.firstVisibleItemIndex
val lastOffset = scrollState.firstVisibleItemScrollOffset
SaveMap[key] = KeyParams(params, lastIndex, lastOffset)
}
}
return scrollState
}
example of use
LazyColumn(
state = rememberForeverLazyListState(key = "Overview")
)
Upvotes: 12
Reputation: 6863
Well if you literally want to save it, you must store it is something like a viewmodel where it remains preserved. The remember
ed stuff only lasts till the Composable gets destroyed. If you navigate to another screen, the previous Composables are destroyed and along with them, the scroll state
Upvotes: 13