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

Modern Android dependency injection

Modern Android dependency injection

Slides for my presentation at Appdevcon 2022

A9f24621ff57d380b4e85799edd4a984?s=128

Hugo Visser

June 24, 2022
Tweet

More Decks by Hugo Visser

Other Decks in Technology

Transcript

  1. Modern Android dependency injection Hugo Visser @botteaap hugo@littlerobots.nl

  2. Dependency Injection?

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

    Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  4. Example class MyClass { fun sendEmail() { val mailer =

    Mailer() mailer.setHost("mail.mydomain.nl") mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  5. Example class MyClass { fun sendEmail() { val mailer =

    MailerFactory.createConfiguredMailer() mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  6. Dependency injection class MyClass(private val mailer: Mailer) { fun sendEmail()

    { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  7. 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 } }
  8. 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! } }
  9. It still works! class MyClass(private val mailer: Mailer) { fun

    sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  10. 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()
  11. 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
  12. Dependency injection frameworks • Reduce & standardise configuration code •

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

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

    fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  15. Dagger injection class MyClass @Inject constructor(private val mailer: Mailer) {

    fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  16. Dagger injection (framework classes) class TestActivity: FragmentActivity() { @Inject lateinit

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

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

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

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

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

    • Not specific to Android https://insert-koin.io/
  24. Koin injection (no changes) class MyClass(private val mailer: Mailer) {

    fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  25. Koin injection / lookup class TestActivity : FragmentActivity() { private

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

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

    fun onCreate() { super.onCreate() startKoin { androidContext(this@MyKoinApplication) modules(mailerModule) } } }
  30. 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 Dagger
  31. 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
  32. Hilt injection (same as Dagger) class MyClass @Inject constructor(private val

    mailer: Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  33. Hilt injection (framework classes) @AndroidEntryPoint class TestActivity : FragmentActivity() {

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

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

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

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

    provideMailer(): Mailer = if (BuildConfig.DEBUG) { NoOpMailer() } else { SMTPMailer("mail.mydomain.nl") } }
  38. Android setup: create injector

  39. Android setup: create injector @HiltAndroidApp class MyApplication: Application()

  40. Component hierarchy

  41. Components & scopes • Components define availability of dependencies •

    Scopes define the scope of dependencies
  42. ViewModel support @HiltViewModel class MyViewModel @Inject constructor( private val myClass:

    MyClass ) : ViewModel() { fun sendEmail() { myClass.sendEmail() } }
  43. ViewModel support @AndroidEntryPoint class ViewModelActivity : FragmentActivity() { private val

    viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.sendEmail() } }
  44. ViewModel support @AndroidEntryPoint class ViewModelActivity : FragmentActivity() { private val

    viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.sendEmail() } }
  45. 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, you don’t have to implement this override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory { return super.getDefaultViewModelProviderFactory() } }
  46. Hilt hits the sweet spot • 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 has similar goals)
  47. Tips / tricks / misc ramblings

  48. 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") } }
  49. 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") } }
  50. • Configuration is part of your feature! • Prevents “god”

    application module or a large set modules in a top level package Tips & tricks
  51. Inversion is nice, but… • Dependency inversion is great, but

    adds overhead of interfaces • Start out with a concrete implementation, extract interface when needed
  52. Prefer typed qualifiers class MyClass @Inject constructor(@Named("smtp") private val mailer:

    Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  53. Prefer typed qualifiers class MyClass @Inject constructor(@Named("smtp") private val mailer:

    Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  54. Prefer typed qualifiers @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides @Named("smtp")

    fun provideSmtpMailer(): Mailer = SMTPMailer("mail.mydomain.nl") }
  55. Prefer typed qualifiers @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides @SmtpMailer

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

    Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "hugo@littlerobots.nl", "This is an email!" ) } }
  57. More complex needs? Consider Anvil • For existing (large) Dagger

    setups that can’t (or won’t) migrate to Hilt • Simplifies generating components (similar to Hilt) • KSP for code generation + option for Anvil compiler plugins https://github.com/square/anvil
  58. End of ramblings!

  59. Modern Android dependency injection Hugo Visser @botteaap hugo@littlerobots.nl