KMM UI Architecture - Part 1
This is part 1 of a series of two articles:
- Part 1: a summary of common ui patterns and what we learn from them
- Part 2: creating a simple, adaptable, predictable and composable UI framework for Kotlin Multiplatform
The promise of KMM (Kotlin Multiplatform Mobile) is literally:
Write the business logic for your iOS and Android apps just once, in pure Kotlin (https://kotlinlang.org/lp/mobile)
Unlike other cross-platform frameworks like Flutter or React Native, KMM does not promise a single code base for the user interface. This raises questions like:
- What exactly is the user interface?
- How can the user interface be decomposed and which of the decomposed pieces are platform agnostic?
By answering these questions we will be able to:
- better separate the different user interface components in general and the platform independent and the platform specific code specifically
- increase the amount of code shared across target platforms
To look at the problem and potential solutions from different angles, the first part of this series will explain some common architectural patterns like MVC, MVP, MVVM, MVI, SAM, BLoC, React/Redux and try to extract their essential ideas.
In its most basic form, an ui architecture has two main concerns (I call it concern following the separation of concern principle, which is “a design principle for separating a computer program into distinct sections”, also heeding this advice to ignore the academic discussion around SRP and SoC):
- Display: show some content to the user = render state (in most models that’s the “view”)
- State Management: retrieve, store, transform state / data -> business logic
Two triggers exist:
- The user interacts with the displayed content which can lead to state changes which can lead to changes in the displayed content
- State updates can be triggered by elements outside the ui framework, e.g. asynchronous network calls or device events (location updates, network events, incoming messages etc.) which can lead to changes in the displayed content
Theoretically the code that displays can read and update the state directly or the code that manages state can listen to ui events to update itself and also update/manipulate the display directly. We all know that such a tightly coupled design is a terrible idea (on Android this was common practice before Google introduced ViewModels). In order to achieve some decoupling we need “something” in between:
Every ui architecture defines this “something” in between that:
- observes ui changes to update state
- takes state and prepares it to be rendered
The observant reader will have noticed that I just defined (at least) four more concerns:
- Collect: listen to user interface events (keyboard, touch, mouse), filter events (e.g. debounce), aggregate events (e.g. aggregate touch events to a swipe to dismiss event)
- Transform: translate the result from the collect step into state changes (in some frameworks that result is called intent)
- Select: observe state and select the changes relevant for the user interface
- Bind: prepare state to be displayed e.g. by filtering relevant data and/or mapping it to a user friendly format like converting UTC timestamps to local timezone etc.
Intermezzo: Clean Architecture
Now that we have a generic (alas abstract) ui architecture, let’s put it into the larger context of a Clean Architecture (skip this chapter if you’re not interested in Clean Architecture):
While it was created with web and not mobile applications in mind, it’s still a useful model not just because of the proposed decomposition but because of the dependency rule:
This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.
Applying the dependency inversion principle across all layers of the architecture:
- no component has knowledge of a component living in an outer circle
- components in an outer circle work with abstractions (e.g. interfaces) of components in inner circles
When proposing a KMM ui architecture, we will follow this principle.
In his original article, “Uncle Bob” puts all the concerns we defined above into the green ring:
It is this layer, for example, that will wholly contain the MVC architecture of a GUI. The Presenters, Views, and Controllers all belong in here.
I hate to break it to you but I disagree with that statement.
The model in MVC is not just data, it’s:
the central component of the pattern. […] dynamic data structure […] directly manages the data, logic and rules of the application.
According to this it’s also part business logic and as such also lives in the red “Use Cases” circle.
This article explains the interactions of view, presenter, controller and model/use cases in great detail and maps it all out for us:
This is exactly what we defined above:
So in the context of a Clean Architecture our ui architecture spans across parts of the outer three layers.
Existing Patterns / Frameworks
When researching MVC, you’ll find that it’s anything but clearly defined. There are two main patterns:
- the triangle pattern (which is the original pattern from 1979): views interact directly with the model and the controller which also interacts directly with the model
- the chain pattern: the controller interacts with the view and the model but there’s no direct interaction between the view and the model
The chain pattern is what matches our own ui architecture with the controller being the “something” in-between:
The interactions between the three components and the dependencies (which component knows what about what other component) aren’t clearly defined either. There are pull patterns, push patterns or reactive patterns and different levels of decoupling (view knows controller or view doesn’t know anything about the controller, controller knows model, controller works with injected model etc.). Due to this lack of consensus there’s really not much more we can learn from the MVC pattern.
In MVP the presenter is the glue between the view and the model:
The presenter updates the model and observes or receives updates from the model. In most implementations the presenter has a reference to the view and interacts with the view directly (like here or here) and vice versa (the view interacts with the presenter directly to forward ui events). Both view and presenter should be coded against interfaces but they still reference each other. MVP compares favorably to MVC in terms of decoupling but the presenter has too much responsibility and too much knowledge about the other components.
MVVM (Model-View-View Model)
There are two main differences between MVVM and MVP:
- The view model in MVVM has no reference to the view while in most MVP implementations the presenter has a reference to the view. View models are usually injected into the view.
- MVVM introduces data binding between the view and the view specific part of the model to allow the separation of the gui development (the view) from the development of the business logic.
The novelty of the MVVM model is the declarative data (model) and command (event) binding that eliminates virtually all gui code (and replaces it by some markup language like XML or XAML). In the absence of such binding technology one would typically use an MVP pattern.
MVI is mostly an Android/KMM pattern. Its novelty is the unidirectional and cyclical data flow inspired by the Cycle.js framework:
MVI doesn’t mention the “Something” component that usually sits between the model and the view. It’s agnostic to the pattern used to glue these two components together. All it defines is:
- state is immutable data derived from the model and rendered by the view
- user actions are translated into
intentsthat are used to update the model (although in some definitions the
intentis a function not a “thing”)
- the data flow is unidirectional
SAM is a pattern for web applications but its concepts are worth discussing in the context of mobile apps. The two main ideas/concepts are:
viewis a function of the
modelmeaning there’s some piece of code converting the
state representationto be rendered / displayed by the
The piece of code performing the conversion from a
state representationis a function called
State(the naming is unfortunate since state is data in all other patterns). In MVP the
Statewould be part of the
presenter, in MVVM this would be the
eventsare translated into a
proposalto be presented to the
modelpotentially resulting in
Actionsare the functions that perform the translations from
You see in the diagram that
State functions can also trigger
Actions before they create the
state presentation so the flow in pseudo code is:
// accept/reject proposal and alter state
model.accept(proposal) // optional: trigger another action (State function)
.then(nextAction) // create the state representation (State function)
.then(createStateRepresentation) // render the state representation (View)
.then(renderView) // record and send events (View)
.then(sendEvent) // create a proposal (Action function)
.then(createProposal) // and here we come full circle
Like MVI, SAM defines an unidirectional flow between model and view and
Actions are the
Intents in MVI. So how is this different from MVI?
MVI only defines immutability, unidirectional flow and Intents as the vehicle to express the user’s intention. The rest is open to interpretation and different implementations have different solutions to fill the gaps.
SAM on the other side is more specific / precise in defining the different parts of the pattern, especially when it comes to defining what’s a function, an object or a data structure. E.g.
State is a function as are
State Representation is data. This matters because ultimately we need to make concrete decisions on what the “something” between a view and the model really is. Most patterns we have discussed so far are rather vague. E.g. the Wikipedia article about MVVM describes the view model as:
The view model is an abstraction of the view exposing public properties and commands. […]. MVVM has a binder, which automates communication between the view and its bound properties in the view model.
It’s unclear what the view model really is. It exposes public properties (is it an object or a function?) but also commands (does it implement the command pattern?) and what is the binder, a class/object or a function?
With SAM it’s clear what’s what.
BLoC (Business Logic Components)
Blocs are a Flutter concept and very specific in defining the the interaction between the view and the bloc (the “something”) but doesn’t define how blocs and the model interact:
Stateis emitted by the Bloc as a stream and consumed by the view. A
Streamin Dart is the equivalent of an Rx
Observable(can be observed to retrieve emitted items).
Eventsare sent by the view to the Bloc into a sink. A
Sinkin Dart is the equivalent of an Rx
Observer(can receive items).
What’s new with Bloc is that it propagates reactive patterns between the view and the bloc and introduces naming conventions (event -> sink, stream -> state) that make sense because they are self explaining.
On top of that Bloc touches a topic we haven’t discussed yet, the decomposition of the ui into components.
React / Redux
React itself is a framework to create declarative, composable GUI/web components. These components render the user interface, have a lifecycle and can have local state. There’s no mention of controllers, models, view models and such. As a result many different frameworks try to fill the gaps (state management and view binding). We’ll only discuss the most popular state management framework and its official binding library: Redux and React Redux.
Redux is fairly simple:
- The centralized
storemanages application state. It holds the current state (including the view state) and modifies it using
reducers, which is a function with the signature:
(state: State, action: Action) -> State)
- The view subscribes to state changes and updates itself accordingly
- The view dispatches
actionswhich are input for
- State is immutable and flows are unidirectional
Other state management libraries/patterns like Flux or MobX are quite similar but use multiple stores instead of a single one. They all share the uni-directional flow of immutable state served by the store(s). While Redux covers state management and React the view part, we still need the “something” that connects these two.
React Redux is the official React UI binding layer connecting store state to component properties (Store -> View,
mapStateToProps) and exposes functions to trigger state changes (View -> Store,
React Redux pretty much follows the MVVM pattern with
React Redux being the binder that is particular to MVVM.
I want to mention one more mechanism that is import to the interaction between views and store(s)s interact and that’s the use of
selectors. If you’d subscribe to the store directly you’d get notified whenever anything changes even if it’s not relevant for a ui component. With the use of libraries like Reselect the view can select sub-state and retrieves updates only if that particular sub-state changes (the selectors are memoized functions that remember the result of previous calls).
React / Redux bring some new ideas to the table (compared to the other patterns we discussed earlier):
- A centralized, global state store
- The concept of reducers to update the global state
- Declarative ui components
- Reactive patterns all over the place
- Memoized selectors
Summary and outlook
There’s an evolution in the last 40+ years that has brought us from tightly coupled components in MVC (in it’s original definition from 1979) to decoupling of view and model via presenter in MVP to (tooling supported) data binding in MVVM.
MVI adds immutable data and unidirectional flow to the equation while SAM formalizes the flow and defines state mutations and view binding as functions and as first class citizens of the design pattern.
Bloc adds reactive programming and some useful nomenclature (bloc, stream, sink) while React/Redux introduce declarative ui components, a global state store (or multiple with Flux/MobX) and memoized selectors (among other things).
We will put all this together in part 2 of this series.