{{announcement.body}}
{{announcement.title}}

Android App Architecture Part 2: Domain Layer

DZone 's Guide to

Android App Architecture Part 2: Domain Layer

in this article, we discuss the Domain layer of Uncle Bob's Clean Architecture in our continuation of our WeatherApp.

· Web Dev Zone ·
Free Resource

storm-on-horizon

In 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_weatherdomain_weatherfeature_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

Topics:
android ,app architecture ,web dev ,java ,kotlin ,app dev ,domain layer ,uncle bob

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}