movableContentOf and movableContentWithReceiverOf
Tracking compositions in Jetpack Compose
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 (
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
Checkboxes 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
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
(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
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
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
parent are used differently within the LookaheadLayout depending on the value of the
fullScreen mutable state.
Stay tuned for more interesting Jetpack Compose content.