CommonStateMachine

Please checkout the Medium article on pattern/library usage.
Contents
Introduction
An MVI pattern of architecting modern applications has been getting more and more popular in the
recent time. There are a lot of articles describing the pattern but to recap let's see the key points
that make up the approach:
- Model - A model holds the representation of the data state and changes it with reducing logic.
Data changes are propagated to the view layer as a stream of complete view-states.
- View - The view layer observes user actions and view-state changes from the model. As a result,
it sets the intention for the triggered UI gesture passing it to the model to process.
- Intent - A representation of user's gestures that changes the state of the model. View handles
widget interactions and provides a stream of gestures to the model through the unified interface.
Key advantages of MVI:
- Single source of truth - one set of data and logic to define complete view-state
- Unidirectional data flow
- Thorough and complete testing of all logic with unit tests
However (as with any technology) there are some downsides that you come across along with your
app growth:
- Too much overkill for simple functions like LCE (Load/Content/Error) display
- Too much reducer logic based on if/else of the current data state which plays badly in complex
multi-step scenarios.
- Quite a learning curve to grasp the technology
The simple pattern presented by this project aims to overcome the above drawbacks and to give you
more freedom to choose technology and to build a cohesive logic and data processing in a
"less-opinionated" way.
Key features:
v3.X Breaking change
To be able to get the current UI state of the state-machine and to get rid of inconsistency of the
UI state is not yet defined (no updates of UI state happened) the FlowStateMachine and
the ProxyMachineState now require to pass initial UI state in constructors.
v4.X Breaking change
The view lifecycle MachineLifecycle)
implementation is moved to the separate multiplatform library:
dependencies {
// View lifecycle implementation
implementation "com.motorro.commonstatemachine:lifecycle:x.x.x"
}
Dependencies
The project has a very simple core to implement yourself but you could also grab the latest core
version like that:
dependencies {
// Base state-machine components
implementation "com.motorro.commonstatemachine:commonstatemachine:x.x.x"
// Coroutine extensions (optional)
implementation "com.motorro.commonstatemachine:coroutines:x.x.x"
}
Multiplatform:
val commonMain by getting {
dependencies {
implementation("com.motorro.commonstatemachine:commonstatemachine:x.x.x")
implementation("com.motorro.commonstatemachine:coroutines:x.x.x")
}
}
Examples
The basic task - Load-Content-Error
Let's start with a basic example. Imagine we need to implement the classic master-detail view of
items with the following screen flow:

Let's break down business requirements...
We have four application logical states which correspond to screen states for this application:
- Item list - the list of items to load is displayed. User clicks an item to load it's contents.
- Loading item - the network operation is running. User waits for operation to complete.
- Item content - the loaded item content is displayed. User may return back to item list.
- Item load error - the load operation has failed and we have a choice to retry load or to quit the
application.
States and transitions
The state diagram with the corresponding transitions will be the following:

The diagram above, as you can see, has two types of inter-state transitions:
Red - user Intentions: clicks,
swipes and other interactive Gestures that are originated by application user.
Blue - transitions made by application
logic: content display, errors, etc.
Each logical state may transition to another logical state as a result of Gesture or state's internal
logic.
Let's take a look at which Gestures each logical state processes and how they transition
logical states:
Each logical state should be able to:
- Update
UiState
- Process some relevant user interactions -
Gestures ignoring irrelevant
- Hold some internal data state
- Transition to another logical state passing some of the shared data between
Logical state may be implemented as a self-contained controller having an input, output and
internal data and a set of rules to process gestures, to reduce data and to pass it to the next
state when logic falls behind what's relevant for this state.
State machine
First of all we need some kind of a bridge between the current logical state and the outside world.
The state machine
should be able to:
- Hold the active logical state
- Transition between states
- Delegate gesture processing to the current state
- Propagate UI-state changes to the outside world
- Clean-up all resources on shutdown

