MVI with state-machine. Modules | by Nikolai Kotchetkov | Medium

Part III. Multi-module and multi-platform logic

“Learning strait” to adopt the state-machine MVI architecture by Ulyana Kotchetkova

This is the third part of the ‘MVI with state-machine’ series that describes how to split your logic modules and promotes writing a multi-platform logic. Check the other parts of the series for basic steps and handy tools to mix-in overview:

The source code for the article and the very basic core library is available on GitHub. The library is totally optional — just in case you want to save some time on not writing your own core.

The code snippets in this article are based on the login module and the register multiplatform module of the advanced example — ‘Welcome app’. The modules are then provided to the main application.

Note on multiplatform

Although the logic for registration flow is made common, I’ve failed to implement registration view in androidMain source of registration module due to some Kotlin-multiplatform glitches or misconfiguration. The problem is Android sources fail to import common dependencies from other common modules. If anyone could help me fixing that issue I’ll much appreciate this 🙂

Multi-module applications

Let’s take a more complicated example with a multi-screen flow like the imaginary customer on-boarding:

Welcome application flow
Welcome application screen flow

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.

Here is the state diagram for the app:

Welcome app state diagram
Welcome application state diagram

The project consists of the following modules:

  • 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.
  • login — login flow.
  • commonregister — multi-platform registration logic.
  • register — android view module for registration (separate because I’ve failed to implement it in android source of commonregister. See the note above)

Common API

As you could see in the diagram above login and commonregister logic starts after the email is checked and the answer to user’s registration status is obtained. Each feature-module flow starts from password entry screen though different for each one. Each module flow returns to the main flow either:

  • when flow completes successfully – transfers to Complete
  • when user hits Back – transfers back to email entry

Let’s define the main flow interaction API then:

Host module interface

We place the definition to the module available to all modules: commonapi and provide the interface through the common state context like this:

Feature module context

We also need some way to start the feature flow. As soon as we know that each feature starts given a user’s email, let’s have a common interface that will create a startup-state for each feature-module state-machine:

Feature-flow starter

Module flow

Each module has it’s own sealed system of gesture/view-states:

Module gestures and UI-states

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 state 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:

  1. 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…
  2. 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:

Adopting foreign gestures

UI-state adapter:

Adopting foreign UI state

View implementation

Now let’s build feature and host composables to take advantage of our adapters.

Feature master-view:

Feature master-view

Application master-view:

Application master-view

To sum up:

  1. We delegate rendering of ui-states to feature composables by unwrapping proprietary states from common view-state system.
  2. We wrap any feature gesture to master-gesture system and pass it to our model to process.

Adopting foreign machine-state system

The last thing we need to do to be able to run a feature module in our host system is to run the feature machine-states in our application state machine. Remember we have bound both the gesture system and the ui-state system to both our state-machine and machine-state:

Base state-machine interfaces

Given that our states has a simple and clear state interface and lifecycle we could encapsulate the feature state-machine logic in our host state with the 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 systems. It also bridges two incompatible gesture/view systems by wrapping/unwrapping and adopting one system to another by calling two adaptor methods:

  • mapGesture — maps parent system gesture to child system if it has a relevant mapping (we could return null to skip gesture processing).
  • mapUiState — maps child UI-state to parent UI-state system. For example by wrapping a child state to some wrapper like we have done in previous section.

Let’s bring everything together and build a login flow proxy to make things clear:

Login flow proxy

Let’s break down the implemented methods:

  • init() – creates a starting state for a proxy state-machine. We fetch a FlowStarter interface from feature’s DI component to create a starting state and initialize the proxy state-machine. The child requires the WelcomeFeatureHost to provide backToEmailEntry and complete methods for host interaction, so we implement it here and provide the state instance to component factory. The proxy implements WelcomeFeatureHost by switching host machine to email or complete states.
  • mapGesture – maps a gesture from the main gesture system to the child system. You may unwrap the gesture we have implemented in the previous section, adopting one system to another as with the Back gesture or discard irrelevant gesture by returning null from your implementation.
  • mapUiState – performs a transition from a child ui-state system to the main one. We just wrap one from another as we designed before.

Switching flows

Now that we have proxy states implemented for both login and registration flows, we use our state factory to create them and switch the host state-machine to the appropriate flow depending on the email check outcome:

Email check and flow switch

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 logic. Of course, it is not a silver bullet but the flexibility in structuring your app in this pattern plays well in most scenarios. You could combine all your application steps in a single machine-state flow or build separate view-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 the multi-platform approach in mind as it contains no concrete platform dependencies and coroutines extensions are optional. So you may create your view logic once and adopt its output to your platform view components.

The full library and sample code are available at GitHub. Have fun!

Leave a Comment