What is architecture and what is best for Android projects?

What first comes to your mind when someone mentions the word architecture? You probably connect it immediately with construction as most people do. And it really is. Architecture comes from the Latin word architecture, which in turn is derived from the ancient Greek architecton, which means the builder or, in the modern world, the science and art of designing objects. An architect is a person who designs the appearance of buildings and puts ideas into reality taking into account various factors. The situation is similar in the software world.

What then would be the architecture in the software? Well, they would best describe it as a way of organizing code into meaningful entities. The easiest way for you to understand this is through the example of your homes. The rooms are separated into separate units which have their own task. The kitchen is for storing meals, the bathroom is for maintaining hygiene, the bedroom is for resting, etc. And they are made up of several different components (shower, kitchen cabinets, bed, etc.) and form a unit.

Let me answer this second question that I get asked often, which is: “What is the best architecture for the Android platform?”. I’ll tell you immediately the answer to this: “There is no best one but it depends on the structure of the project.” In this article, I will explain why I chose MVVM architecture and how I organize the Androd project, but with the added Clean approach that makes my code even more decoupled, easily maintainable and testable.

MVVM architecture explanation

MVVM (Model – View – ViewModel) is an architectural pattern in programming. The main task of the pattern is to separate presentation logic from business logic. The most important component in MVVM is the ViewModel, which behaves more like a model and less like a view. It is responsible for converting and delegating data objects to presentation logic that displays those objects to the user on the device screen. It is for the reasons above that we can say that this pattern is event-driven because of every View reaction. ViewModel does certain actions.

MVVM in Android projects with Clean approach

The fact is that architecture is important in the development of any software project because unless you choose the right architecture at the beginning, you will later have major problems maintaining how existing requirements change or new ones come and how those people on the project expand. It is also important not to spend too much time developing the architecture because if your application has poor UI / UX, no architecture will save it. You need to find a balance between these two concepts in order for a project to be sustainable and successful.

As Google introduced the Android Architecture Components library whose navigation component I talked about in a previous post, MVVM has emerged as the best architectural choice since ViewModel is part of that library and is form-compatible by default. Other reasons are because ViewModel supports Android Data Binding which I use in projects. has a much larger lifecycle scope and can survive configuration changes.

To get Clean approach in the MVVM architecture on Android, we need to add another layer between the ViewModel and the Model we specifically call the Domain layer (UseCases). The domain is in charge of implementing the business logic, rules, and requirements prescribed by the functional specification in the development of the project. Like everything in life, Clean MVVM has its advantages and disadvantages. I have listed some of them below:

  • Higher testability of program code than regular MVVM
  • The biggest advantage is that the code is even more separated into separate entities
  • Navigating through project packages is easier and more intuitive
  • Light sustainability of the whole project
  • Adding new features is much easier
  • It takes time to learn how layers work together
  • Creating additional classes
  • Not suitable for some less complex projects. But even in such projects, it is not a stretch to use Clean Code.

Example of Clean MVVM architecture from own project

Now we come to the part where I will show you how to implement this approach on a real project. Being an Android Engineer at AutoZubak, where I am participating in the development of a native Android version of a new used car sales platform that will be new to the market, I will take an example from that project. An example will be of a more descriptive character, not including complete implementation or any business information. You can read more about the project here.

Apps without information are useless. In order to get information, most of the time, certain network requests are made via APIs or local caching, so data is displayed to users. Here is an example of retrieving data from a vehicle model server on which I will explain the Clean MVVM architecture. The organization of this part of the application and the package layout in Android Studio is shown in the image below:

As you can see in the picture below, I organize each part of the project into three layers:

  1. Data layer (Repositories, Models, Sources)
  2. Domain layer (UseCases)
  3. Presentation layer (Activities, Fragments, ViewModels, Adapters)

so let’s go in order!

Data layer

Data layer is the layer that is in charge of data modeling and retrieval. He knows exactly how data is modeled, how to retrieve it, and where he reveals a list of methods to the Domain layer that can call those methods. Usually I make this layer from Model, Repository and Source. In this particular case, that layer knows how Vehicle Models are displayed (Model), how that data is obtained (Source), and the methods that the Domain layer can call (Repository). Examples of these components are given below.

Model

data class VehicleModel (

    @SerializedName("code")
    val code: Int?,

    @SerializedName("id")
    val id: Int?,

    @SerializedName("value")
    val value: String?
)

Repository Interface

interface VehicleModelRepository {

    suspend fun getVehicleModels(): Response<List<VehicleModel>>
}

Repository Implementation

class VehicleModelRepositoryImpl(context: Context,
                                retrofit: Retrofit):
    BaseRepository(context), VehicleModelRepository {

    private var vehicleModelSource: VehicleModelSource = retrofit.create(VehicleModelSource::class.java)

    override suspend fun getVehicleModels(): Response<List<VehicleModel>> {
        return vehicleModelSource
            .getVehicleModels()
    }
}

IMPORTANT! The repository class or the specific implementation do not know how to retrieve data. Source knows and that’s why they call his methods!

Source

interface VehicleModelsSource {

    @GET("... endpoint path ...")
    suspend fun getVehicleModels()
            : Response<List<VehicleModel>>
}

Domain layer

