Where should I read lazy column item offset? - android

Basically I have a launched effect reading the item offset of a lazy column. and based on the offset I change the height/alpha etc..
AS is warning me that it should not be read inside a compose function because it will change a lot, so my question is where should I be reading and what is the best practice here?
Should I make a lambda updating the offset outside of the function and read in the highest one? Or the parent one?
Open to suggestions

You can read it inside a derivedStateOf.
val someData = remember {
derivedStateOf {
val offset = lazyListState.firstVisibleItemScrollOffset
val firstVisibleItem = lazyListState.firstVisibleItemIndex
// Convert to some data here and read this data
}
}
I did similar thing in this library for animating color, scale and alpha of items with
val animationData by remember {
derivedStateOf {
val animationData = getAnimationProgress(
lazyListState = lazyListState,
initialFirstVisibleIndex = initialFirstVisibleIndex,
indexOfSelector = indexOfSelector,
itemScaleRange = itemScaleRange,
showPartialItem = showPartialItem,
globalIndex = globalIndex,
selectedIndex = selectedIndex,
availableSpace = availableSpace,
itemSize = itemSizePx,
spaceBetweenItems = spaceBetweenItems,
visibleItemCount = visibleItemCount,
totalItemCount = totalItemCount,
inactiveScale = inactiveItemScale,
inactiveColor = inactiveColor,
activeColor = activeColor
)
selectedIndex = animationData.globalItemIndex
animationData
}
}
If you read a change you can read LaunchedEffect with snapshotFlow either but as i checked same lazyListState is returned so it's not possible to read any change but you can read lazyListState.firstVisibleItemScrollOffset or any changing value as alternative to derivedStateOf.
For instance
LaunchedEffect(Unit){
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.onEach {
// Here we get the change in offset of first visible item
// You might read another value here and do calculation
lazyListState.firstVisibleItemIndex
}.launchIn(this)
}
I think reading lazyListState wouldn't pose any problems either. I use this when i only read one value like firstVisibleItemScrollOffset, or layoutInfo inside snapshotFlow

Related

Android jetpack compose pagination : Pagination not working with staggered layout jetpack compose

