Architecture and design of Android applications (my experience)

Habr, hello!

Today I want to talk about the architecture that I follow in my Android applications. I take Clean Architecture as the basis, and use the Android Architecture Components (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines as tools. Attached is a fictitious example code that is available on GitHub .

Disclaimer


I want to share my development experience, I in no way pretend that my solution is the only true and devoid of flaws. The architecture of the application is a kind of model that we choose to solve a particular problem, and for the selected model, its adequacy in applying to a specific task is important.

Problem: why do we need architecture?


Most of the projects that I have participated in have the same problem: the application logic is placed inside the android environment, which leads to a large amount of code inside Fragment and Activity. Thus, the code is surrounded by dependencies that are not needed at all, unit testing becomes almost impossible, as well as reuse. Fragments become God objects over time, even small changes lead to errors, supporting a project becomes expensive and emotionally expensive.

There are projects that have no architecture at all (everything is clear here, there are no questions for them), there are projects with a claim to architecture, but exactly the same problems appear there anyway. Now it’s fashionable to use Clean Architecture in Android. I often saw that Clean Architecture is limited to creating repositories and scripts that invoke these repositories and do nothing else. Even worse: such scripts return models from called repositories. And in such an architecture there is no sense at all. And because Since scripts simply call the necessary repositories, often the logic rests on the ViewModel or, even worse, settles in fragments and activities. All this then turns into a mess, not amenable to automatic testing.

The purpose of architecture and design


The purpose of architecture is to separate our business logic from the details. By details I mean, for example, external APIs (when we develop a client for a REST service), Android - an environment (UI, services), etc. At the core, I use Clean architecture, but with my implementation assumptions.

The purpose of the design is to tie together the UI, API, Business logic, models so that all this lends itself to automatic testing, is loosely coupled, and easily extends. In design, I use Android Architecture Components.

For me, architecture should satisfy the following criteria:

  1. UI is as simple as possible and it has only three functions:
  2. Present data to the user. The data comes ready to display. This is the main function of the UI. Here are widgets, animations, fragments, etc.
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


The schematic diagram of the architecture is shown in the figure below:

image

We are moving from bottom to top in layers, and the layer that is below does not know anything about the layer above. And the top layer refers only to a layer that is one level lower. Those. API layer cannot refer to a domain.

A domain layer contains a business entity with its own logic. Usually there are entities that exist without an application. For example, for a bank, there may be loan entities with complex logic for calculating interest, etc.

The application logic layer contains scripts for the application itself. It is here that all the connections of the application are determined, its essence is built.

The api layer, android is just a specific implementation of our application in the Android environment. Ideally, this layer can be changed to anything.

, , — . . 2- . , . . TDD , . Android, API ..

Android-.

image

So, the logic layer is the key, it is the application. Only a logic layer can refer to and interact with a domain. Also, the logic layer contains interfaces that allow the logic to interact with the details of the application (api, android, etc.). This is the so-called principle of dependency inversion, which allows logic not to depend on details, but vice versa. The logic layer contains the usage scenarios of the application (Use Cases), which operate on different data, interact with the domain, repositories, etc. In development, I like to think in scripts. For each user action or event from the system, a certain script is launched that has input and output parameters, as well as just one method - to run the script.

Someone introduces an additional concept of an interactor, which can combine several usage scenarios and wind up additional logic. But I do not do this, I believe that each script can expand or include any other script, this does not require an interactor. If you look at the UML schemas, then there you can see the connection of inclusion and extension.

The general scheme of the application is as follows:

image

  1. An android environment is created (activity, fragments, etc.).
  2. A ViewModel is created (one or more).
  3. ViewModel creates the necessary scripts that can be run from this ViewModel. Scenarios are best injected with DI.
  4. The user commits an action.
  5. Each component of the UI is associated with a command that it can run.
  6. A script is run with the necessary parameters, for example, Login.execute (login, password).
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

Those. the key role we have is logic and its data model. We saw a double conversion: the first is the transformation of the repository into the scenario data model and the second is the conversion when the script gives the data to the environment as a result of its work. Usually the result of the script is given to the viewModel for display in the UI. The script should return such data with which the viewModel and UI do nothing else.

Commands The

UI starts script execution using a command. In my projects I use my own implementation of commands, they are not part of architectural components or anything else. In general, their implementation is simple, as a deeper understanding of the idea, you can see the implementation of commands in reactiveui.netfor c #. Unfortunately, I can not lay out my working code, only a simplified implementation for an example.

The main task of the command is to run some script, passing the input parameters into it, and after execution, return the result of the command (data or error message). Normally, all commands are executed asynchronously. Moreover, the team encapsulates the background-calculation method. I use coroutines, but they are easy to replace with RX, and you only have to do this in the abstract bunch of command + use case. As a bonus, a team can report its status: whether it is being executed now or not and whether it can be executed in principle. Teams easily solve some problems, for example, the double-call problem (when the user clicked the button several times while the operation is running) or the visibility and cancellation problems.

Example


Implement feature: login to the application using login and password.
The window should contain the login and password input fields and the “Login” button. The logic of work is as follows:

  1. The “Login” button should be inactive if the username and password contain less than 4 characters.
  2. The “Login” button must be inactive during the login process.
  3. During the login procedure, an indicator (loader) should be displayed.
  4. If the login is successful, a welcome message should be displayed.
  5. If the login and / or password is incorrect, then an error message should appear above the login field.
  6. If an error message is displayed on the screen, then any input of a character in the login or password fields will remove this message until the next attempt.

You can solve this problem in various ways, for example, by putting everything in MainActivity.
But I always follow the implementation of my two main rules:

  1. The business logic is independent of the details.
  2. UI is as simple as possible. He only deals with his task (he presents the data that was transferred to him, and also broadcasts commands from the user).

This is what the application looks like:

image

MainActivity looks like this:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


The activity is simple enough, the UI rule is executed. I wrote some simple extensions, such as bindVisibleWithCommandIsExecuting, to associate commands with UI elements and not duplicate code.

The code of this example with comments is available on GitHub , if interested, you can download and familiarize yourself.

That's all, thanks for watching!

All Articles