Everybody used a form of DI in their projects, Android developers use(d) Dagger2/Hilt at least once in their careers.
Throughout this experience, your life was simplified and just one @Inject
saved you from writing boilerplate code. This wasn’t always the case, you needed to add something dynamically in one of your dependencies, this is where @AssitedInject
comes into play.
An 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 (you).
The assisted injection uses a factory to provide your assisted dependency, the steps are as follows:
- Annotate your dependency with
@AssitedInject
- Provide the dependencies that can be automatically wired by the DI library
- Annotate your dynamically added dependencies with
@Assisted
and provide them with a name if needed - Create a factory for your dependency annotated with
@AssistedFactory
and a function that creates and returns your assisted dependency
To facilitate the aforementioned steps, in this blog post you’ll now build a reusable one shot shared preferences dependency.
In order to have our “OneTimePreference”, we create a common contract so that each dependency that implements it will behave as agreed.
interface OneTimePrefContract {
val isOneTimeShown: Boolean
fun setOneTimeShown()
val oneTimePrefs: SharedPreferences
}
The real implementation comes in a form of an “assisted” dependency that implements the contract and is provided from a factory.
class OneTimePref @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 fun setOneTimeShown() = oneTimePrefs.edit { putBoolean(prefsBooleanKey, true) }
}
As you see the factory provides the same assisted parameters that are needed in order for the assisted inject to happen while having an external dependency from the outside as with our application context.
You can now reuse it anywhere.
@AndroidEntryPoint
class WalkThroughFragment : Fragment (){
@Inject
lateinit var oneTimePrefFactory : OneTimePref.OneTimePrefFactory
private val walkThroughPreferences : OneTimePref by lazy {
oneTimePrefFactory.create("walkthrough-prefs", "walkthrough-isShown") // consider using constants, this is for demonstration purposes only
}
}
Congratulations, you’ve learned @AssistedInject
There is one limitation by the DI framework and one big issue with this code.
@AssistedInject
dependencies can’t be scoped- This is a lot of boilerplate to write
In order to write less boilerplate Kotlin’s delegation is one hell of a powerful tool to know and we want our dependency to be scoped to the lifecycle of a Fragment (for demonstration purposes).
@FragmentScoped
class WalkThroughPrefsProvider @Inject constructor(
private val oneTimePrefFactory: OneTimePref.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"
}
}
Now you can go around injecting your WalkThroughPrefsProvider
and having more readable code.
@AndroidEntryPoint
class WalkThroughFragment : Fragment (){
@Inject
lateinit var oneTimePrefFactory : WalkThroughPrefsProvider
}
The code is publicly available as a Gist.