Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android dependency injection in 2022

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
Tweet

More Decks by Hugo Visser

Other Decks in Technology

Transcript

  1. Example class MyClass { fun sendEmail() { val mailer =

    Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  2. Example class MyClass { fun sendEmail() { val mailer =

    Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  3. Example class MyClass { fun sendEmail() { val mailer =

    MailerFactory.createConfiguredMailer() mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  4. Dependency injection class MyClass(private val mailer: Mailer) { fun sendEmail()

    { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  5. 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 } }
  6. 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! } }
  7. It still works! class MyClass(private val mailer: Mailer) { fun

    sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  8. 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()
  9. 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
  10. Dependency injection frameworks • Reduce & standardise configuration code •

    Manage scoping → singletons, lifecycle • Validate configuration • Qualifiers & other useful tools
  11. Dagger (2) • javax.Inject • Code generation, compile time checking

    • Java • Not specific to Android https://dagger.dev
  12. Dagger injection class MyClass @Inject constructor(private val mailer: Mailer) {

    fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  13. Dagger injection class MyClass @Inject constructor(private val mailer: Mailer) {

    fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  14. Dagger injection (framework classes) class TestActivity: FragmentActivity() { @Inject lateinit

    var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { //TODO injection super.onCreate(savedInstanceState) } }
  15. Module (dependency configuration) @Module class AppModule { @Provides fun provideMailer():

    Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } }
  16. 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
  17. Component (injector) @Component(modules = [AppModule::class]) interface AppComponent { // this

    is the activity we are going to inject in fun inject(activity: TestActivity) // other targets... }
  18. 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 }
  19. Perform injection class TestActivity: FragmentActivity() { @Inject lateinit var myClass:

    MyClass override fun onCreate(savedInstanceState: Bundle?) { (application as MyApplication).getAppComponent().inject(this) super.onCreate(savedInstanceState) } }
  20. Problems • Android lifecycle + injecting at the “right” moment

    • More complex setup when scoping dependencies to lifecycle (components/subcomponents) • Many ways to setup Dagger
  21. Koin • Injection library written in Kotlin • Runtime evaluation

    • Not specific to Android https://insert-koin.io/
  22. Koin injection / lookup class TestActivity : FragmentActivity() { private

    val myClass: MyClass by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } }
  23. Koin injection / lookup class TestActivity : FragmentActivity() { private

    val myClass: MyClass by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } }
  24. 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()) } }
  25. 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()) } }
  26. Android setup: create injector class MyApplication : Application() { override

    fun onCreate() { super.onCreate() startKoin { androidContext(this@MyKoinApplication) modules(mailerModule) } } }
  27. 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
  28. 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
  29. 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!" ) } }
  30. Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {

    @Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } }
  31. Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {

    @Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } }
  32. Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {

    @Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) myClass.sendEmail() } }
  33. Module (dependency configuration) @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides fun

    provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } }
  34. Module (dependency configuration) @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides fun

    provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } }
  35. ViewModel support @HiltViewModel class MyViewModel @Inject constructor( private val myClass:

    MyClass ) : ViewModel() { fun sendEmail() { myClass.sendEmail() } }
  36. 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() } }
  37. 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)
  38. 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") } }
  39. 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") } }
  40. Inversion is nice, but… • Dependency inversion is great, but

    adds overhead of interfaces • Start out with a concrete implementation, extract interface when needed
  41. 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!" ) } }
  42. 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!" ) } }
  43. Prefer typed qualifiers @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides @SmtpMailer

    fun provideSmtpMailer(): Mailer = SMTPMailer("mail.mydomain.nl") } @Qualifier annotation class SmtpMailer
  44. Prefer typed qualifiers class MyClass @Inject constructor(@SmtpMailer private val mailer:

    Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  45. Restrict “generic” dependencies @Provides @Singleton fun provideMoshiForNetwork(): Moshi { //

    setup moshi return Moshi.Builder.build() } @Provides fun provideService(moshi: Moshi): SomeService { // ... create service with moshi }
  46. Restrict “generic” dependencies @Module @InstallIn(SingletonComponent::class) class NetworkModule { private val

    moshi = Moshi.Builder.build() @Provides fun provideService(): SomeService { // ... create service with moshi } }
  47. Restrict “generic” dependencies @Module @InstallIn(SingletonComponent::class) class NetworkModule { @Provides @NetworkMoshi

    fun provideMoshi(): Moshi { } @Provides @NetworkMoshi fun provideService(): SomeService { } } @Qualifier internal annotation class NetworkMoshi