Android App Architecture Part 2: Domain Layer
Join the DZone community and get the full member experience.
Join For FreeIn the previous article, we talked about the basics of Clean Architecture, MVVM, and app modularization. Then, we created a sample WeatherApp with initial package structure (core, WeatherApp, data, domain, feature, navigation module, Gradle files, etc.).
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).
You may also like: Clean Architecture Is Screaming.
What Is the Domain Layer?
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 dependencies. The Domain layer interacts with the Data and Feature (presentation) layers 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 a UseCase. We need to interact with the data layer in our domain_weather, so we’ll create a package, UseCase
under that module and a 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.
UseCase:
package com.rostojic.core.domain
abstract class UseCase<T> {
abstract fun executeUseCase(onStatus: (status: T) -> Unit)
}
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 subscriptions. 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 them 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. 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.
SchedulerProvider:
package com.rostojic.core.rx
import io.reactivex.Scheduler
interface SchedulerProvider {
val mainThread: Scheduler
val io: Scheduler
val newThread: Scheduler
}
As you can see here, we’re using the 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 the SchedulerProvider
interface. The class will use AndroidMainScheduler
— that way, we can achieve the necessary abstraction.
DefaultSchedulerProvider:
package com.rostojic.core.rx
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
class DefaultSchedulerProvider : SchedulerProvider {
override val mainThread: Scheduler
get() = AndroidSchedulers.mainThread()
override val io: Scheduler
get() = Schedulers.io()
override val newThread: Scheduler
get() = Schedulers.newThread()
}
The next step is to create a domain representation of the data model, and this model will represent 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 on 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 data class called Weather
inside that package.
Weather:
package com.rostojic.weather.model
data class Weather(
val city: String,
val dateTime: String,
val weatherImage: String,
val temperature: String,
val feelsLike: String,
val description: String,
val precipitation: String,
val uvIndex: String
)
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 outside data layer ( data_weather
), and it will implement the logic for the UseCase
of our Domain layer ( domain_weather
). In this interface, we will provide one method for getting weather data, called getWeather()
. This will return a RxJava Single
instance (because we are getting a single value). When we change local JSON with an 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.
WeatherRepository:
package com.rostojic.weather.repository
import com.rostojic.weather.model.Weather
import io.reactivex.Single
interface WeatherRepository {
fun getWeather(): Single<Weather>
}
Before implementing GetWeatherUseCase
, we need to set up a 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.
WeatherDomainModule:
package com.rostojic.weather.di
import com.rostojic.core.rx.SchedulerProvider
import com.rostojic.weather.repository.WeatherRepository
import dagger.Module
import dagger.Provides
import javax.inject.Provider
@Module
class WeatherDomainModule(
private val repository: Provider<WeatherRepository>,
private val schedulerProvider: SchedulerProvider
) {
@Provides
fun provideWeatherRepository(): WeatherRepository = repository.get()
@Provides
fun provideSchedulerProvider(): SchedulerProvider = schedulerProvider
}
Create two more files after WeatherDomainModule, component called WeatherDomainComponent and an object called WeatherDomainInjector. We’re following the same principle for di as we did in the previous modules.
WeatherDomainComponent:
package com.rostojic.weather.di
import com.rostojic.weather.usecase.GetWeatherUseCase
import dagger.Component
@Component(modules = [WeatherDomainModule::class])
interface WeatherDomainComponent {
fun getGetWeatherUseCase(): GetWeatherUseCase
}
This domain component has only one method with the return type of GetWeatherUseCase.
WeatherDomainInjector:
package com.rostojic.weather.di
import com.rostojic.core.rx.SchedulerProvider
import com.rostojic.weather.repository.WeatherRepository
import javax.inject.Provider
object WeatherDomainInjector {
lateinit var component: WeatherDomainComponent
fun initialise(repository: Provider<WeatherRepository>, schedulerProvider: SchedulerProvider) {
component = DaggerWeatherDomainComponent.builder()
.weatherDomainModule(
WeatherDomainModule(
repository = repository,
schedulerProvider = schedulerProvider
)
)
.build()
}
}
Once we set a 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 the base UseCase
class from the core
module:
class GetWeatherUseCase @Inject constructor(
private val weatherRepository: WeatherRepository,
private val schedulerProvider: SchedulerProvider
) : UseCase<>()
In this case, our compiler will complain about two things. First, it will require us to override executeUseCase
. The second issue 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.
Status:
sealed class Status {
data class Success(val weather: Weather) : Status()
object ConnectionError : Status()
object UnknownError : Status()
}
Now we can pass this sealed class as a type to UseCase:
: UseCase<GetWeatherUseCase.Status>()
Once we do this, we can override the required method and we should get this:
override fun executeUseCase(onStatus: (status: Status) -> Unit) {}
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, so let’s make it protected:
protected val compositeDisposable: CompositeDisposable = CompositeDisposable()
After we create compositeDisposable
, we also need to create a method to dispose of all the previously contained disposables:
open fun clear() {
compositeDisposable.clear()
}
Now, we can go back to GetWeatherUseCase
to the method, executeUseCase
and call getWeather()
from the weatherRepository that we received through the constructor of usecase:
override fun executeUseCase(onStatus: (status: Status) -> Unit) {
weatherRepository.getWeather().map<Status> { Status.Success(it) }
.subscribeOn(schedulerProvider.newThread)
.observeOn(schedulerProvider.mainThread)
.subscribe(onStatus)
}
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:
.onErrorReturn(::onError)
We will create the following executeUseCase
method onError
, which will take one parameter of type, Throwable. Then, based on what type of error is thrown we can perform our logic:
private fun onError(throwable: Throwable): Status {
return when (throwable) {
is SocketTimeoutException -> Status.ConnectionError
is UnknownHostException -> Status.ConnectionError
is ConnectException -> Status.ConnectionError
else -> {
Status.UnknownError
}
}
}
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.
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
fun Disposable.disposeWith(disposables: CompositeDisposable) {
disposables.add(this)
}
When we call disposeWith()
in executeUseCase
,the final implementation for this usecase method should look like this:
override fun executeUseCase(onStatus: (status: Status) -> Unit) {
weatherRepository.getWeather().map<Status> { Status.Success(it) }
.onErrorReturn(::onError)
.subscribeOn(schedulerProvider.newThread)
.observeOn(schedulerProvider.mainThread)
.subscribe(onStatus)
.disposeWith(compositeDisposable)
}
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.
Further Reading
Published at DZone with permission of Radivoje Ostojic. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Tomorrow’s Cloud Today: Unpacking the Future of Cloud Computing
-
Redefining DevOps: The Transformative Power of Containerization
-
Chaining API Requests With API Gateway
-
Cypress Tutorial: A Comprehensive Guide With Examples and Best Practices
Comments