Share
Embed
Android dependency injection in 2022
Slides for the talk at the Dutch Android Usergroup
https://www.meetup.com/dutch-aug/events/284161724/
Hugo Visser
March 03, 2022
Transcript
60 slides
-
Example class MyClass { fun sendEmail() { val mailer =
Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Example class MyClass { fun sendEmail() { val mailer =
Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Example class MyClass { fun sendEmail() { val mailer =
MailerFactory.createConfiguredMailer() mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Dependency injection class MyClass(private val mailer: Mailer) { fun sendEmail()
{ mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Dependency inversion class Mailer { fun setHost(host: String) { //
set the host for the SMTP server } fun sendEmail(subject: String, to: String, body: String) { // call some SMTP server } } -
Dependency inversion interface Mailer { fun sendEmail(subject: String, to: String,
body: String) } class SMTPMailer(private val host: String): Mailer { override fun sendEmail(subject: String, to: String, body: String) { // call some SMTP server } } class NoOpMailer: Mailer { override fun sendEmail(subject: String, to: String, body: String) { // nothing! } } -
It still works! class MyClass(private val mailer: Mailer) { fun
sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Configuring Mailer & MyClass // For debug builds create a
dummy Mailer fun createMailer() = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } val myClass = MyClass(createMailer()) myClass.sendEmail() -
Recap Dependency injection: “push” vs “pull” → supply dependency to
a class Benefits: separation of concerns, configuration of a dependency Dependency inversion: abstraction to not depend on a specific implementation Benefits: switch implementation for different configurations / tests etc -
Dependency injection frameworks • Reduce & standardise configuration code •
Manage scoping → singletons, lifecycle • Validate configuration • Qualifiers & other useful tools -
Dagger (2) • javax.Inject • Code generation, compile time checking
• Java • Not specific to Android https://dagger.dev -
Dagger injection class MyClass @Inject constructor(private val mailer: Mailer) {
fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Dagger injection class MyClass @Inject constructor(private val mailer: Mailer) {
fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Dagger injection (framework classes) class TestActivity: FragmentActivity() { @Inject lateinit
var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { //TODO injection super.onCreate(savedInstanceState) } } -
Module (dependency configuration) @Module class AppModule { @Provides fun provideMailer():
Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } -
Module • When Dagger does not have enough information to
instantiate a class • When injecting an interface • When conditional logic is needed • To add/override scoping -
Component (injector) @Component(modules = [AppModule::class]) interface AppComponent { // this
is the activity we are going to inject in fun inject(activity: TestActivity) // other targets... } -
Android setup: create injector class MyApplication: Application() { private lateinit
var appComponent: AppComponent override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent.builder().build() } fun getAppComponent(): AppComponent = appComponent } -
Perform injection class TestActivity: FragmentActivity() { @Inject lateinit var myClass:
MyClass override fun onCreate(savedInstanceState: Bundle?) { (application as MyApplication).getAppComponent().inject(this) super.onCreate(savedInstanceState) } } -
Problems • Android lifecycle + injecting at the “right” moment
• More complex setup when scoping dependencies to lifecycle (components/subcomponents) • Many ways to setup Dagger -
Koin • Injection library written in Kotlin • Runtime evaluation
• Not specific to Android https://insert-koin.io/ -
Koin injection / lookup class TestActivity : FragmentActivity() { private
val myClass: MyClass by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } } -
Koin injection / lookup class TestActivity : FragmentActivity() { private
val myClass: MyClass by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } } -
Module (dependency configuration) val mailerModule = module { factory {
if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } // A factory is needed for MyClass too factory { MyClass(get()) } } -
Module (dependency configuration) val mailerModule = module { factory {
if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } // A factory is needed for MyClass too factory { MyClass(get()) } } -
Android setup: create injector class MyApplication : Application() { override
fun onCreate() { super.onCreate() startKoin { androidContext(this@MyKoinApplication) modules(mailerModule) } } } -
Koin recap • Easier setup vs pure Dagger w/ dsl
• Runtime vs compile time validation • In essence still “pulling” dependencies, tighter coupling vs annotations (testing?) • Scoping is possible, but manual like w/ Dagger -
Hilt • Opinionated Dagger setup for Android apps • Standard
components • Gradle plugin replaces manual setup tasks • Collecting of modules across modules and packages • Android integrations for view model, workmanager https://developer.android.com/training/dependency-injection/hilt-android -
Hilt injection (same as Dagger) class MyClass @Inject constructor(private val
mailer: Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {
@Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } } -
Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {
@Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } } -
Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {
@Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } } -
Module (dependency configuration) @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides fun
provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } -
Module (dependency configuration) @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides fun
provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } -
Components & scopes • Components define availability of dependencies •
Scopes define the scope of dependencies -
ViewModel support @HiltViewModel class MyViewModel @Inject constructor( private val myClass:
MyClass ) : ViewModel() { fun sendEmail() { myClass.sendEmail() } } -
ViewModel support @AndroidEntryPoint class ViewModelActivity : FragmentActivity() { private val
viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.sendEmail() } // already overridden by Hilt override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory { return super.getDefaultViewModelProviderFactory() } } -
Hilt hits the sweetspot • Simple setup • Compile time
safety • @Inject vs by inject() • Android constructs like viewModels() just work • Built-in Android aware scoping (if you need it) • Aggregation of modules (Koin annotations also introduced this) -
Don’t scope unless you need to @Module @InstallIn(SingletonComponent::class) class AppModule
{ @Provides @Singleton // there's probably no need to make Mailer singleton here fun provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } -
Don’t scope unless you need to @Module @InstallIn(SingletonComponent::class) class AppModule
{ @Provides @Singleton // there's probably no need to make Mailer singleton here fun provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } } -
Inversion is nice, but… • Dependency inversion is great, but
adds overhead of interfaces • Start out with a concrete implementation, extract interface when needed -
Prefer typed qualifiers class MyClass @Inject constructor(@Named("smtp") private val mailer:
Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Prefer typed qualifiers class MyClass @Inject constructor(@Named("smtp") private val mailer:
Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Prefer typed qualifiers @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides @Named("smtp")
fun provideSmtpMailer(): Mailer = SMTPMailer("mail.mydomain.nl") } -
Prefer typed qualifiers @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides @SmtpMailer
fun provideSmtpMailer(): Mailer = SMTPMailer("mail.mydomain.nl") } @Qualifier annotation class SmtpMailer -
Prefer typed qualifiers class MyClass @Inject constructor(@SmtpMailer private val mailer:
Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } } -
Restrict “generic” dependencies @Provides @Singleton fun provideMoshiForNetwork(): Moshi { //
setup moshi return Moshi.Builder.build() } @Provides fun provideService(moshi: Moshi): SomeService { // ... create service with moshi } -
Restrict “generic” dependencies @Module @InstallIn(SingletonComponent::class) class NetworkModule { private val
moshi = Moshi.Builder.build() @Provides fun provideService(): SomeService { // ... create service with moshi } } -
Restrict “generic” dependencies @Module @InstallIn(SingletonComponent::class) class NetworkModule { @Provides @NetworkMoshi
fun provideMoshi(): Moshi { } @Provides @NetworkMoshi fun provideService(): SomeService { } } @Qualifier internal annotation class NetworkMoshi