[译][2,安卓应用开发项目
原文作者:Farshid ABZ
作者这篇文章到目前为止已经收到了 2.4k+ 的赞,冲上了 Medium 热门,非常好的一篇文章,我也使用 Koin + kotlin + databinding 结合着 inline、reified 强大的特性封装了基础库,包含了 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 等等, 正在陆续添加新的组件。
通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案
Dagger 和 Koin 优势劣势对比?应该选择 Dagger 还是 Koin?
koin 语法特性?
Koin 为什么可以做到无代码生成、无反射?
Inline 修饰符做什么用的?如何正确使用?带来的性能损失?
只是用 Inline 修饰符,为什么编译器会给我们一个警告?
为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
什么时候应该使用 inline 修饰符?
Reified 修饰符做什么用?如何使用?
Koin 带来性能损失的那些事?
Kotlin 用 5 行代码实现快排算法?
这篇文章涉及很多重要的知识点,请耐心读下去,我相信应该会给大家带来很多不一样的东西。
译文
当我正在反复学习 Dagger 的时候,我遇见了 Koin,Koin 不仅节省了我的时间,还提高了效率,将我从复杂 Dagger 给释放出来了。
这篇文章将会告诉你什么是 Koin,与 Dagger 对比有那些优势,以及如何使用 Koin。
是什么 Koin
Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。
Dagger vs Koin
为了正确比较这两种方式,我们用 Dagger 和 Koin 去实现了一个项目,项目的架构都是 MVVM,其中用到了 retrofit 和 LiveData,包含了 1 个 Activity、4 个 fragments、5 个 view models、1 个 repository 和 1 个 web service 接口, 这应该是一个小型项目的基础架构了
先来看一下 DI 包下的结构,左边是 Dagger,右边是 Koin
如你所见配置 Dagger 需要很多文件 而 Koin 只需要 2 个文件,例如 用 Dagger 注入 1 个 view models 就需要 3 个文件(真的需要用这么多文件吗?)
比较 Dagger 和 Koin 代码行数
我使用 Statistic 工具来统计的,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,结果是非常吃惊的
正如你看到的 Dagger 生成的代码行比 Koin 多两倍
Dagger 和 Koin 编译时间怎么样呢
每次编译之前我都会先 clean 然后才会 rebuild,我得到下面这个结果
Koin:BUILD SUCCESSFUL in 17s88 actionable tasks: 83 executed, 5 up-to-date
Dagger:BUILD SUCCESSFUL in 18s88 actionable tasks: 83 executed, 5 up-to-date
我认为这个结果证明了,如果是在一个更大、更真实的项目中,这个代价是非常昂贵。
Dagger 和 Koin 使用上怎么样呢
如果你想在 MVVM 和 Android Support lib 中使用 Dagger 你必须这么做。
首先在 module gradle 中 添加 Dagger 依赖。
kapt "com.google.dagger:dagger-compiler:dagger_version"implementation "com.google.dagger:dagger:$dagger_version"
然后创建完 modules 和 components 文件之后, 需要在 Application 中 初始化 Dagger(或者其他方式初始化 Dagger)。
Class MyApplication : Application(), HasActivityInjector {@Injectlateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>override fun activityInjector() = dispatchingAndroidInjectorfun initDagger() {DaggerAppComponent.builder().application(this).build().inject(this)}}
所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector 和 inject DispatchingAndroidInjector。
对于 view models,我们需要在 BaseFragment 中注入 ViewModelFactory,并实现 Injectable。
但这并不是全部。还有更多的事情要做。
对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 DI 如何注入它们,正如你所见我们有 ActivityModule、FragmentModule、和 ViewModelModule。
我们来看一下下面的代码
@Moduleabstract class ActivityModule {@ContributesAndroidInjector(modules = [FragmentModule::class])abstract fun contributeMainActivity(): MainActivity
//Add your other activities here}
Fragments 如下所示:
@Moduleabstract class FragmentModule {@ContributesAndroidInjectorabstract fun contributeLoginFragment(): LoginFragment
@ContributesAndroidInjectorabstract fun contributeRegisterFragment(): RegisterFragment
@ContributesAndroidInjectorabstract fun contributeStartPageFragment(): StartPageFragment}
ViewModels 如下所以:
@Moduleabstract class ViewModelModule {
@Bindsabstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds@IntoMap@ViewModelKey(loginViewModel::class)abstract fun bindLoginFragmentViewModel(loginViewModel: loginViewModel): ViewModel
@Binds@IntoMap@ViewModelKey(StartPageViewModel::class)abstract fun bindStartPageViewModel(startPageViewModel: StartPageViewModel): ViewModel......}
所以你必须在 DI modules 中添加这些 Fragments、Activities 和 ViewModels。
那么在 Koin 中如何做
你需要在 module gradle 中添加 Koin 依赖
implementation "org.koin:koin-android-viewmodel:$koin_version"
然后我们需要创建 module 文件,稍后我告诉你怎么做,实际上我们并不需要像 Dagger 那么多文件。
Dagger 还有其他问题
学习 Dagger 成本是很高的,如果有人加入你的项目或者团队,他/她不得不花很多时间学习 Dagger,我使用 Dagger 两年了,到现在还不是很了解,每次我开始学习 Andorid 新技术的时候,我不得不去搜索和学习如何用 Dagger 实现新技术。
来看一下 Koin 代码
首先我们需要添加 Koin 依赖,如下所示:
implementation "org.koin:koin-android-viewmodel:$koin_version"
我们使用的是 koin-android-viewmodel 库,因为我们希望在 MVVM 中使用它,当然还有其他的依赖库。
添加完依赖之后,我们来实现第一个 module 文件,像 Dagger 一样,可以在一个单独的文件中实现每个模块,但是由于代码简单,我决定在一个文件中实现所有模块,你也可以把它们分开。
首先我们需要了解一下 koin 语法特性
get(): 解析 Koin 模块中的实例,调用 get() 函数解析所请求组件需要的实例,这个 get() 函数通常用于构造函数中,注入构造函数值
factory:声明这是一个工厂组件,每次请求都为您提供一个新实例
single:采用单例设计模式
name:用于命名定义,当您希望具有不同类型的同一个类的多个实例时,需要使用它
我们没有创建具有多个注释和多个组件的许多文件,而是为 DI 注入每个类的时候,提供一个简单、可读的文件。
了解完 koin 语法特性之后,我们来解释下面代码什么意思
private val retrofit: Retrofit = createNetworkClient()
createNetworkClient 方法创建 Retrofit 实例,设置 baseUrl,添加 ConverterFactory 和 Interceptor
private val generalApi: GeneralApi = retrofit.create(GeneralApi::class.java)private val authApi: AuthApi = retrofit.create(AuthApi::class.java)
AuthApi 和 GeneralApi 是 retrofit 接口
val viewModelModule = module {viewModel { LoginFragmentViewModel(get()) }viewModel { StartPageViewModel() }
}
在 module 文件中声明为 viewModel, Koin 将会向 ViewModelFactory 提供 viewModel,将其绑定到当前组件。
正如你所见,在 LoginFragmentViewModel 构造函数中有调用了 get() 方法,get() 会解析一个 LoginFragmentViewModel 需要的参数,然后传递给 LoginFragmentViewModel,这个参数就是 AuthRepo。
最后在 Application onCreate 方法中添加如下代码
startKoin(this, listOf(repositoryModule, networkModule, viewModelModule))
这里只是调用 startKoin 方法,传入一个上下文和一个希望用来初始化 Koin 的模块列表。
现在使用 ViewModel 比使用纯 ViewModel 更容易,在 Fragment 和 Activity 视图中添加下面的代码
private val startPageViewModel: StartPageViewModel by viewModel()
通过这段代码,koin 为您创建了一个 StartPageViewModel 对象,现在你可以在 Fragment 和 Activity 中使用 view model
译者思考
作者总共从以下 4 个方面对比了 Dagger 和 Kotlin:
文件数量:基于 mvvm 架构,分别使用了 Dagger 和 koltin 作为依赖注入框架,初始化 Dagger 时至少需要 9 个文件,而 koltin 只需要 2 个文件,Dagger 文件数量远超过 koltin
代码行数:作者使用了 Statistic 工具,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,如下图所示
反复的对比了 Dagger 和 Koin 编译时间,结果如下所示 koin 比 Dagger 快
Koin:BUILD SUCCESSFUL in 17s88 actionable tasks: 83 executed, 5 up-to-date
Dagger:BUILD SUCCESSFUL in 18s88 actionable tasks: 83 executed, 5 up-to-date
学习成本巨大,如果使用了 Dagger 朋友,应该和作者的感觉是一样的,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂
注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:
startKoin {// Use Koin Android LoggerandroidLogger()// declare Android contextandroidContext(this@MainApplication)// declare modules to usemodules(module1, module2 ...)}
Koin 为什么可以做到无代码生成、无反射
Koin 作为一个轻量级依赖注入框架,为什么可以做到无代码生成、无反射?因为 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,我们先来看一段代码。
koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt
案例一
// typealias 是用来为已经存在的类型重新定义名字的 typealias ModuleDeclaration = Module.() -> Unit
fun module(createdAtStart: Boolean = false, override: Boolean = false, moduleDeclaration: ModuleDeclaration): Module {// 创建 Moduleval module = Module(createdAtStart, override)// 执行匿扩展函数 moduleDeclaration(module)return module}
// 如何使用 val mModule: Module = module {single { ... }factory { ... }}
Module 是一个 lambda 表达式,才可以在 “{}” 里面自由定义 single 和 factory,会等到你需要的时候才会执行。
案例二
inline fun <reified T : ViewModel> Module.viewModel(qualifier: Qualifier? = null,override: Boolean = false,noinline definition: Definition<T>): BeanDefinition<T> {val beanDefinition = factory(qualifier, override, definition)beanDefinition.setIsViewModel()return beanDefinition}
内联函数支持具体化的类型参数,使用 reified 修饰符来限定类型参数,以在函数内部访问它了,由于函数是内联的,不需要反射,通过上面两个案例,说明了为什么 Koin 可以做到无代码生成、无反射。建议大家都去看看 Koin 的源码,能够从中学到很多技巧,后面我会花好几篇文章分析 Koin 源码。
Inline 修饰符带来的性能损失
Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方。
如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记函数会怎么样?
只使用 inline 修饰符会有性能问题,在这篇文章 Consider inline modifier for higher-order functions 也分析了只使用 inline 修饰符为什么会带来性能问题,并且 Android Studio 也会给一个大大大的警告。
编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
1. 既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告?
刚才我们说过调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方,来看一下下面这段代码。
inline fun twoPrintTwo() {print(2)print(2)}
inline fun twoTwoPrintTwo() {twoPrintTwo()twoPrintTwo()}
inline fun twoTwoTwoPrintTwo() {twoTwoPrintTwo()
评论