Pagination given by android (https://developer.android.com/topic/libraries/architecture/paging/v3-overview) is working fine with Column,Row,lazy column, lazy rows. Problem occurs when I am trying to achieve pagination in staggered layout (Answer How to achieve a staggered grid layout using Jetpack compose? was very helpful).
Problem statement is there is no further network call when I scroll towards bottom of the list. As per docs there is no method for making paginated calls for next items it just automatically does as soon as we make input list as itemList.collectAsLazyPagingItems() and pass it to lazycolumn/lazyrow. But its not automatically happening for above mentioned staggered layout.
One solution I am testing is there is manual observation on the index of visible items and if they are near the end of the list and manually calling the network request. (see start code for this code lab ( https://developer.android.com/codelabs/android-paging#0 )
Staggered layout somehow in an essence of implementation of creating and using multiple COLUMNS inside and distributing items to them columns. Challenge here is how do we know we are approaching towards the end of the list.
Code for staggered layout is something like this (tbh i don't completly understand how this works)
#Composable
private fun CustomStaggeredVerticalGrid(
// on below line we are specifying
// parameters as modifier, num of columns
modifier: Modifier = Modifier,
numColumns: Int = 2,
content: #Composable () -> Unit
) {
// inside this grid we are creating
// a layout on below line.
Layout(
// on below line we are specifying
// content for our layout.
content = content,
// on below line we are adding modifier.
modifier = modifier
) { measurable, constraints ->
// on below line we are creating a variable for our column width.
val columnWidth = (constraints.maxWidth / numColumns)
// on the below line we are creating and initializing our items
constraint widget.
val itemConstraints = constraints.copy(maxWidth = columnWidth)
// on below line we are creating and initializing our column height
val columnHeights = IntArray(numColumns) { 0 }
// on below line we are creating and initializing placebles
val placeables = measurable.map { measurable ->
// inside placeble we are creating
// variables as column and placebles.
val column = testColumn(columnHeights)
val placeable = measurable.measure(itemConstraints)
// on below line we are increasing our column height/
columnHeights[column] += placeable.height
placeable
}
// on below line we are creating a variable for
// our height and specifying height for it.
val height =
columnHeights.maxOrNull()?.coerceIn(constraints.minHeight,
constraints.maxHeight)
?: constraints.minHeight
// on below line we are specifying height and width for our layout.
layout(
width = constraints.maxWidth,
height = height
) {
// on below line we are creating a variable for column y pointer.
val columnYPointers = IntArray(numColumns) { 0 }
// on below line we are setting x and y for each placeable item
placeables.forEach { placeable ->
// on below line we are calling test
// column method to get our column index
val column = testColumn(columnYPointers)
placeable.place(
x = columnWidth * column,
y = columnYPointers[column]
)
// on below line we are setting
// column y pointer and incrementing it.
columnYPointers[column] += placeable.height
}
}
}
}
Calling above code as below
Column(
// for this column we are adding a
// modifier to it to fill max size.
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.then(layoutModifier)
) {
// on below line we are creating a column
// for each item of our staggered grid.
CustomStaggeredVerticalGrid(
// on below line we are specifying
// number of columns for our grid view.
numColumns = numColumns,
) {
// inside staggered grid view we are
// adding images for each item of grid.
itemList.forEachIndexed { index, singleItem ->
// on below line inside our grid
// item we are adding card.
SomesingleItemCompose(singleItem , singleItemModifier ,index) // this one single grid item Ui as per requirement
}
}
}
As I said above , I was testing paginated data loading in compose staggered layout. It working. Trick was to use and tweak a little 'advance-pagiantion-start' codelab code (manual pagination data handling) and move it to compose. (there is still no way to use pagination library yet)
Solution : https://github.com/rishikumr/stackoverflow_code_sharing/tree/main/staggered-layout-compose-with_manual_pagination
Working video : https://drive.google.com/file/d/1IsKy0wzbyqI3dme3x7rzrZ6uHZZE9jrL/view?usp=sharing
How does it work :
Make a network request , fed it to UI custom staggered layout ( How to achieve a staggered grid layout using Jetpack compose? )
Listen to manual scroll and make a network request (for next page) when it scrolled to the end.
(https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt)
val itemList = remember { mutableStateListOf<Repo>() }
var lastListSize by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
viewModel.fetchContent()
.collect { result ->
when (result) {
is RepoSearchResult.Success -> {
Log.d("GithubRepository", "result.data ${result.data.size}")
itemList.clear()
itemList.addAll(result.data)
}
is RepoSearchResult.Error -> {
Toast.makeText(
this#SearchRepositoriesActivity,
"\uD83D\uDE28 Wooops $result.message}",
Toast.LENGTH_LONG
).show()
}
}
}
}
val scrollState = rememberScrollState()
val endReached = remember {
derivedStateOf {
(scrollState.value == scrollState.maxValue) && (lastListSize != itemList.size) && (scrollState.isScrollInProgress)
}
}
Column(Modifier.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(100.dp)) {
Text("Other Top composable")
}
StaggeredVerticalScreen(
itemList = itemList,
numColumns = 2,
layoutModifier = Modifier.padding(
start = 12.dp,
bottom = 12.dp
),
singleItemModifier = Modifier.padding(
end = 12.dp,
top = 12.dp
)
) { singleGridItem, singleItemModifier, index ->
SingleArticleItem(singleGridItem , index)
}
if (endReached.value) {
lastListSize = itemList.size
Log.d("SearchRepositoriesActivity", "End of scroll lazyItems.itemCount=${itemList.size}")
viewModel.accept(UiAction.FetchMore)
}
}
you have to maintain the current page number and few other things. (https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt)
A. Go through the code lab advance-pagination-start-code and you will understand how it all works. (I removed the part of code for on text change api calls because I didn't needed them)
B. this currently inMemory caching only, I am working on Room database storing. i believe this should not be difficult.
If you have to have to use paging library then we need to embed view inside our compose
OPTION 2: Another option is to inflate view-staggered layout xml in parent compose using 'androidx.compose.ui:ui-viewbinding' library. I also tried with this and this works fantasticaly. Beaware all thigs need to be wrt view, adapter and all
setContent {
// get the view model
val viewModel = ViewModelProvider(
this, Injection.provideViewModelFactory(
context = this,
owner = this
)
)[SearchRepositoriesViewModel::class.java]
AndroidViewBinding(ActivitySearchRepositoriesBinding::inflate) {
val repoAdapter = ReposAdapter()
val header = ReposLoadStateAdapter { repoAdapter.retry() }
list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
header = header,
footer = ReposLoadStateAdapter { repoAdapter.retry() }
)
val staggeredGridLayoutManager =
StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)
this.list.apply {
layoutManager = staggeredGridLayoutManager
setHasFixedSize(true)
adapter = repoAdapter
}
lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest { movies ->
repoAdapter.submitData(movies)
}
}
retryButton.setOnClickListener { repoAdapter.retry() }
lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest(repoAdapter::submitData)
}
lifecycleScope.launch {
repoAdapter.loadStateFlow.collect { loadState ->
// Show a retry header if there was an error refreshing, and items were previously
// cached OR default to the default prepend state
header.loadState = loadState.mediator
?.refresh
?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 }
?: loadState.prepend
val isListEmpty =
loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
// show empty list
emptyList.isVisible = isListEmpty
// Only show the list if refresh succeeds, either from the the local db or the remote.
list.isVisible =
loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
// Show loading spinner during initial load or refresh.
progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
// Show the retry state if initial load or refresh fails.
retryButton.isVisible =
loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
val errorState = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
errorState?.let {
Toast.makeText(
this#SearchRepositoriesActivity,
"\uD83D\uDE28 Wooops ${it.error}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
I have both the sample (option 1 and option 2) working and tested. There is no flickering no abrupt behaviour so far. Its working good, with both options. let me know if there any thing I could help. (Also this is my first answer yeyy...!!)

Is there downside replacing all remember(value) with remember { derivedStateOf { value } }?

As in https://developer.android.com/jetpack/compose/side-effects, we have derivedStateOf which helps prevent unnecessary Recomposition.
With that, it makes me think, should we always use derivedStateOf instead of remember(value) as below?
// Instead of using
remember(value1) { ... }
// Use
remember { derivedStateOf {value1} }
Doing so will also ensure we don't recompose the function that contains the mutableState, but just all the composable functions using that mutableState.
Is there any downside of using derivedStateOf as opposed to remember(value)?
It is not recommended that you replace them all. If the object is re assigned, use remember. If the object content changes, use derivedstateof
If you replace them all with derivedStateOf, object changes will not be detected
remember is more flexible in some situations:
You can have mutableState inside, which can be updated between reconfigurations and reset to the original value when value1 (or other keys) have changed.
There may be a situation where you only need to update the value when some of the states used in the computation change, not any of them, in which case you can specify them in remember.
On the other hand, derivedStateOf also has its advantages:
It doesn't cause recomposition unless the result of the computation has actually changed.
It can be used outside the composite function, for example in a view model.
In general, if using derivedStateOf gives the expected behavior, you should prefer it over remember.
derivedStateOf is for observing a change or or a property change in State.
if value1 in both cases is a class that is not State derivedStateOf won't update
#Composable
private fun DerivedStateOfExample() {
var numberOfItems by remember {
mutableStateOf(0)
}
val count = numberOfItems
val rememberedCount = remember(count) {
count
}
val derivedCountWithoutState by remember {
derivedStateOf {
count
}
}
val derivedCountWithState by remember {
derivedStateOf {
numberOfItems
}
}
// Use derivedStateOf when a certain state is calculated or derived from other state objects.
// Using this function guarantees that the calculation will only occur whenever one
// of the states used in the calculation changes.
val derivedStateMax by remember {
derivedStateOf {
numberOfItems > 5
}
}
val newMax = remember(count > 5) {
count
}
val derivedStateMin by remember {
derivedStateOf {
numberOfItems > 0
}
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Items: $numberOfItems\n" +
"derivedCountWithoutState: $derivedCountWithoutState\n" +
"derivedCountWithState: $derivedCountWithState\n" +
"newMax:$newMax\n" +
"rememberedCount:$rememberedCount, count: $count",
modifier = Modifier.weight(1f)
)
IconButton(onClick = { numberOfItems++ }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "add")
}
Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = { if (derivedStateMin) numberOfItems-- }) {
Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
}
}
if (derivedStateMax) {
Text("Can't be more than 5 items", color = Color(0xffE53935))
}
}
}
derivedCountWithState won't update because it doesn't observe a state. Same goes for derivedStateMax when the value was not derived from numberOfItems but count > 5
Also newMax is updated when when remember(count > 5) turns from false to true. This is where derivedStateOf comes handy, you can check change in each update of State with a condition.

