Android Clean App Base Library (Clean Architecture + MVVM) — Part II
Hello everyone🙋♂️ In this part I will show you my Clean App Base Library. It mainly consists of base classes for the structures that I talked about Part I and some base methods to reduce our code one line in some places. Here is a link of my lib:
You can find here the source code and installation instructions.
Motivation
Firstly, whenever I start a project I was creating the same base classes for the same reasons. Because I use clean architecture, there are many classes. This library saves developer from creating classes again and again.
Secondly, especially in presentation showing data that comes from domain level requires lots of code. Most basic flow is like this: Activity requests some data to show. By the meantime it must show a loading indicator. For this it observes a live data from view model. That live data returns DataHolder<T> . Activity then behaves differently according to the data holder’s type. If it is Success, uses data. If it is Loading, shows loading indicator. If it is Fail shows some error dialog. That was the observer side. In the view model firstly observed data holder’s value must set be to DataHolder.Loading. Then the interactor must be executed in view model scope. Then coming data must be passed to observed live data. Of course that execution can be more parametrized. Different dispatchers can be needed or execution result can be intercepted by view model. Or even executions can be chained.
This library mostly reduces these case codes to one line. By doing this it respects clean architecture rules. Besides library’s presentation layer written as interface extension functions. So you can use them without your parent classes.
Library consists of 4 modules: dataholder, cabdata, cabdomain and cabpresentation. “CAB” prefix is short version of “Clean App Base”
So we can dive in module by module
1- Dataholder Module
Actually it must belong to domain module but it can be used in other projects that doesn’t use clean app base library. This module is seperated because of the distribution reasons. Domain module uses this module as ‘api’ so projects that use clean app base domain module doesn’t require to add it separately.
It contains DataHolder<T> class and it’s extensions. Also defines a base error that DataHolder<T> has it as a variable. Apps can extend this base error and handle it in necessary places.
DataHolder<T> class is a sealed generic class. It means that DataHolder<T> class can be used as only DataHolder.Success<T>, DataHolder.Fail and DataHolder.Loading. This mechanism provides us to carry data in the app in a standard way. It indicates data state(Success, Fail and Loading) and can contain data according to its type. Let’s examine this types:
DataHolder.Success<T>
This state tells us that getting data is successful and we have the data. This class gets its data type from base class(DataHolder<T>). It has a data:T
variable that is not nullable. Making this variable nullable would require null check everywhere that it is used and this makes code crowded. This data can be used as nullable data with a wrapper like class TWrapper(val t: T?)
in DataHolder<TWrapper>
DataHolder.Fail
This state indicates that returned data failed with or without a reason. It has errorResourceId and errStr variables to show error with a message. And has a BaseError class that can be used as base class for any type of error.
DataHolder.Loading
Making a mobile app 99% would require a long running task. Long means making users: “Bruh… this app is freezing ”. So almost every platform shows a loading indicator to the user while executing long running tasks. If our DataHolder<T> type is DataHolder.Loading tells us to show that: “I’m just waiting for the result, don’t think I’m freezed dude!” to the user.
It has loadingResourceId and loadingStr fields to show a message to the user while loading. Has a cancellable property for determining whether user can cancel execution or not. Progress indicates progress of execution between 0–100 and tag to identify executions and trigger a callback to executor classes.
HandleSuccess Extension
Most of the time data flow is intercepted. For example, repository maps enterprise models to the app models. Without extension you must check all types of DataHolder<T> in a when
structure. And the operation is meaningful only if it is DataHolder.Success. So it generates boilerplate code. With the extension you can operate your function in one-line code.
2- Data Module
This module defines base classes for the app’s data layer. DataSource, ApiCallAdapter and BaseResponse are the classes. Last two are base for network connections, designed for Retrofit 2.
ApiCallAdapter takes a lambda function as a parameter and returns a BaseApiResponse<T>. The app should implement it’s api response that extends BaseApiResponse<T> according to backend result model.
DataSource is the base for all data sources in the app. It has two types that contain two methods. Async data source’s methods are suspend functions while Sync data source is not. So that sync data source can be used to get data directly from RAM (it is safe to use in the main thread) or simple calculations. Async data source functions can be called from another suspend function or coroutine.
Fetch prefix means get data without a parameter. Request prefix means getting data needs a parameter. These parameters and returned models are usually enterprise objects, for example: ProfileNetworkDTO, ProfileLocalDBDTO and ProfileRequestNetworkDTO.
3- Domain Module
Domain module only contains the Interactor interface. Actually this is the place of DataHolder but it is moved to another module because of the distribution reasons. Still it provides DataHolder by including it as api
instead of implementation
. Here is the Interactor interface:
Yes, there are a lot of interfaces, but logic is simple. They are generated according to return type(DataHolder<T> or T), being suspend fun or not and taking a parameter. They all have an execute
method that does work. Most used interfaces are:
With these, we can get our data in a coroutine
or another suspend
function and get data in DataHolder<T> wrapper. Retrieve
means there is no need to a parameter to request data. Speaking of parameter, it has an important role at separating data and presentation layers. Parameter of SingleInteractor is a subclass of Interactor.Params class. By this way parameters of Interactor is wrapped and any change in parameters can be arranged from this wrapper. In the next part of this series there will be examples to usage of these Interactors.
4- Presentation Module
This module contains interfaces for ui elements, extensions for these interfaces and some helper classes. Interfaces are mostly used to tag ui elements for using them at the other parts of the library and use extension functions for ui elements. Extension functions do actual job for this layer. They handle executing interactor, observing them and take action according to DataHolder state. So let’s see them in detail.
Interfaces
Why interfaces? Because I want developers to use this library without changing their base classes. As you know a class can be extended from multiple interfaces. I put all logic in interface extensions so it can be used with a minimal change of user class.
CAB means Clean Application Base. SAP means Simple Animation Popup which is an animation library included in the presentation module.
In async programming, it is important to show the user the current state(Loading, Success and Fail). In my library this is done by dialogs or give the library implementer class a signal about current state. Users can choose to use built-in dialogs in the library. If so, CAPSAPActivity or CAPSAPFragment interfaces must be used. Otherwise CAPActivity or CAPFragment must be used. These interfaces have state callbacks which must be handled by the developer. I used CAPSAPXXX interfaces in the demo project. Let’s look inside of CAPSAPActivity interface:
Yes, there is no method in interface. It is just for use extensions and recognized by other classes. It has two extension methods: handleDataHolderResult()
and observeDataHolder().
First method takes action according to DataHolder’s state. If it is DataHolder.Loading calls showDHLoadingDialog()
function of DialogHolderInterface. It helps to showing a loading indicator dialog with notifying view model when is cancelled. It is usefull when we want to stop a specified interactor execution.
This is actually a classic SAP loading action but when dialog is cancelled it notifies cabViewModel of DialogHolderActivity. I will explain it at the CABViewModel section.
When the result data holder’s state is DataHolder.Fail it dismisses current dialog. if bypassErrorHandling
parameter is true errorBody callback is invoked with the error messages. If it is false an error dialog presented with error messages and error dialog button click callback.
When the result data holder’s state is DataHolder.Success it invokes successBody
callback with DataHolder.data
. Before that if bypassDisableCurrentPopupOnSucces
parameter is true current dialog is not dismissed. It is useful when chaining observations. Otherwise the current dialog is dismissed. Here current dialog is most probably a loading dialog.
Usage is mostly the same in CABSAPFragment interface too. It just looks for CABSAPActivity as fragment holder to show dialogs.
When it comes to view model there is CABViewModel. It needs child class to implement val jobMap : HashMap<String, Job>
. It is used to keep track of executing Interactors in a coroutine
. When an execution is completed or cancelled they are removed from the list.
It has execInteractor()
extension functions for SingleInteractor and SingleRetrieveInteractor. These functions handle all the execution logic for us with various parameters. They do basically set the value of MutableData<DataHolder<T>> instance state to DataHolder.Loading in a coroutine, then set result to this instance when execution finishes. When this live data is observed in CABActivity or CABFragment in observeDataHolder()
function appropriate dialogs and ux is provided to the user. Then the job is removed from the jobMap. Because the execution runs in viewModelScope it is safely executed. No need to consider life cycle events of Activity, Fragment or ViewModel.
The execution can be intercepted. It is useful when chaining executions or lookup to data before sending it to the observer. Even the result can be consumed in the view model. It is determined by interceptorBlock result. It is a lambda function that called when between execution finishes and passes the result to the observer. If it returns a true value, the result is not passed to the observer.
Additionally you can set cancellability of execution. It sets the loading dialog cancellable or not when presenting to the user. If you remember I mentioned that when an execution cancelled by the user view model gets notified. When loadingCancelled()
function of this interface is called with a tag the job
has the tag found in jobMap
and cancelled safely. You can also set coroutine dispatcher for this execution. It is given as a parameter to execInteractor()
methods.
Finally there are some view extensions to simplify controlling of views.
show()
and hide()
arrange view visibility. setVisibility()
is useful when using databinding
.
That was all for this chapter. In the next chapter I’ll be talking about demo app that uses this library and some Android concepts.
Happy coding 🎉
Part III: