Koin in Android: 更简单的依赖注入

Dagger 的麻烦

若是还不清楚什么是依赖注入,那么请参考以前写的 Dagger2 in Android(一)通俗基础开头部分。若是你不了解 Dagger 倒也无妨,本文会进行必定的对比,但仅针对接触过 Dagger 的同窗,不然大能够忽略。java

Dagger2 做为著名优秀的依赖注入框架广为流传,况且仍是 Android 的亲爸爸 - Google 在维护,所以相信不少人会将其做为 Android 开发的首选 DI 框架。Dagger 从入门到放弃必定是不少不少人必经甚至屡次经历的历程。android

诚然,Dagger 很强大。但它的学习曲线太过于陡峭,即便好不容易搞清楚了各类注解与概念,也很难适当地运用到项目中。同时对于 Activity 之类重要但却不能咱们本身初始化的类 Dagger 明显水土不服。为此,Google 搞了个 .android 扩展库来「简化」使用。我不否定最终确实简化了代码,可是这玩意自己就很难度,学习成本堪称指数级。git

除此以外,.android 扩展库对于 ViewModel 依然是严重的水土不服,甚至 Google 官方 Demo 的实现也是一堆问题。github

Koin 基础

Koin 是纯 Kotlin 编写的轻量级依赖注入框架,轻量是由于它只使用 Kotlin 的函数解析特性,没有代理,没有代码生成,没有反射!官方声称5分钟快速上手。随着 Kotlin 的推广,Koin 这个后起之秀也得到了愈来愈多的关注。固然它也提供了 implementation "org.koin: koin-java:1.0.0" 扩展库来支持 java,但本文不会涉及。缓存

不建议新手阅读 Koin 源码。做为 DSL,它大量使用了 Kotlin 的高级特性,例如 inline 函数。相对来讲难以理解。架构

使用 Koin 所需的依赖在官方文档已经说得很明确了。这里由于使用了 AndroidX 库,因此引入 org.koin: koin-androidx-viewmodel:2.0.1,事实上它已经包含了 Koin 基础库以及 Android 扩展库,没有必要手动依赖了。框架

依然是使用厨师与火炉的例子来帮助理解,建立一个厨师 Chef 类,他须要一个火炉 Stoveide

class Stove() {}

class Chef(val stove: Stove) {}
复制代码

Module

Module 同时充当了 Dagger 里的 @Inject@Module。Module 是一个容器,它储存了全部须要注入的对象的实例化方式。换句话说,假如咱们想要在某个类中注入 Stove,那么就必须在 Module 中定义究竟如何取得或建立 Stove 的实例,这一过程本质是将 Service 插入 Module 图中。函数

在 Dagger 中,咱们本身编写的类只需加上 @Inject 就能够被框架所识别,可是 Koin 要求手动设置。布局

鉴于 Koin 是一个 DSL,因此 Module 的定义很是简单,不用啰里啰嗦的注解,只须要使用 module 函数便可:

val myModule = module{
	factory { Stove() }
}
复制代码

注意:这里是定义在 top-level 的,而不是在某个类中。

就这么简单,咱们建立了一个 Module 叫作 myModule,而且添加了一个 Service 就是 Stove。添加 Service 有两个函数分别是 factorysingle。区别在于前者将在每次被须要时都建立(获取)一个新的实例,也就是说后边代码块将被屡次运行。而 single 会让 Koin 保留实例用于从此直接返回,相似于 Dagger 中 @Singleton 的做用。

Get

get 用于最终实现注入,顾名思义就是得到一个实例。在 Dagger 中,依赖 @Inject@Component 来实现注入很别扭。相比之下 get 很是符合常规习惯,在须要获取实例的地方直接填个 get,Koin 就会根据数据类型自动从上文 Module 中找到匹配的方法取得实例。

val myModule = module{
	factory { Stove() }
	
	factory { Chef(get()) } // 注意这行
}
复制代码

在正式使用注入以前咱们先新增一个 Chef Service。根据以前的定义,Chef 构造函数中须要传入一个 Stove,这里就能够直接使用 get 获取。在运行时 Koin 判断出这里须要一个 Stove 类型的对象,因而去搜寻全部装载的 Module 是否有对应的 Service,显然以前咱们已经定义过了,所以会直接调用 Stove() 来建立一个新的实例并返回,完成了依赖注入流程。

若是所需类型不肯定,或者须要手动指定一个类型,也能够这么写:get<TYPE>()

初始化与使用

前面咱们已经完成了全部准备工做,是否是特别简单?距离成功注入只有一步之遥啦,如今须要初始化 Koin,一般来讲咱们会在 Application.onCreate 中进行。

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger(Level.INFO)
            androidContext(this@MyApplication)
            modules(localModule)
        }

    }
}
复制代码

