Skip to content

Coordinator

The coordinator the is the main component in our architecture. Every statefull Composable component, such as LazyRow and Scaffold has a state that drives how the component works, similiary, you can think of a Coordinator as the state of your flow or feature.

Following figure demonstrates the flow of the data and how each component interacts with each other.

This a simplistic version of whats going on. Looking closely, when we say the action is handled by the Coordinator is that will delegate this action some other component. Similiraly, the Coordinator doesn't store any state but rather relies other components to provide it. Let's call them State Providers and Action Handlers

State Providers & Action Handlers

The State Provider is the source of truth of some component. We can have multiple of those. The Coordinator then acts as a mediator allowing us to combine the states or expose partial states.

The most common example of the State Provide can be the android ViewModel that exposes the state of our business logic. However, we can also consider other composable components state as a state provider, for example: PagerState

data class ViewModelState(
    val items: List<Item>
)

data class ScreenState(
    val currentItem: Item?
)

class ExampleCoordinator(
    val viewModel: ViewModel,
    val pagerState: PagerState
 ) {
    val stateFlow = combine(
        viewModel.stateFlow,
        pagerState.currentPagerFlow()
    ) { uiState, currentPage } {
        ScreenState(uiState.items[currentPage])
    }
 }

The Action Handlers is something that can mutate our State. Very often the Action Handler can also act as the State Provider.

In summary, the main idea of the Coordinator is to "coordinate" different State Providers and Action Handlers to perform some user journey

// coordinator scope
class LocationCoordinator(
    val permissionState: PermissionState,
    val mapState: MapState
)
fun focusOnUserLocation() {
    if (permissionState.isGranted) {
        mapState.focusOnCurrentLocation()
    } else {
        permissionState.launchPermissionRequest()
    }
}

Following diagram demonstrates this in a more generic way

React to State Changes - Side effects

Because of the Reactive nature of Compose handling something that we consider one shot events can be bit tricky.

When inside @Composable we have access to different effects so we can do something only when certain conditions are met without worrying about re-compositions.

But since we have Coordinator we can let it do that for us as well. Because the Coordinator is outside of Composable scope and doesn't get re-created due to the recompositions we can easily subscribe to the state changes and react to them in more straight forward fasion, just like we would normally do in our ViewModel

class Coordinator(
    val viewModel: ViewModel,
    val context: Context,
    val scope: CoroutineScope
) {
    init {
        viewModel.errorFlow()
            .onEach { msg -> 
                context.showToast(msg) 
                viewModel.dismissError()
            }
            .launchIn(scope)
    }
}

Again this is basically the same thing we would with the Action coming in from the Screen, except now the source of this interaction is the State Provider it self and it doesn't leave the Coordinator and goes directly to the Action Handlers

Comments