Blog Infos
Author
Published
Topics
,
Author
Published
Posted by: Musab Alothman

I’m not going to explain how important Clean Architecture or Hilt are, there are too many posts about that. Today, I’m going to implement them in the simplest way, so you can get the idea and start using them in your own apps.

At the end I will share with you a GitHub repository for the whole project.

Now let’s get started to create a simple Android Clean Architecture project with the latest libraries.

Let’s start with Gradle and add all dependencies.

First: Project Gradle file
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.10"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
view raw build.gradle hosted with ❤ by GitHub
Second: Module Gradle file
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.cleanarchitectkotlinflowhiltsimplestway"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation "androidx.fragment:fragment-ktx:1.3.5"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'com.google.dagger:hilt-android:2.35'
kapt 'com.google.dagger:hilt-android-compiler:2.35'
implementation("com.squareup.retrofit2:retrofit:2.9.0") {
exclude module: 'okhttp'
}
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.9.1"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"
implementation "com.google.code.gson:gson:2.8.6"
}
view raw build.gradle hosted with ❤ by GitHub

We will use a network calls so we need an internet permission in the manifest.

<uses-permission android:name="android.permission.INTERNET" />

And add a custom Application subclass for the Hilt annotation we will see later on in this post.

Under the application tag in AndroidManifest.xml add the custom app class:

android:name=".presentation.App"

Now we have everything ready let’s start adding some classes.

For Android Clean Architecture we need to sort our packages like below

As you can see we have three main layers :

Data

Here we put the logic of bringing data either from local source or server.

Domain

Here we put the logic of business: convert, filter, mix and sort raw data that comes from Data layer to be ready and easy to handle in Presentationlayer.

Presentation

Here we put the UI components and the logic that related to user interactions or navigation in order to get data from the user.

Utils

Constants

Contains constant variables, in this example we have only one constant:

const val BASE_URL = "https://reqres.in/"

Utils

package com.cleanarchitectkotlinflowhiltsimplestway.utils
import com.cleanarchitectkotlinflowhiltsimplestway.domain.AuthenticationException
import com.cleanarchitectkotlinflowhiltsimplestway.domain.NetworkErrorException
import com.cleanarchitectkotlinflowhiltsimplestway.domain.State
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
class Utils {
companion object{
fun resolveError(e: Exception): State.ErrorState {
var error = e
when (e) {
is SocketTimeoutException -> {
error = NetworkErrorException(errorMessage = "connection error!")
}
is ConnectException -> {
error = NetworkErrorException(errorMessage = "no internet access!")
}
is UnknownHostException -> {
error = NetworkErrorException(errorMessage = "no internet access!")
}
}
if(e is HttpException){
when(e.code()){
502 -> {
error = NetworkErrorException(e.code(), "internal error!")
}
401 -> {
throw AuthenticationException("authentication error!")
}
400 -> {
error = NetworkErrorException.parseException(e)
}
}
}
return State.ErrorState(error)
}
}
}
view raw Utils.kt hosted with ❤ by GitHub

The Utils class contains common used functions all over the app.

In this example we have only one function to resolve the errors from data layer, we will use it in ViewModel

DI

This class is for everything related to Hilt. I preferred to put it in one class which contains all components and modules to make it easy to reach and maintain.

In this example we are going to use only one Module for Network calls:

