We’ve seen increasing use of the Circuit framework in Kotlin/Compose Multiplatform projects and I thought it was time to update one of the samples I have to make use of it. This article outlines some of the key changes made to use Circuit in the BikeShare sample. That project had already been using the kotlin-inject DI framework (more about that here) so we’ll also show how that can be used to configure the Circuit related components we’re using. Note that this implementation is inspired by the excellent Tivi sample.
Circuit
Circuit is summarised in the official documentation as “a simple, lightweight, and extensible framework for building Kotlin applications that’s Compose from the ground up”. It strongly enables a unidirectional data flow (UDF) approach that is based on the following principles.
Circuit’s core components are its Presenter and Ui interfaces.
- A Presenter and a Ui cannot directly access each other. They can only communicate through state and event emissions.
- UIs are compose-first.
- Presenters are also compose-first. They do not emit Compose UI, but they do use the Compose runtime to manage and emit state.
- Both Presenter and Ui each have a single composable function.
- In most cases, Circuit automatically connects presenters and UIs.
- Presenter and Ui are both generic types, with generics to define the UiState types they communicate with.
- They are keyed by Screens. One runs a new Presenter/Ui pairing by requesting them with a given Screen that they understand.
Implementation
As mentioned, what’s outlined in this article is based on changes made to the BikeShare KMP/CMP sample. That project makes use of the CityBikes API to show bike share networks and associated bike availability in different countries around the world. The following shows screenshots for the country, network, and station list screens in the Compose for Desktop client.
In the case of the country list for example we have the following key Circuit components
CountryListScreen
CountryListPresenter
(and associatedCountryListPresenterFactory
factory)CountryListUi
(and associatedCountryListUiFactory
)
CountryListScreen
A Screen
, as mentioned above, is used as the key for a particular Circuit Presenter/Ui pairing. In this case here it’s also used to encapsulate the state that a presenter can emit to the Ui and the events that the Ui can send to the presenter.
1
2
3
4
5
6
7
8
9
10
11
@Parcelize
data object CountryListScreen : Screen {
data class State(
val countryList: List<Country>,
val eventSink: (Event) -> Unit
) : CircuitUiState
sealed class Event : CircuitUiEvent {
data class CountryClicked(val countryCode: String) : Event()
}
}
CountryListPresenterFactory/CountryListPresenter
The following is the factory for creating CountryListPresenter
presenters, keyed as mentioned by CountryListScreen
.
1
2
3
4
5
6
7
8
9
10
11
@Inject
class CountryListPresenterFactory(
private val presenterFactory: (Navigator) -> CountryListPresenter,
) : Presenter.Factory {
override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {
return when (screen) {
CountryListScreen -> presenterFactory(navigator)
else -> null
}
}
}
And then this is the implementation for CountryListPresenter
. This includes a single composable function that’s used to create the state that’s emitted to our UI (note this is making use of Compose Runtime as opposed to Compose UI)…it also handles any events sent to it from the UI (e.g. CountryClicked
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Inject
class CountryListPresenter(
@Assisted private val navigator: Navigator,
private val cityBikesRepository: CityBikesRepository
) : Presenter<CountryListScreen.State> {
@Composable
override fun present(): CountryListScreen.State {
val groupedNetworkList by cityBikesRepository.groupedNetworkList.collectAsState()
val countryCodeList = groupedNetworkList.keys.toList()
val countryList = countryCodeList.map { countryCode -> Country(countryCode, getCountryName(countryCode)) }
.sortedBy { it.displayName }
return CountryListScreen.State(countryList) { event ->
when (event) {
is CountryListScreen.Event.CountryClicked -> navigator.goTo(NetworkListScreen(event.countryCode))
}
}
}
}
CountryListUi
Finally we have the UI component. This gets notified of state emissions from the associated presenter (CountryListPresenter
) and sends any UI events to that presenter (in this example indicating that a country was selected in the list).
1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun CountryListUi(state: CountryListScreen.State, modifier: Modifier = Modifier) {
Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text("Countries") }) }) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding)) {
items(state.countryList) { country ->
CountryView(country) {
state.eventSink(CountryListScreen.Event.CountryClicked(country.code))
}
}
}
}
}
BikeShareApp
We also have the following in our “root” composable that uses Circuit to launch that initial CountryListScreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typealias BikeShareApp = @Composable () -> Unit
@Inject
@Composable
fun BikeShareApp(circuit: Circuit) {
MaterialTheme {
val backStack = rememberSaveableBackStack(root = CountryListScreen)
val navigator = rememberCircuitNavigator(backStack) {}
CircuitCompositionLocals(circuit) {
NavigableCircuitContent(navigator = navigator, backStack = backStack)
}
}
}
kotlin-inject configuration
The following then shows how these components are configured using the kotlin-inject DI framework (we’re also showing the setup here for the network and station list related components). Note also use of that framework’s support for multi-binding to allow setting up the set of presenter and ui factories.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
abstract val bikeShareApp: BikeShareApp
@IntoSet
@Provides
fun bindCountryListPresenterFactory(factory: CountryListPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
fun bindCountryListUiFactory(factory: CountryListUiFactory): Ui.Factory = factory
@IntoSet
@Provides
fun bindNetworkListPresenterFactory(factory: NetworkListPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
fun bindNetworkListUiFactory(factory: NetworkListUiFactory): Ui.Factory = factory
@IntoSet
@Provides
fun bindStationListPresenterFactory(factory: StationListPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
fun bindStationListUiFactory(factory: StationListUiFactory): Ui.Factory = factory
@Provides
fun provideCircuit(
uiFactories: Set<Ui.Factory>,
presenterFactories: Set<Presenter.Factory>
): Circuit = Circuit.Builder()
.addUiFactories(uiFactories)
.addPresenterFactories(presenterFactories)
.build()
Note that a recent release of Circuit added support for kotln-inject code generation that removes need for some of the boilerplate code shown here. Hope to take a look soon at using that!
Featured in Android Weekly #643
Related tweet
Using Circuit with kotlin-inject in a Kotlin/Compose Multiplatform project https://t.co/jncGH5HYDU
— John O'Reilly (@joreilly) October 5, 2024
A short article outlining some of changes made to the BikeShare #KMP sample to make of the really nice Circuit framework.