One Off the Slack: The Role of Ambients
Ambients are a way of providing semi-global data through a composable hierarchy
without passing the data as parameters. Roughly speaking, ambients are a key-value
store, keyed by an Ambient
, whose value is an Any
. At any level of the
composable hierarchy, you can use Providers
to supply values for an Ambient
,
including overriding values from higher in the hierarchy. Then, deeper in the
hierarchy, you can use a current
extension property on the Ambient
to retrieve
the current value, based on where we are in the hierarchy.
And, as with almost all forms of semi-global data, ambients have issues.
Google’s Jim Sproch didn’t mince words in the Slack thread:
But know that every time you read from an Ambient, I cry a little inside.
The scenario in question was having a composable start an activity:
@Composable
fun something(samples: List<Whatever>) {
AdapterList(data = samples) {
ListItem(onClick = {
ContextAmbient.current.launchActivity(it.clazz)
}) {
Text(text = it.name)
}
}
}
This was not working for Henrique Horbovyi. Jim pointed out that the way to fix
the code would be reference the ambient outside of the onClick
handler:
@Composable
fun something(samples: List<Whatever>) {
AdapterList(data = samples) {
val context = ContextAmbient.current
ListItem(onClick = {
context.launchActivity(it.clazz)
}) {
Text(text = it.name)
}
}
}
However, Jim’s argument is that this code probably should not be starting an activity directly. Rather, it should be reacting to clicks by passing the event upward, via a function type serving as a handler:
@Composable
fun something(samples: List<Whatever>, onClickHandler: (Whatever) -> Unit) {
AdapterList(data = samples) {
val context = ContextAmbient.current
ListItem(onClick = {
onClickHandler(it)
}) {
Text(text = it.name)
}
}
}
Or, instead of a function type, perhaps you are passing a ViewModel
or controller
or similar sort of object that is responsible for handling the clicks. This approach
makes our composable more easily testable, particularly in a unit test, as it no
longer depends upon Android things like Context
.
Or, as Jim put it:
Widgets should generally accept lambdas instead of “knowing how to perform bespoke actions, like launching a specific activity”.
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!