Stefan
Stefan

Reputation: 4301

Remember LazyColumn Scroll Position - Jetpack Compose

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

Answers (7)

Mendroid
Mendroid

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

Mattia Ferigutti
Mattia Ferigutti

Reputation: 3738

Save ScrollState

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

Derek K
Derek K

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

Fantasma Plasma
Fantasma Plasma

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

Fantasma Plasma
Fantasma Plasma

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

/**
 * 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

Richard Onslow Roper
Richard Onslow Roper

Reputation: 6863

Well if you literally want to save it, you must store it is something like a viewmodel where it remains preserved. The remembered 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

Related Questions