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

Hugo Visser

June 24, 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 (no changes) class MyClass(private val mailer: Mailer) {

    fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  23. Koin injection / lookup class TestActivity : FragmentActivity() { private

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

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

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

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

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

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

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

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

    viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.sendEmail() } }
  39. 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() } }
  40. 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)
  41. 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") } }
  42. 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") } }
  43. • Configuration is part of your feature! • Prevents “god”

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

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

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

    Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }
  49. 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