If you know anything about software/application development, you are most likely familiar with software architecture patterns and the concept that any application should be highly scalable and testable without introducing the need for refactoring when creating new features.
There is no perfect app architecture that checks all the boxes, but a lot of the most popular ones were defined long ago when the software industry had other interests and demographics in mind. Today, users want software that is quick and reactive, with new features able to be implemented constantly and without bugs or crashes.
A lot of app architectures provide high testability that reduces bugs and crashes, while others provide reactiveness but are hard to test. Some architectures provide both, but features are too time-consuming to implement due to steep difficulty curves. In this blog post, you’ll learn about a mobile app architecture that can be applied to almost any consumer-grade software or platform you can think of.
What is this mobile app architecture?
This mobile app architecture, which I decided to call ReArch, is based on the redux architecture for web development and the composable architecture that you can find tutorials for on the point free website at the following link:
The concept of this mobile app architecture is to be able to create a composable application that you can extend instead of modifying anytime you need to implement something new. It also lets you manage one single source of truth for the whole app, or per feature if you want. On the really positive side of things, this mobile app architecture provides unidirectional data flow, meaning you don’t have to worry about modifying the state of your app incorrectly in unnecessary places.
How does this mobile app architecture work?
According to the above diagram, this mobile app architecture is based on the thought that a View should only depend on its view model. From there, the view model dispatches any events that happen to the store, which, in turn, passes the event and the store’s state to a reducer that will change the state accordingly using any services, workers, or other dependencies.
This means any of the business logic affecting code should live inside your reducer, the other dependencies like workers, services, and repositories should be mostly used for fetching data or executing background jobs.
Single source of truth
For any given View, Screen, Feature, or even the whole app, you should only depend on one single state. In this mobile app architecture, the only entity in charge of the state is the store and the only other entity capable of changing it is the reducer (not the view model or even the services).
Unidirectional data flow
One of the advantages of this approach is that the state flows in a single direction towards the view that needs to display it. None of the dependencies, except for the reducer, can directly modify the state of the feature; the view model receives and formats it for the view to display.
This mobile app architecture is designed to be flexible and modular enough so each feature and sub-feature can be designed and developed independently from each other. This means that developers can work on multiple screens at a time and even on the same screen but with different components, each one with its own state and functionality.
Components of this mobile app architecture
The State is the data the feature needs in order to function properly. The definition of state is recommended to be a data struct, which contains only simple data types and, if possible, as many primitive types as possible over complex types.
In iOS, you should mostly implement them as a simple struct containing as simple data as possible.
On Android, you can implement the state using data classes.
The state should be composed of properties relevant to what the user is seeing or interacting with. For example, properties to indicate data or if a list is loading or if there’s some sort of error, etc.
This mobile app architecture is based on events, meaning that every time the state of the app needs to change, you must dispatch an event (ie. tell all the other parts of the code that something has happened). Events are defined by enum cases, making it a more readable codebase. New functionality is easier to implement since you must only add a new case and fill in the blanks where the enum is being used. With this, we can have a deterministic application and don’t need to define business logic in any of the event cases, only on the reducer.
Each event can be as obvious as events triggered by the UI, like a button tap, but normally, events are more complicated than that. Whenever you trigger an event, it can trigger more events, which we call side effects. For example, you can define two events for fetching data, one where you actually fetch the data and another one for when the data is done being fetched.
This can be implemented in iOS as follows:
On Android, we can use sealed classes to mimic enumerations with associated data:
A view is a representation of what the user of the app sees on screen. Ideally, any screen should be composed of multiple view objects, allowing reusability amongst different parts of the application.
The view only depends on a ViewModel and observes its changes of state. This means that whenever an event alters the state of the app, the ViewModel will update automatically to reflect the changes and format the data in a presentational way.
Anytime there’s an event, such as when you need to fetch data or do some action when tapping on a button or a list item, you only need to call the dispatch method on the ViewModel with the respective event and it will trigger the whole data flow until it gets back to the view.
On iOS you can implement a view like this:
On Android, using jetpack composable is the easier way, as of now:
The same logic applies, you depend on the ViewModel and dispatch events to it.
The ViewModel and ViewState
The ViewModel is responsible for giving the view the state it needs via a property binding. The ViewModel dispatches events to the store and receives the new state, which it formats and forwards back to the view.
The ViewModel has an associated store to which it forwards the events. It needs to be an observable object so the view can react to the state changes.
The stateChanged method will always be called when the store’s state changes. Here’s where you need to implement the display logic, like formatting titles, sorting new data, setting the loading state, etc.
Each ViewModel has an associated ViewState, which is different from the app/feature’s state. This ViewState contains all the properties necessary for the view to behave the way the user expects.
Here’s a ViewModel example on iOS:
On Android, the same logic applies. You need to define the state, events, and store. The ViewModel will have and only observe the store’s state and mutate the ViewState.
The store is a simple class with the sole purpose of dispatching events to the reducer and forwarding the state to the ViewModels through a property publisher. There should be only one implementation of a store since it is not prone to change. It also shouldn’t have any business logic or any other logic at all other than forwarding events to a reducer.
The only thing the store does is send events to the reducer and observe if the reducer sends some events back. One example of this is when you tell the reducer to fetch data. Then the reducer fetches the data and emits a new event called fetchDataCompleted, which modifies the state of the store with the new data and triggers re-composition of the views via observation from the ViewModel.
Here’s a store on iOS:
Here’s a store on Android:
A Reducer is a type that processes events passed to it and modifies a given state, which it receives by reference as an input parameter. If the change required needs to happen asynchronously, like when calling a service or a background worker, the reducer can return a publisher/coroutine or a new event, which the store will dispatch again to the reducer and then process the according to state.
Here you can see an implementation of a reducer in iOS:
And on Android:
What the reducer does is intercept the fetchList event and then call the dependencies to get new data and observe for when the services are done obtaining the data. When this happens, the reducer will emit a new event (ie. side effect) named fetchListCompleted.
This new event goes back to the store, the store sends it back again to the reducer, and finally, the reducer processes the new list of data and mutates the store’s state.
Packages and Modularity
The approach to dependency injection and architecture allows for development to be modularized by having certain components be isolated with packages and frameworks:
- The architecture definitions are themselves stored in a separate package.
- The dependency injection logic can be stored in a separate package.
- The Domain logic, which includes the possible states and events besides the reducers for each functionality, are stored in another package which we can call the domain package.
- Each feature could also be stored in a different package since they don’t depend on each other.
- The Models are stored in a models package.
- The services are stored in the PokeServices package.
The app’s views and their respective view models are defined inside the application target. This way, different teams can work on different packages and scale the application in a better way without affecting the other components.
As we discussed at the beginning of this article, there is no perfect app architecture that checks all the boxes, but this mobile app architecture comes pretty close, and it can be applied to almost any consumer-grade software or platform you can imagine.