When using a normal `RecyclerView` that holds `Vie...
# compose
s
When using a normal
RecyclerView
that holds
ViewHolders
that just hold a
ComposeView
inside it, I am not getting the expected recomposition behaviour I am expecting. More details in thread 🧵
I am using a normal
RecyclerView
backed by a
ListAdapter
with diffUtil. My
ViewHolder
I am referring to looks something like this:
Copy code
class MyComposeViewHolder(
    private val composeView: ComposeView,
) : MainViewHolder(composeView) {
    init { composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) }

    override fun bind(model: MainModel) {
        composeView.setContent {
            AppTheme {
                Row {
                    AnimatedVisibility(model.someBoolean) {
                        // Should trigger to showing/not showing backed by `model.someBoolean`
                        Row {
                            Canvas(Modifier.size(8.dp)) { drawCircle(Color.Red) }
                        }
                    }
                    // Always showing composable
                }
            }
        }
    }
}
Now my problem is that I can’t manage to make it so that whenever the data changes, what actually happens is that the new model is fed inside the composable, and proper recomposition happens. Now what happens is that a whole new view is created, and instead of getting the nice
AnimatedVisibility
animation I get the generic
RecyclerView
flickering. The DiffUtil does indeed return true for
areItemsTheSame
and false for
areContentsTheSame
so my expected behaviour here is that the new data gets send to the bind method, not that the entire ViewHolder is initialised. I feel like I am either not understanding something correctly, or I am doing some silly
RecyclerView
mistake after not having used it for so many months. Does anyone have an example of a properly implemented
RecyclerView
with
ComposableView
items inside of it? I’d love to look as some open sourced examples.
c
Is
model.someBoolean
a
mutableStateOf<Boolean>()
?
a
I guess you are calling
bind()
whenever the data changes, right? If so, you are basically changing the entire content every time discarding the old composition. You should probably do something like this:
Copy code
class MyComposeViewHolder(
    composeView: ComposeView,
) : MainViewHolder(composeView) {
    private var model by mutableStateOf<MainModel?>(null)

    init {
        composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
        composeView.setContent {
            // Use `model` here
        }
    }

    override fun bind(model: MainModel) {
        this.model = model
    }
}
👍 1
a
This ^
Note, unless you need diff util, go with lazy column, you're taking a performance hit with interop
Also make sure you're not calling dispose
s
My
model.boolean
is in fact not a mutableState. And I see how initialising the composable once in the init and then changing the local nullable
mutableState
makes sense, these seem to be good pointers. I feel like after the changes you’ve suggested it should work, and I am just doing something wrong on the normal RecyclerView implementation, like it runs
init{}
on the ViewHolder every time even if areItemsTheSame returns true. I swear I’ve just forgotten how RV works after doing so much Compose. Does anyone have a link to a properly implemented RV that extends
ListAdapter
so I can do a sanity check and see what I’m messing up? If you have a sample of an RV with composables in some of its VH too that’d be perfect! p.s: As far as lazy column goes, I’d love to, but I’m editing a RecyclerView with many ViewHolder types so I’d rather just not do more than I need to right now and risk other type of regressions. Just want to add one more type which happens to be a composable and has some animation inside of it.
And in general, is there explicit documentation or a codelab for this use case? This feels like it’s gonna be how people implement RecyclerViews for years to come with most projects being a combination of Compose and non-Compose code. I’d love to see a codelab by Google about this that simply explains everything I’d need to know about this super common use-case. Who from Google, should I give this suggestion to? It really feels like a no-brainer to me.
The only way I made it work was by setting the
itemAnimator
on my
recyclerView
as null or by setting the
supportsChangeAnimations
to false on the existing
itemAnimator
like this:
(itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
But is this what we’re supposed to do? This way I am removing the default change animation for the other non-compose views which I might actually want to have. I absolutely must be missing something super obvious here, it shouldn’t be that hard to achieve what I want.
🙏 1
a
This is obviously the intended behavior of RecyclerView as it runs the animation whenever the content changes. It doesn't know whether you are using Compose UI.
s
That is totally understandable. However, is this the best way to achieve this result? As I said before, my guess is that for many years going forward, people will be using these interoperability features to add compose UI in their existing code, and one of the first points of entry will be adding ViewHolder types that have compose under the hood. I am just a bit surprised if this is the best we can get. And I still believe that this should be part of a codelab to explain these limitations and possibly provide better workarounds than my hack.
a
I don't think using RecyclerView with ComposeView children is a recommended way at the first place. There are some issues such as interoperability issues and performance issues. Even using LazyColumn with AndroidViews is better IMO.
Basically there isn't official support of Compose UI in RecyclerView but there is official support for AndroidView in Compose UI.
s
Hmm interesting. So we have an existing codebase. With a recyclerView with let’s say 20 different item types. And one wants to add another type but the team has decided to go full compose on all new views. What is the optimal approach?
a
It is your own decision after all, but you need to understand that both approaches has limitations.
c
Honestly I understand the hesitancy to change more than you need to. In our project we've been converting all our recycler views to LazyColumns/Rows and it's been very easy even with 10+ viewholder types. If you're worried about list item animations you can checkout the compose cookbook which has a lot of really good samples on how to do animations like that. https://github.com/Gurupreet/ComposeCookBook My recommendation would be to just take the hit up front and convert the recycler view. Otherwise you're just incurring tech debt with interop.
s
Thank you for the suggestion! Do you also have some examples/tips on how to properly implement AndroidViews as LazyColumn items? The hesitancy probably comes from the fact that I’ve never done it and don’t want to risk breaking existing stuff that have proven to work for months now if I am not 101% sure about what I’m doing.
👍 1
c
I'm not quite sure what your lazyColumn items are doing in terms of animations, but we've added shimmers that animate in LazyColumns. I think you just have to make sure you use best practices in your items to make sure you're not doing unnecessary compositions or allocating animation objects you don't need to on every composition. Things like
derivedStateOf
and
remember
That's if you want to convert your items to composables. Using AndroidView should be easy to use. If you have performance problems, first remember on debug builds LazyColumns are a bit slower, and second, that's probably when you wanna look into converting your list items to compose 😅
You'll probably just use the default LazyColumn IMPL and inside the LazyColumnScope DSL then use a when statement for your item types
s
I’ll have a talk with my team and see which approach makes more sense. Thanks for the tips btw, I’m sure they’ll prove to be super valuable!
c
Sure! Sorry I couldn't share more information. I'm also not at my computer at the moment 😅
s
Hehe yeah I don’t have any good questions yet anyway, probably more will arise as I get down to try implementing some changes like that. Maybe sometime in the near future, who knows
👍 1
c
Feel free to ping me when you do
🙏 1