本文已授權 微信公眾號 玉剛說 (@任玉剛)獨家發布。
本文中我將嘗試分享我個人 搭建個人MVVM項目 的過程中的一些心得和踩坑經歷,以及在這過程中目前對 編程本質 的一些個人理解和感悟,特此分享以期討論及學習進步。
緣由
最近在嘗試搭建自己理解的 MVVM模式 的應用程序,在這近一個月中,我思考了很多,也參考了若干Github上MVVM項目源碼,并從中獲益匪淺。
我根據所得搭建了一個MVVM開發模式的Github客戶端,并托管在了自己的github上:
MVVM-Rhine: MVVM+Jetpack的Github客戶端
創建這個項目的原因是我想有一個自己寫的 Github客戶端 方便我查看,目前我基本實現了自己的目標,App整體的效果是這樣的:
在開發過程中,我根據自己對于編程的理解,在技術選型中,加了一些自己喜歡的庫,寫了一些自己比較滿意的風格的代碼,特此和大家一起分享我的所得,謬誤之處,歡迎拍磚。
1.我為什么選擇Kotlin?
回顧近半年來,我博客中的編程語言使用的清一色是 Kotlin,這樣做的最初目的是督促自己學習Kotlin。
我曾在 某篇文章 中這樣聲明我用Kotlin的原因:
不僅如此,Kotlin語言國外已經有相當的熱度了,只是目前相比Java,國內還沒有完全推廣起來而已。
此外,Kotlin的一些特性能夠讓我們實現Java實現不了的東西(不是空安全,無需findViewById這些基本的語法糖),對于某些設計點,Kotlin是Java無法替代的,這點我會在后文中提到。
2.MVVM的本質:異步觀察者模式
很多朋友對RxJava的理解是 鏈式調用、線程切換 等等,對我來說,在RxJava的逐漸使用過程中,我對它的理解慢慢趨于 異步 一詞——RxJava 強迫開發者從思想上將異步代碼和同步代碼歸于一統,對于任何業務功能,都可以抽象為一個可觀察的對象。
MVVM的本質亦是如此,DataBinding 幫我們為 數據驅動視圖 提供了可實現的方案,因此它成為了大多數MVVM項目中的核心庫。
MVVM觀察者模式的本質也意味著,即使沒有DataBinding,我們通過RxJava或者其他方式也能夠實現 MVVM,只不過DataBinding更方便搭建MVVM而已。
這里不拿MVC、MVP和MVVM進行比較,因為不同的架構思想,都有不同的優劣勢,我非常沉迷于RxJava和其優秀的思想,我認為它的思想相當一部分和MVVM不謀而合,因此我更傾向使用MVVM,配合以RxJava,能夠讓代碼更加賞心悅目。
3.Android Jetpack: Architecture Components
Android Jetpack(下稱Jetpack) 是Google今年IO大會上正式推出官方的新一代 組件、工具和架構指導 ,旨在加快開發者的 Android 應用開發速度:
這是一套非常迷人的架構組件,Google今年還同步(其實晚了2個月)開源了一個Jetpack的示例項目 Sunflower。
這個示例項目有著豐富的學習價值,也很方便開發者迅速上手并熟悉Jetpack的組件——當然,只是上手當然滿足不了我的需求,我想通過自己參與一個項目的實踐來深入了解并感受這些組件,于是 我在這個項目中使用了這些組件:
我簡單通過個人感受分別闡述一下這些組件真正融入MVVM項目中的感受:
3.1 DataBinding
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,我的解決方案是統一 狀態管理,這個后文再提。
3.2 Lifecycle
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去調用它。
3.3 ViewModel
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() // 是否自動登錄
// ......
}
3.4 LiveData
參照 RxJava 豐富的生態圈, LiveData 看起來似乎實在雞肋,但是DataBinding在最近的版本中提供了對 LiveData 的支持,考慮再三,我采用了 LiveData,正如上文示例代碼,配合以 ViewModel, UI完整的驅動系統被搭建起來。
LiveData并非一無是處,它確實值得我作為依賴添加進自己的項目中,原因有二:
- 原生支持 DataBinding 和 Room
實際上 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
無以倫比 強大的操作符和函數式編程思想,這簡直讓我如沐春風。
3.5 Room
ORM數據庫,市面上太多了不解釋,我選擇使用它的原因有二:
- 1.Google爸爸官方出品,無腦用
- 2.原生支持
RxJava
和LiveData
, 無腦用
真香。
3.6 Navigation
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
的配置是 項目級 的。
4. 天馬行空:RxJava
關于項目中RxJava相關庫的配置,我選擇了這些:
我是RxJava
的重度依賴使用者,它讓我沉迷于 業務邏輯的抽象,嘗試將所有代碼歸 異步 于一統,因此我依賴了這些庫。
5. 依賴注入:Kodein
編程的樂趣在于 探索,對于Android開發者來說,Dagger2 可能會是更多開發者的首選,但對于一個 探索性質更多 的項目來說,Dagger2 并不是最優選,最終我選擇了Kodein:
Kodein官網:Painless Kotlin Dependency Injection
如果您完整的閱讀了 《Kotlin 實戰》這本書,你能在書末的附錄中找到選擇它的原因:
常見的Java依賴注入框架,比如 Spring/Guide/Dagger,都能很好地和Kotlin一起工作,如果你對原生的Kotin方案感興趣,試試 Kodein, 它 提供了一套漂亮的DSL來配置依賴,而且它的實現也非常高效。
總結一下我個人的感受:
- 更Kotlin,整個框架都由Kotlin實現
- 實現方式依賴于 Kotlin 的 屬性委托
- 很簡潔,相比復雜的Dagger,上手更簡單
- 超級漂亮的DSL && 說出去更唬人......
以 Http網絡請求 相關為例,來看看依賴注入的代碼:
很漂亮,對吧?
當然,對于依賴注入庫,Dagger2是一個不會錯的選擇,但是如果僅僅只是個人項目,或者您已經厭倦了Dagger的配置,Kodein是一個不錯的建議。
如果你對 Kodein 感興趣,可以參考這篇文章,參考本文的項目代碼,相信很快就能上手:
告別Dagger2,Android的Kotlin項目中使用Kodein進行依賴注入
6.函數式支持庫:Arrow
對于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的代碼。
7. 其他庫
除上述庫之外,我還引用了目前比較優秀的三方庫:
基于OkHttp的 網絡請求庫Retrofit,不贅述。
Glide 和 Timber,已經被大眾所熟知的 圖片加載庫 和 小巧精致的 日志打印庫,不贅述。
DslAdapter 是低調的Yumenokanata開發的RecyclerViewAdapter,API的DSL設計加上對 DataBinding 的支持,我認為我還遠遠沒達到寫這個庫的水平,因此在閱讀完源碼之后,我選擇使用它。
8. 面向工具編程:模版插件
無論是MVP還是MVVM,對于一種開發模式而言,代碼規范是很重要的,這意味著界面的實現總是需要用 同一種開發模式 進行規范化。
以MVP為例,標準的MVP,實現一個Activity的容器頁面,我們需要定義Contract
和其對應的View
,Presenter
,Model
層的接口及其實現類,這就引發了另外一個問題,類似這種死板的開發模式的流程是否太繁瑣(即簡單的界面是否就沒寫這么多接口類的必要)?
我不這樣認為,模版代碼意味著開發的規范,這在團隊開發中尤其重要,這樣能夠保證項目品質的穩定性和一致性,并且便于擴展,對于繁瑣的生成重復性模版代碼的情況,我認為MVP的代表性框架 MVPArms做出了非常值得學習的方案,即配置模版插件。
因此我也花了一點時間配置了一套屬于自己MVVM開發模式的模版插件,對于每個界面的初始化,可以很方便一鍵生成:
就這樣幾步,Activity/Fragment,ViewModel,ViewDelegate以及依賴注入的KodeinModule類,都通過模版插件自動生成,我只需要關注UI的繪制和業務邏輯的編寫即可。
無論是哪種開發模式,我認為模版插件都是一個能大大提高開發效率的工具,而且它的學習成本并不高,以我個人經驗,即使沒有相關經驗,也只需要3~4小時,就能開發出一套屬于自己的模版插件。
9.沒有使用的一些嘗試
9.1 組件化/模塊化開發
從我個人經驗來看,對于簡單的項目并不需要進行復雜的模塊化配置,因為開發者和維護者也只有我一個人。
9.2 Paging和WorkManager
這兩個也是 Android Jetpack 的架構組件,但我并沒有使用它們。
Paging
是一個優秀的庫,我曾舉出它的優點(參考我的這篇文章),但是正如有朋友提到的,它的缺點很明顯,那就是Paging
本身是對RecyclerView.Adapter
的繼承,這意味著使用了Paging
,就必須拋棄其他的Adapter
庫,或者自己造輪子,最終我選擇了擱置。
WorkManager
的原因就很簡單了,項目中的功能暫時用不到它....
9.3 事件總線
說到事件總線,國內比較容易被提及的有 EventBus
和RxBus
,此外之前還看到某位大佬曾經分享過 LiveDataBus
,印象很深刻,但是文章找不到了。
沒有采用事件總線的原因是,我已經有RxJava
了。
有同學說既然你有RxJava
,為什么不使用RxBus
呢,因為對于依賴來說并沒有額外的負擔?
對此我推薦這篇文章放棄RxBus,擁抱RxJava:為什么避免使用EventBus/RxBus。
引用文章中作者@W_BinaryTree對Jake Wharton對RxBus的評價翻譯:
W_BinaryTree的相關文章寫的都很有深度,我讀完很受啟發,冒昧推薦一下這位作者。
我認為RxJava
本身就是對發布-訂閱者模式最優秀的體現,我盡量保證我的工程中處處都由RxJava
去串聯就夠了。
于我個人而言,我完全贊同沒有引入RxJava
的項目中使用EventBus
,但是我確實不推薦RxBus
,因為這意味著業務模塊之間層級設計得不清晰,才會導致全部交由RxJava
中全局的Subject
的訂閱情況的產生。
9.4 協程
協程的整體替換也在我下一步的學習計劃中。
這需要一段時間的發展,因為我認為目前協程還沒有發展足夠的生態環境——我更期待更多類似 retrofit2-kotlin-coroutines-adapter這樣優秀的拓展庫,能夠讓我下決定把所有RxJava的代碼給替換掉。
目前項目中,Room
,網絡請求以及Databinding
依賴的LiveData
,都是通過RxJava
進行編織串在一起的,這些代碼糅合很深,因此Kotlin1.3
發布后(協程從實驗性的功能正式Release),我只先嘗試性的使用了類似 Result
這樣的API在異常處理上代替Arrow
的Either
, 而協程則處于觀察狀態。
此外,我還沒有開始深入學習協程,從新手角度來看,可能還需要一段時間學習深入并理解它,因此我期待更多關于協程的分析和相關分享的文章。
10.關于狀態管理
狀態的管理一直是爭論不休的話題,甚至基于狀態管理還引申了 MVI (Model-View-Intent)的開發模式,關于MVI中文相關的博客我推薦這篇文章:
從狀態管理(State Manage)到MVI(Model-View-Intent)
這是一篇分析非常透徹的文章,閱讀之如飲甘怡,其中最重要的優勢便是對狀態額統一管理,讀后收獲甚豐,并做出了一些實驗性的嘗試,篇幅所限,不再贅述,詳情請參考 項目中ViewModel 的源碼。
11.感受
MVVM模式和設計理念相關博客已經爛大街了,而且我也不認為我能夠講的比別人更透徹。
我寫本文的原因是分享自己對于編程本質的理解,于我對編程的認知,探索過程中所帶來的樂趣和成就感才是最重要的,追究本質可能是探索和創造。
我不喜歡拘泥于固定的開發模式,日復一日的重復操作讓我想起了工廠的流水線,編程不同,每個人的代碼風格的迥異背后代表著思想的碰撞,這是很多工作不能給予我的。
回顧本文,我希望本文的每一小節都能給您帶來有益的東西,它可能是一種積極狀態的傳遞,也可能某小節涉及的知識點讓您感興趣,或是其他——項目本身意義和這種收獲 相比反而不大,因為每個人的思想不同,對于MVVM的理解也不同。
因此,我不敢妄言這個項目代表了MVVM的規范,但至少目前我對它的設計很滿意(對您來說可能嘈點滿滿),它代表了我是這一階段持續學習的結果,,很期待不久之后的我能夠用懷疑的眼光去看待這個項目,那將意味著下一階段的進步。
項目地址:https://github.com/qingmei2/MVVM-Rhine
--------------------------廣告分割線------------------------------
系列文章
爭取打造 Android Jetpack 講解的最好的博客系列:
Android Jetpack 實戰篇:
關于我
Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ??,也歡迎關注我的個人博客或者Github。
如果您覺得文章還差了那么點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?