Is there any way to make two horizontal pager of accompanist library to work synchronously?

What I am trying to achieve is if there are two horizontal pagers, then on swiping top one to left then the bottom horizontal pager should swipe to right and vice-versa, have tried using pagerState scrollBy method but not getting desired output
First of all, you need to determine which pager is scrolled and which one should follow it. This can be done with isScrollInProgress, I use it inside derivedStateOf to avoid unnecessary re-compositions.
Then I run LaunchedEffect. I'm passing pair of pager states in needed order, or null, as a key, so LaunchedEffect will be re-launched when scrolling stops/starts. Using snapshotFlow, it is possible to track changes in the result of a block whose calculations depend on state changes.
PagerState has scroll position information in the properties currentPage and currentPageOffset. scrollToPage takes only 0..1 values for page offset, but currentPageOffset can be less than zero when scrolling backward.
Suppose currentPage = 2 and currentPageOffset = -0.1. In this case, I will get 1.9 in pagePart, and I need to split it back to get 1 and 0.9. To do this I use divideAndRemainder: it will return a list of the form listOf(1.0, 0.9).
Column {
val count = 10
val firstPagerState = rememberPagerState()
val secondPagerState = rememberPagerState()
val scrollingFollowingPair by remember {
derivedStateOf {
if (firstPagerState.isScrollInProgress) {
firstPagerState to secondPagerState
} else if (secondPagerState.isScrollInProgress) {
secondPagerState to firstPagerState
} else null
}
}
LaunchedEffect(scrollingFollowingPair) {
val (scrollingState, followingState) = scrollingFollowingPair ?: return#LaunchedEffect
snapshotFlow { scrollingState.currentPage + scrollingState.currentPageOffset }
.collect { pagePart ->
val divideAndRemainder = BigDecimal.valueOf(pagePart.toDouble())
.divideAndRemainder(BigDecimal.ONE)
followingState.scrollToPage(
divideAndRemainder[0].toInt(),
divideAndRemainder[1].toFloat(),
)
}
}
HorizontalPager(
count = count,
state = firstPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
HorizontalPager(
count = count,
state = secondPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
}
Result:

How do I update a LottieCompose clipSpec at runtime

I have a Lottie animation that I want to start then when it gets to a certain frame I want to loop from frame X to frame Y.
The issue I am running up against is that I cant seem to figure out how to update the clipSpec at runtime with LottieCompose as clipSpec cannot be reassigned.
I was able to do this no problem with normal XML layout based Lottie like this
loading_animation.addAnimatorUpdateListener {
if(initialLoad && loading_animation.frame == 15){
initialLoad = false
startTime = System.currentTimeMillis()
loading_animation.setMinAndMaxFrame(15,28)
}
if(!initialLoad && authenticated && System.currentTimeMillis() >= (startTime+1000)){
loading_animation.pauseAnimation()
startActivity()
}
}
here is my LottieCompose code I have so far
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.sun_icon))
val animatable = rememberLottieAnimatable()
val progress by animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
LottieAnimation(composition = composition, progress = progress, modifier = Modifier.wrapContentSize())
clipSpec can be specified with animateLottieCompositionAsState. You can update it just like any other state in Compose: create a state variable and update it with some action/side-effect.
Learn more about state in Compose documentation, including this youtube video, which explains the basic principles.
getFrameForProgress returns a frame in Float, I am not sure if it is safe to convert it to Int and compare equals (is it guaranteed that every frame is rendered? on slow devices it might not be), so my comparison code looks not too nice.
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw. sun_icon))
var clipSpec by remember { mutableStateOf<LottieClipSpec?>(null)}
val progress by animateLottieCompositionAsState(
composition = composition,
clipSpec = clipSpec,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
composition = composition,
progress = progress,
modifier = Modifier.wrapContentSize()
)
var initialLoad by remember { mutableStateOf(true) }
if (initialLoad && composition?.getFrameForProgress(progress)?.let { it >= 15f } == true) {
SideEffect {
initialLoad = false
clipSpec = LottieClipSpec.Frame(
min = 15,
max = 28
)
}
}

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