Dagger2 is hard, but it can be easy, part 7
In the previous post we’ve encountered when’s the right place to inject, named and qualifiers.
In this post we’ll learn about multi bindings and their power.
They’re your best buddies in the Dagger world.
There’s only two of them Set
and Map
multi bindings and as you might’ve guessed they’re indeed collections.
Go ahead and include
1
2
3
4
5
6
7
implementation "com.squareup.retrofit2:converter-moshi:$retrofit"
implementation "com.squareup.retrofit2:converter-gson:$retrofit"
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation "com.squareup.moshi:moshi-kotlin:$moshi"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
Retrofit’s version at the moment of writing this blog is “2.9.0” and “moshi” is 1.12.0
Go ahead and create a Retrofit module and Interceptors module
1
2
3
4
@Module
object RetrofitModule {
}
1
2
3
4
@Module
object InterceptorsModule{
}
don’t forget to include the modules in the singleton component
1
2
3
4
5
6
@Component(modules = [RetrofitModule::class, InterceptorsModule::class]) //<-this here
@Singleton
interface SingletonComponent
.
.
.
Create an interface called TestApi
1
2
3
4
5
6
7
8
9
10
interface TestApi {
@GET("posts")
suspend fun getPostsAdapter(): Response<List<TestModel>>
companion object {
const val BASE_URL = "https://jsonplaceholder.typicode.com/"
}
}
and a TestModel
which will get the response from the url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TestModel(
@Json(name = "body")
val body: String,
@Json(name = "id")
val id: Int,
@Json(name = "title")
val title: String,
@Json(name = "userId")
val userId: Int
)
we’re using moshi codegen for the json transformation
Let’s create some Intereceptor
s
1
2
3
4
5
6
7
class ContentTypeInterceptor(private val contentType: String = "application/json") : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain
.proceed(with(chain.request().newBuilder()) {
header("Content-Type", contentType).build()
})
}
1
2
3
class EmptyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
}
A bootleg retry interceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RetryRequestInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response = chain.proceed(request)
var tryCount = 0
while (!response.isSuccessful && tryCount < 3) {
tryCount++
response = chain.proceed(request)
}
return response
}
}
and
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UnauthorizedInterceptor(private val customMessage: String? = null) : Authenticator {
companion object {
private const val unAuthorized = 401
}
override fun authenticate(route: Route?, response: Response): Request {
if (!response.isSuccessful && response.code == unAuthorized)
throw UnauthorizedException(customMessage)
return response.request
}
class UnauthorizedException(private val customMessage: String?) : IOException() {
override val message: String
get() = customMessage ?: "Un-authorized, please check credentials or re-login/authorize"
}
}
Let’s populate our Retrofit
module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Module
object RetrofitModule {
@Singleton
@Provides
fun moshiConverterFactory() = MoshiConverterFactory.create()
@Singleton
@Provides
fun okHttpClientConfiguration(
interceptors: Set<@JvmSuppressWildcards Interceptor>
): OkHttpClient {
val timeout = 10L //even this can be provided
val timeUnit = TimeUnit.SECONDS //even this too, but for the sake of keeping this short we aren't
val client = OkHttpClient().newBuilder()
.apply {
connectTimeout(timeout, timeUnit)
callTimeout(timeout, timeUnit)
readTimeout(timeout, timeUnit)
writeTimeout(timeout, timeUnit)
}
interceptors.forEach {
client.addInterceptor(it)
}
return client.build()
}
@Provides
@Singleton
fun retrofitClient(
moshiConverterFactory: MoshiConverterFactory,
okHttpClient: OkHttpClient
) = Retrofit.Builder()
.addConverterFactory(moshiConverterFactory)
.client(okHttpClient)
.baseUrl(TestApi.BASE_URL)
.build().create<TestApi>()
}
and our Interceptors
module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Module
object InterceptorsModule {
@Provides
@IntoSet
@Singleton
fun contentTypeInterceptor() = ContentTypeInterceptor()
@Provides
@IntoSet
@Singleton
fun unauthorizedInterceptor() = UnauthorizedInterceptor()
@Provides
@IntoSet
@Singleton
fun emptyInterceptor() = EmptyInterceptor()
@Provides
@IntoSet
@Singleton
fun retryInterceptor() = RetryRequestInterceptor()
@Provides
@Singleton
@IntoSet
//this one we don't create don't be confused
fun httpLoggingInterceptor() :Interceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
Set
The aforementioned example is just creating a Retrofit Client that’s all, the new comer you’re seeing here is @IntoSet
Once you get creating a multi module project, the Set
multibindings (@Intoset) is really coming in handy, especially when decoupling of the implementation by the type you’re gonna use.
It can scale from so called Initializers
(that hide away the initialization logic), Adapters
that bind some data and in our example Interceptor
s and it doesn’t stop here…
What @IntoSet
does is, it creates a Set
for you of the type you’re providing, in this example you can also levarage the set for providing the MoshiConverterFactory
with some Adapters
of your own that can lie in a separate module called :moshi-adapters
or whatever you named them, but for the sake of this example let’s keep it short, hopefully by now you’ve realized the power.
The part that you should know is that if you’ve provided one thing into the graph you can use it as a parameter into a function.
As you see our Client configuration function that provides an OkHttpClient
1
2
3
fun okHttpClientConfiguration(
interceptors: Set<@JvmSuppressWildcards Interceptor>
)
accepts set of interceptors from other Dagger module (InterceptorsModule
) but they’re brought together with @Component(modules = [RetrofitModule::class, InterceptorsModule::class])
, also there’s this @JvmSuppressWildcards
, the explanation of why’s this happening can be a bit confusing, let’s keep it simple:
- We’re injecting
Set<Interceptor>
, what Kotlin does is it generates Java code - The java code looks like
Set<? extends Interceptor>
where as you know?
is a wild card, Dagger gets confused - If we add
@JvmSuppressWildcards
that generated code gets transformed intoSet<Interceptor>
and Dagger isn’t confused anymore into the Kotlin world.
Now this approach of Set gets to keep your Interceptor
s into a separate module and this scales really good because all you have to do is include a parameter of Set in your function that provides a dependency maybe coming from a module that you once wrote and reused at multiple times with other modules than our IntereceptorsModule
.
Map
@IntoMap
binds a provided dependency into a Map collection using a key, Dagger comes with:
@StringKey
annotation which binds a dependency to aString
key@ClassKey
which binds it to a class@MapKey
for keys that are of type enums or parameterized classes (your own)
Go ahead and create a TestViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TestViewModel @Inject constructor(private val testApi: TestApi) : ViewModel() {
private val listData: MutableStateFlow<SimpleResult<List<TestModel>>> = MutableStateFlow(SimpleResult.Loading)
val list = listData.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
listData.value = try {
transformPosts(testApi.getPostsAdapter())
} catch (throwable: Throwable) {
SimpleResult.Exception(throwable)
}
}
}
private fun transformPosts(response: Response<List<TestModel>>): SimpleResult<List<TestModel>> =
if (response.isSuccessful) {
SimpleResult.Success(response.body() ?: emptyList())
} else {
SimpleResult.ApiError(response.code(), response.errorBody())
}
sealed class SimpleResult<out T> {
data class Exception(val throwable: Throwable) : SimpleResult<Nothing>()
data class ApiError(val responseCode: Int, val errorBody: ResponseBody?) : SimpleResult<Nothing>()
data class Success<T>(val data: T) : SimpleResult<T>()
object Loading : SimpleResult<Nothing>()
}
}
We’re including our testApi : TestApi
using constructor injection and just calling an api to get the result, don’t forget to add the Internet
permission <uses-permission android:name="android.permission.INTERNET"/>
inside your manifest.
Before you move onto the next part add
1
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle"
where “lifecycle” is ‘2.4.0-alpha01’ or better, to add a way to collect flows safely without leaking resources by using addRepeatingJob
.
For the sake of this demonstration into your activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
inside MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MainActivity : AppCompatActivity() {
init {
addOnContextAvailableListener { availableContext->
injector {
activityComponentFactory().create(availableContext).also { it.inject(this@MainActivity) }
}
}
}
@Inject
lateinit var powerReceiver: PowerReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED).also {
it.addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(powerReceiver,filter)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, HomeFragment(), "HomeFragment")
.commit()
}
}
}
Inside your HomeFragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class HomeFragment : Fragment(R.layout.home_fragment) {
override fun onAttach(context: Context) {
injector {
fragmentComponentFactory().create().inject(this@HomeFragment)
}
super.onAttach(context)
}
private val testViewModel by viewModels<TestViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED){
testViewModel.list.collect {
handleAPICall(it)
}
}
}
private fun handleAPICall(simpleResult: TestViewModel.SimpleResult<List<TestModel>>) {
when(simpleResult){
is TestViewModel.SimpleResult.ApiError -> {
Log.d("SimpleResult.ApiError", "Response code ${simpleResult.responseCode}")
}
is TestViewModel.SimpleResult.Exception -> {
when(simpleResult.throwable){
is UnauthorizedInterceptor.UnauthorizedException->{
Log.d("Exception", "Log off")
}
}
}
TestViewModel.SimpleResult.Loading -> {
Log.d("SimpleResult.Loading", "SHOW SPINNER")
}
is TestViewModel.SimpleResult.Success -> {
val successData = simpleResult.data
Log.d("SimpleResult.Success", successData.toString())
}
}
}
}
now you’re ready to run the application, once you do that
1
java.lang.RuntimeException: Cannot create an instance of class com.example.test.TestViewModel
You might ask yourself why, what did I do wrong? Nothing, it’s Android again complicating things…
The ViewModel can’t be created because Dagger doesn’t know how to provide the dependency to that particular ViewModel of ours, as you know that ViewModels are created by a ViewModelProvider.Factory
we need to tell Dagger about that somehow and also provide a way for it to know how to create our TestViewModel
, which is where Map
multi bindings shine.
First, we need a key that will provide us a class that inherits from ViewModel
1
2
3
4
5
6
7
8
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@MapKey
annotation class ViewModelKey(val kotlinClassKey: KClass<out ViewModel>)
as you can see as we’ve mentioned before that class has a @MapKey
which means it’s something specialized for our use case. The @Target
is specialized where do we apply that annotation to, in our case means that we need to use @Provides
on a function in a module, the key is of a KClass
that has a out
which is a covariant that can accept every kotlin class name that inherits from ViewModel
(we’re restricting this to ViewModel only, instead we could’ve just used Any, but we don’t need that because we might have another key of type Any and confusion… confusion).
Secondly we need a ViewModel.Factory
1
2
3
4
5
6
7
8
9
10
11
12
@Suppress("UNCHECKED_CAST")
@Singleton
class DaggerViewModelFactory @Inject constructor(
private val viewModelMap: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModelMap[modelClass] ?: viewModelMap.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("Unable to locate class $modelClass")
return creator.get() as T
}
}
Thirdly we need a module
1
2
3
4
5
6
7
8
9
10
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(TestViewModel::class)
abstract fun bindTestViewModel(testViewModel: TestViewModel) : ViewModel
@Binds
abstract fun bindViewModelFactory(factory: DaggerViewModelFactory) : ViewModelProvider.Factory
}
You might ask yourself why an abstract class, because the abstract class only accepts an implementation of dependencies which we did already in our case this is TestViewModel
and we return it as a : ViewModel
because our @IntoMap
expects a type of Provider<ViewModel>>
and of course we tell Dagger that the map it gives to our DaggerViewModelFactory
has to have TestViewModel::class
so that it can create our TestViewModel
later on.
For example if you have a class
1
class Car () : Vehicle
and
1
interface Vehicle
inside your abstract module you can do
1
2
3
4
5
@Module
abstract class CarsModule {
@Binds
abstract fun bindCar(car: Car) : Vehicle
}
@Binds
works nearly the same as @Provides
which can only provide dependencies of abstract functions that you already have an implementation for and the way it creates them is way more effecient than @Provides
, which means try to use @Binds
as much as you can, clean architecture picture in 3…2…1…
You might not be familiar with why Provider
is used in this case, sometimes you need multiple instances of the same type to be returned instead of just injecting a single value, meaning that you can have that TestViewModel
inside a HomeFragment
, LoginFragment
and YourNameHereFragment
etc… etc.. As we’ve talked into part 1, Dagger is really one interface and that’s Provider
which has only one function called get()
which gets the dependency.
let’s go step by step:
- Every view model factory inherits from
ViewModelProvider.Factory
which has only onecreate()
method and that method has a parameter of amodelClass: Class<T>
which is our custom map key - Since we have the key from step one, now we need to
@Inject
the map where we have the Key that’s of our class for the ViewModel and a provider which’ll give us an instance of that ViewModel - We access the
viewModelMap[modelClass]
that means we might not have a value for that key, we traverse the map and we check ifmodelClass.isAssignableFrom(it.key)
, meaning that they’re either the same types and we just access the value using.value
creator
is of a typeProvider<ViewModule>
which returned ourViewModel
and all we need to do is callcreator.get()
as we’ve discussed that Providers have only one function- We cast it as
T
because thecreate()
function expects a typed parameter
With this we’ve created a generic ViewModelProvider.Factory
that we can use now to create our TestViewModel
Now we need to head out for our SingletonComponent
and include the ViewModelModule
1
2
3
4
5
6
7
8
@Component(modules = [RetrofitModule::class, InterceptorsModule::class, ViewModelModule::class])
@Singleton
interface SingletonComponent {
.
.
.
}
The only thing we need to change now in our HomeFragment
is
1
2
3
4
5
6
@Inject
lateinit var daggerViewModelFactory: ViewModelProvider.Factory
private val testViewModel by viewModels<TestViewModel>(){
daggerViewModelFactory
}
which means now dagger knows how to create our ViewModel using the factory we created leveraging the by viewModels
delegate that expects a creator of a type factory.
Our HomeFragment
can look like
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class HomeFragment : Fragment(R.layout.home_fragment) {
override fun onAttach(context: Context) {
injector {
fragmentComponentFactory().create().inject(this@HomeFragment)
}
super.onAttach(context)
}
@Inject
lateinit var daggerViewModelFactory: ViewModelProvider.Factory
private val testViewModel by viewModels<TestViewModel>(){
daggerViewModelFactory
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED){
testViewModel.list.collect {
handleAPICall(it)
}
}
}
private fun handleAPICall(simpleResult: TestViewModel.SimpleResult<List<TestModel>>) {
when(simpleResult){
is TestViewModel.SimpleResult.ApiError -> {
Log.d("SimpleResult.ApiError", "Response code ${simpleResult.responseCode}")
}
is TestViewModel.SimpleResult.Exception -> {
when(simpleResult.throwable){
is UnauthorizedInterceptor.UnauthorizedException->{
Log.d("Exception", "Log off")
}
}
}
TestViewModel.SimpleResult.Loading -> {
Log.d("SimpleResult.Loading", "SHOW SPINNER")
}
is TestViewModel.SimpleResult.Success -> {
val successData = simpleResult.data
Log.d("SimpleResult.Success", successData.toString())
}
}
}
}
Run it and it works.
You might think there’s nothing wrong with this but there actually is, everything is scoped to @Singleton
, also getting a SavedStateHandle
requires to use AssistedInject
that requires a lot of changing which won’t matter once we start using Hilt from the next part as this is sufficient for today and this blog post is longer than I anticipated also every bit of this informations needs some time for you to stomach it.
From the next blog post, we’re jumping onto the Hilt train and see where it goes, also Hilt in a nutshell
Thank you again for your wholehearted attention and stay tuned for the Hilt blog series.