Domain layer is the layer that contains all the business logic for our project. I usually make this layer from UseCases, each of which has a specific task. These classes receive in the constructor a Dependency Injection repository class that contains a list of methods that UseCases can call. Because this is an API call, all UseCases extend the parameterized ApiUseCase base class, which contains all things in common such as Internet connection checking, API response handling, and error handling. We should write these things in every UseCase, and we don’t want to duplicate the code so it is separated into a separate class. An example of a UseCase is given below:

UseCase

class GetVehicleModelsUseCase (context: Context,
                              errorDelegateManager: ErrorDelegateManager<List<VehicleModel>>,
                              private val vehicleModelRepository: VehicleModelRepository
):
    ApiUseCase<List<VehicleModel> (context, errorDelegateManager){

    override suspend fun executeAPICall(): Response<List<VehicleModel>> {
        return vehicleModelRepository.getVehicleModels()
    }
}

Presentation layer

Presentation layer is the layer in charge of displaying things on the screen, handling user clicks and taking specific actions. I create this layer from the Adapters if it is to show the list to the user, the UI of the part (Activities and Fragments) and the ViewModels. The adapter class, if any, gets the AdapterDelegateManager via Dependency Injection in a constructor that knows how to display a particular item list using AdapterDelegate. The AdapterDelegate contains a reference to a ViewHolder that is tasked with displaying a particular ViewType using Android Data Binding. The Activities and Fragments in the UI part have an injected ViewModel that is responsible for performing actions and retrieving data. Activities and Fragments observe the data retrieval status and extend the result further to the Adapter. The ViewModel class has injected domain UseCases through which they access data and notify the Observers of the change.

Fragment

class VehicleModelFragment: BaseFragment() {
    
    private val viewModel: VehicleModelViewModel by inject()
    private val adapter: RecyclerViewAdapter by inject() 

    override fun initUI() {
        setupObservers()
    }

    private fun setupObservers() {
        viewModel.items.observe(this, Observer {
            updateAdapter(it, adapter)
        })
}

ViewModel

class VehicleModelViewModel (private val getVehicleModelsUseCase: GetVehicleModelsUseCase)
: BaseViewModel() {
    
    var items = liveData {
        emit(getVehicleModelsUseCase.execute())
    }
}

IMPORTANT! ViewModel should never directly interact with the Repository class!

Adapter

class RecyclerViewAdapter(private val adapterDelegateManager: AdapterDelegateManager,
                          diffUtilCallback: DiffUtilCallback)
    : ListAdapter<VehicleModel, BaseViewHolder>(diffUtilCallback) {

    var data: List<VehicleModel> = ArrayList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        return adapterDelegateManager.onCreateViewHolder(parent, viewType)
    }

    override fun getItemCount(): Int {
        return data.size
    }

    override fun getItemViewType(position: Int): Int {
        return adapterDelegateManager.getItemViewType(data, position)
    }

    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        adapterDelegateManager.onBindViewHolder(data, position, holder)
    }

    override fun onBindViewHolder(
        holder: BaseViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        adapterDelegateManager.onBindViewHolder(data, position, holder, payloads)

    }
}

Adapter Delegate

class VehicleModelDelegate(context: Context,
    private val viewModel: VehicleModelViewModel):
AdapterDelegate(context){

    override fun isForViewType(items: List<VehicleModel>, position: Int): Boolean {
        return items[position] is VehicleModel
    }

    override fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder {
        val modelBinding: RecyclerViewVehicleModelBinding =
            RecyclerViewVehicleModelBinding.inflate(layoutInflater, parent, false)
        return VehicleModelViewHolder(modelBinding, viewModel)
    }

    override fun onBindViewHolder(
        items: List<VehicleModel>,
        position: Int,
        holder: BaseViewHolder,
        payloads: List<Any?>
    ) {
        val model: VehicleModel  = items[position]
        holder.bind(model)
    }
}

Adapter ViewHolder

class VehicleModelViewHolder (private var binding: RecyclerViewVehicleModelBinding,
private var viewModel: VehicleModelViewModel
): BaseViewHolder(binding) {

    override fun bind(vehicleModel: VehicleModel) {
        binding.model = vehicleModel
        binding.viewModel = viewModel
        binding.executePendingBindings()
    }
}

How changes affect our code and conclusion

As for the example above, let’s go through some real situations when a request changes or new things are added and see how it will affect our code we wrote!

Due to circumstances, it decides to change the endpoint from which the data is retrieved. Assuming the Model remains the same, all we need to do is:

  • Go to Source and change the endpoint path. That’s all!

Let’s say you decide to add a new parameter to the Model that you want to display to the user. Assuming Source remains unchanged, all we need to do is:

  • Go to Model and add a new parameter. Also in the ViewHolder layout add a new presentation field and associate that new field with Data Binding with an existing Model. Done!

Let’s say there is a new functional requirement that only models of a particular brand of vehicle be displayed. All we have to do is:

  • Go to the specific UseCase in the Domain layer in charge of data retrieval and set the condition. Done!

The conclusion of the whole Clean Architecture story is that every part of the application behaves like a black box whose modification should not affect the other parts. The other parts should not even be aware of the change that has taken place and should work as before without scraping anything in the application. I consider this method to be one of the best and most scalable to organize architecture in Android projects.