Dependency Injection in Clean Architecture
After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers to use Dagger 2 for Dependency Injection, Although they admit it is a complex framework with a steep learning curve
On the other hand, Kotlin language features made a great job in simplifying manual Dependency Injection, using these features with Clean Architecture makes Manual Dependency Injection the best fit for any Project of any size, and 100% easy to scale
Getting Started
As this article will show you a big project, it will be full of details, but if you want the summery of how to do it, there is a Summery section at the bottom of this article
If you did not understand any of the points above, don't worry, they will all be explained in this article
First of all we have to follow some Rules and Techniques in our daily coding practices, the Rules belong to the Clean Architecture Guide lines and they are better to be followed weather you will do Dependency Injection or not, and the Techniques belong to the language features of Kotlin ... once we do both, you will see that you do not need any DI library or any similar thing
We will go through the Architecture of a Project, Layer by Layer, and see the application of those Clean Architecture Rules and Kotlin Techniques
Data Sources Layer
This layer is called "Entity Gateways" in Clean Architecture, the nature of this Layer is that it's not Test-able, so we do not put any logic more than getting data and sending data to it ... if it is a data-base, we just put simple SQL queries, if it is a Server, we put our Retrofit Builder and the APIs interfaces, and so on, lets see the Data Sources in this project :
Server Data Source
First We declare our Retrofit interfaces :
interface AuthenticationApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...): List<...> ... } interface SearchApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...) ... } interface HistoryApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...) ... } ...
Then we add There Dependency providers, using the "lazy" delegate, we guarantee that the instance will not be initialized unless it is used
private val retrofit: Retrofit by lazy { Retrofit.Builder() .baseUrl(...) ... .build() } val authenticationApis by lazy { retrofit.create(AuthenticationApis::class.java) } val searchApis by lazy { retrofit.create(SearchApis::class.java) } val historyApis by lazy { retrofit.create(HistoryApis::class.java) } ...
Since we are using Kotlin, we do not need to declare those Dependencies in a class, they can be a global value, and since it is a "val", it is pretty safe to be public
Notice that global values are converted to "static" variables in Java, but here is the interesting part ... we are not in a UI layer, we are actually dealing with Data Sources, which are Singletons by nature, we do not want multiple Retrofit instances (except for some cases), and we do not want multiple data base instances ... Data sources are Singletons by nature, and that is what we exactly want
So the End result for this Server Data Source will be as follows :
private val retrofit: Retrofit by lazy { Retrofit.Builder() .baseUrl(...) ... .build() } val authenticationApis by lazy { retrofit.create(AuthenticationApis::class.java) } interface AuthenticationApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...): List<...> ... } val searchApis by lazy { retrofit.create(SearchApis::class.java) } interface SearchApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...) ... } val historyApis by lazy { retrofit.create(HistoryApis::class.java) } interface HistoryApis { @GET(... ) suspend fun getXxx(): List<...> @POST(... ) suspend fun postXxx(body: ...) ... } ...
These can be divided onto multiple files or single file, no problem ... and the cost for Dependency Injection for every new interface is one line above that interface that provides it's singleton dependency in a lazy fashion
Database Data Source
Assuming that we are using Room, first we declare our Database class and DAO interfaces
@Database(...) @TypeConverters(...) abstract class AppDatabase : RoomDatabase() { abstract fun firstDao(): FirstDao abstract fun secondDao(): SecondDao abstract fun thirdDao(): ThirdDao ... } @Dao interface FirstDao{ ... } @Dao interface SecondDao{ ... } @Dao interface ThirdDao{ ... } ...
And Then we declare the Dependency Provider, the same way we did with Retrofit, which will be a lazy global value :
val appDatabase: AppDatabase by lazy { Room.databaseBuilder(/*Context*/, AppDatabase::class.java, "database").build() }
But here we need an Application context, which is not available unless the application starts
Generally, we will need the Application Context in many things in this Layer, so we have to pass it some how from the Application class to this layer
And weather you are putting these classes in a separate module (which is recommended for big projects) or putting it in the same module of your Android application, we usually need an Integration point that takes the required parameters from the Application class, and pass it down to other layers ... usually it is the App context only, so we will create a small class that is responsible for integrating this layer (which is the Domain layer) with our App
// this class is created in the Domain module / package object Domain { internal lateinit var application: Application private set fun integrateWith(application: Application) { this.application = application }
}
* Maybe this is the only place in the code where you will find a "var", we usually use values, and that's why it's setter is private, and it is marked with "late-init" to crash if it is not initialized before use (and that's exactly what we want)
By the way, this step will be done weather you use DI or not, because in Clean Architecture, the Dependency direction forces the Domain layer (and data sources) to not to be able to access the upper layers (like Presentation layer or Application class), so the Upper layers (presentation layer) is in charge of integrating with lower layers
Last step is to integrate with our Domain layer from the Application class :
class MyApplication : Application() { override fun onCreate() { super.onCreate() Domain.integrateWith(this) ... } }
So what we have done here is connected our Presentation layer with our Domain layer, the domain layer can be another module ... usually we wont need any thing more than this step to integrate with the domain module, if we divide our domain into multiple modules, we can create an integration class for every module, but no need for this in normal scenarios ... at the end the cost will be a 6 lines class for every module
Back to our Database Dependency Provider, it will be now like this :
val appDatabase: AppDatabase by lazy { Room.databaseBuilder(Domain.application, AppDatabase::class.java, "database").build() }
So our Database Data source will look like this :
val appDatabase: AppDatabase by lazy { Room.databaseBuilder(Domain.application, AppDatabase::class.java, "database").build() } @Database(... ) @TypeConverters(... ) abstract class AppDatabase : RoomDatabase() { abstract fun firstDao(): FirstDao abstract fun secondDao(): SecondDao abstract fun thirdDao(): ThirdDao ... } @Dao interface FirstDao { ... } @Dao interface SecondDao { ... } @Dao interface ThirdDao { ... }
Preferences Data Source
Preferences is a Data Source, and it is not accepted to access it directly from UI related classes ... If you still did not know
So we start with declaring our Preferences class :
class Preferences(val sharedPreferences: SharedPreferences) { suspend inline fun <reified T : Any> save(key: String, value: T) { sharedPreferences.edit().apply { putValue(key, value) }.apply() } suspend inline fun <reified T : Any> load(key: String, defaultValue: T): T { return sharedPreferences.getValue(key, defaultValue) } suspend fun isSaved(key: String): Boolean { return sharedPreferences.contains(key) } suspend fun remove(key: String) { sharedPreferences.edit().remove(key).apply() } } inline fun <reified T : Any> SharedPreferences.Editor.putValue(key: String, value: T) { when (T::class) { Boolean::class -> putBoolean(key, value as Boolean) Int::class -> putInt(key, value as Int) Long::class -> putLong(key, value as Long) Float::class -> putFloat(key, value as Float) String::class -> putString(key, value as String) else -> throw UnsupportedOperationException("not supported preferences type") } } inline fun <reified T : Any> SharedPreferences.getValue(key: String, defaultValue: T): T { return when (T::class) { Boolean::class -> getBoolean(key, defaultValue as Boolean) as T Int::class -> getInt(key, defaultValue as Int) as T Long::class -> getLong(key, defaultValue as Long) as T Float::class -> getFloat(key, defaultValue as Float) as T String::class -> getString(key, defaultValue as String) as T else -> throw UnsupportedOperationException("not supported preferences type") } }
By the way this is a fully functional Preferences class that is using Kotlin features to detect the data type and invoke it's related put or get method
then we declare it's Dependency Provider the same way we did with Server and Database :
private const val NAME = "PREFERENCES_NAME" val preferencesGateway by lazy { Preferences(Domain.application.getSharedPreferences(NAME, MODE_PRIVATE)) }
And so the end result will be as follows :
private const val NAME = "PREFERENCES_NAME" val preferencesGateway by lazy { Preferences(Domain.application.getSharedPreferences(NAME, MODE_PRIVATE)) } class Preferences(val sharedPreferences: SharedPreferences) { ... }
And So on ...
Clean Architecture Rules
For Data Source classes, they are not part of our testable code, so they MUST be as dummy as possible, they should not hold any thing other than getting and setting data to the target data source
Kotlin Techniques
We provide Dependencies through a lazy value, each new Data Source needs a one line above it declaring the lazy value that provides it's singleton dependency
And As Clean Architecture prevents the Domain Layer to access Presentation layer, we need an Integration class (declared in the domain layer) that is passed the Application Context when the Application is Created, so all Domain layer classes can access it
Repositories
We must elaborate some rules here first
Clean Architecture Rules
If you are not using a Testing framework, you must declare the repository as an interface, because this is the class that will be mocked in your Unit tests, and usually declare Repositories as interfaces even if you use a testing framework because repositories are usually accessed from multiple Inter-actors / Use-cases, to avoid being tightly coupled with the implementation in the production code
Also Repositories are NOT TEST-ABLE, and this means they should not hold ANY logic of any kind, They hold just getters and setters to data from and to the data sources
We use repositories as a layer between our Inter-actors / Use-cases (business-logic) and the data sources, So there responsibilities is to make it easy to change data sources while keeping our code the same ... and this is the Single Responsibility for Repositories
Important note to mention here as well, Repositories are Stateless, which means that it does not store any changing data in it's variables, it's variables should be final / "val"s, and they are limited to the data sources only
Kotlin Techniques
Following the same techniques with Data Sources, we will apply them to repositories
First we declare our Repository interfaces :
interface AuthenticationRepository { suspend fun login(...) suspend fun register(...) suspend fun forgotPassword(...) ... } interface HistoryRepository { suspend fun searchHistory(...) : List<HistoryRecord> suspend fun requestAllHistory() : List<HistoryRecord> suspend fun loadAllHistoryLocally() : List<HistoryRecord> suspend fun saveAllHistoryLocally(records: List<HistoryRecord>) ... } ...
Then we implement them for production code :
class AuthenticationRepositoryImplementer : AuthenticationRepository { ... }
class HistoryRepositoryImplementer( private val historyServerDataSource : HistoryApis = historyApis, private val searchServerDataSource : SearchApis = searchApis, private val databaseDataSource : AppDatabase = appDatabase ) : HistoryRepository{ suspend fun searchHistory(...) = searchServerDataSource.getHistory(...) suspend fun requestAllHistory() = historyServerDataSource.getAll() suspend fun loadAllHistoryLocally() = databaseDataSource.historyDao().queryAll() suspend fun saveAllHistoryLocally(...) = databaseDataSource.historyDao().insertAll() ... } ...
Notice the Transition of the vocabulary of the code (Domain language), The Repository hides the Data sources specific languages (like queryAll, or insertAll), and use OUR projects own language instead (like search, request, load, save)
In many scenarios we will need to request data from server, then save it locally then return it to be displayed on the UI, this is part of business-logic, and so it should not be done in a Repository, all the repository can do is to declare the methods that can be used to do so, but it hides the implementation detail of what is local or what is remote, yes we know that saveAllHistoryLocally() will save the data locally, but our code does not know weather this is preference or database of even a Hash-Map ... and this is how Repositories act as a Boundary between our code and the Data sources
Now we have to declare the Dependency providers :
val authenticationRepository : AuthenticationRepository by lazy { AuthenticationRepositoryImplementer() } val historyRepository : HistoryRepository by lazy { HistoryRepositoryImplementer() }
So now the final code will look like this :
val authenticationRepository : AuthenticationRepository by lazy { AuthenticationRepositoryImplementer() } interface AuthenticationRepository { ... } class AuthenticationRepositoryImplementer : AuthenticationRepository { ... } val historyRepository : HistoryRepository by lazy { HistoryRepositoryImplementer() } interface HistoryRepository { ... } class HistoryRepositoryImplementer( private val historyServerDataSource : HistoryApis = historyApis, private val searchServerDataSource : SearchApis = searchApis, private val databaseDataSource : AppDatabase = appDatabase ) : HistoryRepository { ... } ...
Inter-actors / Use-cases
These Objects / Functions hold the code that invokes any logic in our app, they hold the business-rules ... when ever we have a User Story with requirements, those Objects are the ones that will hold the logic to fulfill the requirements of those stories
They are responsible to take Input and process the logic, get some data from Repositories, maybe call another Use-cases, and then return the output to be displayed to the UI (or maybe just notify there caller that they finished)
we try to achieve 100% Test coverage for this part of our code, actually they are the core of our project, So the cycle goes like this :
Presenter/ViewModel -> Use-case -> Repository Presenter/ViewModel <- Use-case <- Repository
And as said, inside the Use-case, we can invoke another Use-cases, or any validation logic, etc...
Clean Architecture Rules
Inter-actors / Use-cases should be stateless, Although they invoke Repository functions which has side-effects, but Use-cases them selves should not have side-effects, if we mock the repository, every time we pass them the same input, they should return the exact output ... they do not have variables inside them that may affect the result on next execution
They must be implement interfaces to be able to replace them with mocks while testing, In Java 8 and RxJava we have the following interfaces with one public method, predefined for us :
- Runnable / Action: take no parameters and return void
- Callable: take no parameters but returns a value
- Consumer: take a parameter but returns void
- BiConsumer: take 2 parameters but returns void
- Function: take a parameter and return a value
- BiFunction: take 2 parameters and return a value
- Function3: take 3 parameters and return a value
Kotlin Techniques
In Kotlin we can declare Inter-actors / Use-cases as functions, the following is the functional signature for the Java counter parts :
- Runnable / Action: () -> Unit
- Callable: () -> T
- Consumer: (T) -> Unit
- BiConsumer: (T1, T2) -> Unit
- Function: (T) -> R
- BiFunction: (T1, T2) -> R
- Function3: (T1, T2, T3) -> R
Although Kotlin way is much more flexible, many developers still feel that the syntax is strange when they declare variables types as functions ... so let us practice this here :D
So for every Inter-actor / Use-case, we declare it as a function, and pass it the repositories as default parameters, as follows :
// Login User Story @Throws(ValidationException::class) suspend fun executeLogin( userName: String, password: String, repository: AuthenticationRepository = authenticationRepository ) { ... } // Register User Story @Throws(ValidationException::class) suspend fun executeRegister( userName: String, password: String, repository: AuthenticationRepository = authenticationRepository ) { ... } // Show History User Story suspend fun executeShowHistory( repository: HistoryRepository = historyRepository ) : List<HistoryRecord> { // request history then save it // or load local history on error ... } ...
And then we can replace the "repository" parameter with a fake one in Unit testing, and make sure that all our Inter-actors / Use-cases are tested and working, in isolation of the rest of the Application
Another important rule to put in mind, all the code should be invoked in a blocking manner, as Inter-actors / Use-cases will be executed in background by there caller, so you do not need to handle concurrency in these functions, which makes them easy to test
Presentation Layer (View-Models, Presenters, etc...)
For these classes, all the dependencies that they know are the Inter-actors, so a View-Model will look like this :
class LoginViewModel( val login: suspend (String, String) -> Unit = { userName, password -> executeLogin(userName, password) }, val register: suspend (String, String) -> Unit = { userName, password -> executeRegister(userName, password) } ) : ViewModel() { ... fun onLoginClicked(userName: String, password: String) { viewModelScope.launch { login(userName, password) } } ... } class HistoryViewModel( val showHistory: suspend () -> List<HistoryRecord> = { executeShowHistory() } ) : ViewModel() { ... }
The above View-Models will work fine when loading them through ViewModelProviders, no Factory needed, and at the same time, unit testing them is pretty simple, we replace the Inter-actor / Use-case with a fake one that returns the expected result ... and we do not need to replace all of them because they are functions, they do nothing unless they are invoked ... which is considered as lazy initialization as well
Summery
- For Data Sources, Every Data source will be provided by a global value that is lazily initialized
- For Repositories, each repository interface will have a global lazy value that provides it's production implementation
- For Inter-actors / Use-Cases, they will declare the repositories dependencies in there default parameters, and use the production version, while in unit testing, we can replace those default parameters with Fake repositories ... Inter-actors are our major concern while we are unit testing
- For Presenters/View-Models, they use only Inter-actors/Use-Cases, and they declare them as default parameters, where we can replace them with fake ones while unit testing
- For Activities and Fragments, they will always initialize no-args Presenters/View-Models in production code, while they can be passed Presenters/View-Models with fake Inter-actors in Integration tests
This article is more about Software Design and Architecture, since Dependency Injection is a Design decision, instead of digging deep into a DI framework that is not part of your project, and coupling your code to that framework and keep updating your code every time this framework is updated, you can avoid this coupling if you follow the Other Design decisions mentioned here, which are based on Clean Architecture ... I highly recommend watching this video
Staff Software Engineer - Mobile at Talabat | Delivery Hero
4yWell organized and easy to ready. Really helpful. Thank you Ahmed Adel Ismail for the very useful article 😊
--
4yThis will not work well. Through the chains of lazy, you will actually invoke Room..build() on Main thread in ViewModel ctor.
Android Lead @ Seidor Opentrends ♦ Kotlin enthusiastic ♦ Continuous evolving ♦ Native apps ♦ Moving towards Kotlin Mobile Multiplatform
5yShould not be Room and Retrofit inside the framework layer instead of Data layer?
Android Lead @ Seidor Opentrends ♦ Kotlin enthusiastic ♦ Continuous evolving ♦ Native apps ♦ Moving towards Kotlin Mobile Multiplatform
5yAmazing article! Clear, concise and really well explained. Thanks a lot Ahmed.