爭取打造 Android Jetpack 講解的最好的博客系列:android
Android Jetpack 實戰篇:git
本文中我將嘗試分享我我的 搭建我的MVVM項目 的過程當中的一些心得和踩坑經歷,以及在這過程當中目前對 編程本質 的一些我的理解和感悟,特此分享以期討論及學習進步。github
最近在嘗試搭建本身理解的 MVVM模式 的應用程序,在這近一個月中,我思考了不少,也參考了若干Github上MVVM項目源碼,並從中獲益匪淺。數據庫
我根據所得搭建了一個MVVM開發模式的Github客戶端,並託管在了本身的github上:編程
MVVM-Rhine: MVVM+Jetpack的Github客戶端安全
建立這個項目的緣由是我想有一個本身寫的 Github客戶端 方便我查看,目前我基本實現了本身的目標,App總體的效果是這樣的:網絡
在開發過程當中,我根據本身對於編程的理解,在技術選型中,加了一些本身喜歡的庫,寫了一些本身比較滿意的風格的代碼,特此和你們一塊兒分享個人所得,謬誤之處,歡迎拍磚。架構
回顧近半年來,我博客中的編程語言使用的清一色是 Kotlin,這樣作的最初目的是督促本身學習Kotlin。app
我曾在 某篇文章 中這樣聲明我用Kotlin的緣由:框架
不只如此,Kotlin語言國外已經有至關的熱度了,只是目前相比Java,國內尚未徹底推廣起來而已。
此外,Kotlin的一些特性可以讓咱們實現Java實現不了的東西(不是空安全,無需findViewById這些基本的語法糖),對於某些設計點,Kotlin是Java沒法替代的,這點我會在後文中提到。
不少朋友對RxJava的理解是 鏈式調用、線程切換 等等,對我來講,在RxJava的逐漸使用過程當中,我對它的理解慢慢趨於 異步 一詞——RxJava 強迫開發者從思想上將異步代碼和同步代碼歸於一統,對於任何業務功能,均可以抽象爲一個可觀察的對象。
MVVM的本質亦是如此,DataBinding 幫咱們爲 數據驅動視圖 提供了可實現的方案,所以它成爲了大多數MVVM項目中的核心庫。
MVVM觀察者模式的本質也意味着,即便沒有DataBinding,咱們經過RxJava或者其餘方式也可以實現 MVVM,只不過DataBinding更方便搭建MVVM而已。
這裏不拿MVC、MVP和MVVM進行比較,由於不一樣的架構思想,都有不一樣的優劣勢,我很是沉迷於RxJava和其優秀的思想,我認爲它的思想至關一部分和MVVM不謀而合,所以我更傾向使用MVVM,配合以RxJava,可以讓代碼更加賞心悅目。
Android Jetpack(下稱Jetpack) 是Google今年IO大會上正式推出官方的新一代 組件、工具和架構指導 ,旨在加快開發者的 Android 應用開發速度:
這是一套很是迷人的架構組件,Google今年還同步(其實晚了2個月)開源了一個Jetpack的示例項目 Sunflower。
這個示例項目有着豐富的學習價值,也很方便開發者迅速上手並熟悉Jetpack的組件——固然,只是上手固然知足不了個人需求,我想經過本身參與一個項目的實踐來深刻了解並感覺這些組件,因而 我在這個項目中使用了這些組件:
我簡單經過我的感覺分別闡述一下這些組件真正融入MVVM項目中的感覺:
MVVM的 核心組件,經過良好的設計,個人項目中避免了95%以上的 冗餘代碼—— 它的做用簡單直接,就是 數據驅動視圖,我不再須要去經過控件設置UI,相反,全部UI的變更都交給了 被觀察的成員屬性 去驅動。
View的點擊事件:
<ImageView android:id="@+id/btnEdit" android:layout_width="40dp" android:layout_height="40dp" android:src="@drawable/ic_edit_pencil" app:bind_onClick="@{ () -> delegate.edit() }" />
複製代碼
ImageView的url加載:
<ImageView android:id="@+id/ivAvatar" android:layout_width="80dp" android:layout_height="80dp" app:bind_imageUrl_circle="@{ delegate.viewModel.user.avatarUrl }" />
複製代碼
TextView的設置值:
<TextView android:id="@+id/tvNickname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ delegate.viewModel.user.name }" />
複製代碼
有同窗以爲這太簡單,那咱們換一些有說服力的。
你還在 Activity
代碼配置 RecyclerView
?直接xml裏一次性配置RecyclerView
,包括 滑動動畫,下拉刷新,點擊按鈕列表滑動到頂部:
<android.support.v4.widget.SwipeRefreshLayout android:layout_width="match_parent" android:layout_height="match_parent" app:onRefreshListener="@{ () -> delegate.viewModel.queryUserRepos() }" // 刷新監聽 app:refreshing="@{ safeUnbox(delegate.viewModel.loading) }"> // 刷新狀態
<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" app:bind_adapter="@{ delegate.viewModel.adapter }" // 綁定Adapter app:bind_scrollStateChanges="@{ delegate.fabViewModel.stateChangesConsumer }" app:bind_scrollStateChanges_debounce="@{ 500 }" app:layoutManager="android.support.v7.widget.LinearLayoutManager" tools:listitem="@layout/item_repos_repo" />
</android.support.v4.widget.SwipeRefreshLayout>
<android.support.design.widget.FloatingActionButton android:id="@+id/fabTop" android:src="@drawable/ic_keyboard_arrow_up_white_24dp" app:bind_onClick="@{ () -> recyclerView.scrollToPosition(0) }" // 點擊事件,列表直接回到頂部 app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" />
複製代碼
還在配置 ViewPager+Fragment+BottomNavigationView的切換效果,包括ViewPager滑動切換監聽,自動配置Adapter,BottomNavigation的點擊監聽, 咱們都在Xml聲明好,交給DataBinding就好了:
<android.support.v4.view.ViewPager android:id="@+id/viewPager" app:onViewPagerPageChanged="@{ (index) -> delegate.onPageSelectChanged(index) }" app:viewPagerAdapter="@{ delegate.viewPagerAdapter }" app:viewPagerDefaultItem="@{ 0 }" app:viewPagerPageLimit="@{ 2 }" />
<android.support.design.widget.BottomNavigationView android:id="@+id/navigation" app:bind_onNavigationBottomSelectedChanged="@{ (menuItem) -> delegate.onBottomNavigationSelectChanged(menuItem) }" app:itemBackground="@color/colorPrimary" app:itemIconTint="@drawable/selector_main_bottom_nav_button" app:itemTextColor="@drawable/selector_main_bottom_nav_button" app:menu="@menu/menu_main_bottom_nav" />
複製代碼
篇幅所限,省略了一些常見的屬性,上述的全部源碼,你均可以在個人項目中找到。
個人意思不是想說 DataBinding 多麼強大(它確實能夠實現足夠多的功能),對我而言,它最強大的好處是—— 節省了足夠多UI控件的設置代碼,讓我可以 抽出更多時間去寫純粹業務邏輯的代碼。
有朋友以爲DataBinding最大的問題就是很差Debug,個人解決方案是統一 狀態管理,這個後文再提。
Lifecycle 讓我可以更專一於 業務邏輯 而非 生命週期,我認爲這是不可代替的,若是你熟悉 Lifecycle,你能夠看個人這篇文章:
Android官方架構組件Lifecycle:生命週期組件詳解&原理分析
Lifecycle可以讓我想要的組件也擁有 生命週期(其實是對生命週期容器的觀察),好比,我再也不須要讓Activity或者Fragment在onCreated()
中去請求網絡,取而代之的是:
class LoginViewModel(private val repo: LoginDataSourceRepository) : BaseViewModel() {
override fun onCreate(lifecycleOwner: LifecycleOwner) {
super.onCreate(lifecycleOwner)
// 自動登陸
autoLogin.toFlowable()
.filter { it }
.doOnNext { login() }
.bindLifecycle(this)
.subscribe()
}
}
複製代碼
上文的示例代碼展現了,Login界面的自動登陸邏輯(固然也能夠是網絡請求展現數據的邏輯),ViewModel檢測到了Activity的生命週期並自動調用了
onCreate()
函數——我並無經過Activity去調用它。
ViewModel可以檢測到持有者的 生命週期,並避免了 橫豎屏切換時額外的代碼的配置,它的內部是經過一個不可見的 Fragment 對數據進行持有,並在真正該銷燬數據的時候去銷燬它們。
同時,它是MVVM中的 核心組件,我在項目的規範定義中,layout中全部的屬性配置都應該依賴於ViewModel
中的MutableLiveData
屬性:
class LoginViewModel(
private val repo: LoginDataSourceRepository
) : BaseViewModel() {
val username: MutableLiveData<String> = MutableLiveData() // 用戶名輸入框
val password: MutableLiveData<String> = MutableLiveData() // 密碼輸入框
val loading: MutableLiveData<Boolean> = MutableLiveData() // ProgressBar
val error: MutableLiveData<Option<Throwable>> = MutableLiveData() // Errors
val userInfo: MutableLiveData<LoginUser> = MutableLiveData() // 用戶信息
private val autoLogin: MutableLiveData<Boolean> = MutableLiveData() // 是否自動登陸
// ......
}
複製代碼
參照 RxJava 豐富的生態圈, LiveData 看起來彷佛實在雞肋,可是DataBinding在最近的版本中提供了對 LiveData 的支持,考慮再三,我採用了 LiveData,正如上文示例代碼,配合以 ViewModel, UI完整的驅動系統被搭建起來。
LiveData並不是一無可取,它確實值得我做爲依賴添加進本身的項目中,緣由有二:
實際上 Paging
也是支持的,可是我沒有用到Paging
。
RxJava
在子線程進行UI的更新依賴於 observerOn(AndroidSchedudler.mainThread())
,可是LiveData
不須要,你只須要經過 postValue()
,就能安全的進行數據更新,就像這樣:
val loading: MutableLiveData<Boolean> = MutableLiveData()
this.loading.postValue(value) // 數據的設置會在主線程上
複製代碼
可是我仍然須要面臨一個問題,就是LiveData
的生態圈實在沒辦法和 RxJava
相關的庫對比,想要經過LiveData
的操做符進行業務處理實在不靠譜,所以我選擇將LiveData
的observe()
變成RxJava
的Flowable
:
private val autoLogin: MutableLiveData<Boolean> = MutableLiveData()
autoLogin.toFlowable() // 變成了一個Flowable
.filter { it }
.doOnNext { login() }
.bindLifecycle(this)
.subscribe()
複製代碼
得益於 kotlin 強大的 擴展函數,二者之間的融合如 絲滑般的流暢:
fun <T> LiveData<T>.toFlowable(): Flowable<T> = Flowable.create({ emitter ->
val observer = Observer<T> { data ->
data?.let { emitter.onNext(it) }
}
observeForever(observer)
emitter.setCancellable {
object : MainThreadDisposable() {
override fun onDispose() = removeObserver(observer)
}
}
}, BackpressureStrategy.LATEST)
複製代碼
如今,咱們一邊享受着 LiveData
安全的數據更新和DataBinding的原生支持,一邊享受 RxJava
無以倫比 強大的操做符和函數式編程思想,這簡直讓我如沐春風。
ORM數據庫,市面上太多了不解釋,我選擇使用它的緣由有二:
RxJava
和LiveData
, 無腦用真香。
Google官方 單Activity多Fragment 的架構組件,若是你不是很熟悉,能夠參考這篇文章:
Android官方架構組件Navigation:大巧不工的Fragment管理框架
很感謝文章吹來以後,不少同窗對文章的確定,我也相信不少同窗已經熟悉甚至嘗試上手了這個庫,我此次嘗試在項目中使用它,緣由是,我想試試 它是否是真的像我文章吹的那麼好用。
經實戰,初步結果是:
能夠用,但不必。
在大多數狀況下,Navigation
都顯得很是穩健,可是 框架是死的,可是需求是變幻無窮的,我老是不可避免去面對一些問題:
1.官方提供了Navigation
對 Toolbar
和 BottomNavigationView
的原生支持,可是令我啼笑皆非的是,Navigation
內部對Fragment
的切換採用的是replace()
,這意味着,每次點擊底部導航控件,我都會銷燬當前的Fragment
,而且實例化一個新的Fragment
。
2.不少APP採用了Home界面,雙擊返回纔會退出Application的需求,正常咱們能夠重寫Activity的onBackPress()
方法,而使用了Navigation
,咱們不得不把導航的返回行爲委託給了Navigation
:
class MainActivity : BaseActivity<ActivityMainBinding>() {
override val layoutId = R.layout.activity_main
override fun onSupportNavigateUp(): Boolean =
findNavController(R.id.navHostFragment).navigateUp()
// ...
}
複製代碼
固然,這些問題都是有解決方案的,以BottomNavigationView
每次切換都會銷燬當前Fragment
並實例化新的Fragment
爲例,個人建議是:
對根佈局的View使用
Navigation
,界面內部的佈局採用常規實現方式(好比ViewPager+Fragment)。
好比我在MainActivity中聲明NavHostFragment
:
<android.support.constraint.ConstraintLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent">
<fragment android:id="@+id/navHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/navigation_main" />
</android.support.constraint.ConstraintLayout>
複製代碼
個人BottomNavigationView
導航界面,則是一個MainFragment:
<android.support.constraint.ConstraintLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent">
<android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="0dp" android:layout_height="0dp"" />
<android.support.design.widget.BottomNavigationView android:id="@+id/navigation" android:layout_width="0dp" android:layout_height="wrap_content" app:menu="@menu/menu_main_bottom_nav" />
</android.support.constraint.ConstraintLayout>
複製代碼
我保證 只有根佈局的頁面經過Navigation進行導航,至於Navigation
對BottomNavigationView
的原生支持,我選擇無視......
總而言之,對因而否使用Navigation
,個人建議是持保守態度,由於這個東西和其它三方庫不一樣,Navigation
的配置是 項目級 的。
關於項目中RxJava相關庫的配置,我選擇了這些:
我是RxJava
的重度依賴使用者,它讓我沉迷於 業務邏輯的抽象,嘗試將全部代碼歸 異步 於一統,所以我依賴了這些庫。
編程的樂趣在於 探索,對於Android開發者來講,Dagger2 可能會是更多開發者的首選,但對於一個 探索性質更多 的項目來講,Dagger2 並非最優選,最終我選擇了Kodein:
Kodein官網:Painless Kotlin Dependency Injection
若是您完整的閱讀了 **《Kotlin 實戰》**這本書,你能在書末的附錄中找到選擇它的緣由:
常見的Java依賴注入框架,好比 Spring/Guide/Dagger,都能很好地和Kotlin一塊兒工做,若是你對原生的Kotin方案感興趣,試試 Kodein, 它 提供了一套漂亮的DSL來配置依賴,並且它的實現也很是高效。
總結一下我我的的感覺:
以 Http網絡請求 相關爲例,來看看依賴注入的代碼:
很漂亮,對吧?
固然,對於依賴注入庫,Dagger2是一個不會錯的選擇,可是若是僅僅只是我的項目,或者您已經厭倦了Dagger的配置,Kodein是一個不錯的建議。
若是你對 Kodein 感興趣,能夠參考這篇文章,參考本文的項目代碼,相信很快就能上手:
告別Dagger2,Android的Kotlin項目中使用Kodein進行依賴注入
對於Kotlin的各類優勢,函數是第一等公民 是一個沒法忽視的閃光點,它與其餘簡單的語法糖不一樣,它可以讓你的代碼更加優雅。
Arrow是提供了一些簡單函數式編程的特性,利用Arrow提供的各類各樣的函子,你的代碼能夠更加簡潔而且優雅。
好比,配合RxJava
,你能夠實現這樣的代碼以免各類分支的處理,好比隨時都有可能的if..else()
,並將這些額外的操做放在最終的操做符中(Terminal Operator)去處理:
interface ILoginLocalDataSource : ILocalDataSource {
fun fetchPrefsUser(): Flowable<Either<Errors, LoginEntity>>
}
class LoginLocalDataSource(
private val database: UserDatabase,
private val prefs: PrefsHelper
) : ILoginLocalDataSource {
override fun fetchPrefsUser(): Flowable<Either<Errors, LoginEntity>> =
Flowable.just(prefs)
.map {
when (it.username.isNotEmpty() && it.password.isNotEmpty()) {
true -> Either.right(LoginEntity(1, it.username, it.password))
false -> Either.left(Errors.EmptyResultsError)
}
}
}
複製代碼
如今咱們將特殊的分支(數據錯誤)也一樣像正常的流程同樣交給了 Either<Errors, LoginEntity>
統一返回,只有咱們在真正須要使用它們時,它們纔會被解析:
fun login() {
when (username.value.isNullOrEmpty() || password.value.isNullOrEmpty()) {
true -> applyState(isLoading = false, error = Errors.EmptyInputError.some())
false -> repo
.login(username.value!!, password.value!!) // 返回的是 Flowable<Either<Errors, LoginUser>>
.compose(globalHandleError())
.map { either -> // 用到的時候再處理它
either.fold({
SimpleViewState.error<LoginUser>(it)
}, {
SimpleViewState.result(it)
})
}
.startWith(SimpleViewState.loading())
.startWith(SimpleViewState.idle())
.onErrorReturn { it -> SimpleViewState.error(it) }
.bindLifecycle(this)
.subscribe { state ->
// ...
}
}
}
複製代碼
在函數式編程的領域,我只是一個滿懷敬意且不斷學習探索的新人,可是它的好處在於,即便沒有徹底理解 函數式編程 的思想,我也能夠經過運用一些簡單的函子寫出更加Functional的代碼。
除上述庫以外,我還引用了目前比較優秀的三方庫:
基於OkHttp的 網絡請求庫Retrofit,不贅述。
Glide 和 Timber,已經被大衆所熟知的 圖片加載庫 和 小巧精緻的 日誌打印庫,不贅述。
DslAdapter 是低調的Yumenokanata開發的RecyclerViewAdapter,API的DSL設計加上對 DataBinding 的支持,我認爲我還遠遠沒達到寫這個庫的水平,所以在閱讀完源碼以後,我選擇使用它。
不管是MVP仍是MVVM,對於一種開發模式而言,代碼規範是很重要的,這意味着界面的實現老是須要用 同一種開發模式 進行規範化。
以MVP爲例,標準的MVP,實現一個Activity的容器頁面,咱們須要定義Contract
和其對應的View
,Presenter
,Model
層的接口及其實現類,這就引起了另一個問題,相似這種死板的開發模式的流程是否太繁瑣(即簡單的界面是否就沒寫這麼多接口類的必要)?
我不這樣認爲,模版代碼意味着開發的規範,這在團隊開發中尤爲重要,這樣可以保證項目品質的穩定性和一致性,而且便於擴展,對於繁瑣的生成重複性模版代碼的狀況,我認爲MVP的表明性框架 MVPArms作出了很是值得學習的方案,即配置模版插件。
所以我也花了一點時間配置了一套屬於本身MVVM開發模式的模版插件,對於每一個界面的初始化,能夠很方便一鍵生成:
就這樣幾步,Activity/Fragment,ViewModel,ViewDelegate以及依賴注入的KodeinModule類,都經過模版插件自動生成,我只須要關注UI的繪製和業務邏輯的編寫便可。
不管是哪一種開發模式,我認爲模版插件都是一個能大大提升開發效率的工具,並且它的學習成本並不高,以我我的經驗,即便沒有相關經驗,也只須要3~4小時,就能開發出一套屬於本身的模版插件。
從我我的經驗來看,對於簡單的項目並不須要進行復雜的模塊化配置,由於開發者和維護者也只有我一我的。
這兩個也是 Android Jetpack 的架構組件,但我並無使用它們。
Paging
是一個優秀的庫,我曾舉出它的優勢(參考個人這篇文章),可是正若有朋友提到的,它的缺點很明顯,那就是Paging
自己是對RecyclerView.Adapter
的繼承,這意味着使用了Paging
,就必須拋棄其餘的Adapter
庫,或者本身造輪子,最終我選擇了擱置。
WorkManager
的緣由就很簡單了,項目中的功能暫時用不到它....
說到事件總線,國內比較容易被說起的有 EventBus
和RxBus
,此外以前還看到某位大佬曾經分享過 LiveDataBus
,印象很深入,可是文章找不到了。
沒有采用事件總線的緣由是,我已經有RxJava
了。
有同窗說既然你有RxJava
,爲何不使用RxBus
呢,由於對於依賴來講並無額外的負擔?
對此我推薦這篇文章放棄RxBus,擁抱RxJava:爲何避免使用EventBus/RxBus。
引用文章中做者@W_BinaryTree對Jake Wharton對RxBus的評價翻譯:
W_BinaryTree的相關文章寫的都頗有深度,我讀完很受啓發,冒昧推薦一下這位做者。
我認爲RxJava
自己就是對發佈-訂閱者模式最優秀的體現,我儘可能保證個人工程中到處都由RxJava
去串聯就夠了。
於我我的而言,我徹底贊同沒有引入RxJava
的項目中使用EventBus
,可是我確實不推薦RxBus
,由於這意味着業務模塊之間層級設計得不清晰,纔會致使所有交由RxJava
中全局的Subject
的訂閱狀況的產生。
協程的總體替換也在我下一步的學習計劃中。
這須要一段時間的發展,由於我認爲目前協程尚未發展足夠的生態環境——我更期待更多相似 retrofit2-kotlin-coroutines-adapter這樣優秀的拓展庫,可以讓我下決定把全部RxJava的代碼給替換掉。
目前項目中,Room
,網絡請求以及Databinding
依賴的LiveData
,都是經過RxJava
進行編織串在一塊兒的,這些代碼糅合很深,所以Kotlin1.3
發佈後(協程從實驗性的功能正式Release),我只先嚐試性的使用了相似 Result
這樣的API在異常處理上代替Arrow
的Either
, 而協程則處於觀察狀態。
此外,我尚未開始深刻學習協程,重新手角度來看,可能還須要一段時間學習深刻並理解它,所以我期待更多關於協程的分析和相關分享的文章。
狀態的管理一直是爭論不休的話題,甚至基於狀態管理還引伸了 MVI (Model-View-Intent)的開發模式,關於MVI中文相關的博客我推薦這篇文章:
從狀態管理(State Manage)到MVI(Model-View-Intent)
這是一篇分析很是透徹的文章,閱讀之如飲甘怡,其中最重要的優點即是對狀態額統一管理,讀後收穫甚豐,並作出了一些實驗性的嘗試,篇幅所限,再也不贅述,詳情請參考 項目中ViewModel 的源碼。
MVVM模式和設計理念相關博客已經爛大街了,並且我也不認爲我可以講的比別人更透徹。
我寫本文的緣由是分享本身對於編程本質的理解,於我對編程的認知,探索過程當中所帶來的樂趣和成就感纔是最重要的,追究本質多是探索和創造。
我不喜歡拘泥於固定的開發模式,日復一日的重複操做讓我想起了工廠的流水線,編程不一樣,每一個人的代碼風格的迥異背後表明着思想的碰撞,這是不少工做不能給予個人。
回顧本文,我但願本文的每一小節都能給您帶來有益的東西,它多是一種積極狀態的傳遞,也可能某小節涉及的知識點讓您感興趣,或是其餘——項目自己意義和這種收穫 相比反而不大,由於每一個人的思想不一樣,對於MVVM的理解也不一樣。
所以,我不敢妄言這個項目表明瞭MVVM的規範,但至少目前我對它的設計很滿意(對您來講可能嘈點滿滿),它表明了我是這一階段持續學習的結果,,很期待不久以後的我可以用懷疑的眼光去看待這個項目,那將意味着下一階段的進步。
--------------------------廣告分割線------------------------------
Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人博客或者Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?