Android Clean App Base Library (Clean Architecture + MVVM) — Part III
Hello everyone 🙋♂️ In this part I will show the demo app of Clean App Base Library.
What does this app?
It gets users from a server and lists them. If any user clicked a detail page opened. In the detail page user details and editing profile picture implemented as two tabs.
It uses Fakejson as the data source. It is a really good tool to mock a server. It can respond how you want the answer or can respond built-in data or list.
Structure
It is an one-module app that consists of three packages. I named every package as an app feature. These are: core
, detail
and main
. They are all divided by three familiar packages: data
,domain
and presentation
. They are all connected with Koin dependency injection library.
core
package includes app wide base classes ,network specifications and CABDemoApp class. detail
package includes profile detail screens and features. main
package includes main page screens and features. Let’s examine one by one:
core
core package includes all app-specific base classes. When you completed the core package based of Clean Application Base library, development of features becomes very easy.
core.data
This package handles data layer base classes and models. Because of our data source is only network, it contains network classes mostly. It also has a base DataSource and ApiCallAdapter implementation.
Networking is an app-specific feature. Server addresses, server request-response types, authentications and sessions differ app to app. First of all I created server data models.
There are some json request and response examples to understand better above structures:
In the request: data
, parameters
and token
sent to server. data
is what we want from the server as a response data. parameters
are api properties to manage how we want the data. token
is required to get data from the api.
In the response: Only our data type is returned from the server.(ResponseTemplate<T>). Here T is ProfileDetail model which resides in detail package.
But how this data is processed?
Converting server text data to our classes done by ApiCallAdapter. It is a base class for app specific implementations.
It takes a lambda function to handle service calls. The function returns a subclass of BaseApiResponse<T>
Here is the implementation of ApiCAllAdapter. It must be app specific because server responses and handling is different at every app.
It handles retrofit calls and produces a DataHolder<T> according to server statuses. It also handles error cases such as wrong response format, invalid token and no internet conditions. It executes response as a try,catch block, so there will be no runtime crash because of these errors.
BaseDataSource is a app-specific base class for CAB DataSource. It can be used to intercept all data sources and modify them. It is not used to modify data for now but I added to demonstration purposes:
It is a base for AsyncDataSource.RequestDataSource. It can also be implemented for other data source types.
core.domain
This package has Base Interactors and RequestResultType model. Again there is no need to create Base class for interactors. It is created for just demonstration purposes.
core.presentation
This package has base Activity
, Fragment
and ViewModel
. Additionally it has navigation and UI helper classes. These component bases actually just implement the CAB library component interfaces. Let’s look at them:
They are implemented the CAB library to prevent every feature component implementing it again and again. All feature components are childs of this package base component classes.
Navigation
Navigation is again an app-specific issue. You can use multi-activity
, single-activity
or hybrid
navigation strategies. I used a navigation helper class to provide a simple navigation between activities. It has an Interface named NavigatorOwner that provides an AppNavigator. It is implemented by CABDemoBaseActivity, so every child activity
will have this navigation helper class. The AppNavigator instance provided by the DI
.
CAB library has a helper class named IntentLoader to navigate between modules by using activity’s full class name in modular projects.
This package also contains CabDemoApp class. Again App class is an app-specific class so it must belong here. This class is needed to initialize DI
.
core
package has the main implementation code of CAB library.
We will continue with the feature packages, so I suggest taking a coffee break :)
Let’s look at feature packages to how CAB library properties is used via core package:
main
This package is responsible for showing the user list as a first screen. It must also navigate to detail screen when a row in list clicked. This package also has data
, domain
and presentation
layers. When the user enter the app MainActivity opened. Then it shows a dialog using CABActivity extension methods. In the dialog user asked to choose get Fail or Succes data. If the user selects fail an error dialog is shown. Otherwise data is requested from the server and RecyclerView is populated.
main.data
It has its own models, data sources, repository implementation and DI module. Everything related with the main data is implemented here. It moves data to the upper layer via repository. Let’s look at them:
First class is the request model. We request a list model from api. This is usage of api’s person create feature. It creates a random person with requested properties.
Second class is a network data transfer object model. This model belongs to the data layer. It is an enterprise model so it must be converted to an app model which resides in the domain layer. This conversation is provided by mapToUserListItem()
extension function. As you can see it converts the server date string to Date object in desired format. It also generates a working image url link from userId
(personNickName
).
GetUsersDataSource is the data source implementation of main.data
It takes user service and ApiCallAdapter as variables. It just calls adapt
function of ApiCAllAdapter. Its request type is ResponseTemplate<UserListDataTemplate> and return type is DataHolder<List<UserListItemNetworkDTO>>
UserService is an interface with a function annotated with Retrofit annotation. It actually gets data from the server.
It is a suspend
function. That means it can be called from another suspend
function. If you remember all functions are suspend
functions up to viewModel
in the call chain.
ApiCall adapter is provided by DI at core.data
package. It is provided as a Singleton
object so all data sources use the same call adapter.
The final class to examine is UserRepositoryImpl.
Repository is the place that the business logic must be implemented. DataSources are responsible for only getting data in the form of data layer models. Mixing data sources, filtering or transforming models are job of the repository. In this scenario the server returns a successful response if there is no other error. But we want to see error response from the server.(with our data types). So we provide a RequestTemplate<UserListDataTemplate> to the data source as parameter that what type of result we want. Then we put data to be returned from the server(UserListDataTemplate or Error). Finally execute data source and return data mapped to List<UserListItem>, which is an application model if the execution result is DataHolder.Success. Otherwise return DataHolder to the interactor without intercepting.
But how are they connected?
The answer is dependency injection. I defined all class creation definitions in MainDataModule.
factory{} means everytime the object requested create a new instance for it. By that way, the instance will be removed from memory when there is no reference to it. So they will not cause a memory leak after the user ViewModel is closed.
named<T>
is used because there are other implementations of BaseDataSource. So we must tell to DI this: Give me a BaseDataSource but it must be GetUserDataSource. But why didn’t we requested GetUserDataSource directly? Because UserRepositoryImpl takes a constructor parameter type of BaseDataSource<ResponseTemplate<UserListDataTemplate>,
List<UserListItemNetworkDTO>>. So management of the DI is important if we don’t want to struggle with the runtime errors.
main.domain
This package has the application data model, interactor and the repository interface. Application data model has app or platform class type variables. So we can use it in the presentation layer.
Since our feature does one job(Getting user list) there is one interactor. Our interactor is GetUsersInteractor:
It takes a userRepository
at the constructor. Its type is a generic type of BaseSingleInteractor<GetUsersInteractor.Params,List<UserListItem>>
Param is an interactor specific type. It provides separation of data and presentation layers.
It has an override function: executeInteractor(params:GetUsersInteractor.Params)
. User of this interactor must call only this function with the required parameters.
main.presentation
It consists of three classes: MainPresentationModule, MainActivity and MainViewModel. MainPresentationModule provides MainViewModel via Koin DI
:
It is binded to CABDemoBaseViewModel because DialogHolder expects CABDemoBaseActivity(which is the parent class of MainActivity) to provide a CABViewModel(which is the parent class of CABDemoBaseViewModel)
MainViewModel executes the interactor and passes the result to a LiveData<List<UserListItem>.
It takes a Interactor.SingleInteractor<GetUsersInteractor.Params,List<UserListItem>> as a constructor parameter. Again it is not Interactor itself, it is base class. It gives a huge advantage to us when unit testing. It has _usersLiveData
and usersLiveData
. First one is a MutableLiveData to modify in the class. The other one is a LiveData to expose our live data to outside. We don’t want to our live data to be modified outside of this class.
To get users from api, getUsers()
function is called. It just calls execInteractor()
function of the CABViewModel extension. Under the hood, it makes _usersLiveData
’s value to DataHolder.Loading. Then executes the interactor and posts result to _usersLiveData
. Look for part 2 for further details.
In the MainActivity, this execution is triggered and results are observed. Let’s look inside it:
In MainActivity the user is asked to select the success or fail state of data that will be taken from the server via askForSuccessOrFail()
function. It is asked in two places: In onStart()
method and in button click callback of error dialog. Then according to state the data view model’s getUsers(requestResultType: RequestResultType)
function is called.
In onCreate()
function view model’s userLiveData
is listened via observeDataHolder()
extension function of SAPActivity.
It basically manages dialogs according to live data state. Above code, it takes three parameters. First: liveData
— LiveData to observe. Second: errorButtonClick
— show error or success dialog to user again when error dialog button clicked. Third: successBody
— fill recycler view when data holder result is success.
This is the end of main package.
The remaining package/feature is detail . It’s data and domain layers are mostly the same as main package. Since this part is quite long, I will show only one difference in the presentation layer. DetailActivity hosts 2 fragments: ProfileDetailFragment to show detail of selected profile and ChangePPFragment to edit selected user’s profile picture. These fragments are child classes of CABDemoBaseFragment that implements CABSAPFragment. Let’s look at ProfileDetailFragment as an example:
It has two buttons: Get Success and Get Fail. They call view model’s getProfileDetail()
function with parameters that received from arguments. Then observes the view model’s profileDetailLiveData
. If it’s state is success, fills the screen with the detail data. Implementation of CABSAPFragment is explained in start of this part.
This was part 3.
Happy coding! 🎉
Part IV: