- 原標題: Koin vs Dagger, Say hello to Koin
- 原文地址: blog.usejournal.com/android-koi…
- 原文做者:Farshid ABZ
做者這篇文章到目前爲止已經收到了 2.4k+ 的贊,衝上了 Medium 熱門,很是好的一篇文章,我也使用 Koin + kotlin + databinding 結合着 inline、reified 強大的特性封裝了基礎庫,包含了 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 等等, 正在陸續添加新的組件。html
經過這篇文章你將學習到如下內容,將在譯者思考部分會給出相應的答案前端
這篇文章涉及不少重要的知識點,請耐心讀下去,我相信應該會給你們帶來不少不同的東西。java
當我正在反覆學習 Dagger 的時候,我碰見了 Koin,Koin 不只節省了個人時間,還提升了效率,將我從複雜 Dagger 給釋放出來了。android
這篇文章將會告訴你什麼是 Koin,與 Dagger 對比有那些優點,以及如何使用 Koin。git
Koin 是爲 Kotlin 開發者提供的一個實用型輕量級依賴注入框架,採用純 Kotlin 語言編寫而成,僅使用功能解析,無代理、無代碼生成、無反射。github
爲了正確比較這兩種方式,咱們用 Dagger 和 Koin 去實現了一個項目,項目的架構都是 MVVM,其中用到了 retrofit 和 LiveData,包含了 1 個Activity、4 個 fragments、5 個 view models、1 個 repository 和 1 個 web service 接口, 這應該是一個小型項目的基礎架構了web
先來看一下 DI 包下的結構,左邊是 Dagger,右邊是 Koin算法
如你所見配置 Dagger 須要不少文件 而 Koin 只須要 2 個文件,例如 用 Dagger 注入 1 個 view models 就須要 3 個文件(真的須要用這麼多文件嗎?)編程
我使用 Statistic 工具來統計的,反覆對比了項目編譯前和編譯後,Dagger 和 Koin 生成的代碼行數,結果是很是吃驚的json
正如你看到的 Dagger 生成的代碼行比 Koin 多兩倍
每次編譯以前我都會先 clean 而後纔會 rebuild,我獲得下面這個結果
Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date
Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date
複製代碼
我認爲這個結果證實了,若是是在一個更大、更真實的項目中,這個代價是很是昂貴。
若是你想在 MVVM 和 Android Support lib 中使用 Dagger 你必須這麼作。
首先在 module gradle 中 添加 Dagger 依賴。
kapt "com.google.dagger:dagger-compiler:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
implementation "com.google.dagger:dagger:$dagger_version"
複製代碼
而後建立完 modules 和 components 文件以後, 須要在 Application 中 初始化 Dagger(或者其餘方式初始化 Dagger)。
Class MyApplication : Application(), HasActivityInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun activityInjector() = dispatchingAndroidInjector
fun 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。
咱們來看一下下面的代碼
@Module
abstract class ActivityModule {
@ContributesAndroidInjector(modules = [FragmentModule::class])
abstract fun contributeMainActivity(): MainActivity
//Add your other activities here
}
複製代碼
Fragments 以下所示:
@Module
abstract class FragmentModule {
@ContributesAndroidInjector
abstract fun contributeLoginFragment(): LoginFragment
@ContributesAndroidInjector
abstract fun contributeRegisterFragment(): RegisterFragment
@ContributesAndroidInjector
abstract fun contributeStartPageFragment(): StartPageFragment
}
複製代碼
ViewModels 以下因此:
@Module
abstract class ViewModelModule {
@Binds
abstract 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 語法特性
咱們沒有建立具備多個註釋和多個組件的許多文件,而是爲 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:
Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date
Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date
複製代碼
注意:做者在 Application 中調用 startKoin 方法初始化 Koin 的模塊列表,是 Koin 1X 的方式,Koin 團隊在 2x 的時候作了不少改動(下面會介紹),初始化 Koin 的模塊列有所改動,代碼以下所示:
startKoin {
// Use Koin Android Logger
androidLogger()
// declare Android context
androidContext(this@MainApplication)
// declare modules to use
modules(module1, module2 ...)
}
複製代碼
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 {
// 建立 Module
val 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 修飾符的函數,會把裏面的代碼放到我調用的地方。
若是閱讀過 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()
twoTwoPrintTwo()
}
fun twoTwoTwoTwoPrintTwo() {
twoTwoTwoPrintTwo()
twoTwoTwoPrintTwo()
}
複製代碼
執行完最後一個方法 twoTwoTwoTwoPrintTwo,反編譯出來的結果是很是使人吃驚的,結果以下所示:
public static final void twoTwoTwoTwoPrintTwo() {
byte var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print();
}
複製代碼
這顯示了使用 Inline 修飾符的主要問題,當咱們過分使用它們時,代碼會快速增加。這就是爲何 IntelliJ 在咱們使用它的時候會給出警告。
2. 爲何編譯器建議 inline 修飾符須要和 lambda 表達式一塊兒使用呢?
由於 JVM 是不支持 lambda 表達式的,非內聯函數中的 Lambda 表達式會被編譯爲匿名類,這對性能開銷是很是巨大的,並且它們的建立和使用都較慢,當咱們使用 inline 修飾符時,咱們根本不須要建立任何其餘類,來看一下下面代碼。
fun main(args: Array<String>) {
var a = 0
// 帶 inline 的 Lambda 表達式
repeat(100_000_000) {
a += 1
}
var b = 0
// 不帶 inline 的 Lambda 表達式
noinlineRepeat(100_000_000) {
b += 1
}
}
複製代碼
編譯結果以下:
// Java 代碼
public static final void main(@NotNull String[] args) {
int a = 0;
// 帶 inline 的 Lambda 表達式, 會把裏面的代碼放到我調用的地方
int times$iv = 100000000;
int var3 = 0;
for(int var4 = times$iv; var3 < var4; ++var3) {
++a;
}
// 不帶 inline 的 Lambda 表達式,會被編譯爲匿名類
final IntRef b = new IntRef();
b.element = 0;
noinlineRepeat(100000000, (Function1)(new Function1() {
public Object invoke(Object var1) {
++b.element;
return Unit.INSTANCE;
}
}));
}
複製代碼
那麼咱們應該在何時使用 inline 修飾符呢?
使用 inline 修飾符時最多見的場景就是把函數做爲另外一個函數的參數時(高階函數),例如 filter、map、joinToString 或者一些獨立的函數 repeat。
若是沒有函數類型做爲參數,也沒有 reified 實化類型參數時,不該該使用 inline 修飾符了。
從分析 Koin 源碼,inline 應該 lambda 表達式或者 reified 修飾符配合在一塊兒使用的,另外 Android Studio 愈來愈智能了,若是在不正確的地方使用,會有一個大大大的警告。
reified (具體化的類型參數):使用 reified 修飾符來限定類型參數,結合着 inline 修飾符具體化的類型參數,能夠直接在函數內部訪問它。
我想分享兩個使用 Reified 修飾符很常見的例子 reified-type-parameters,使用 Java 是不可能實現的。
案例一:
inline fun <reified T> Gson.fromJson(json: String) =
fromJson(json, T::class.java)
// 使用
val user: User = Gson().fromJson(json)
val user = Gson().fromJson<User>(json)
複製代碼
案例二:
inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)
複製代碼
思考了好久需不須要寫這部份內容,由於在 Koin 2x 的版本的時候已經修復了,這是官方的連接 News from the trenches — What’s next for Koin?,後來想一想仍是寫寫吧,做爲本身的一個學習筆記。
這個源於有我的開了一個 Issue(Bad performance in some Android devices) 如今已經被關閉了,他指出了當 Dependency 數量愈來愈多的時候,Koin 效能會愈來愈差,並且還作了一個對好比下圖所示:
若是使用過 Koin 1x 的朋友應該會感受到,引入 Koin 1x 冷啓動時間邊長了,並且在有大量依賴的時候,查找的時間會有點長,後來 Koin 團隊也發現確實存在這個問題,究竟是怎麼回事呢?
由於他們用了 HashSet 存儲了 BeanDefinition,而後在搜索的時候查找對應的 BeanDefinition,找出一個 BeanDefinition 時間複雜度是 O(n),若是平均有 M 層 Dependency,那麼時間複雜度會變成 O(m*n)。
Koin 團隊的解決方案是用了 HashMap,使用空間換取時間,查找一個 Definition 時間複雜度變成了 O(1),優化以後的結果以下:
Koin 2x 不只在性能優化上有很大的提高,也拓展了不少新的特性,例如 FragmentFactory 可以依賴注入到 Fragments 中就像 ViewModels 同樣,還有自動拆箱等等,在後面的文章會詳細的分析一下。
我想分享一個快速排序算法,這是一個很酷的函數編程的例子 share cool examples,當我看到這段代碼的時候驚呆了,竟然還能夠這麼寫。
fun <T : Comparable<T>> List<T>.quickSort(): List<T> =
if(size < 2) this
else {
val pivot = first()
val (smaller, greater) = drop(1).partition { it <= pivot}
smaller.quickSort() + pivot + greater.quickSort()
}
// 使用 [2,5,1] -> [1,2,5]
listOf(2,5,1).quickSort() // [1,2,5]
複製代碼
譯者基於 Material Design 的響應式框架,擼了一個 "爲互聯網人而設計 國內國外名站導航" ,收集了國內外熱門網址,涵括新聞、體育、生活、娛樂、設計、產品、運營、前端開發、Android開發等等導航網站,若是你有什麼好的建議,也能夠留言,點擊前去瀏覽 若是對你有幫助,請幫我點個贊,感謝
ps: 網站中的地址若是有原做者不但願展現的,能夠留言告訴我,我會馬上刪除
國際資訊網址大全
Android 網址大全
致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯相關的文章,目前正在翻譯一系列歐美精選文章,不只僅是翻譯,更重要的是翻譯背後對每篇文章思考,若是你喜歡這片文章,請幫我點個贊,感謝,期待與你一塊兒成長。