使用 startKoin 启动一个全局 Koin。做为 Android 平台,还能够指定 Logger 与 Context。

androidLogger 能够将 Koin 日志输出从默认的 java logger 框架切换到 Android Logcat,更加符合习惯,同时也能够自定义日志级别。

androidContext 能够传入一个全局 Context,通常来讲就采用 Application。做为 Android 开发对于 Context 必定不陌生,许许多多的地方都须要用到,例如发送广播或读取 SharedPreferences。这里传入 Context 后至关于在 Module 中插入了 Context Service 定义,在任何须要的地方直接使用 get 就能够拿到。

最后经过 modules 装载咱们写好的 Module。

初始化完成后就能够在任何地方实现注入了:

class LocalWatchFaceAty : AppCompatActivity() {
	private val chef: Chef = get()
}
复制代码

OK,官网说的5分钟入门确实不算夸张~

进一步

Bind

Bind 是一个中缀函数,能够用于把一个 Service 关联到多个类。例如如今有两个接口:Tool, FlammableStove 实现了他们。显然若是只定义1个 Service 是不能同时注入 Stove 和这两个接口的。这时候就轮到 Bind 大显身手了:

val myModule = module{
	factory { Stove() } bind Tool::class bind Flammable::class // <- here! factory { Chef(get()) } } 复制代码

这么一来,下面的三个注入都是合法的,并都会获得一个 Stove 实例:

val chef: Chef = get()
val tool:Tool = get()
val flammable:Flammable = get()
复制代码

Scope

Scope 用于控制对象在 Koin 内的生命周期。事实上,前面所讲的 singlefactory 都是 scope。

  • single 建立的对象在整个容器的生命周期内都是存在的,所以任意地方注入都是同一实例。
  • factory 每次都建立新的对象,所以它不被保存,也不能共享实例。

定义 Scope 比较简单:

val myModule = module{
	scope(named("MY_SCOPE")){
        scoped {
            Stove()
        }
    }
}
复制代码

可是使用起来就比较麻烦了,咱们须要建立或关闭 scope,毕竟 Kolin 怎么会知道你究竟想实现怎样的生命周期呢?

// 若是存在则直接获取,不然建立 scope
val scope = getKoin().getOrCreateScope("myScope", named("MY_SCOPE"))
val stove1: Stove = scope.get()
val stove2: Stove = scope.get()
scope.close()
复制代码

这里首先获得了一个 scope 实例,而后进行注入,最后关闭 scope。那么在同一个 scope 中注入的实例是相同的。例如 stove1stove2 其实是同一个实例。当 scope 被关闭时其缓存会被清空,天然下一次从新建立后会注入新的对象。

注意区分一点,定义 Scope 时使用的叫作 Qualifier,经过 named 能够用字符串包装。在建立 scope 时须要经过 Qualifier 关联到定义,并同时给一个字符串类型的 idid 仅在运行时使用。能够类比成 Android 的布局文件的 View id 与实际变量名的关系。咱们须要经过 View id 来获取实例并赋值给变量保存,变量名与 View id 没有必然的关系。


在 Android 中咱们常常须要以 Activity 为单位建立 scope,为了简化使用,Koin 提供了 Android 扩展库。在 ActivityFragment 中,能够直接使用 currentScope 变量来表示当前 scope,他会被自动建立,并绑定到 Android 组件的生命周期。

class LocalWatchFaceAty : AppCompatActivity() {
	private val stove: Stove by currentScope.inject()
}
复制代码

除了以前使用的 get,还能够像这样使用 inject 实现懒加载。

定义 scope 也变得简单。以前咱们使用字符串做为限定符定义了 scope,如今直接使用类做为限定符:

val myModule = module{
	scope(named<LocalWatchFaceAty>()){
        scoped {
            Stove()
        }
    }
}
复制代码

ViewModel

ViewModel 能够说是 Android 架构组件发布后最流行的部分了,幸运的是 Kolin 对其作了很是方便的适配。对于 ViewModel 类直接使用 viewModel 来定义 Service:

val localModule = module {
	viewModel {
        KitchenViewModel()
    }
}
复制代码

ActivityFragment 中直接使用 by viewModel()getViewModel() 来注入。

class LocalWatchFaceAty : AppCompatActivity() {
	private val vm: KitchenViewModel by viewModel()
}
复制代码

这样一来获得的 ViewModel 能够自动与 UI 生命周期关联。而若是使用传统的 get 只能获得实例但没有任何关联,失去了 ViewModel 最重要的做用。

总结

能够明显感觉到,Koin 小巧精美,上手难度低,与现代化架构技术很是协调。使用起来符合常规习惯,不要被迫学习一堆概念与复杂的模式。

事实上,Koin 还有更多的高级功能,例如动态加载 Module、本地配置项读取等,也都很简单,经过官方文档能够快速了解。

相关文章
相关标签/搜索