📜 ⬆️ ⬇️

Modern MVot-based Kotlin architecture



Over the past two years, Android developers at Badoo have come a long, thorny path from MVP to a completely different approach to application architecture. ANublo and I want to share a translation of the article by our colleague Zsolt Kocsi , describing the problems we have encountered and their solution.

This is the first of several articles devoted to the development of modern MVI architecture at Kotlin.

Let's start from the beginning: problems of states


At each moment in time, the application has a certain state that determines its behavior and what the user sees. If you focus only on a pair of classes, this state includes all the values ​​of variables - from simple flags to individual objects. Each of these variables lives its own life and is controlled by different parts of the code. You can determine the current state of the application only by checking them all one by one.
')
Working on the code, we create an existing model of the system in our head. We easily realize the ideal cases when everything goes according to plan, but completely unable to calculate all possible problems and conditions of the application. And sooner or later, one of the states that we have not foreseen will overtake us, and we will encounter a bug.

Initially, the code is written in accordance with our ideas about how the system should work. But later, going through five stages of debugging , you have to painfully redo everything, changing the existing model in your head at the same time. It remains to hope that sooner or later an understanding of what went wrong will come to us and the bug will be fixed.

But so luck is not always. The more complex the system, the more likely it is to encounter any unforeseen condition, which debugging will still be a nightmare for a long time.

In Badoo, all applications are essentially asynchronous - not only because of the extensive functionality available to the user through the UI, but also because of the possibility of one-way sending data by the server. A lot affects the state and behavior of the application - from changing the payment status to new matches and verification requests.

As a result, in our chat module, we came across several strange and hard-to-reproduce bugs, which spoiled a lot of blood. Sometimes testers managed to write them down, but they did not repeat on the developer’s device. Due to the asynchronous code, the repetition of the full extent of this or that chain of events was extremely unlikely. And since the application did not fall, we didn’t even have a stack trace that would show where to start the search.

Clean Architecture ( pure architecture ) also could not help us. Even after we rewrote the chat module, the A / B tests revealed small but significant inconsistencies in the number of messages from users who used the new and old modules. We decided that this was due to the hard reproducibility of bugs and the state of the race. The discrepancy persisted after checking all other factors. The interests of the company suffered, developers had a hard time maintaining the code.

It is impossible to release a new component, if it works worse than the existing one, but it is also impossible not to release it - since an update was required, it means that there was a reason. So, it is necessary to figure out why in a system that looks completely normal and does not crash, the number of messages drops.

Where to start the search?

Spoiler: this is not the fault of Clean Architecture - the human factor, as always, is to blame. In the end, of course, we fixed these bugs, but spent a lot of time and effort on this. Then we thought: Is there an easier way to avoid these problems?

The light at the end of the tunnel ...


Fashionable terms like Model-View-Intent and “unidirectional data flow” are well known to us. If in your case this is not the case, I advise them to google it - there are many articles on the Internet on these topics. Android developers especially recommend the Hannes Dorfman material in eight parts .

We started playing with these ideas from web development back in early 2017. Approaches like Flux and Redux have proven very useful - they helped us cope with many problems.

First of all, it is very useful to contain all state elements (variables that affect the UI and trigger various actions) in one object - State . When everything is stored in one place, the overall picture is better visible. For example, if you want to submit data loading using this approach, then you will need payload and isLoading fields. Looking at them, you will see when the data is received ( payload ) and whether an animation ( isLoading ) is shown to the user.

Further, if we move away from the parallel execution of the code with callbacks and express the state changes of the application in the form of a series of transactions, we will get a single entry point. We present you the Reducer , which arrived to us from functional programming. It takes the current state and data about further actions ( Intent ) and creates from them a new state:

Reducer = (State, Intent) -> State

Continuing with the previous example of loading data, we get the following actions:



Then you can create a Reducer with the following rules:

  1. In the case of StartedLoading, create a new State object by copying the old one, and set the isLoading value to true.
  2. In the case of FinishedWithSuccess, create a new State object by copying the old one, in which the isLoading value will be set to false and the payload value will be
    match loaded.

If we put the resulting State series into a log, we see the following:

  1. State ( payload = null, isLoading = false) - initial state.
  2. State ( payload = null, isLoading = true) - after StartedLoading.
  3. State ( payload = data, isLoading = false) - after FinishedWithSuccess.

By connecting these states to the UI, you will see all the stages of the process: first, a blank screen, then the boot screen, and finally the required data.

This approach has many advantages.



Or not?

... can be a train rushing at you


One Reducer is not enough. How to deal with asynchronous tasks with different results? How to respond to the push from the server? How to deal with the launch of additional tasks (for example, clearing the cache or loading data from the local database) after changing the state? It turns out that either we do not include all this logic in the Reducer (that is, a good half of the business logic will not be covered, and those who decide to use our component will have to take care of it), or force the Reducer to do everything at once.

Requirements for the MVI framework


Of course, we would like to conclude all the business logic of a separate feature in a separate component, with which developers from other teams could easily work by simply creating its copy and subscribing to its state.

Besides:


We did not immediately switch from Reducer to the solution we use today. Each team faced problems using different approaches, and developing a universal solution that would suit everyone seemed unlikely.

And yet, the current state of affairs suits everyone. We are glad to present you MVICore! The source code of the library is open and available on GitHub .

What is MVICore good about




A quick introduction to the feature


Since step-by-step instructions have already been posted on GitHub, I’ll omit the detailed examples and focus on the main components of the framework.

Feature is the central element of the framework containing the entire business logic of the component. Feature is defined by three parameters: interface Feature <Wish, State, News>

Wish corresponds to Intent from Model-View-Intent - these are the changes that we want to see in the model (since the term Intent has its meaning in the Android developer environment, we had to find another name). Wish is the entry point for Feature.

State is, as you already understood, the state of the component. State is immutable: we cannot change its internal values, but we can create new States. This is the output: every time we create a new state, we transfer it to the Rx stream.

News - a component for processing signals that should not be in the State; News is used once at creation ( problem SingleLiveEvent ). Using News is optional (you can use Nothing from Kotlin in the Feature signature).

Also in the Feature must be present Reducer .

Feature may contain the following components:



The layout might look simple:


or include all the additional components listed above:


The very same Feature, containing all the business logic and ready to use, looks easier nowhere:



What else?


Feature, the foundation stone of the framework, works at a conceptual level. But the library has much more to offer.


We hope you try our library and its use will give you as much joy as we do - its creation!

November 24 and 25, you can try your hand and join us! We will hold a mobile hiring event: in one day, it will be possible to go through all the stages of selection and get an offer. My colleagues from iOS- and Android-teams will come to communicate with candidates to Moscow. If you are from another city, Badoo charges you for travel expenses. To get an invitation, pass the qualifying test on the link . Good luck!

Source: https://habr.com/ru/post/429728/


All Articles