In the previous article, we talked about the basics of Clean Architecture, MVVM and app modularization. Then we’ve created a sample WeatherApp with an initial package structure (core, WeatherApp, data, domain, feature, navigation module), gradle files…
In this article, I’ll take you through the process of creating the first feature for data parsing from local JSON that’ll display the results to the user. We can simply call this feature weather.
Following Uncle Bob’s principle of clean architecture, we’ll start with the domain layer.
The first step is to create a new module called domain_weather inside the domain directory. Each feature should have its layer, the weather feature will have three modules for each layer (data_weather, domain_weather, feature_weather – it represents the presentation layer).
What is the Domain layer and how to implement it in an Android application?
The Domain layer is the central layer of our feature. All of our business logic needs to be placed in this layer and it needs to be pure Kotlin (if you work on a Java project, it should be pure Java) with no Android dependency. The Domain layer interacts with the Data and Feature (presentation) layer using interfaces and interactors. It is also completely independent and can be tested regardless of external components. Each domain layer has a unique use case, repository, and business model.
UseCase is nothing more than a logic executing class, every logic we have in our domain layer should have UseCase. We need to interact with the data layer in our domain_weather, so we’ll create a package usecase under that module and file called GetWeatherUseCase inside the package.
Before we start coding our use case, we need to create a domain package inside our core module. Inside this package, we’ll create an abstract class called UseCase. This abstract class will be a base class for all of our usecases and when extended, it’ll force that particular usecase to provide the implementation for a method called executeUseCase where specific logic will be implemented.
Those usecase classes aren’t just responsible for performing some operations (in the first article I mention that I’ll use RxJava2) but they also manage which threads will be used for performing and observing the subscription. We’ll learn more about this when it comes to the actual implementation of these usecase classes. When using RxJava in the application we’re dealing with different threads and observing in the main thread of the app (AndroidSchedulers.mainThread()). The main issue of this approach is that it requires reference to RxAndroid, so we’ll have the reference to the Android framework, and because the domain layer needs to be pure Kotlin, this situation will break the concept of separation of concerns.
Therefore, we need to create an interface to abstract our observing thread because we don’t want our domain layer to know about it. The best place to create this interface is the core module.
Inside the core module, create a package called rx, inside of it create an interface called SchedulerProvider.
As you can see here, we’re using Scheduler from RxJava framework, this is fine because we don’t want the domain layer to be aware of RxAndroid. The next step is to create a class DefaultSchedulerProvider which will implement SchedulerProvider interface. The class will use AndroidMainScheduler – that way we can achieve the necessary abstraction.
The next step is to create a domain representation of the data model, and this model will represent the business rule for the feature. In the feature, we’ll parse dummy data from the local JSON, so that the response is represented with an instance of this data model. We don’t want to know how the data layer (in our case data_weather) gets this data (it can perform some API call or retrieve data in some other way not only parsing local JSON) in this layer, but we do want to know how the data that we receive looks like so that we can construct our model. We’ll start by creating a package called model inside domain_weather and a data class called Weather inside that package.
Once we’ve created the Weather model, we need to set and define rules of what needs to be implemented to obtain this model. That’s why we need to create a repository interface that will contain this business rule. We’ll start by creating a package repository inside the feature domain layer, once we do that, we can create an interface called WeatherRepository. This interface will be implemented by an outside data layer (data_weather) and it will implement the logic for usecase of our domain layer (domain_weather). In this interface we will provide one method for getting weather data called getWeather(), this will return RxJava Single instance (because we are getting single value, when we change local JSON with actual API call this method will return Observable or Flowable if we want to support backpressure and we will return a list of Weather instances).
Before implementing GetWeatherUseCase we need to set up dependency injection for this module. Under main/java, where the packages model, repository, and usecase are located, create one more package called di. Start by creating a module class called WeatherDomainModule, this module will provide a weather repository and scheduler provider.
Create two more files after WeatherDomainModule, a component called WeatherDomainComponent and an object called WeatherDomainInjector. We’re following the same principle for di as we did in the previous modules.
Once we set dependency injection, we can implement our use case GetWeatherUseCase. First of all, we need to inject WeatherRepository and SchedulerProvider through the constructor of the GetWeatherUseCase and extend base UseCase class from the core module:
In this case, our compiler will complain about two things, the first it will require us to override the method executeUseCase and the second one is related to the missing type in UseCase<>. Let’s fix the second error first. To have clear information about the result of usecase execution, we will create a sealed class called Status inside GetWeatherUseCase. This sealed class will contain one data class and two objects. The Data class will be returned if the execution of usecase is successful and as a parameter, it will receive our domain model Weather. Objects in Status will be used for error handling, they will describe which error happened during the execution of our usecase.
Now we can pass this sealed class as a type to UseCase:
Once we do this, we can override the required method and we should get this:
Before we provide an implementation for method executeUseCase we need to create CompositeDisposable to keep all our disposables in the same place, we will create it in our base UseCase class, let’s make it protected:
After we create compositeDisposable we also need to create a method to dispose of all the previously contained disposables:
Now we can go back to GetWeatherUseCase to method executeUseCase and call getWeather() from weatherRepository that we received through the constructor of usecase:
As we can see from the code snippet above, we are performing a subscription on onStatus in a new thread and observing it on the main thread. The next step is to add error handling:
We will create below executeUseCase method onError which will take one parameter of Throwable type, based on what type of error is thrown we can perform our logic:
The final step is to add this disposable to our compositeDisposable.
Create a package called rx, under the core module, and add a class called RxExtensions.kt inside of it.
When we call disposeWith() in executeUseCase, the final implementation for this usecase method should look like this:
With this, we’ve finished our feature domain layer and reached the end of our second article about Android app architecture! In the next article, I’ll write about the feature data layer (data_weather), how to parse data from local JSON, and connect it with the domain layer using more code and examples.