Methods:
process(gesture: G) - Called by view upon user action. Delegated to current state.
clear() - Called by view/framework to cleanup resources. Like in onCleared of ViewModel.
setMachineState(machineState: CommonMachineState<G, U>)- Called by active state to transition
to the new one.
The concrete state machine implementation provides a way to update the view with a new UI state.
For example the FlowStateMachine
exports UI state changes through uiState shared flow:
State
The base state class has three interaction methods:
doProcess(gesture: G) - Called by the state-machine to process gesture.
setUiState(uiState: U) - Call from within your state implementation to update UI State.
setMachineState(machineState: CommonMachineState<G, U>) - Call from implementation to transition
to the new state
and two lifecycle methods:
doStart() - Called by the state-machine when your state becomes active.
doClear() - Called by the state machine when your current state is about to be destroyed either
by replacing by the new state or when state-machine is about to be destroyed.
The state lives between doStart and doClear calls. You could safely call interaction methods
and expect gesture processing calls within that period. Make sure to cleanup all your pending
operations in doClear handler. For example, the CoroutineState
provides you the stateScope coroutine scope that is being cancelled in doClear:
abstract class CoroutineState<G: Any, U: Any>: CommonMachineState<G, U>() {
protected val stateScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
override fun doClear() {
stateScope.cancel()
}
}
Implementation
Let's implement our application now. For this simple example we will skip some handy abstractions
like state-factory, context and renderer that help you to build your states and separate
concerns. Will talk about it later.
Item list state
ItemListState is a
starting state for our application. It displays the list of items to load. The list is hardcoded for
this example so we just emit a complete view-state when started:
private val items = listOf(
ItemId.LOADS_CONTENT to "Item that loads",
ItemId.FAILS_WITH_ERROR to "Item that fails to load"
)
override fun doStart() {
setUiState(LceUiState.ItemList(items.map { ItemModel(it.first, it.second) }))
}
Handle relevant gestures by transitioning the state-machine to the newly created states.
The LoadingState constructor accepts an id of item to load as an inter-state common data.
override fun doProcess(gesture: LceGesture) = when(gesture) {
is LceGesture.ItemClicked -> onItemClicked(gesture.id)
is LceGesture.Back -> onBack()
else -> .doProcess(gesture)
}
{
setMachineState(LoadingState(id))
}
{
setMachineState(TerminatedState())
}
Item loading state
LoadingState emulates
an asynchronous operation:
override fun doStart() {
setUiState(LceUiState.Loading)
load()
}
private fun load() {
stateScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
}
}
}
Depending on the item we pass the state transitions to either a ContentState or an ErrorState
passing either the mock content or the error occurred as an inter-state data:
private fun toContent() {
setMachineState(ContentState("Some item data..."))
}
private fun toError() {
setMachineState(ErrorState(id, IOException("Failed to load item")))
}
Item contents state
ContentState is very
simple. It just sets the UI state to display data passed to the constructor and handles a Back
gesture to return to the item list:
override fun doStart() {
setUiState(LceUiState.Item(contents))
}
override fun doProcess(gesture: LceGesture) = when (gesture) {
LceGesture.Back -> onBack()
else -> super.doProcess(gesture)
}
private {
setMachineState(ItemListState())
}
Error state
The ErrorState gives a
user the ability to retry item load or to exit the app. Also it handles Back gesture to return to
the item list. Handling all user interactions through your state machine gives you a precise control
on what happens next. The item ID passed in the constructor as an inter-state data makes it possible
to preserve user's selection and to restart loading from scratch.
Wiring with the application
Now that we have all states in place let's connect them together with a state machine. We need some
place to retain a machine through the application flow so let's wrap it to the Jetpack ViewModel
which is common now:
All we need to do here is:
- to create a state-machine instance
- to figure out the initial state that machine will start from
- to wire ui-state and gesture processing with the outside world
And here is an abstract of the view
that interacts with the model:
@Composable
fun LceScreen(onExit: @Composable () -> Unit) {
val model: LceViewModel = viewModel()
val state = model.state.collectAsState(LceUiState.Loading)
BackHandler(onBack = { model.process(Back) })
when ( uiState = state.value) {
LceUiState.Loading -> Loading()
LceUiState.ItemList -> ItemList(
state = uiState,
onItemClicked = { model.process(ItemClicked(it)) }
)
LceUiState.Error -> LoadError(
state = uiState,
onRetry = { model.process(Retry) },
onBack = { model.process(Back) },
onExit = { model.process(Exit) }
)
LceUiState.Item -> ItemDetails(state = uiState)
LceUiState.Terminated -> onExit()
}
}
Compose library plays greatly here but you could easily adapt a fragment transaction or a
recycler view architecture as well.
Result
As you can see the state-machine pattern may be a good choice in implementing your MVI architecture.
It produces a clean and easy to grasp step-by-step logic with well-separated concerns and easy and
thorough testing. The pattern also
attempts to be as non-opinionated as possible. Each state is a black-box with a defined contract and
developers may choose the most suitable tools to implement each one without affecting the other. The
example above is a very basic one. However you could do things a bit more clean by using some of the
additional abstractions (see below).
Tools and Handy abstractions to mix-in
In the basic example above all the work was done by the state objects. They did:
- running a "network operation"
- view-state data rendering
- next state creation
That is a quite a huge responsibility which might not be so good in terms of coupling and testing.
So let's introduce some abstractions that will lift the burden off the state's shoulders.
Use-cases
By use-case I assume any business logic external to your view logic implemented in a state. Be it
some network operation or some other "use-case" - provide it to your state and use them as you like.
There is nothing new here - I'm sure you already use the approach in your flavour of
Clean Architecture or similar. Example of using an external use-case could be found in
examples/welcome/welcome example:
class CredentialsCheckState(private val checkCredentials: CheckCredentials) {
override fun doStart() {
stateScope.launch {
val valid = checkCredentials()
}
}
}
Note on threading: the library doesn't provide any threading support and not thread-safe. So it
is your responsibility to implement correct thread handling so all state changes happen on
the desired thread.
CoroutineState
creates it's scope with Dispatchers.Main.immediate.
View-state renderer
Preparing the complex view-state from your state data might be a non-trivial task in applications
with complex interface. Moving a coupling to the view-state and data structures from your state logic
might be a good idea. Testing the exact view-state creation would be much easier if you make it
as more or less a clean function. Also your logic states may share the same rendering logic so
externalizing it would play greatly in terms of code reuse. For example the same view-state
rendering is used by PasswordEntryState
and ErrorState of
examples/welcome/welcome example. You could inject your renderer in a state factory or get it from common context
(see below).
State factories and dependency provision
Creating new states explicitly to pass them to the state-machine later (like in the basic example)
is not a good idea in terms of coupling and dependency provision.
The machine state, when created, may require three main classes of dependencies:
- State-specific dependencies like use-cases state operates.
- Inter-state data e.g. data loaded in a previous state, common data state, etc.
- Common dependencies for all states in machine: renderers, resource providers, factories
You are free to choose the way to provide dependencies however let's take a look at the approach
that I came to while using the state-machine pattern.
State-specific dependencies
To provide dependencies that are specific to each particular state I go with dedicated state
factories that are injected with your DI framework. Let's take an example above and
extend it with a state-factory:
class CredentialsCheckState(private val checkCredentials: CheckCredentials) {
@LoginScope
class Factory @Inject constructor(private val checkCredentials: CheckCredentials) {
: LoginState = CredentialsCheckState(
checkCredentials
)
}
}
Inter-state data
By inter-state data I assume any dynamic data that is passed between states. It may be a product of
some calculation, user-generated data, etc. The most obvious way is providing it through the state
constructor:
class CredentialsCheckState(
private data: LoginDataState,
private val checkCredentials: CheckCredentials
) {
private val email = requireNotNull(data.commonData.email) {
"Email is not provided"
}
password = requireNotNull(.password) {
}
}
Dependencies common to all states of a state-machine
Common dependencies may include renderers, state factories, common external interfaces and anything
else that is required by all states that make up the state-machine. For convenience and to save the
number of constructor params I suggest to bind them to some common interface and provide it as a
whole. Let's name it a common Context:
interface LoginContext {
val factory: LoginStateFactory
val host: WelcomeFeatureHost
val renderer: LoginRenderer
}
Then you could provide it to your state through the constructor parameters. To make things even
easier let's make some base state
for the state-machine assembly and use a delegation to provide each context dependency:
abstract class LoginState(
context: LoginContext
): CoroutineState<LoginGesture, LoginUiState>(), LoginContext by context {
override fun doProcess(gesture: LoginGesture) {
Timber.w("Unsupported gesture: %s", gesture)
}
}
Thus every sub-class of the LoginState has any context dependency at hand by getting it from the
corresponding property as if the were provided explicitly:
Common state factory
As I've already mentioned, creating new states explicitly to pass them to the state-machine later
(like in the basic example) is not a good idea in terms of coupling and dependency provision.
Let's move it away from our machine states by introducing a common factory interface
that will take the responsibility to provide dependencies and abstract our state creation logic:
interface LoginStateFactory {
: LoginState
: LoginState
: LoginState
}
Each factory method here will accept only the inter-state data providing both context and
state-specific dependencies implicitly. This will decouple state logic from the concrete
implementations and increase our testability
greatly.
The exact factory implementation that binds together all data and dependencies will look like that:
The factory is made available to your machine states through the common context:
class CredentialsCheckState(context: LoginContext) : LoginState(context) {
override fun doProcess = (gesture) {
LoginGesture.Back -> onBack()
-> .doProcess(gesture)
}
{
setMachineState(factory.passwordEntry())
}
}
Then we could mock the factory in our tests and check state transitions:
class CredentialsCheckStateTest {
private val data = LoginDataState()
private val factory: LoginStateFactory = mockk()
private val passwordEntry: LoginState = mockk()
@Test
fun returnsToPasswordEntryOnBack() = runTest {
every { factory.passwordEntry(any()) } returns passwordEntry
state.start(stateMachine)
state.process(LoginGesture.Back)
verify { stateMachine.setMachineState(passwordEntry) }
verify { factory.passwordEntry() }
}
}
We can also provide the state factory to the ViewModel and use it to initialize our state-machine:
@HiltViewModel
class LoginViewModel @Inject constructor(private val factory: LoginStateFactory) : ViewModel() {
: CommonMachineState<WelcomeGesture, WelcomeUiState> {
commonData: LoginDataState = LoginDataState()
factory.passwordEntry(commonData)
}
stateMachine = FlowStateMachine(::initializeStateMachine)
}
View lifecycle with FlowStateMachine
Imaging we have a resource-consuming operation, like location tracking, running in our state. It may
save client's resources if we choose to pause tracking when the view is inactive - app goes to
background or the Android activity is paused. In that case I suggest to create some special gestures
and pass them to state-machine for processing. For example, the FlowStateMachine
exports the property that is a flow of number of subscribers listening to
the property. If you use some
to subscribe , you could use this property to figure out some special processing. For
convenience there is an extension function available to reduces boilerplate.
It accepts two gesture-producing functions and updates the state-machine with them when subscriber's
state changes:
class WithIdleViewModel : ViewModel() {
private : CommonMachineState<SomeGesture, SomeUiState> = InitialState()
stateMachine = FlowStateMachine(Loading, ::initStateMachine)
state: StateFlow<SomeUiState> = stateMachine.uiState
{
stateMachine.mapUiSubscriptions(
viewModelScope,
onActive = { SomeGesture.OnActive },
onInactive = { SomeGesture.OnInactive }
)
}
}
Multi-module applications
Let's take a more complicated example with a multi-screen flow like the customer on-boarding.
The user is required to accept terms and conditions and to enter his email. Then the logic checks if
he is already registered or a new customer and runs the appropriate flow to login or to register
a user. Imagine we want the login flow and the registration flow to be in separate modules to split
the work between teams. The state diagram would be the following:

The project uses the following modules:
- examples/welcome/welcome - common flow: preloading, email entry, customer check, complete
- commoncore - common abstractions to build application: dispatchers, resources, etc.
- commonapi - common multi-platform module to connect the main app with modules
Common API
As you could see in the diagram above each login and commonregister start after the email is
checked and the answer to user's registration status is obtained. The module flow starts from
password entry screen though a bit different. Each module flow returns to the main flow either:
- when flow completes succefully - transfers to
Complete
- when user hits
Back - transfers back to email entry
Let's define the main flow interaction API
then:
interface WelcomeFeatureHost {
fun backToEmailEntry()
fun complete()
}
We then place the definition to the module available to all modules: commonapi and provide the
interface through the common state context like this:
interface LoginContext {
val host: WelcomeFeatureHost
}
Module flow
Each module has it's own sealed system of gesture/view-states:
| Module name | Gestures | UI-states |
|---|
| welcome | WelcomeGesture |
Each module is completely independent in terms of gestures and UI states, and we also have a
proprietary set of 'handy abstractions' for each module: renderers, factories, use-cases, etc.
See the source code for more details. Now that we have all module-flows designed and tested we need
to find a way to connect completely heterogeneous systems to a single flow.
Adopting feature-flows
Given that gesture and view system are bound to state-machine through generics we need to build
some adapters to be able to run the flow within the main application state-flow. Things to do:
- Adapt gestures so they are plugged-in to the
welcome gesture flow.
- Adapt view-states so the view-system could display them.
- Somehow run the alien state-flow within the
welcome state-machine.
Gestures and view-states
To adopt feature-module gestures there are at least two solutions:
- Get rid of sealed systems and inherit the common-api base marker interface for all gestures and
view-states. Though simple, the solution is not ideal as we lose the type-safe
when exhaustive
checks when we dispatch gestures in our states. So let's drop it...
- Make a wrapping adapter that wraps the foreign gesture/view-state and unwrap it later when
passing them to concrete implementation. Thus we don't loose compiler support and type-safety.
Let's follow this route
Gesture adapter:
sealed class WelcomeGesture {
data class Login(val value: LoginGesture) : WelcomeGesture()
data class ( value: RegisterGesture) : WelcomeGesture()
}
UI-state adapter:
sealed class WelcomeUiState {
data class Login(val value: LoginUiState) : WelcomeUiState()
data class Register(val value: RegisterUiState) : WelcomeUiState()
}
View implementation
Now let's build feature and host composables to take advantage of our adapters.
Feature master-view:
@Composable
fun LoginScreen(state: LoginUiState, onGesture: (LoginGesture) -> Unit) {
}
@Composable
fun RegistrationScreen(state: RegisterUiState, onGesture: (RegisterGesture) -> Unit) {
}
Application master-view:
fun WelcomeScreen(onTerminate: @Composable () -> Unit) {
val model = hiltViewModel<WelcomeViewModel>()
val state = model.state.collectAsState(WelcomeUiState.Loading)
BackHandler(onBack = { model.process(Back) })
when (val uiState = state.value) {
WelcomeUiState.Login -> LoginScreen(
state = uiState.value,
onGesture = { model.process(Login(it)) }
)
WelcomeUiState.Register -> RegistrationScreen(
state = uiState.value,
onGesture = { model.process(Register(it))}
)
}
}
To sum-up:
- We delegate rendering of ui-states to feature composables by unwrapping proprietary states from
common view-state system
- We wrap any feature gesture to our master-gesture system and pass them to our model to process.
Adopting foreign state-flow
The last thing we need to do to be able to run a feature module in our host system is to be able to
run feature logical states in our application state machine. Remember we have bound both a gesture
system and the ui-state system to both our state-machine and machine-state:
interface CommonStateMachine<G: Any, U: Any> : MachineInput<G>, MachineOutput<G, U>
open class CommonMachineState<>
Seems like a problem but not really. Given that our states has a simple and clear state lifecycle
we could encapsulate the feature state-machine logic in our host state with a ProxyMachineState
by running a child state-machine inside the host state!

Whenever a ProxyMachineState is started it launches it's internal instance of a state-machine
bound to the feature gesture and view system. It also bridges two incompatible gesture/view systems
by wrapping/unwrapping and adopting one system to another. Let's see the example of a login flow
proxy
to make things clear:
To create a proxy you need to implement three core methods:
init() - creates a starting state for a proxy state-machine. We fetch a FlowStarter
interface (which is just a feature state factory segregation) to create a starting state.
For our example project we provide the WelcomeFeatureHost interface to return back to email entry
or to advance to Complete state as described in Common Api. The proxy implements
this interface by switching host machine to email or complete states in corresponding
backToEmailEntry and complete functions.
Common child flow API
For your convenience there are couple of ready-made interfaces to adopt child flow.
The interfaces are located in a separate libraries:
dependencies {
// If you use it for state machines only
implementation "com.motorro.commonstatemachine:commonflow-data:x.x.x"
// If you go with compose
implementation "com.motorro.commonstatemachine:commonflow-compose:x.x.x"
}
The libraries include the following interfaces:
- CommonFlowHost -
the interface the proxy should provide to the child flow. Child uses this flow to signal its termination.
- CommonFlowDataApi -
contains methods to initiate the flow and to adapt it to the hosting flow.
- CommonFlowUiApi -
An interface with the single
Screen method to inject and put to the composition
For more details on multiplatform compose viewmodel follow this link
Check the example that shows the use of this flow:
Additionally, there is another artefact that could make the bridge between conventional view
and state-machine easier:
dependencies {
// Common view-model bridge
implementation "com.motorro.commonstatemachine:commonflow-viewmodel:x.x.x"
}
The library contains common ViewModel implementation that hosts common-flow and works with
Activities, Fragments and Compose compositions.
The CommonFlowComposition
is a basic scaffold to quickly introduce common-flow state-machine flow to the conventional view or composable.
Take a look at the books project to see how it works.
Running state-machines in parallel (composition)
In case you want several state-machines to run in parallel producing a single combined UI state or you
want to persist several machines on a single screen (like a page with a bottom navigation) there is an
option to do it with the MultiMachineState
and ProxyMachineContainer
MultiMachineState
This state is a proxy that holds several machines at once. It is in charge for combining the UI state
whenever the running machine updates and for dispatching gestures from a single parent gesture to
proxied machines inside the composition. To distinguish machines and to ensure type-safety each machine in
composition is identified with the MachineKey
The state has three things to override:
- container:
manages machines lifecycle. More on this follows.
- mapUiState:
called each time your proxied machine updates UI state or explicitly when calling updateUi.
Here you take a decision on changes and build a common resulting UI state. See the dedicated section below.
- :
called when state gesture is processed. Here you can map the gesture and update your proxied machine.
Now let's see how the things work a bit closer.
ProxyMachineContainer
Container is in charge for creating and managing the lifecycle of the state machines. So far the
interface has two companion functions:
-
allTogether:
runs all machines in parallel with common lifecycle - startup and cleanup. - running two
timers simultaneously:
Container is initialized with a collection of MachineInit
structures:
interface MachineInit<G: Any, U: Any> {
val key: MachineKey<G, U>
val initialUiState: U
val init: (MachineLifecycle) -> CommonMachineState<G, U>
}
The init function is called each time the container needs to create a new machine. The MachineLifecycle
interface passed to initialization may be used by your states to determine if the machine is suspended
or active. If you use coroutines you could use asFlow
function to convert it to Flow. See example on how to start/stop
your pending operations that are not needed when your machine is inactive: gps tracking, server messaging, etc.
For example:
Check example states for each case:
- Parallel - two machines running in parallel in one proxy state
- Navbar - several machines running in proxy state, one of them active at a time
Mapping UI states
The gesture/ui type systems for each machine in composition are different, so we need some kind of
type casting to be on a safe side. Binding machines with keys in MachineInit makes sure the machine type
corresponds to the key and is used to map key to correct UI-state in mapUiState
method of MultiMachineState. To be able to do it, take the UiStateProvider
provided to the method to get the correct ui-state type:
Check example states for use cases:
- Parallel - two machines running in parallel in one proxy state
- Navbar - several machines running in proxy state, one of them active at a time
Dispatching gestures
As with UI-state mapping, binding machines with keys in MachineInit makes sure the machine type
corresponds to the key and is used to map key to correct gesture processor. Whenever the proxy receives
a gesture it calls mapGesture.
Using the provided GestureProcessor
and a key you can get access to the proxied machine instance to map and process your gesture:
Check example states and test class for use cases:
- Parallel - two machines running in parallel in one proxy state
- Navbar - several machines running in proxy state, one of them active at a time
- MultiMachineStateTest - unit test
MachineLifecyle bonus
The interface used to pass the machine activity to proxied state machine could also be used as an
view lifecycle monitor for your app. Pass UiMachineLifecycle
to your model initialization to by able to suspend your machines when app is not in use.
Similar to state collection methods optimized with lificycle.
Check the example to get the details.
Conclusion
I hope someone finds the article (and the library if you like to take it as-is) helpful in building
complex multi-screen applications with multi-module ability. This approach aims to give you as much
freedom as possible to implement your flows. Of cause it is not a silver bullet but the flexibility
in structuring your app it promotes plays well in most scenarios. You could combine all your
application steps in a single state flow or build separate models and inject them to the parts of
your navigation library graph. And you could also use any architecture inside your states - simple
coroutines to fetch the data, complex RxJava flows or even another MVI library in more complex cases.
The library was created with multi-platform approach in mind as it contains no concrete platform
dependencies and coroutines extentions are optional. So you may create your view logic once and
adopt it's output to your platform view components.