movableContentOf and movableContentWithReceiverOf provides
a means for tracking compositions in Jetpack Compose. They convert a lambda (or a lambda with a receiver) into one that moves all the remembered state and nodes created (within the block) in a previous call to any new location where it is called.
This might sound a bit confusing, but it is essentially about keeping track of the composition in a variable (formerly known as composition as a value) to allow inlining it in different places in our Composables, and avoid losing the state of all its Composables along the way.
Different overloads of each for a different amount of parameters are available in the official docs.
There are many use cases for movableContentOf
. Let’s learn about a couple of them and then learn how to combine it with LookaheadLayout
in order to maintain the state during and after an animation (shared element transitions).
This would be a great moment to refresh our knowledge about LookaheadLayout, how it works, how to use it, and its internals. It will come handy later. Here you have a previous issue I wrote about it 👇
One use case of movableContentOf
is to retain the state of the composition of the block (content
here) in order to switch between different layout modes. This snippet is extracted from the official docs, where a vertical state allows the Composable to toggle between two different layout modes (Row
and Column
):
The movableContent
variable tracks the composition from the block (its direct or indirect child Composables and their state) and gets called for both layout modes. If the content
lambda contained a few Checkbox
es where some were checked and some weren’t, if vertical
toggled, that state would stay as it is after recomposing.
Another use case is when we have a list of Composables with state, and we change their order. Here is an example that shows a button and a list of items. Each item has a checkbox with its own state. Then we click the button to remove the first element on the list.
Here is how it behaves:
The behavior is not what we would expect: Only Item 2 and Item 3 are checked initially, but when we remove Item 1, Item 2 shows unchecked, and if we click again to remove Item 2, then Item 3 shows unchecked 🤔 The state of the elements on the list does not update correctly and seems to be moving down whenever an element from the top of the list is removed.
The explanation for this can be found in the official docs: “by default, each item's state is keyed against the position of the item in the list or grid”. The problem with this is that when items change their position, their state does not match their position anymore, and therefore we get the unsync issue we see above.
To deal with this, we have two options. One of them is to give an explicit unique key
to each element that always matches the model it represents, in order to make sure that the runtime can differentiate both elements properly. An example of such a key
could be the item.name
:
This already fixes the issue because the name is a property of the item itself, so it moves with it wherever it goes, and always matches.
But note that this is problematic, since different items might have the same name, and that would cause inconsistencies once again. We should make sure that the key is completely unique. In practice, this is frequently not an issue, because items are normally loaded from the server and they usually carry a unique key that we can use for this. Otherwise, we could generate one.
Another option to ensure that the state of each item is always in sync and not lost, is to map the list of items to a list of movableContentOf
blocks 👇
Result is the same than before, but in this case you don’t need to deal with unique keys, and it works by tracking the composition of each item, along with its state, and inlining it on its corresponding position.
Another use case of movableContentOf
is to retain state while animating a Composable. We see an example of this in the shared element transitions we can implement with LookaheadLayout
.
(The rest of this article is only available to section is available to paid customers only)
If you didn’t yet, please read the article about LookaheadLayout
first.
This is the CraneDemo
extracted from the animation demos in the Compose sources:
It is a bit long, but we can focus on a couple things only. See how avatar
and parent
are Composable lambdas declared as movableContentWithReceiver.
This is to keep their state during the animation. The animation is achieved using LookaheadLayout
via an experimental sharedElement modifier that adapts the size and position of the child (with the modifier applied) gradually towards the lookahead values. SceneHost
is just a wrapper for LookaheadLayout to hide some boilerplate.
Note how the avatar
and parent
are used differently within the LookaheadLayout depending on the value of the fullScreen
mutable state.
👨🏫 Fully fledge course - “Jetpack Compose and internals”
You might want to consider attending the next edition of the highly exclusive “Jetpack Compose and internals” course I’m giving in February. I have carefully crafted it so attendees can master the library from the Android development perspective, while learning about its internals in order to grow a correct and accurate mental mapping. I wrote its content and I will be your teacher. This course will allow to position yourself well in the Android development industry. Limited slots left.
Stay tuned for more interesting Jetpack Compose content.
Jorge.