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!