✨ Subscribe for component library

Exploring MovableContentOf in Jetpack Compose

Exploring MovableContentOf in Jetpack Compose

Jetpack Compose recently introduced a new function named movableContentOfin version 1.2.0-alpha03. As the name suggests, this enables us to move around content without the need for recomposition. This could save time and increase performance if the recompositions are expensive and or numerous. Jetpack compose already has some quite intelligent optimizations that are applied automatically in order to avoid unnecessary recompositions. As intelligent as it may be, we sometimes need to manually avoid recompositions and movableContentOf is one of the tools for the job.

It works by receiving a composable lambda function that it will remember and move to wherever it is invoked. I think the best way to understand and grasp the benefits is through an example.

...
var isRow by remember {  
    mutableStateOf(true)  
}  
  
Column(  
    horizontalAlignment = Alignment.CenterHorizontally  
) {  
    Button(onClick = { isRow = !isRow }) {  
        Text(text = "Switch")  
    }  
    if (isRow) {  
        Row(  
            Modifier.weight(1f),  
            verticalAlignment = Alignment.CenterVertically  
        ) {  
            LetterBox(letter = 'A')  
            LetterBox(letter = 'B')  
        }  
    } else {  
        Column(  
            Modifier.weight(1f),  
            verticalArrangement = Arrangement.Center  
        ) {  
            LetterBox(letter = 'A')  
            LetterBox(letter = 'B')  
        }  
    }  
}
...

The example above contains two boxes that can displayed in a row or column. The button can be used to switch between the two modes. The catch is that every time the button is clicked, the tiles are recomposed. We can see this by using log statements whenever a recomposition occurs.

letter example without movableContentOf

To prevent the tiles from being unnecessarily recomposed when switching orientations, we can introduce movableContentOf.

...
val boxes = remember {  
    movableContentOf {  
        LetterBox(letter = 'A')  
        LetterBox(letter = 'B')  
    }  
}  
  
Column(  
    horizontalAlignment = Alignment.CenterHorizontally  
) {  
    Button(onClick = { isRow = !isRow }) {  
        Text(text = "Switch")  
    }  
    if (isRow) {  
        Row(  
            Modifier.weight(1f),  
            verticalAlignment = Alignment.CenterVertically  
        ) {  
            boxes()  
        }  
    } else {  
        Column(  
            Modifier.weight(1f),  
            verticalArrangement = Arrangement.Center  
        ) {  
            boxes()  
        }  
    }  
}
...

Using movableContentOf, the boxes would only be composed once. When the orientation switches, the same boxes would just be rearranged accordingly without recomposition.

letter example with movableContentOf

Another scenario where movableContentOf could come in handy is in a list. In the next example, it is used to maintain state properly and avoid unnecessary recompositions in a column.

@Composable  
fun ColumnExample() {  
    val list = remember {  
        mutableStateListOf<String>().apply {  
            for (i in 0..20) add("Counter ${'A' + i}")  
        }  
    }  
  
    Column {  
        Button(onClick = {  
            list.removeFirstOrNull()  
        }) {  
            Text(text = "Remove first")  
        }  
        Column(  
            modifier = Modifier  
				.verticalScroll(state = rememberScrollState())  
                .weight(1f)  
        ) {  
            list.forEach {  
                Counter(text = it)  
            }  
        }  
    }  
}

In this example, we have a column of counters and a button that removes the first counter in the list. But if we run the app and check the log statements, we will notice two major issues.

column example without movableContentOf

First, the app does not maintain state properly. When the first counter is removed, the state of counters moves to the position below. Second, the log statements indicate that removing one counter recomposes every item that has its position affected. Using movableContentOf we can maintain proper state of the counter plus avoid recompositions when we just need to move a counter's position in the column.

...
val listComposables = list.movable {  
    Counter(text = it)  
}  
  
Column {  
    Button(onClick = {  
        list.removeFirstOrNull()  
    }) {  
        Text(text = "Remove first")  
    }  
    Column(  
        modifier = Modifier  
			.verticalScroll(state = rememberScrollState())  
            .weight(1f)  
    ) {  
        list.forEach {  
            listComposables(it)  
        }  
    }  
}
...

And the extension function movable is defined as:

@Composable  
fun <T> List<T>.movable(  
    transform: @Composable (item: T) -> Unit  
): @Composable (item: T) -> Unit {  
    val composedItems = remember(this) { mutableMapOf<T, @Composable () -> Unit>() }  
    return { item: T ->  
        composedItems.getOrPut(item) {  m
            movableContentOf { transform(item) }  
        }.invoke()  
    }  
}

Now, if we run it now, we will see that the state of the counter is being preserved correctly and at the same time, the counters are not recomposed when moving positions.

column example with movableContentOf

The full code is available here. These are just two examples, but the possibilities of using movableContentOf to improve the performance of our code are many. If you need more information on how to use it, you can go directly to the android docs. Here, you will also find more info on its behavior in different situations.

Thanks for reading and good luck!

Mastodon