Pass data in the Modifier tree and beyond
One of the reasons why the order of Modifiers is important could be summed by passing data from one modifier node to other nodes right of it.
Why to the right? You can pass data from a node in the tree to the right along the modifier chain. Modifiers can pass data to all the modifiers to the right using Modifier.modifierLocalProvider(key) { value }
. Modifiers can retrieve and update data using Modifier.modifierLocalConsumer { }
. The lambda block of the consumer will be called each time the state (attached, detached, etc.), the order of the modifier nodes, or the provided data changes.
Similar to CompositionLocal
, ModifierLocal
works the same way but in the modifier tree.
Let's look at this example where we declare our ModifierLocal
to allow other modifiers in the tree to pass the instance of MyScope
.
val ModifierLocalMyScope =
modifierLocalOf<MyScope> { error("Scope not provided") }
@Composable
fun MyLayout(
modifier: Modifier = Modifier,
properties: MyProperties = MyProperties(),
colors: MyLayoutColors = MyLayoutDefaults.colors(),
content: @Composable (MyScope.() -> Unit)
) {
val scope = rememberMyScope(properties)
Surface(modifier = Modifier.fillMaxSize()
.modifierLocalProvider(ModifierLocalMyScope) { scope }
.then(modifier)
) {
scope.content()
}
}
You can notice that passed modifier
is concatenated last in the chain, to the right of the modifier that starts the chain and to the right of modifierLocalProvider { }
.
Modifier.fillMaxSize()
.modifierLocalProvider(ModifierLocalMyScope) { scope }
.then(modifier)
Within Modifier tree
There are two ways to read data from ModifierLocal
s: using modifierLocalConsumer { }
or implementing ModifierLocalConsumer
. The ModifierLocalConsumer
interface has an onModifierLocalsUpdated
function that is called whenever there is a change in the modifier order or local data. By implementing this interface, you can delegate the task of passing data to other modifier implementations.
Beyond Modifier tree
As the passed modifier is added last, we can get an instance of the scope at the calling site and use it in the composition context. By setting the passed modifier last in the chain, you can pass data up from the modifier tree to higher levels and even interact with the composition context.
@Composable
fun SampleScreen(modifier: Modifier = Modifier) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
//when modifierLocalConsumer { } block is called, next composition
// will make this local prop non null.
val myScope by remember { mutableStateOf<MyScope?>(null) }
Column {
MyLayout(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.modifierLocalConsumer {
myScope = ModifierLocalMyScope.current
}) {
Greeting(
name = "Hi $this",
modifier = Modifier
.align(Alignment.Center)
.border(2.dp, Color.Red),
)
}
Greeting(
name = "Hello $myScope",
modifier = Modifier.border(2.dp, Color.Blue),
)
}
}
}
}
Observing changes in the Locals within the modifier trees allows us to get an instance and use it in the composition context later.
.modifierLocalConsumer {
myScope = ModifierLocalMyScope.current
}
Subscribe to my newsletter
Read articles from Nikola Despotoski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nikola Despotoski
Nikola Despotoski
Android Tech Lead @ WSAudiology. My current interests are replacing my code reviews for my teams with Lint, optimizing Composables, code generation with KSP, improving module reusability across organization and KMP. I have one talk so far: https://speakerdeck.com/despotoski/kotlin-symbol-processing-ksp-and-remedy-to-dry-code