I have a simple app with a single screen, displaying movies in a Composable items list:
I use Android's paging3 library in order to load the movies page by page, and things seem to be working well:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesList(lazyMovieItems)
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) {
itemsIndexed(lazyPagedMovies) { index, movie ->
MoviesListItem(index, movie!!)
}
}
}
In an attempt to add a progress indicator to the initial loading phase (e.g. as explained in an Android code-lab), I've tried applying the following conditional, based on loadState.refresh:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
// Added: Show a progress indicator while the data is loading
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
MoviesList(lazyMovieItems)
}
Instead of displaying the progress indicator, this naive addition seem to be putting the paging loader into an infinite loop, where the first page gets fetched over and over indefinitely, without any items effectively being loaded (let alone displayed) into the list.
Side note: Just to rule out that this all has something to do with the condition itself, it appears that even adding as little as this log: Log.i("DBG", ""+lazyPagesMovies.loadState) with no conditions at all, introduces the undesired behavior.
I'm using Kotlin version 1.7.10 and the various Compose libraries in version 1.3.1.
Seems that with this simple code I might have somehow hit some Compose related edge-case. I've managed to work around things by introducing the progress-indicator conditional under a sub-function (composable) that accepts the paging items directly:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesScreen(lazyMovieItems) // was: MoviesList(lazyMovieItems)
}
// Newly added intermediate function
#Composable
fun MoviesScreen(lazyPagedMovies: LazyPagingItems<Movie>) {
MoviesList(lazyPagedMovies)
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
// ... (unchanged)
}
Related
I've got simple LazyColumn:
LazyColumn {
val lazySportEvents: LazyPagingItems<RecyclerItem> = stateValue.pagingItems.collectAsLazyPagingItems()
lazySportEvents.apply {
when (loadState.refresh) {
is LoadState.NotLoading -> {
itemsIndexed(
lazyPagingItems = lazySportEvents,
itemContent = { index, item ->
when (item) {
is SportEvent -> Text(item.name)
is AdItem -> AndroidView(
factory = { context ->
AdImageView(context).apply{
loadAdImage(item.id)
}
}
)
}
}
)
}
}
}
}
When I scroll screen down, everything loads fine. But when I scroll up, I end up with fun loadAdImage() called. It means that recomposition for AdItem happened even if that is the very same item (values and reference) like before scrolling screen down! Why does recomposition even happen then? I would like to omit it, to not load the same ad image every time while scrolling.
Is it even possible to skip recomposition for lazy paging items?
Edit: I realised the recomposition for items was infinite and that caused aforementioned behavior.
It turned out that when I changed
modifier = Modifier.fillParentMaxWidth()
to
modifier = Modifier.fillMaxWidth()
for some Boxes in the composable layout, infinite recomposition in LazyColumn stopped.
Edit: while working on the fix I experienced some Gradle dependencies caching problems. Anyway, when the problem approached for the 2nd time - the solution was to... upgrade compose.foundation dependency:
implementation "androidx.compose.foundation:foundation:$1.2.0-alpha07"
Not sure which one helped.
The Code A is from the project ThemingCodelab, you can see full code here.
I think that the keyword remember is not necessary in Code A.
I have tested the Code B, it seems that I can get the same result just like Code A.
Why need the author to add the keyword remember in this #Composable ?
Code A
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
MaterialTheme {
Scaffold(
topBar = { AppBar() }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
item {
Header(stringResource(R.string.top))
}
item {
FeaturedPost(
post = featured,
modifier = Modifier.padding(16.dp)
)
}
item {
Header(stringResource(R.string.popular))
}
items(posts) { post ->
PostItem(post = post)
Divider(startIndent = 72.dp)
}
}
}
}
}
Code B
#Composable
fun Home() {
val featured =PostRepo.getFeaturedPost()
val posts = PostRepo.getPosts()
...//It's the same with the above code
}
You need to use remember to prevent recomputation during recomposition.
Your example works without remember because this view will not recompose while you scroll through it.
But if you use animations, add state variables or use a view model, your view can be recomposed many times(when animating up to once a frame), in which case getting data from the repository will be repeated many times, so you need to use remember to save the result of the computation between recompositions.
So always use remember inside a view builder if the calculations are at least a little heavy, even if right now it looks like the view is not gonna be recomposed.
You can read more about the state in compose in documentation, including this youtube video, which explains the basic principles.
Came across a curious situation with AndroidView this morning.
I have a ProductCard interface that looks like this
interface ProductCard {
val view: View
fun setup(
productState: ProductState,
interactionListener: ProductCardView.InteractionListener
)
}
This interface can be implemented by a number of views.
A composable that renders a list of AndroidView uses ProductCard to get a view and pass in state updates when recomposition happens.
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
provider.setup(product, interactionListener)
}
}
}
}
With this sample, any interaction with the ProductCard view’s (calling ProductCard.setup()) doesn’t update the screen. Logging shows that the state gets updated but the catch is that it’s only updated once per button. For example, I have a favourites button. Clicking it once pushes a state update only once, any subsequent clicks doesn’t propagate. Also the view itself doesn’t update. It’s as if it was never clicked.
Now changing the block of AndroidView.update to use it and casting it as a concrete view type works as expected. All clicks propagate correctly and the card view gets updated to reflect the state.
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
// provider.setup(product, interactionListener)
(it as ProductCardView).setup(product, interactionListener)
}
}
}
}
What am I missing here? why does using ProductCard not work while casting the view to its type works as expected?
Update 1
Seems like casting to ProductCard also works
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
// provider.setup(product, interactionListener)
(it as ProductCard).setup(product, interactionListener)
}
}
}
}
So the question is why do we have to use it inside AndroidView.update instead of any other references to the view?
The answer here is I was missing the key value which is needed for LazyColumn items in order for compose to know which item has changed and call update on it.
I'm trying to create a Pull-to-Refresh logic in my app.
I know it starts with handling Overscrolling, but I can't seem to find anything in compose that has to do with Overscrolling.
Is it not implemented in Compose yet? Or is it hidden somewhere?
I'm using a LazyColumn right now, I didn't find anything in the LazyListState.
You can use the Swipe Refresh feature included in Google's Accompanist library.
Example usage:
val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn {
items(30) { index ->
// TODO: list items
}
}
}
See the docs for more details.
So, I have implemented a lazycolumnfor to work with a list of recipe elements, the thing is that it does not smooth scroll, if I just scroll fast it stutters till the last element appears and not smooth scroll.
Is this an error from my side or do I need to add something else?
data class Recipe(
#DrawableRes val imageResource: Int,
val title: String,
val ingredients: List<String>
)
val recipeList = listOf(
Recipe(R.drawable.header,"Cake1", listOf("Cheese","Sugar","water")),
Recipe(R.drawable.header,"Cake2", listOf("Cheese1","Sugar1","Vanilla")),
Recipe(R.drawable.header,"Cake3", listOf("Bread","Sugar2","Apple")))
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RecipeList(recipeList = recipeList)
}
}
}
#Composable
fun RecipeCard(recipe:Recipe){
val image = imageResource(R.drawable.header)
Surface(shape = RoundedCornerShape(8.dp),elevation = 8.dp,modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth().clip(shape = RoundedCornerShape(8.dp))
Image(asset = image,modifier = imageModifier,contentScale = ContentScale.Crop)
Spacer(modifier = Modifier.preferredHeight(16.dp))
Text(text = recipe.title,style = typography.h6)
for(ingredient in recipe.ingredients){
Text(text = ingredient,style = typography.body2)
}
}
}
}
#Composable
fun RecipeList(recipeList:List<Recipe>){
LazyColumnFor(items = recipeList) { item ->
RecipeCard(recipe = item)
}
}
#Preview
#Composable
fun RecipePreview(){
RecipeCard(recipeList[0])
}
Currently (version 1.0.0-alpha02) Jetpack Compose has 2 Composable functions for loading image resources:
imageResource(): this Composable function, load an image resource synchronously.
loadImageResource(): this function loads the image in a background thread, and once the loading finishes, recompose is scheduled and this function will return deferred image resource with LoadedResource or FailedResource
So your lazyColumn is not scrolling smoothly since you are loading images synchronously.
So you should either use loadImageResource() or a library named Accompanist by Chris Banes, which can fetch and display images from external sources, such as network, using the Coil image loading library.
UPDATE:
Using CoilImage :
First, add Accompanist Gradle dependency, then simply use CoilImage composable function:
CoilImage(data = R.drawable.header)
Using loadImageResource() :
val deferredImage = loadImageResource(
id = R.drawable.header,
)
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
deferredImage.resource.resource?.let {
Image(
asset = it,
modifier = imageModifier
)
}
Note: I tried both ways in a LazyColumnFor, and although loadImageResource() performed better than imageResource() but still it didn't scroll smoothly.
So I highly recommend using CoilImage
Note 2: To use Glide or Picasso, check this repository by Vinay Gaba
On the other note, LazyColumn haven't been optimised for scrolling performance yet, but I've just tested on 1.0.0-beta07 release and can confirm it's way smoother than 1.0.0-beta06
Compose.UI 1.0.0-beta07 relevant change log:
LazyColumn/Row will now keep up to 2 previously visible items active (not disposed) even when they are scrolled out already. This allows the component to reuse the active subcompositions when we will need to compose a new item which improves the scrolling performance. (Ie5555)