package com.cleanarchitectkotlinflowhiltsimplestway.utils
import android.content.Context
import com.cleanarchitectkotlinflowhiltsimplestway.data.APIs
import com.cleanarchitectkotlinflowhiltsimplestway.presentation.App
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@Suppress("unused")
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideApplication(@ApplicationContext app: Context): App {
return app as App
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder().baseUrl(Constants.BASE_URL).client(client)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
}
private val READ_TIMEOUT = 30
private val WRITE_TIMEOUT = 30
private val CONNECTION_TIMEOUT = 10
private val CACHE_SIZE_BYTES = 10 * 1024 * 1024L // 10 MB
@Provides
@Singleton
fun provideOkHttpClient(
headerInterceptor: Interceptor,
cache: Cache
): OkHttpClient {
val okHttpClientBuilder = OkHttpClient().newBuilder()
okHttpClientBuilder.connectTimeout(CONNECTION_TIMEOUT.toLong(), TimeUnit.SECONDS)
okHttpClientBuilder.readTimeout(READ_TIMEOUT.toLong(), TimeUnit.SECONDS)
okHttpClientBuilder.writeTimeout(WRITE_TIMEOUT.toLong(), TimeUnit.SECONDS)
okHttpClientBuilder.cache(cache)
okHttpClientBuilder.addInterceptor(headerInterceptor)
return okHttpClientBuilder.build()
}
@Provides
@Singleton
fun provideHeaderInterceptor(): Interceptor {
return Interceptor {
val requestBuilder = it.request().newBuilder()
//hear you can add all headers you want by calling 'requestBuilder.addHeader(name , value)'
it.proceed(requestBuilder.build())
}
}
@Provides
@Singleton
internal fun provideCache(context: Context): Cache {
val httpCacheDirectory = File(context.cacheDir.absolutePath, "HttpCache")
return Cache(httpCacheDirectory, CACHE_SIZE_BYTES)
}
@Provides
@Singleton
fun provideContext(application: App): Context {
return application.applicationContext
}
@Provides
@Singleton
fun provideApi(retrofit: Retrofit): APIs {
return retrofit.create(APIs::class.java)
}
}
view raw DI.kt hosted with ❤ by GitHub

SingletonComponent is ready-to-use component provided by Hilt so no need to create one.

We have finished the Hilt logic, now if we want to use it in our app there are a simple tiny things need to be added in our classes we will see them later in this post.

Data Layer

As you can see in the domain package we have sub packages:

  • bodies for network calls that requires a body we put all bodies in this package.
  • containers for network response bodies, we name it containers to differentiate between them and request bodies.
  • enums optional class but I prefer to separate the enums to a sprit package.
  • APIs this class for Retrofit calls:
package com.cleanarchitectkotlinflowhiltsimplestway.data
import com.google.gson.JsonObject
import retrofit2.http.GET
interface APIs {
@GET("api/users")
suspend fun sampleGet(): JsonObject
}
view raw APIs.kt hosted with ❤ by GitHub

You may notice “suspend” keyword for background thread and direct response object “JsonObject” without wrapping it with any retrofit object for this is part of flow implementation that we will see next in this post

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Tests unitaires avec KotlinFlow

Lorsque nous développons une fonctionnalité, nous devons écrire les tests unitaires. C’est une partie essentielle du développement. Cela assure le bon fonctionnement du code lors de futurs changements ou refactorisations. Kotlin Flow ne fait pas…
Watch Video

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Jobs

Domain Layer

Contains only Use Cases. In this project we are going to need only one use case:

package com.cleanarchitectkotlinflowhiltsimplestway.domain
import com.google.gson.JsonObject
import com.cleanarchitectkotlinflowhiltsimplestway.data.APIs
import javax.inject.Inject
class SampleUseCase @Inject constructor(
private val apIs: APIs
) {
suspend operator fun invoke(): JsonObject {
val response = apIs.sampleGet()
//here you can add some domain logic or call another UseCase
return response
}
}

“@Inject constructor” this to notify Hilt that we want to inject the variables in this constructor

Use cases can contain only one part of business logic. Keep it for single small tasks. This will be useful if you want to reuse some part of business logic in another use case, you can create a use case to call multiple use cases inside it.

Presentation Layer

State

We will use this sealed class for network calls and connection between ViewModel and UI

package com.cleanarchitectkotlinflowhiltsimplestway.presentation
sealed class State<out T> {
object LoadingState : State<Nothing>()
data class ErrorState(var exception: Throwable) : State<Nothing>()
data class DataState<T>(var data: T) : State<T>()
}
view raw State.kt hosted with ❤ by GitHub

