Slide 1

Slide 1 text

Modern Android dependency injection Hugo Visser @botteaap [email protected]

Slide 2

Slide 2 text

Dependency Injection?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Example class MyClass { fun sendEmail() { val mailer = MailerFactory.createConfiguredMailer() mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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 } }

Slide 8

Slide 8 text

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! } }

Slide 9

Slide 9 text

It still works! class MyClass(private val mailer: Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }

Slide 10

Slide 10 text

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()

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Dependency injection frameworks ● Reduce & standardise configuration code ● Manage scoping → singletons, lifecycle ● Validate configuration ● Qualifiers & other useful tools

Slide 13

Slide 13 text

Dagger (2) ● javax.Inject ● Code generation, compile time checking ● Java ● Not specific to Android https://dagger.dev

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Dagger injection (framework classes) class TestActivity: FragmentActivity() { @Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { //TODO injection super.onCreate(savedInstanceState) } }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Component (injector) @Component(modules = [AppModule::class]) interface AppComponent { // this is the activity we are going to inject in fun inject(activity: TestActivity) // other targets... }

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

Perform injection class TestActivity: FragmentActivity() { @Inject lateinit var myClass: MyClass override fun onCreate(savedInstanceState: Bundle?) { (application as MyApplication).getAppComponent().inject(this) super.onCreate(savedInstanceState) } }

Slide 22

Slide 22 text

Problems ● Android lifecycle + injecting at the “right” moment ● More complex setup when scoping dependencies to lifecycle (components/subcomponents) ● Many ways to setup Dagger

Slide 23

Slide 23 text

Koin ● Injection library written in Kotlin ● Runtime evaluation ● Not specific to Android https://insert-koin.io/

Slide 24

Slide 24 text

Koin injection (no changes) class MyClass(private val mailer: Mailer) { fun sendEmail() { mailer.sendEmail( "Hello there", "[email protected]", "This is an email!" ) } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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()) } }

Slide 28

Slide 28 text

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()) } }

Slide 29

Slide 29 text

Android setup: create injector class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyKoinApplication) modules(mailerModule) } } }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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!" ) } }

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Android setup: create injector

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Component hierarchy

Slide 41

Slide 41 text

Components & scopes ● Components define availability of dependencies ● Scopes define the scope of dependencies

Slide 42

Slide 42 text

ViewModel support @HiltViewModel class MyViewModel @Inject constructor( private val myClass: MyClass ) : ViewModel() { fun sendEmail() { myClass.sendEmail() } }

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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() } }

Slide 46

Slide 46 text

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)

Slide 47

Slide 47 text

Tips / tricks / misc ramblings

Slide 48

Slide 48 text

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") } }

Slide 49

Slide 49 text

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") } }

Slide 50

Slide 50 text

● Configuration is part of your feature! ● Prevents “god” application module or a large set modules in a top level package Tips & tricks

Slide 51

Slide 51 text

Inversion is nice, but… ● Dependency inversion is great, but adds overhead of interfaces ● Start out with a concrete implementation, extract interface when needed

Slide 52

Slide 52 text

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!" ) } }

Slide 53

Slide 53 text

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!" ) } }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

End of ramblings!

Slide 59

Slide 59 text

Modern Android dependency injection Hugo Visser @botteaap [email protected]