The Fragment or UIViewController becomes a massive class that contains business logic as well as view details. Both are so intertwined that it’s impossible to test them independently. Code-readability suffers and future code changes are hard to implement.
MVVM tries to solve that by splitting up business logic and view details. In this article with briefly explain MVVM, but more importantly give you a glimpse of our MVVM style. We use a very functional MVVM approach with RxJava/RxSwift and platform-independent ViewModels.
Model-View-ViewModel (MVVM) gets more and more attention among app developers these days. I’ll first give you a short explanation of MVVM in general here. If you are already familiar with it, just skip ahead to see our personal MVVM approach.
The main goal behind MVVM is to move as much of the state and logic from the View into a separate entity called the ViewModel. The ViewModel also contains the business logic and serves as the mediator between the View and the Model.
The ViewModel has basically two responsibilities:
- it reacts to user inputs (e.g. by changing the model, initiating network requests or routing to different screens)
- it offers output data that the View can subscribe to
The View, on the other hand, does not contain any business logic. These are the responsibilities of the view:
- it reacts to new output states of the ViewModel and renders them accordingly (e.g. by showing a String in a text field)
- it tells the ViewModel about new user inputs (e.g. button-clicks, text-changes, screen touches)
In contrast to popular MVC approaches the Fragment/Activity/UIViewController does not contain business logic in MVVM. It is a humble view that simply renders the ViewModel’s output states. The ViewModel does NOT KNOW the View (a difference to forms of MVP and MVC). It simply offers output states that the View observes:
Our functional MVVM approach
Our MVVM approach has two specific characteristics.
- Functional input-to-output mapping
We use a functional approach. An output of a ViewModel is just the result of a function applied to an input, an idea we borrowed from Kickstarter. There is almost no imperative code in our ViewModels. If we have side effects (like routing between screens), these are isolated to a dedicated function in our ViewModel.
- Platform-independent ViewModels
Our ViewModels don’t have any reference to the Android/iOS SDK. They are pure Kotlin/Java/Swift classes. All of the platform-specific logic happens in the View or encapsulated in an external class. The ViewModels are therefore easy to test and the logic can be shared between different platforms (you could easily reuse a Kotlin ViewModel in a Java Desktop application or Kotlin Website).
Our ViewModels have Input properties and Output properties. The View supplies data to the input properties and observes the output properties. The “only” thing the ViewModel does is mapping the Inputs to Outputs in a meaningful way.
The beauty of this approach lies in the functional implementation. Inputs are just mapped to outputs with a specific function. If possible, there are no side effects at all. For a certain set of inputs we will always get the exact same set of outputs. That makes it so predictable and helps to prevent bugs related to state changes.
f(input) = output
f(english) = german
Example: Power Translator 3000
Let’s take a look at an example, the Power Translator 3000. This stupid little app translates English text to German text (but in real time! I mean, as you type! How great is that). We’ll also add a button to copy the German translation to the clipboard.
Below you can see the ViewModel (left) and its inputs and outputs for the View (right). Since we use reactive streams for mapping input data to output data the whole thing really behaves like a set of connected pipes.
I’ll show you an implementation example on Android using Kotlin as the programming language. The ViewModel on iOS with Swift would look almost identical though.
Let’s first take a look at the ViewModel’s interface. It consists of observable Input and Output properties. We DON’T have any traditional functions in our ViewModels. We use Subjects from RxJava/RxSwift to get input and provide outputs via Observables’s from RxJava/RxSwift. Don’t worry about the Rx details if you are not familiar with it. Implementation details are secondary for now.
The outputs are implemented as a mapping from inputs using Rx-operators in the ViewModel-Implementation. That’s the business logic. Here you can see how that looks like in our example.
Now that we have a ViewModel let’s allow the user to interact with it by providing a View. I am going to show you how to bind a Fragment (which is a View in MVVM) to a ViewModel. You can do the same with Android View classes, Android XML (via Data Binding) or iOS ViewControllers.
We first delegate the View’s user input (English text changes & save button clicks) to the ViewModel’s Input properties. To make this process more convenient we wrote extension functions like receiveTextChangesFrom and receiveClicksFrom. Internally we use RxBinding for that.
On the other hand, we subscribe to the outputs. We change the displayed view accordingly whenever Outputs change (I left out the Lifecycle-management code for the sake of simplicity).
That’s it, that’s everything we need to do in the View to get our example running. This is one of the big advantages of MVVM: Your Views (may it be Fragments, Views or ViewControllers) are super compact classes only serving the purpose of supplying input data and rendering output data.
Hopefully, I managed to give you a grasp of how our MVVM approach at QuickBird Studios looks like. We are a big fan of the MVVM pattern but of course, there is no perfect architecture. This approach comes with a lot of advantages but also some disadvantages:
- scalability: we built both small as well as big, complex apps using MVVM. The separation of concerns helps a lot to be ready for future requirement changes.
- testability: the ViewModel does not have any View dependency or any other Android/iOS SDK dependency. Nothing needs to be mocked. We can easily unit test ViewModels by providing sample inputs and asserting on the outputs. We’re going to talk about this in our next article.
- predictability: side effects are isolated using this functional approach. Therefore the probability of bugs because of unpredictable internal state changes decreases.
- steep learning curve: it takes some time to get used to RxJava and this functional style of thinking in the beginning
- set-up time: binding the View to the ViewModel is a lot more fun using custom extension functions or XML bindings which take some time to write (we open-sourced some of our bindings here)
We optimized this approach over the last year by applying it to several apps. It’s working really well for us. In the next articles, we’ll look at the details of how to make this approach work super smoothly on Android as well as on iOS.