Remember scroll position in lazycolumn? - android

I am aware of the remember lazy list state and it works fine
setContent {
Test(myList) // Call Test with a dummy list
}
#Composable
fun Test(data: List<Int>){
val state = rememberLazyListState()
LazyColumn(state = state) {
items(data){ item ->Text("$item")}
}
}
It will remember scroll position and after every rotation and change configuration it will be the same
But whenever I try to catch data from database and use some method like collectAsState
it doesn't work and it seem an issue
setContent{
val myList by viewModel.getList.collectAsState(initial = listOf())
Test(myList)
}

Unfortunately for now there's not a native way to do so, but you can use this code:
val listState = rememberLazyListState()
listState has 3 methods:
firstVisibleItemIndex
firstVisibleItemScrollOffset
isScrollInProgress
All of them are State() so you will always get the data as it updates. For example, if you start scrolling the list, isScrollInProgress will change from false to true.
SAVE AND RESTORE STATE
val listState: LazyListState = rememberLazyListState(viewModel.index, viewModel.offset)
LaunchedEffect(key1 = listState.isScrollInProgress) {
if (!listState.isScrollInProgress) {
viewModel.index = listState.firstVisibleItemIndex
viewModel.offset = listState.firstVisibleItemScrollOffset
}
}

Related

How to get LazyRow's visible items in viewmodel

I have created a LazyRow in jetpack compose. At a certain point in the viewmodel, I want to get the list of currently visible items from that LazyRow.
I know that I can get the list of visible items in the Composable function using the following code:
val listState = rememberLazyListState()
val visibleItemIds = remember {
derivedStateOf { listState.layoutInfo.visibleItemsInfo.map { it.key.toString() } }
}
The problem is how can I pass this data to the viewmodel during a viewmodel event (not a button click etc)
You can add a side effect to know what are the visible items in any time.
LaunchedEffect(visibleItemIds){
//update your viewModel
}
You can also have a List<T> instead of State<List<String>> as in your code with:
val state = rememberLazyListState()
val visibleItemIds: List<Int> by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) {
emptyList()
} else {
visibleItemsInfo.map { it.index }
}
}
}
Also note that visibleItemsInfo returns also the partially visible items.

Jetpack Compose navigation state doesn't restore

I'm struggling with the Jetpack Compose navigation. I'm following the NowInAndroid architecture, but I don't have the correct behaviour. I have 4 destinations in my bottomBar, one of them is a Feed-type one. In this one, it makes a call to Firebase Database to get multiple products. The problem is that everytime I change the destination (e.j -> SearchScreen) and go back to the Feed one, this (Feed) does not get restored and load all the data again. Someone know what is going on?
This is my navigateTo function ->
fun navigateToBottomBarRoute(route: String) {
val topLevelOptions = navOptions {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
when (route) {
HomeSections.FEED.route -> navController.navigateToFeed(navOptions = topLevelOptions)
HomeSections.SEARCH.route -> navController.navigateToSearch(navOptions = topLevelOptions)
else -> {}
}
}
My Feed Screen ->
#OptIn(ExperimentalLifecycleComposeApi::class)
#Composable
fun Feed(
onProductClick: (Long, String) -> Unit,
onNavigateTo: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: FeedViewModel = hiltViewModel()
) {
val feedUiState: FeedScreenUiState by viewModel.uiState.collectAsStateWithLifecycle()
val productsCollections = getProductsCollections(feedUiState)
Feed(
feedUiState = feedUiState,
productCollections = productsCollections,
onProductClick = onProductClick,
onNavigateTo = onNavigateTo,
onProductCreate = onProductCreate,
modifier = modifier,
)
}
The getProductsCollections function returns a list of ProductCollections
fun getProductsCollections(feedUiState: FeedScreenUiState): List<ProductCollection> {
val productCollection = ArrayList<ProductCollection>()
if (feedUiState.processors is FeedUiState.Success) productCollection.add(feedUiState.processors.productCollection)
if (feedUiState.motherboards is FeedUiState.Success) productCollection.add(feedUiState.motherboards.productCollection)
/** ........ **/
return productCollection
}
I already tried to make rememberable the val productsCollections = getProductsCollections(feedUiState) but does not even load the items.
I hope you guys can help me, thanks!!
I already tried to make rememberable the val productsCollections = getProductsCollections(feedUiState) but does not even load the items.
I want to restore the previous state of the FeedScreen and not reload everytime I change the destination in my bottomBar.

Best practice for hiltviewmodel in compose

So i have a few questions about using hiltviewmodels, states and remember in compose.
For some context, i have a ViewPager set up
HorizontalPager(
count = 4,
modifier = Modifier.fillMaxSize(),
state = pagerState,
) { page ->
when (page) {
0 -> PagerOne()
1 -> PagerTwo()
2 -> PagerThree()
3 -> PagerFour()
}
}
Lets say i have a State in my viewmodel declared like this
private val _data: MutableState<DataClass> = mutableStateOf(DataClass())
var data: State<DataClass> = _data
First, where do i inject my viewmodel? Is it fine to do it in the constructor of my pager composable?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
...
And if i want to get the value from that viewmodel state, do i need to wrap it into a remember lambda?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
val myState = viewmodel.data or var myState by remember { viewmodel.data }
Next question about flow and .collectasstate. Lets say i have a function in my viewmodel which returns a flow of data from Room Database.
fun getRoomdata() = roomRepository.getLatesData()
Is it correct to get the data like this in my composable?
val roomData = viewmodel.getRoomdata().collectasState(initial = emptyRoomdata())
Everything is working like expected, but im not sure these are the best approaches.

Paging 3 list auto refresh on navigation back in jetpack compose navigation

I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.
ViewModel
fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
PagingConfig(PAGE_SIZE, enablePlaceholders = false)
) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)
HomeView
val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
...
}
Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.
So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)
data.apply {
when {
// refresh
loadState.refresh is LoadState.Loading -> {
ItemLoading()
}
// reload
loadState.append is LoadState.Loading -> {...}
// refresh error
loadState.refresh is LoadState.Error -> {...}
// reload error
loadState.append is LoadState.Error -> {...}
}
}
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
Is this an issue with PagingLibrary which is still in alpha??
Update 1 (I am not sure if this is a good solution, but I am solving
the swipe refresh issue as follows)
// get images
var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
// reload items
fun reload(){
images = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
}
// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.
You should make it a val items = Pager(... for the caching to work.
For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.
EDIT: Ok, so based on the coments and edits to your question:
In your viewmodel, do as I suggested before:
val items = Pager( // define your pager here
Your composable can then look like this:
#Composable
fun Screen() {
val items = viewModel.items.collectAsLazyPagingItems()
val state = rememberSwipeRefreshState(
isRefreshing = items.loadState.refresh is LoadState.Loading,
)
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = state,
// use the provided LazyPagingItems.refresh() method,
// no need for custom solutions
onRefresh = { items.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// display the items only when loadState.refresh is not loading,
// as you wish
if (items.loadState.refresh is LoadState.NotLoading) {
items(items) {
if (it != null) {
Text(
modifier = Modifier.padding(16.dp),
text = it,
)
}
}
// you can also add item for LoadState.Error, anything you want
if (items.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
}
}
}
// if the loadState.refresh is Loading,
// display just single loading item,
// or nothing at all (SwipeRefresh already indicates
// refresh is in progress)
else if (items.loadState.refresh is LoadState.Loading) {
item {
Box(modifier = Modifier.fillParentMaxSize()) {
Text(
text = "Refreshing",
modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
}

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
)
}
}
}
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
/**
* 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")
)
#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
}
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).
#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
}

Categories

Resources