Assisted Inject for less boilerplate?
Lives of so many developers were easy before @AssistedInject
came to Dagger2
, they still are, but as usual Dagger2
is explained with so many complications that even an experienced developer can go bonkers, when in fact it’s dead simple.
Assisted injection is a dependency injection (DI) pattern that is used to construct an object where some parameters may be provided by the DI framework and others must be passed in at creation time (a.k.a “assisted”) by the developer.
A factory is typically responsible for combining all of the parameters and creating the object, as you would generally do with a factory if you were building a component where you bind (using @BindsInstance
) the dependencies that are used (passed down) in that component, you can take a look at Dagger Part #5 where we created the SingletonComponent and other components.
The building blocks of a dependency that you can inject with an assistance from the DI library (Dagger), consists of:
- The dependency itself, annotated with
@AssistedInject constructor()
- The factory (interface), annoated with
@AssistedFactory
- Create (or better named method by yourself) within the factory that returns the assisted preference that also accepts the same function parameters as the ones you’ll need to assist your DI library with, in order to create your assisted dependency.
Today you’ll build something you’ve used for sure, one time shared preference manager, on steroids.
This article was written to use shared preference instead of Data store for the sake of brevity.
We create a contract for our one time preference manager, that way we can just swap out the functionality later down the road if we decide to migrate to “Data store”.
1
2
3
4
5
6
interface OneTimePrefContract {
fun setOneTimeShown()
val oneTimePrefs: SharedPreferences
val isOneTimeShown: Boolean
val isOneTimeNotShown : Boolean
}
Our manager would be something simple
1
2
3
4
5
class OneTimePreferenceManager @AssistedInject constructor(
@ApplicationContext private val context: Context,
@Assisted private val prefsTag: String,
@Assisted private val prefsBooleanKey: String
)
The context is provided by the DI, the tag and the key used for the shared preferences would be assisted by you.
This sample is using two @Asssited
parameters of the same type, in this case Dagger2
doesn’t know which is which, for this case we have to name them, similarly to @Named
but just passing a name parameter at @Assisted
.
1
2
3
4
5
6
7
8
9
10
11
class OneTimePreferenceManager @AssistedInject constructor(
@ApplicationContext private val context: Context,
@Assisted(PREFS_TAG_KEY) private val prefsTag: String,
@Assisted(PREFS_BOOLEAN_KEY) private val prefsBooleanKey: String
) {
private companion object {
private const val PREFS_TAG_KEY = "prefsTag"
private const val PREFS_BOOLEAN_KEY = "prefsBoolean"
}
}
Now Dagger2
is satisfied with your sacrifice.
In order to have our OneTimePreferenceManager
provided everywhere else, we need a factory, go ahead and implement the contract and create a factory as follows
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
class OneTimePreferenceManager @AssistedInject constructor(
@ApplicationContext private val context: Context,
@Assisted(PREFS_TAG_KEY) private val prefsTag: String,
@Assisted(PREFS_BOOLEAN_KEY) private val prefsBooleanKey: String
) : OneTimePrefContract {
private companion object {
private const val PREFS_TAG_KEY = "prefsTag"
private const val PREFS_BOOLEAN_KEY = "prefsBoolean"
}
@AssistedFactory
interface OneTimePrefFactory {
fun create(
@Assisted(PREFS_TAG_KEY) prefsTag: String,
@Assisted(PREFS_BOOLEAN_KEY) prefsBooleanKey: String
): OneTimePref
}
override val oneTimePrefs: SharedPreferences
get() = context.getSharedPreferences(
prefsTag,
Context.MODE_PRIVATE
)
override val isOneTimeShown get() = oneTimePrefs.getBoolean(prefsBooleanKey, false)
override val isOneTimeNotShown get() = !isOneTimeShown
override fun setOneTimeShown() = oneTimePrefs.edit { putBoolean(prefsBooleanKey, true) }
}
Inside let’s say your DashboardFragment
when your user has opened the dashboard for the first time and you want to show him how to use it through some steps.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@AndroidEntryPoint
class DashboardFragment : Fragment() {
@Inject
lateinit var oneTimePrefFactory: OneTimePreferenceManager.OneTimePrefFactory
private val walkThroughPrefs by lazy { oneTimePrefFactory.create("walk-through", "isWalkThroughShown") }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (walkThroughPrefs.isOneTimeNotShown){
showWalkThrough()
}
}
}
As you no doubt have guessed by now, there are limitations that you have to deal with when using @AssistedInject
@AssistedInject
types(dependencies) cannot be injected directly, only the@AssistedFactory
can be injected@AssistedInject
types(dependencies) cannot be scoped
Let’s say we want to scope our “WalkThroug One time preference” to a Fragment
, we can’t.
This code
1
2
3
4
5
@Inject
lateinit var oneTimePrefFactory: OneTimePreferenceManager.OneTimePrefFactory
private val walkThroughPrefs by lazy { oneTimePrefFactory.create("walk-through", "isWalkThroughShown") }
is boilerplate-ish to be written, it hurts the eyes (inserts burning eyes meme).
For this purpose we’ll work with what we have been blessed by Hilt
and Kotlin
.
1
2
3
4
5
6
7
8
9
10
11
12
@FragmentScoped
class WalkThroughPrefsProvider @Inject constructor(
private val oneTimePrefFactory: OneTimePreferenceManager.OneTimePrefFactory
) : OneTimePrefContract by oneTimePrefFactory.create(
WALK_THROUGH_PREFS,
WALK_THROUGH_PREFS_SHOWN_KEY
) {
private companion object {
private const val WALK_THROUGH_PREFS = "walkThrough"
private const val WALK_THROUGH_PREFS_SHOWN_KEY = "walkThroughKey"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@AndroidEntryPoint
class DashboardFragment : Fragment() {
@Inject
lateinit var walkThroughPrefsProvider: WalkThroughPrefsProvider
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (walkThroughPrefsProvider.isOneTimeNotShown){
showWalkThrough()
}
}
}
That’s all for this blog post, stay tuned for more.