KMM UI Architecture - Part 1
Overview
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
Problem Statement
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.
UI Concerns
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.
The view in MVC obviously also lives in the blue circle. Even if the presenter creates all the HTML/CSS/JavaScript, we still need a browser (which lives in the blue circle) to render and collect user input.
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
MVC (Model-View-Controller)
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.
MVP (Model-View-Presenter)
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 (Model-View-Intent)
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
intents
that are used to update the model (although in some definitions theintent
is a function not a “thing”) - the data flow is unidirectional
You can find several implementations of the pattern, e.g. MVIKotlin, MVFlow, Orbit or Reduce (list not exhaustive) and a great article about MVI here.
SAM (State-Action-Model)
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:
- The
view
is a function of themodel
meaning there’s some piece of code converting themodel
into astate representation
to be rendered / displayed by theview
.
The piece of code performing the conversion from amodel
to astate representation
is a function calledState
(the naming is unfortunate since state is data in all other patterns). In MVP theState
would be part of thepresenter
, in MVVM this would be thebinder
. - View
events
are translated into aproposal
to be presented to themodel
potentially resulting inmodel
updates.Actions
are the functions that perform the translations fromevents
toproposals
.Proposals
are theIntents
in MVI.
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
.then(acceptProposal)
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 Actions
while 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:
State
is emitted by the Bloc as a stream and consumed by the view. AStream
in Dart is the equivalent of an RxObservable
(can be observed to retrieve emitted items).Events
are sent by the view to the Bloc into a sink. ASink
in Dart is the equivalent of an RxObserver
(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
store
manages application state. It holds the current state (including the view state) and modifies it usingreducers
, 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
actions
which are input forreducers
- 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, mapDispatchToProps
). Using React
with Redux
and 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.