Share
Embed
Dependency Injektion with Koin and Kodein
In this talk, I give an overview of what dependency injection is and how Dagger, the current go-to DI framework on Android, provides this for us. Then I discuss some of Dagger's short comings and how we can use Koin and Kodein as alternatives, albeit with some important trade-offs to consider.
Zachary Smith
September 19, 2018
Transcript
50 slides
-
What is Dependency Injection? • Dependency injection is a technique
for applying the principle of Inversion of Control to how we manage our dependencies • The goal of implementing dependency injection is to achieve separation of concerns in our code base • We want to separate the creation of objects from their use • Dagger has been the de facto way of doing this in android for a while • Koin and Kodein are Kotlin libraries that offer an alternative • These libraries accomplish it in a different way than Dagger -
Service Locator vs Dependency Injection • Both solve the problem
of decoupling our code • Application code is independent of instantiating concrete implementations -
Service Locator vs Dependency Injection It is important to note
the difference in their implementation: • With the service locator, our application code asks the locator for specific dependencies • With dependency injection, there is no direct request for dependencies. • Dependencies are injected by the framework • With a service locator, every class needing injected dependencies has a dependency on the service locator. -
interface MovieService { @GET("discover/movie?language=en-US&sort_by=popularity.desc") fun getPopularMovies(@Query("page") pageNumber: Int): Single<PopularMoviesResponse> @GET("movie/{id}")
fun getMovieDetailsById(@Path("id") id: String): Single<MovieDetailsResponse> } -
interface MovieRepository { val popularMoviesStream: Observable<ResponseStatus<PopularMoviesResponse>> fun refreshPopularMovies() fun loadNextPopularMoviesPage()
fun getMovieDetailsById(id: String): Single<ResponseStatus<MovieDetailsResponse>> } -
class MovieBrowserFragment : Fragment() { private lateinit var viewModel: MovieBrowserViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProviders.of(this).get(MovieBrowserViewModel::class.java) } ... } -
class MovieBrowserViewModel : ViewModel() { private val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() private val apiKeyInterceptor = ApiKeyInterceptor() private val okHttpClient = OkHttpClient.Builder() .addInterceptor(apiKeyInterceptor) .build() private val retroFit = Retrofit.Builder() .client(okHttpClient) .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) .build() private val movieService: MovieService = retroFit.create(MovieService::class.java) private val movieRepository: MovieRepository = MovieRepositoryImpl(movieService) ... } -
Dagger • Dagger is a fully static, compile-time dependency injection
framework for both Java and Android. • Dagger uses annotation processing and code generation to build a graph of your application’s dependencies at compile time. • “If it compiles, it will work…” -
class MovieBrowserViewModel : ViewModel() { private val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() private val apiKeyInterceptor = ApiKeyInterceptor() private val okHttpClient = OkHttpClient.Builder() .addInterceptor(apiKeyInterceptor) .build() private val retroFit = Retrofit.Builder() .client(okHttpClient) .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) .build() private val movieService: MovieService = retroFit.create(MovieService::class.java) private val movieRepository: MovieRepository = MovieRepositoryImpl(movieService) ... } -
@Singleton @Component(modules = [ AppModule::class, ViewModelModule::class, ServiceModule::class, RepositoryModule::class ]) interface
Injector { fun inject(browserFragment: MovieBrowserFragment) fun inject(detailsFragment: MovieDetailsFragment) } -
@Module class AppModule() { @Provides @Singleton fun provideApplication(): MovieFinderApplication {
... } @Provides @Singleton fun provideGson(): Gson { ... } @Provides @Singleton fun provideRetroFit(gson: Gson, okHttpClient: OkHttpClient): Retrofit { ... } @Provides @Singleton fun provideApiKeyInterceptor(): ApiKeyInterceptor { ... } @Provides @Singleton fun provideOkHttpClient(apiKeyInterceptor: ApiKeyInterceptor): OkHttpClient { ... } } -
@Module abstract class ViewModelModule { @Binds internal abstract fun bindViewModelFactory(factory:
ViewModelFactory): ViewModelProvider.Factory @Binds @IntoMap @ViewModelKey(MovieBrowserViewModel::class) internal abstract fun movieResultsViewModel(viewModel: MovieBrowserViewModel): ViewModel @Binds @IntoMap @ViewModelKey(MovieDetailsViewModel::class) internal abstract fun movieDetailsViewModel(viewModel: MovieDetailsViewModel): ViewModel } -
@Module class ServiceModule { @Provides @Singleton fun provideMovieService(retrofit: Retrofit): MovieService
{ return retrofit.create(MovieService::class.java) } } -
class MovieFinderApplication : Application() { lateinit var injector: Injector private
set override fun onCreate() { super.onCreate() injector = DaggerInjector.create() } } -
class MovieBrowserFragment : Fragment() { private lateinit var viewModel: MovieBrowserViewModel
@Inject lateinit var viewModelFactory: ViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val injector = (this.activity!!.application as MovieFinderApplication).injector injector.inject(this) viewModel = ViewModelProviders.of(this, viewModelFactory) .get(MovieBrowserViewModel::class.java) } -
class MovieBrowserFragment : Fragment() { private lateinit var viewModel: MovieBrowserViewModel
@Inject lateinit var viewModelFactory: ViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) getInjector().inject(this) viewModel = getViewModel(viewModelFactory) } } -
Koin • A lightweight dependency injection framework for Kotlin •
Written in Kotlin, uses functional resolution only • Leverages Kotlin’s lazy delegate, inline functions, and reified generic types to resolve dependencies • Provides Android specific integrations ◦ Simplifies injection of Architecture Components ViewModels with constructor parameters • Simple, declarative DSL with idiomatic Kotlin API -
val appModule = module { single { provideGson() } single
{ provideApiKeyInterceptor() } single { provideOkHttpClient(apiKeyInterceptor = get()) } single { provideRetroFitInstance(gson = get(), okHttpClient = get()) } } private fun provideRetroFitInstance(gson: Gson, okHttpClient: OkHttpClient): Retrofit { … } private fun provideApiKeyInterceptor(): ApiKeyInterceptor { … } private fun provideOkHttpClient(apiKeyInterceptor: ApiKeyInterceptor): OkHttpClient { … } private fun provideGson(): Gson { … } -
/** * Resolve a component * @param name : component
canonicalName * @param parameters - injection parameters */ inline fun <reified T : Any> get( name: String? = null, noinline parameters: ParameterDefinition = emptyParameterDefinition() ): T = if (name != null) koinContext.get( name, parameters = parameters ) else koinContext.get(parameters = parameters) -
val serviceModule = module { single { createService<MovieService>(retrofit = get())
} } private inline fun <reified T> createService(retrofit: Retrofit): T { return retrofit.create(T::class.java) } val repositoryModule: Module = module { single { MovieRepositoryImpl(movieService = get()) as MovieRepository } } val viewModelModule = module { viewModel { MovieBrowserViewModel(movieRepository = get()) } viewModel { MovieDetailsViewModel(getMovieDetailsInteractor = get()) } } -
class MovieFinderApplication : Application() { override fun onCreate() { super.onCreate()
startKoin(this, koinModules) } private val koinModules = listOf( appModule, serviceModule, repositoryModule, interactorModule, viewModelModule ) } -
/** * Lazy getByClass a viewModel instance * * @param
key - ViewModel Factory key (if have several instances from same ViewModel) * @param name - Koin BeanDefinition name (if have several ViewModel beanDefinition of the same type) * @param parameters - parameters to pass to the BeanDefinition */ inline fun <reified T : ViewModel> LifecycleOwner.viewModel( key: String? = null, name: String? = null, noinline parameters: ParameterDefinition = emptyParameterDefinition() ) = viewModelByClass(T::class, key, name, null, parameters) -
/** * Lazy getByClass a viewModel instance * * @param
clazz - Class of the BeanDefinition to retrieve * @param key - ViewModel Factory key (if have several instances from same ViewModel) * @param name - Koin BeanDefinition name (if have several ViewModel beanDefinition of the same type) * @param from - ViewModelStoreOwner that will store the viewModel instance. null to assume "this" as the ViewModelStoreOwner * @param parameters - parameters to pass to the BeanDefinition */ fun <T : ViewModel> LifecycleOwner.viewModelByClass( clazz: KClass<T>, key: String? = null, name: String? = null, from: ViewModelStoreOwnerDefinition? = null, parameters: ParameterDefinition = emptyParameterDefinition() ) = lazy { getViewModelByClass(clazz, key, name, from, parameters) } -
Kodein • Kodein is a dependency injection / retrieval container
• Like Koin, leverages Kotlin’s lazy delegate, inline functions and reified generic types to resolve dependencies • Simple, declarative DSL with idiomatic Kotlin API • Android specific integrations -
val appModule = Kodein.Module(name = "AppModule") { bind<Gson>() with singleton
{ provideGson() } bind<ApiKeyInterceptor>() with singleton { provideApiKeyInterceptor() } bind<OkHttpClient>() with singleton { provideOkHttpClient(apiKeyInterceptor = instance()) } bind<Retrofit>() with singleton { provideRetroFitInstance(gson = instance(), okHttpClient = instance()) } } -
val serviceModule = Kodein.Module(name = "ServiceModule") { bind<MovieService>() with singleton
{ createService<MovieService>(instance()) } } val repositoryModule = Kodein.Module(name = "RepositoryModule") { bind<MovieRepository>() with singleton { MovieRepositoryImpl(movieService = instance()) as MovieRepository } } val viewModelModule = Kodein.Module(name = "ViewModelModule") { bind<MovieBrowserViewModel>() with singleton { MovieBrowserViewModel(movieRepository = instance()) } bind<MovieBrowserViewModelFactory>() with singleton { MovieBrowserViewModelFactory(movieBrowserViewModel = instance()) } } -
class MovieFinderApplication : Application(), KodeinAware { override val kodein: Kodein
= Kodein.lazy { import(appModule) import(serviceModule) import(repositoryModule) import(viewModelModule) } } -
class MovieBrowserFragment : Fragment(), KodeinAware { override val kodein: Kodein
by closestKodein() private val viewModelFactory: MovieBrowserViewModelFactory by instance() private val movieBrowserViewModel: MovieBrowserViewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(MovieBrowserViewModel::class.java) } ... } -
/** * Binds the previously given type and tag to
the given binding. * * @param binding The binding to bind. * @throws OverridingException If this bindings overrides an existing binding and is not allowed to. */ infix fun <C, A> with(binding: KodeinBinding<in C, in A, out T>) = containerBuilder.bind( Key(binding.contextType, binding.argType, type, tag), binding, moduleName, Overrides ) -
/** * Creates a singleton: will create an instance on
first request and will subsequently always return the same instance. * * T generics will be kept. * * @param T The created type. * @param creator The function that will be called the first time an instance is requested. Guaranteed to be called only once. Should create a new instance. * @return A singleton ready to be bound. */ inline fun <EC, BC, reified T: Any> Kodein.BindBuilder.WithScope<EC, BC, Unit>.singleton( ref: RefMaker? = null, noinline creator: NoArgSimpleBindingKodein<BC>.() -> T ) = Singleton(scope, contextType, generic(), ref, creator) -
Dagger vs Koin vs Kodein All three of these frameworks/libraries
provide resolution of dependencies without using reflection They all provide a mechanism for decoupling the instantiation of dependencies from their use in application code -
Dagger • Dagger uses code generation at compile time to
generate code that satisfies the dependency graph for your application • Dagger is compile time validated ◦ Code is generated at compile time and will fail compilation if dependencies can’t be satisfied • Annotation processing and code generation is slow • Dagger is true dependency injection* ◦ This comes at the cost of a steeper learning curve and more verbose api * It is possible define a component interface which exposes dependencies that you can ask for directly. Using dagger in this way is the same as a service locator. -
Dagger • Dagger cannot inject private or immutable fields ◦
Injected fields must be use lateinit var • Dagger will automatically instantiate objects with @Inject annotated • No incremental annotation processing for dagger • Must regenerate entire dependency graph each build -
Koin • Uses lazily evaluated, inline functions and with reified
generic types to resolve dependencies • Will crash at run-time if it can not resolve requested dependency • Can only be use with Kotlin • Is technically a service locator ◦ Wraps the access of the service locator in lazily executed extension functions ◦ Application code must ask for specific dependencies at run time • Koin is slightly less verbose, and more readable API than Kodein -
Kodein • Uses lazily evaluated, inline functions and with reified
generic types to resolve dependencies • Will crash at run-time if it can not resolve requested dependency • Allows you to retrieve (but not define) dependencies in Java • Supports multi-platform ◦ JVM, Javascript, and Native • Is technically a service locator ◦ Wraps the access of the service locator in lazily executed extension functions ◦ Application code must ask for specific dependencies at run time • More verbose and less readable API -
Resources • Koin Documentation ◦ https://insert-koin.io/ • Kodein Documentation ◦
http://kodein.org/Kodein-DI/ • Inversion of Control Containers and the Dependency Injection pattern ◦ https://martinfowler.com/articles/injection.html • A brief look at Koin on Android ◦ https://overflow.buffer.com/2018/09/13/a-brief-look-at-koin-on-android/ • Dependency injection with Dagger 2 – Introduction to DI ◦ https://mirekstanek.online/dependency-injection-with-dagger-2-introduction-to-di/ • That Missing Guide: How to use Dagger2 ◦ https://medium.com/@Zhuinden/that-missing-guide-how-to-use-dagger2-ef116fbea97 • Dependency Injection and Service Locators in a Kotlin world with Danny Preussler ◦ http://fragmentedpodcast.com/episodes/133/