| # Movable Content |
| |
| ## Background |
| |
| Being able to move content within a composition has many advantages. It allows preserving the |
| internal state of a composition abstractly allowing whole trees to move in the hierarchy. For |
| example, consider the following code, |
| |
| ``` |
| @Composable |
| fun MyApplication() { |
| if (Mode.current == Mode.Landscape) { |
| Row { |
| Tile1() |
| Tile2() |
| } |
| } else { |
| Column { |
| Tile1() |
| Tile2() |
| } |
| } |
| } |
| ``` |
| |
| If `Mode` changes then all states in `Tile1()` and `Tile2()` are reset including things like scroll |
| position. However, if you could treat the composition of the tiles as a unit, such as, |
| |
| ``` |
| @Composable |
| fun MyApplication() { |
| val tiles = remember { |
| movableContentOf { |
| Tile1() |
| Tile2() |
| } |
| } |
| if (Mode.current == Mode.Landscape) { |
| Row { tiles() } |
| } else { |
| Column { tiles() } |
| } |
| } |
| ``` |
| |
| Then the nodes generated for `Tile1()` and `Tile2()` are reused in the composition and any state is |
| preserved. |
| |
| Movable content lambdas can also be used in places where a key would be awkward to use or lead to |
| subtle cases where state is lost. Consider the following example which splits a collection in two |
| columns, |
| |
| ``` |
| @Composable |
| fun <T> NaiveTwoColumns(items: List<T>, composeItem: @Composable (item: T) -> Unit) { |
| val half = items.size / 2 |
| Row { |
| Column { |
| for (item in items.take(half)) { |
| composeItem(item) |
| } |
| } |
| Column { |
| for (item in items.drop(half)) { |
| composeItem(item) |
| } |
| } |
| } |
| } |
| |
| ``` |
| |
| This has the same systemic problem all `for` loops have in that the items are used in order of |
| composition so if data moves around in the collections a lot more recomposition is performed than |
| is strictly necessary. For example, if one item was inserted at the beginning of the collection the |
| entire view would need to be recomposed instead of just the first item being created and the rest |
| being reused unmodified. This can cause the UI to become confused if, for example, input fields with |
| selection are used in the item block as the selection will not track with the data value but with |
| the index order in the collection. If the user selected text in the first item, inserting a new item |
| will cause selection to appear in the new item selecting what appears to be random text and the |
| selection will be lost in the item the user might have expected to still have selection. |
| |
| To fix this you can introduce keys such as, |
| |
| ``` |
| @Composable |
| fun <T> KeyedTwoColumns(items: List<T>, composeItem: @Composable (item: T) -> Unit) { |
| val half = items.size / 2 |
| Row { |
| Column { |
| for (item in items.take(half)) { |
| key(item) { |
| composeItem(item) |
| } |
| } |
| } |
| Column { |
| for (item in items.drop(half)) { |
| key(item) { |
| composeItem(item) |
| } |
| } |
| } |
| } |
| } |
| ``` |
| |
| |
| This allows the composition for an item to be reused if the item is in the same column but discards |
| the composition, and any implicit state such as selection, and creates a new state. The example |
| above for the selection in the first item is addressed but if selection is in the last item in a |
| column then selection is lost entirely as the selection state of the item is discarded. |
| |
| With `movableContentOf`, the state can be preserved across such movements. For example, |
| |
| ``` |
| @Composable |
| fun <T> ComposedTwoColumns(items: List<T>, composeItem: @Composable (item: T) -> Unit) { |
| val half = items.size / 2 |
| val composedItems = |
| items.map { item -> item to movableContentOf { composeItem(item) } }.toMap() |
| |
| Row { |
| Column { |
| for (item in items.take(half)) { |
| composedItems[item]?.invoke() |
| } |
| } |
| Column { |
| for (item in items.drop(half)) { |
| composedItems[item]?.invoke() |
| } |
| } |
| } |
| } |
| ``` |
| |
| which maps a movable content lambda to each value in items. This allows the state to be tracked when |
| the item moves between columns. This implementation is incorrect as it doesn't preserve the same |
| instance of the movable content lambda between compositions. This can be corrected (if somewhat |
| inefficiently) and lifted out into an extension function of `List<T>` to, |
| |
| ``` |
| fun <T> List<T>.movable( |
| transform: @Composable (item: T) -> Unit |
| ): @Composable (item: T) -> Unit { |
| val composedItems = remember(this) { mutableMapOf<T, () -> Unit>() } |
| return { item: T -> composedItems.getOrPut(item) { movableContentOf { transform(item) } } } |
| } |
| ``` |
| |
| The above allows us to easily reuse `NaiveTwoColumns` with the same state preserving behavior of |
| `ComposedTwoColumns` by passing in a `composed` lambda such as, |
| `NaiveTwoColumns(items, items.movable { block(it) })`. This allows a naive layout of items to |
| become state savvy without modification. Further this allows the state the preservation needs of |
| the content to be independent of the layout. |
| |
| |
| ## Issue 1: Composition Locals |
| |
| There are two possibilities for composition locals for a movable content, it has the scope of at the |
| point of the call to `movableContentOf` that creates it, b) it has the scope of the composition |
| locals that are in scope where it is placed. |
| |
| #### Solution chosen: Scope of the placement |
| |
| In this option the scope of the placement is used instead of the scope of the creator. This leads |
| to the most natural implementation of movable content being composed lazily at first placement |
| instead of eagerly. It would also require placing to validate the composition locals of the |
| placement and potentially invalidate the composition if the composition locals are different than |
| expected. To reduce unused locals from causing an invalidation it might require tracking the usage |
| of compostion locals and only invalidate if a composition locals it uses is different instead of |
| just the scope being different. |
| |
| Recomposition of an invalid movable content has the same or slightly slower performance than |
| compositing it from scratch as skipping is disabled as the static composition locals have changed |
| and where they are used is not tracked. This will also affect dynamic composition locals as the |
| providing an alternative dynamic composition local (such as a font property) is providing a static |
| composition local of a state object. |
| |
| |
| * `+` Prevents most eager composition. |
| * `-` Placing a content can potentially invalidate it, requiring it to be fully recomposed |
| (without skipping). To reduce the impact of this might require tracking which composition locals |
| are used by the movable content, which is currently not needed. |
| * `+` Movable content will always use the same ambients as a normal composable lambda. |
| * `+` Movable content will always be placeable. |
| * `+` Movable content behave nearly identically to composable functions except for state and |
| associated nodes. |
| |
| #### Alternate considered: Composition locals of the creator |
| |
| In this option the scope of the creator of the movable content is used in the value. This allows |
| the content to be composed eagerly and just waiting to be placed. |
| |
| This, however, could lead to surprising results. For example, if the content was created in one |
| theme and placed in a different part of the composition with a different theme it would appear out |
| of place. Similar effects would be noticeable with fonts as well as the placed text would appear |
| out of place. Using the creator's context could also lead to not being able to place the |
| content at all if the composition locals collide such as having non-interchangeable locals such as |
| Android `Context`. |
| |
| Using the creator's scope, however, means that a content can always be placed without recomposition |
| being required as the only thing that can change are invalidations inside the content itself, |
| placement will never invalidate it. |
| |
| * `+` Allows for eager composition of the content. |
| * `+` Content can be placed without causing them to be invalidated. |
| * `-` Content will appear out of context when the ambient scope affects its visual appearance. |
| * `-` Content can become unplaceable if the composition locals are incompatible with the |
| placement locals. This would be difficult to detect and might require modification of the |
| composition local API to allow such detection. |
| * `-` Content behavior when placed differs significantly from a normal composable function. |
| |
| ## Issue 2: Multiple placements |
| |
| Once a movable content lambda has been created there is very little that can be done to prevent the |
| developer from placing the content multiple times in the same outer composition. The options |
| are to a) throw an exception on the second placement of the content, b) ignore the placement of any |
| placement except the first or last, c) treat subsequent placements as valid and create as many |
| copies as needed to fulfill the placements used, d) reference the same state from every invocation. |
| |
| #### The solution chosen: Create copies as needed |
| |
| With this option a movable content acts very similar to a key where it is not necessary for them |
| to be unique. In composition, if a key value is used more than once then composition order is used |
| to disambiguate the compositions associated with the key. Similarly, if movable content is used |
| more than once, the composition function whose state is being tracked is just called again to |
| produce a duplicate state. Instead of solely using composition order (which might cause the state |
| to move unexpectedly), a new state will only be created for invocations that were not present in |
| the previous composition and order of composition will be used for movable content whose source |
| locations change. In other words, movable content will only move its state if the lambda was |
| not called in the same location in the composition as it was called in the previous composition. |
| This is not true of keys as the first use of a key might steal the state of a key that was |
| generated in the same location it was previously. |
| |
| * `-` Complex to implement as the state values need to be tracked through the entire composition |
| and many of the decisions must be resolved after all other composition has been completed. |
| * `+` Movable content lambdas behave like normal composition functions except the state moves with |
| their invocation. This allows movable content lambdas and normal composition lambda to be |
| interchangeable. |
| * `-` Composition can occur out of the order that the developer might expect as recomposition of |
| movable content might occur after all other recomposition has completed. |
| * `-` If movable content is placed indirectly, such as might happen when using it in |
| `Crossfade`, the state will not be shared as the new state will be created for the new |
| invocation and the invocation that retains the state will eventually be removed. It is |
| difficult to predict how a child will use a movable content or how to influence its use |
| of the content to retain the state as desired. |
| * `-` In the "list recycling" case this can lead to surprising behavior like selection not being |
| reset during recycling _sometimes_. |
| |
| |
| #### Alternate (A) considered: Throw an exception |
| |
| With this option placing movable content more than once would cause an exception to be thrown |
| on the second placement. |
| |
| * `+` Simpler to implement as no additional tracking of content is necessary. It just |
| needs to detect the second placement. |
| * `-` Movable content need careful handling when placing to ensure that they are not |
| accidentally placed more than once. |
| * `-` Movable content lambdas behave differently than normal composable lambda in that they can |
| only be called once. |
| * `-` Ideally a diagnostic would be generated when a composable function is placed more than |
| once but such analysis requires the equivalent of Rust's borrow checker. |
| |
| #### Alternate (B) considered: Ignore all but the first or last placement |
| |
| With this option either the first or last placement would take precedence over all other |
| placements. If this option is chosen a justification of which to take would need to be |
| communicated to the developer. |
| |
| * `+` Simple to implement with similar complexity to throwing on second placement. |
| * `-` Content might disappear unexpectedly when a content is placed more than once. |
| * `-` Movable content behave differently than normal composable lambda in only one call will |
| have an effect. |
| * `-` Our guidance tells developers to not assume any execution order for composables, as a |
| result "first placement" is strongly undefined if multiple placements are performed in a |
| single Composition |
| |
| |
| #### Alternate (C) considered: Shared state |
| |
| With this option all the state of each invocation of the movable content is shared between all |
| invocations. This, effectively, indirectly hoists the state objects in the movable content for |
| use in multiple places in the composition. |
| |
| The nodes themselves can be shared as the applier would be notified when it is requested to add a |
| node that appears elsewhere in the tree so that it can decide if the nodes should be cloned or |
| used directly. It would return whether it cloned the nodes to indicate to the composer that, for |
| cloned nodes, all changes to the nodes need to be repeated for the clones. |
| |
| Part of this option would be the requirement that movable content could not be parameterized as |
| the node trees and states couldn't be reused if there was a possibility that the parameters could |
| be different at the call sites. |
| |
| * `+` Meets developer expectations that the state is identical for each call allowing multiple |
| calls of movable content to be used as transitionary images. For example, multiple calls |
| are made in a crossfade it is expected that the state would move to the retained |
| composition. However, with the chosen above, the state would reset as the faded out image |
| would retain the state as both exist in the composition simultaneously and the original |
| caller takes precedence over the new caller. |
| * `-` Complex to implement as there is a great deal of additional tracking that needs to be |
| performed if nodes don't support shared nodes directly (which LayoutNodes currently do |
| not). |
| * `-` It is unclear what `WithConstraints` or any similar layout influenced composition would |
| mean in such a scenario as the state is not be able to be shared in this case as the state |
| would need to diverge. It is also unclear how similar divergence needs could be detected. |
| * `-` It is unclear what `Modifiers` that expect to be singletons, such as focus, would mean as |
| the effects of the singleton would appear in two places simultaneously. In some cases, such |
| as a reflection, this is desirable but, in after-images, the expectation is that the effect |
| of the state is not shared. |
| * `-` Movable content, as discussed above, would not be able to take parameters such as |
| contextual `Modifiers`. This requires additional nodes to explicitly be created around |
| the placement to correctly reflect layout parameters to composition which we try to avoid |
| by passing `Modifiers` as parameters. |
| |
| |
| ## Issue 3: Compound hashcode |
| |
| The compound hashcode is used by saved instance state to uniquely identify application state that |
| can be restored across Android lifecycle boundaries. The compound hashcode is calculated by |
| combining the hashcode of all the parent groups and sibling groups up to the point of the call to |
| produce the hashcode. |
| |
| There are two options for calculating a compound hashcode, either a) have a hashcode that is |
| independent where the content is placed, or b) use the compound hashcode at the location of the |
| placement of the conent. |
| |
| #### Chosen solution: Use the hashcode independent of where the content is placed. |
| |
| In this option the compound hashcode is fixed at the time the movable content lambda is created. |
| |
| * `+` The compound hashcode is fixed as the movable content moves. |
| * `-` Multiple placement of content will receive identical compound hashcodes. |
| * `-` Differs significantly from a normal composition function. |
| |
| |
| #### Alternative consider: Use the hashcode relative to the placement |
| |
| In this option the compound hashcode is based on its location where the content is placed which |
| implies that as it moves, the compound hashcode could change. |
| |
| * `-` The compound hashcode can change as the value placement moves. This might cause unexpected |
| changes to state and cause `remember` and `rememberSaveable` behavior to be |
| significantly different. |
| * `+` Multiple placements of the same movable content will receive different compound hash codes. |
| * `+` Behaves identically to a composition lambda called at the same location as the placement. |
| |