NetworkExceptions.kt

package com.cleanarchitectkotlinflowhiltsimplestway.presentation
import org.json.JSONObject
import retrofit2.HttpException
open class NetworkErrorException(
val errorCode: Int = -1,
val errorMessage: String,
val response: String = ""
) : Exception() {
override val message: String
get() = localizedMessage
override fun getLocalizedMessage(): String {
return errorMessage
}
companion object {
fun parseException(e: HttpException): NetworkErrorException {
val errorBody = e.response()?.errorBody()?.string()
return try {//here you can parse the error body that comes from server
NetworkErrorException(e.code(), JSONObject(errorBody!!).getString("message"))
} catch (_: Exception) {
NetworkErrorException(e.code(), "unexpected error!!ً")
}
}
}
}
class AuthenticationException(authMessage: String) :
NetworkErrorException(errorMessage = authMessage) {}

Contains Exceptions types that could comes from data layer, will use this class and parseException in Utils class where we mentioned before.

App

Our custom Application class:

package com.cleanarchitectkotlinflowhiltsimplestway.presentation
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application() {
override fun onCreate() {
super.onCreate()
}
}
view raw App.kt hosted with ❤ by GitHub

As you can see ‘@HiltAndroidApp’ to tell Hilt this is our Application class

Splash

We put every part of the UI in a separate package for example in this package we have two classes:

SplashActivityViewModel

  • ‘HiltViewModel’ to tell Hilt this is a ViewModel.
  • “fun getSampleResponse() =” we use this shortcut in Kotlin if the function has only one line, it’s equal to “fun getSampleResponse(): Flow<State> {”
  • “flow{” this is a global function to convert async to flow

As you can see just like LiveData we can emit to the collector these results.

SplashActivity

package com.cleanarchitectkotlinflowhiltsimplestway.presentation.splash
import android.os.Bundle
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.cleanarchitectkotlinflowhiltsimplestway.R
import com.cleanarchitectkotlinflowhiltsimplestway.presentation.State
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@AndroidEntryPoint
class SplashActivity : AppCompatActivity() {
private val viewModel: SplashActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val textView = findViewById<TextView>(R.id.textView1)
lifecycleScope.launch {
delay(500)
viewModel.getSampleResponse()
.collect {
when (it) {
is State.DataState -> textView.text = "success ${it.data}"
is State.ErrorState -> textView.text = "error ${it.exception}"
is State.LoadingState -> textView.text = "loading"
}
}
}
}
}

activity_splash.xml is a simple xml file with TextView with id textview1 in the center

  • ‘AndroidEntryPoint’ to tell Hilt this is an entry point.
  • We get out our ViewModel by very useful Kotlin function ‘viewModels()’
  • lifecycleScope: to launch the background thread of Flow in this certain scope, if this scope is not exist any more you don’t have to worry about the background threads.
  • .collect”: just like observe in LiveData or Subscribe in RxJava, to receive the emitted variables.
Done

That’s it! Now you have Android Clean Architecture with Kotlin Flow and Hilt.

Full Project

https://github.com/abos3d/CleanArchitectKotlinFlowHiltSimplestway

Things to know

This is a shortcut. There are missing parts you may need if you extend your project, for example:

Repositories: if you have more than one data source for example you have local database and server, then you have to add repositories and keep the responsibility of ‘where should I get this data from’ to them.

ViewBinding: a better approach is to use view binding instead of the legacy ‘findViewById’.

Thanks ?

If you have any comments please feel free.

Tags: Android, Clean Architecture, Kotlin Flow, AndroidDev

 

View original article at:


Originally published: June 22, 2021

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Just like my previous article about coroutines, this is an attempt to group most…
READ MORE
blog
Today I aim to cover the Domain layer. It is a layer that sits…
READ MORE
blog
Hello Kotlin Developers, let’s implement race/amb operator for Flow. This operator is similar to…
READ MORE
blog
In the previous article about app architecture, I covered how to build your Domain,…
READ MORE
Menu