簡(jiǎn)評(píng):繼續(xù)第一部分的文章业栅,作者在第二部分中使用的技術(shù)包括 MVVM,RxJava2.
5. MVVM 架構(gòu) + 存儲(chǔ)庫(kù)模式 + Android 管理封裝器
關(guān)于 Android 世界的一點(diǎn)點(diǎn)架構(gòu)知識(shí)
長(zhǎng)時(shí)間以來(lái)漾抬,Android 開(kāi)發(fā)者們?cè)谒麄兊捻?xiàng)目中沒(méi)有使用任何類型的架構(gòu)。近三年以來(lái)常遂,架構(gòu)在 Android 開(kāi)發(fā)者社區(qū)中被炒得天花亂墜纳令。Activity 之神的時(shí)代已經(jīng)過(guò)去了,Google 發(fā)布了 Android 架構(gòu)藍(lán)圖倉(cāng)庫(kù),提供了許多樣例和說(shuō)明來(lái)實(shí)現(xiàn)不同的架構(gòu)方式平绩。最終圈匆,在 Google IO 17 大會(huì),他們介紹了 Android 架構(gòu)組件捏雌,這些庫(kù)的集合幫助我們編寫(xiě)更清晰的代碼和更好的 app跃赚。你可以使用所有的組件,也可以使用其中的部分性湿。但是纬傲,我覺(jué)得它們都挺有用的。接下來(lái)我們將使用這些組件肤频。我會(huì)先解決這些問(wèn)題叹括,然后用這些組件和庫(kù)來(lái)重構(gòu)代碼,看看這些庫(kù)解決了哪些問(wèn)題宵荒。
有兩種主要的架構(gòu)模式分離了 GUI 代碼:
- MVP
- MVVM
很難說(shuō)哪一種更好汁雷。你應(yīng)該兩種都嘗試一下,然后再做出決定报咳。我傾向于使用管理生命周期組件的 MVVM 模式侠讯,并且接下來(lái)我會(huì)介紹它。如果你沒(méi)試過(guò) MVP少孝,在 Medium 上有大量很好的關(guān)于它的文章继低。
什么是 MVVM 模式?
MVVM 模式是一種架構(gòu)模式稍走。它代表 Model-View-ViewModel袁翁。我覺(jué)得這個(gè)名字會(huì)讓開(kāi)發(fā)者困擾。如果我是那個(gè)命名的人婿脸,我會(huì)稱之為 View-ViewModel-Model粱胜,因?yàn)?ViewModel 是連接視圖和模型的中間件。
View 是你的 Activity狐树,F(xiàn)ragment 或者其他 Android 自定義視圖的抽象焙压。注意千萬(wàn)別和 Android View 混淆了。這個(gè) View 應(yīng)該是個(gè)啞巴抑钟,我們不能在 View 里寫(xiě)任何邏輯涯曲。視圖不應(yīng)該持有任何數(shù)據(jù)。它應(yīng)該有一個(gè) ViewModel 實(shí)例在塔,所有 View 需要的數(shù)據(jù)應(yīng)該從這個(gè)實(shí)例中獲取幻件。同時(shí),View 應(yīng)該觀察這些數(shù)據(jù)蛔溃,一旦 ViewModel 中的數(shù)據(jù)發(fā)生了變化绰沥,布局就會(huì)發(fā)生改變篱蝇。View 有一個(gè)職責(zé):不同的數(shù)據(jù)和狀態(tài)下,布局會(huì)怎樣顯示徽曲。
ViewModel 是持有數(shù)據(jù)和邏輯類的一個(gè)抽象零截,負(fù)責(zé)什么時(shí)候獲取數(shù)據(jù),什么時(shí)候傳遞數(shù)據(jù)秃臣。ViewModel 保留當(dāng)前的狀態(tài)涧衙。同時(shí),ViewModel 持有一個(gè)或多個(gè) Model 實(shí)例甜刻,所有的數(shù)據(jù)都從這些實(shí)例中獲取绍撞。它不應(yīng)該知道這些數(shù)據(jù)是從數(shù)據(jù)庫(kù)中獲取的還是從遠(yuǎn)程服務(wù)器中獲取的。此外得院,ViewModel 也不應(yīng)該知道關(guān)于 View 的一切傻铣。而且,ViewModel 也不應(yīng)該知道任何關(guān)于 Android 框架的事祥绞。
Model 是為 ViewModel 準(zhǔn)備數(shù)據(jù)的抽象層非洲。它是那些從遠(yuǎn)程服務(wù)器或者內(nèi)存緩存或者本地?cái)?shù)據(jù)庫(kù)獲取數(shù)據(jù)的類。這和那些 User蜕径,Car两踏,Square 等等是不一樣的。模型類知識(shí)一些保存數(shù)據(jù)的類兜喻。通常梦染,它是倉(cāng)庫(kù)模式的一種實(shí)現(xiàn),我們接下來(lái)將會(huì)講到朴皆。Model 不該知道關(guān)于 ViewModel 的一切帕识。
MVVM,如果正確實(shí)現(xiàn)遂铡,將是一種很好的分離代碼的方式肮疗,這樣也會(huì)讓它更容易測(cè)試。它幫助我們遵循 SOLID 原則扒接,因此我們的代碼更容易維護(hù)伪货。
現(xiàn)在我將寫(xiě)一個(gè)最簡(jiǎn)單的示例來(lái)解釋它是怎樣工作的。
首先钾怔,創(chuàng)建一個(gè)簡(jiǎn)單的Model來(lái)返回字符串:
class RepoModel {
fun refreshData() : String {
return "Some new data"
}
}
通常碱呼,獲取數(shù)據(jù)是異步的,所以我們必須等待一下宗侦。為了模擬這種情況巍举,我把它改成下面這樣:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
首先,我創(chuàng)建了 OnDataReadyCallback 接口凝垛,它有個(gè) onDataReady 函數(shù)“妹酰現(xiàn)在,我們的 refreshData 函數(shù)實(shí)現(xiàn)了 OnDataReadyCallback 梦皮。為了模擬等待炭分,我使用了 Handler。2 秒后剑肯,OnDataReadyCallback 的實(shí)現(xiàn)將會(huì)調(diào)用 onDataReady 函數(shù)捧毛。
現(xiàn)在來(lái)創(chuàng)建我們的ViewModel:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
}
可以看到,有一個(gè) RepoModel 的示例让网,即將展示的 text 以及保存當(dāng)前狀態(tài)的 isLoading ⊙接牵現(xiàn)在,創(chuàng)建一個(gè) refresh 函數(shù)溃睹,用來(lái)獲取數(shù)據(jù):
class MainViewModel {
...
val onDataReadyCallback = object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
}
fun refresh(){
isLoading.set(true)
repoModel.refreshData(onDataReadyCallback)
}
}
refresh 函數(shù)調(diào)用了 RepoModel 中的 refreshData而账,傳遞了一個(gè)實(shí)現(xiàn) OnDataReadyCallback 接口的實(shí)例。好因篇,那么什么是對(duì)象呢泞辐?無(wú)論何時(shí),當(dāng)你想實(shí)現(xiàn)一些接口或者繼承一些類而不用創(chuàng)建子類時(shí)竞滓,你都會(huì)使用對(duì)象聲明咐吼。如果你想要使用匿名類呢?在這里商佑,你需要使用 object 表達(dá)式:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
fun refresh() {
repoModel.refreshData( object : OnDataReadyCallback {
override fun onDataReady(data: String) {
text = data
})
}
}
當(dāng)我們調(diào)用 refresh锯茄,我們應(yīng)該把 view 改成加載中的狀態(tài),并且一旦獲取到數(shù)據(jù)茶没,就把 isLoading 設(shè)置為 false肌幽。同時(shí),我們應(yīng)該把 text 改成ObservableField<String>礁叔,把 isLoading 改成 ObservableField<Boolean>牍颈。ObservableField 是一個(gè)數(shù)據(jù)綁定庫(kù)中的類,我們可以用它來(lái)創(chuàng)建一個(gè)可觀察對(duì)象琅关。它把對(duì)象包裹成可被觀察的煮岁。
class MainViewModel {
var repoModel: RepoModel = RepoModel()
val text = ObservableField<String>()
val isLoading = ObservableField<Boolean>()
fun refresh(){
isLoading.set(true)
repoModel.refreshData(object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
})
}
}
注意到我使用了 val 而不是 var,因?yàn)槲覀儍H在字段里改變它的值涣易,而不是字段本身画机。如果你想要初始化的話,應(yīng)該這樣:
val text = ObservableField("old data")
val isLoading = ObservableField(false)
現(xiàn)在改變我們的布局新症,讓它可以觀察 text 和 isLoading 枪萄。首先问慎,我們會(huì)綁定 MainViewModel 而不是 Repository:
<data>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
然后:
讓 TextView 觀察 MainViewModel 中的 text
添加一個(gè) ProgressBar,當(dāng) isLoading 為 true 時(shí)才會(huì)顯示
添加一個(gè) Button妇菱,在 onClick 中調(diào)用 refresh 函數(shù),僅當(dāng) isLoading 為 false 時(shí)才能點(diǎn)擊
...
<TextView
android:id="@+id/repository_name"
android:text="@{viewModel.text}"
...
/>
...
<ProgressBar
android:id="@+id/loading"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
...
/>
<Button
android:id="@+id/refresh_button"
android:onClick="@{() -> viewModel.refresh()}"
android:clickable="@{viewModel.isLoading ? false : true}"
/>
...
如果現(xiàn)在運(yùn)行的話长踊,你會(huì)得到一個(gè)錯(cuò)誤,因?yàn)槿绻麤](méi)有導(dǎo)入 View 的話,View.VISIBLE 和 View.GONE 不能使用侯繁。所以,我們應(yīng)該導(dǎo)入:
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
好泡躯,布局完成了≈梗現(xiàn)在我們來(lái)完成綁定。如我們所說(shuō)的 View 應(yīng)該持有 ViewModel 的實(shí)例:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
最終较剃,我們的運(yùn)行效果:
可以看到 old data 變成了 new data咕别。
這就是最簡(jiǎn)單的 MVVM 示例。
還有一個(gè)問(wèn)題写穴,讓我們來(lái)旋轉(zhuǎn)手機(jī):
new data 又變成了 old data惰拱。怎么可能?看下 Activity 的生命周期:
一旦你旋轉(zhuǎn)屏幕确垫,新的 Activity 實(shí)例就會(huì)創(chuàng)建弓颈,onCreate() 方法會(huì)被調(diào)用。現(xiàn)在删掀,看下我們的 Activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
如你所見(jiàn)翔冀,一旦創(chuàng)建了一個(gè)新的 Activity 實(shí)例,MainViewModel 的實(shí)例也被創(chuàng)建了披泪。如果每次重新創(chuàng)建的 MainActivity 都有一個(gè)相同的
MainViewModel 實(shí)例會(huì)不會(huì)好點(diǎn)纤子?
隆重推出生命周期感知組件
因?yàn)樵S多的開(kāi)發(fā)者面臨這個(gè)問(wèn)題,Android 框架團(tuán)隊(duì)的開(kāi)發(fā)者決定創(chuàng)建一個(gè)庫(kù)來(lái)幫我們解決這個(gè)問(wèn)題款票。ViewModel 類是其中一個(gè)控硼。我們所有的 ViewModel 類都應(yīng)該繼承自它。
讓我們的 MainViewModel 繼承來(lái)自于生命周期感知組件的 ViewModel艾少。首先卡乾,我們需要在 build.gradle 文件中添加依賴:
dependencies {
...
implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}
然后繼承 ViewModel:
package me.fleka.modernandroidapp
import android.arch.lifecycle.ViewModel
class MainViewModel : ViewModel() {
...
}
在 MainActivity 的 onCreate() 方法中,你應(yīng)該這樣:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.executePendingBindings()
}
}
注意到我們已經(jīng)沒(méi)有創(chuàng)建 MainViewModel 的實(shí)例了「抗唬現(xiàn)在幔妨,我們從 ViewModelProviders 中獲取它。ViewModelProviders 是一個(gè)功能類谍椅,有一個(gè)獲取 ViewModelProvider 的方法误堡。和作用范圍相關(guān),所以雏吭,如果你在 Activity 調(diào)用 ViewModelProviders.of(this) 锁施,那么你的
ViewModel 會(huì)存活直到 Activity 被銷(xiāo)毀(被銷(xiāo)毀而且沒(méi)有被重新創(chuàng)建)。類似地,如果你在 Fragment 中調(diào)用悉抵,你的 ViewModel 也會(huì)存活直到 Fragment 被銷(xiāo)毀肩狂。看下下面的圖解:
ViewModelProvider 的職責(zé)是在第一次調(diào)用的時(shí)候創(chuàng)建實(shí)例基跑,并在 Activity/Fragment 重新創(chuàng)建時(shí)返回舊的實(shí)例婚温。
不要混淆了:
MainViewModel::class.java
在 Kotlin 中,如果你僅僅寫(xiě)成:
MainViewModel::class
它會(huì)返回一個(gè)KClass媳否,和 Java 中的 Class 不一樣。因此荆秦,如果我們加上.java篱竭,它表示:
返回一個(gè)和給定的 KClass 實(shí)例關(guān)聯(lián)的Java 類實(shí)例。
現(xiàn)在讓我們來(lái)旋轉(zhuǎn)一下屏幕看看會(huì)發(fā)生什么:
我們的數(shù)據(jù)和旋轉(zhuǎn)之前一樣步绸。
上一篇文章中掺逼,我說(shuō)過(guò)我們的 app 將會(huì)獲取 GitHub 倉(cāng)庫(kù)列表并展示。要想完成它瓤介,我們需要添加
getRepositories 函數(shù)吕喘,它會(huì)返回一個(gè)偽造的倉(cāng)庫(kù)列表:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
}
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First", "Owner 1", 100 , false))
arrayList.add(Repository("Second", "Owner 2", 30 , true))
arrayList.add(Repository("Third", "Owner 3", 430 , false))
Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
interface OnRepositoryReadyCallback {
fun onDataReady(data : ArrayList<Repository>)
}
同時(shí),我們的 MainViewModel 會(huì)有一個(gè)調(diào)用 getRepositories 的函數(shù):
class MainViewModel : ViewModel() {
...
var repositories = ArrayList<Repository>()
fun refresh(){
...
}
fun loadRepositories(){
isLoading.set(true)
repoModel.getRepositories(object : OnRepositoryReadyCallback{
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories = data
}
})
}
}
最后刑桑,我們需要在 RecyclerView 中展示這些倉(cāng)庫(kù)氯质。要這么做,我們必須:
創(chuàng)建 rv_item_repository.xml 布局
在 activity_main.xml 布局中添加 RecyclerView
創(chuàng)建 RepositoryRecyclerViewAdapter
set adapter
創(chuàng)建 rv_item_repository.xml 我將使用 CardView 庫(kù)祠斧,所以我們要在 build.gradle 中添加依賴:
implementation 'com.android.support:cardview-v7:26.0.1'
布局看起來(lái)是這樣的:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="repository"
type="me.fleka.modernandroidapp.uimodels.Repository" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/repository_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryName}"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.083"
tools:text="Modern Android App" />
<TextView
android:id="@+id/repository_has_issues"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/has_issues"
android:textStyle="bold"
android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="@+id/repository_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/repository_name"
app:layout_constraintTop_toTopOf="@+id/repository_name"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/repository_owner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryOwner}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_name"
app:layout_constraintVertical_bias="0.0"
tools:text="Mladen Rakonjac" />
<TextView
android:id="@+id/number_of_starts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{String.valueOf(repository.numberOfStars)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_owner"
app:layout_constraintVertical_bias="0.0"
tools:text="0 stars" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</layout>
下一步闻察,在 activity_main.xml 中添加 RecyclerView。別忘了添加依賴:
implementation 'com.android.support:recyclerview-v7:26.0.1'
接下來(lái)是布局:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.fleka.modernandroidapp.MainActivity">
<ProgressBar
android:id="@+id/loading"
android:layout_width="48dp"
android:layout_height="48dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/repository_rv"
android:layout_width="0dp"
android:layout_height="0dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/rv_item_repository" />
<Button
android:id="@+id/refresh_button"
android:layout_width="160dp"
android:layout_height="40dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:onClick="@{() -> viewModel.loadRepositories()}"
android:clickable="@{viewModel.isLoading ? false : true}"
android:text="Refresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
</android.support.constraint.ConstraintLayout>
</layout>
我們刪除了一些之前創(chuàng)建的 TextView 元素琢锋,并且按鈕現(xiàn)在觸發(fā)的是 loadRepositories 而不是 refresh:
<Button
android:id="@+id/refresh_button"
android:onClick="@{() -> viewModel.loadRepositories()}"
...
/>
刪掉 MainViewModel 中的 refresh 和 RepoModel 中的 refreshData 函數(shù)辕漂。
現(xiàn)在,為 RecyclerView 添加一個(gè)適配器:
class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
private var listener: OnItemClickListener)
: RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int)
= holder.bind(items[position], listener)
override fun getItemCount(): Int = items.size
interface OnItemClickListener {
fun onItemClick(position: Int)
}
class ViewHolder(private var binding: RvItemRepositoryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(repo: Repository, listener: OnItemClickListener?) {
binding.repository = repo
if (listener != null) {
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
}
binding.executePendingBindings()
}
}
}
ViewHolder 接受 RvItemRepositoryBinding 類型的實(shí)例吴超,而不是 View 類型钉嘹,這樣我們就能在 ViewHolder 中為每一項(xiàng)實(shí)現(xiàn)數(shù)據(jù)綁定。同時(shí)鲸阻,別被下面一行函數(shù)給弄迷糊了:
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)
它只是這種形式的縮寫(xiě):
override fun onBindViewHolder(holder: ViewHolder, position: Int){
return holder.bind(items[position], listener)
}
并且 items[position] 實(shí)現(xiàn)了索引操作跋涣,和 items.get(position) 是一樣的。
還有一行可能會(huì)迷惑的代碼:
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
你可以用_來(lái)代替參數(shù)赘娄,如果你不需要用它的話仆潮。
我們添加了適配器,但在 MainActivity 中還沒(méi)有把它設(shè)置到 RecyclerView 中:
class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
binding.executePendingBindings()
binding.repositoryRv.layoutManager = LinearLayoutManager(this)
binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)
}
override fun onItemClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
讓我們來(lái)運(yùn)行試試:
很奇怪遣臼。發(fā)生了啥性置?
Activity 被創(chuàng)建了,所以新的適配器也創(chuàng)建了揍堰,但里面的 repositories 實(shí)際上是空的
我們點(diǎn)擊了按鈕
調(diào)用了 loadRepositories 函數(shù)鹏浅,顯示了進(jìn)度條
2 秒后嗅义,我們拿到了倉(cāng)庫(kù)列表,隱藏了進(jìn)度條隐砸,但倉(cāng)庫(kù)列表沒(méi)顯示之碗。因?yàn)闆](méi)有調(diào)用 notifyDataSetChanged
一旦我們旋轉(zhuǎn)屏幕,新的 Activity 被創(chuàng)建季希,帶有倉(cāng)庫(kù)參數(shù)的新的適配器也被創(chuàng)建了褪那,所以實(shí)際上 viewModel 是有數(shù)據(jù)的。
那么式塌,MainViewModel 該怎樣才能通知 MainActivity 更新了項(xiàng)目博敬,好讓我們可以調(diào)用 notifyDataSetChanged 呢?
不應(yīng)該這樣做峰尝。
這點(diǎn)非常重要:MainViewModel 不應(yīng)該知道任何關(guān)于MainActivity的東西偏窝。
MainActivity 才擁有 MainViewModel實(shí)例,所以應(yīng)該讓它來(lái)監(jiān)聽(tīng)數(shù)據(jù)變化并通知Adapter武学。那怎么做祭往?
我們可以觀察repositories,這樣一旦數(shù)據(jù)改變了火窒,我們就能改變我們的 adapter硼补。
這個(gè)方案中可能出錯(cuò)的地方?
我們先來(lái)看看下面的場(chǎng)景:
在 MainActivity 中沛鸵,我們觀察了 repositories:一旦改變括勺,我們調(diào)用 notifyDataSetChanged
我們點(diǎn)擊了按鈕
當(dāng)我們等待數(shù)據(jù)改變時(shí),MainActivity 可能會(huì)因?yàn)?strong>配置改變而被重新創(chuàng)建
我們的 MainViewModel 依然存在
2 秒后曲掰,我們的 repositories 獲得新的數(shù)據(jù)疾捍,然后通知觀察者數(shù)據(jù)已經(jīng)改變
觀察者嘗試調(diào)用不再存在的 adapter 的 notifyDataSetChanged,因?yàn)? MainActivity 已經(jīng)重新創(chuàng)建了
所以栏妖,我們的方案還不夠好乱豆。
介紹 LiveData
LiveData 是另一個(gè)生命周期感知的組件。它能觀察 View 的生命周期吊趾。這樣一來(lái)宛裕,一旦 Activity 因?yàn)榕渲酶淖兌讳N(xiāo)毀,LiveData 就能夠知道论泛,它也就能夠從被銷(xiāo)毀的 Activity 中回收觀察者揩尸。
讓我們?cè)?MainViewModel 中實(shí)現(xiàn)它:
class MainViewModel : ViewModel() {
var repoModel: RepoModel = RepoModel()
val text = ObservableField("old data")
val isLoading = ObservableField(false)
var repositories = MutableLiveData<ArrayList<Repository>>()
fun loadRepositories() {
isLoading.set(true)
repoModel.getRepositories(object : OnRepositoryReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories.value = data
}
})
}
}
然后在 MainActivity 中觀察改動(dòng):
class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
private lateinit var binding: ActivityMainBinding
private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
binding.executePendingBindings()
binding.repositoryRv.layoutManager = LinearLayoutManager(this)
binding.repositoryRv.adapter = repositoryRecyclerViewAdapter
viewModel.repositories.observe(this,
Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} })
}
override fun onItemClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
it關(guān)鍵字是什么意思呢?如果某個(gè)函數(shù)只有一個(gè)參數(shù)屁奏,那么那個(gè)參數(shù)就可以用it來(lái)代替岩榆。假設(shè)我們有個(gè)乘以 2 的 lambda 表達(dá)式:
((a) -> 2 * a)
我們可以替換成這樣:
(it * 2)
如果你現(xiàn)在運(yùn)行,你會(huì)看到一切都正常工作了:
為什么相比 MVP 我更傾向于 MVVM?
沒(méi)有 View 的無(wú)聊的接口勇边,因?yàn)?ViewModel 沒(méi)有 View 的引用犹撒。
沒(méi)有 Presenter 的無(wú)聊的接口,因?yàn)楦静恍枰?/p>
更容易處理配置改動(dòng)粒褒。
使用 MVVM识颊,Activity,F(xiàn)ragment 里的代碼更少奕坟。
存儲(chǔ)庫(kù)模式
我之前說(shuō)過(guò)祥款,Model 是準(zhǔn)備數(shù)據(jù)的抽象層。通常执赡,它包括存儲(chǔ)和數(shù)據(jù)類镰踏。每個(gè)實(shí)體(數(shù)據(jù))類都應(yīng)該對(duì)應(yīng)存儲(chǔ)類。例如沙合,如果我們有個(gè) User 和 Post 數(shù)據(jù)類,我們應(yīng)該也有 UserRepository 和 PostRepository 類跌帐。所有的數(shù)據(jù)都應(yīng)該直接從它們中獲取首懈。我們永遠(yuǎn)不應(yīng)該在 View 或者 ViewModel 中調(diào)用 Shared Preferences 或者 DB 實(shí)例。
所以谨敛,我們可以重命名我們的 RepoModel 為 GitRepoRepository究履,GitRepo 從 GitHub 倉(cāng)庫(kù)中獲取,Repository 從存儲(chǔ)庫(kù)模式中獲取脸狸。
class GitRepoRepository {
fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First", "Owner 1", 100, false))
arrayList.add(Repository("Second", "Owner 2", 30, true))
arrayList.add(Repository("Third", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000)
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
MainViewModel 從 GitRepoRepsitories 獲取 GitHub 倉(cāng)庫(kù)列表最仑,但 GitRepoRepositories 又是從哪來(lái)的呢?你可以在 repository 中調(diào)用
client 或者 DB 實(shí)例直接去拿炊甲,但這仍然不是最佳實(shí)踐泥彤。你必須盡可能地模塊化你的 app。如果你用不同的客戶端卿啡,用 Retrofit 替代 Volley 呢吟吝?如果你在里面寫(xiě)了一點(diǎn)邏輯,你很難去重構(gòu)它颈娜。你的 repository 不需要知道你正在使用哪一個(gè)客戶端來(lái)獲取遠(yuǎn)程數(shù)據(jù)剑逃。
repository 需要知道的唯一一件事是數(shù)據(jù)從遠(yuǎn)程還是本地獲取的。不需要知道我們是如何從遠(yuǎn)程或者本地獲取官辽。
ViewModel 需要的唯一一件事是數(shù)據(jù)
View 需要做的唯一一件事就是展示數(shù)據(jù)
我剛開(kāi)始開(kāi)發(fā) Android 時(shí)蛹磺,我曾經(jīng)想知道應(yīng)用時(shí)如何離線工作的,如何同步數(shù)據(jù)同仆。好的應(yīng)用架構(gòu)允許我們讓這些變得簡(jiǎn)單萤捆。例如,當(dāng) ViewModel 中的 loadRepositories 被調(diào)用時(shí),如果有連接網(wǎng)絡(luò)鳖轰,GitRepoRepositories 就能從遠(yuǎn)程數(shù)據(jù)源中獲取數(shù)據(jù)清酥,然后保存到本地。一旦手機(jī)處于離線模式蕴侣,GitRepoRepository 就能從本地?cái)?shù)據(jù)源獲取數(shù)據(jù)焰轻。這樣一來(lái),Repositories 就應(yīng)該有 RemoteDataSource 和 LocalDataSource 的實(shí)例昆雀,以及處理數(shù)據(jù)從哪里來(lái)的邏輯辱志。
讓我們先來(lái)添加本地?cái)?shù)據(jù)源:
class GitRepoLocalDataSource {
fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First From Local", "Owner 1", 100, false))
arrayList.add(Repository("Second From Local", "Owner 2", 30, true))
arrayList.add(Repository("Third From Local", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000)
}
fun saveRepositories(arrayList: ArrayList<Repository>){
//todo save repositories in DB
}
}
interface OnRepoLocalReadyCallback {
fun onLocalDataReady(data: ArrayList<Repository>)
}
我們有兩個(gè)方法:首先返回偽造的本地?cái)?shù)據(jù),其次就是保存數(shù)據(jù)狞膘。
現(xiàn)在來(lái)添加遠(yuǎn)程數(shù)據(jù)源:
class GitRepoRemoteDataSource {
fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First from remote", "Owner 1", 100, false))
arrayList.add(Repository("Second from remote", "Owner 2", 30, true))
arrayList.add(Repository("Third from remote", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000)
}
}
interface OnRepoRemoteReadyCallback {
fun onRemoteDataReady(data: ArrayList<Repository>)
}
這個(gè)只有一個(gè)方法返回偽造的遠(yuǎn)程數(shù)據(jù)揩懒。
現(xiàn)在可以在我們的 repository 中添加一些邏輯了:
class GitRepoRepository {
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
所以,分離數(shù)據(jù)源可以讓我們更容易把數(shù)據(jù)保存到本地挽封。
如果你只需要從網(wǎng)絡(luò)獲取數(shù)據(jù)已球,你仍需要存儲(chǔ)庫(kù)模式嗎?是的辅愿。這會(huì)讓你的代碼更容易測(cè)試智亮,其他開(kāi)發(fā)者也能更好地理解你的代碼,你也可以更快地維護(hù)点待。:)
Android 管理封裝器
如果你想要在 GitRepoRepository 中檢查網(wǎng)絡(luò)連接阔蛉,這樣你就可以知道用哪個(gè)數(shù)據(jù)源獲取數(shù)據(jù)呢?我們已經(jīng)說(shuō)過(guò)我們不應(yīng)該在 ViewModels 和Models里放任何 Android 相關(guān)的代碼癞埠,那么怎么處理這個(gè)問(wèn)題呢状原?
讓我們來(lái)創(chuàng)造一個(gè)網(wǎng)絡(luò)連接的封裝器:
class NetManager(private var applicationContext: Context) {
private var status: Boolean? = false
val isConnectedToInternet: Boolean?
get() {
val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val ni = conManager.activeNetworkInfo
return ni != null && ni.isConnected
}
}
如果我們?cè)?manifest 中添加權(quán)限的話上面的代碼就可以起作用了:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
但是因?yàn)槲覀儧](méi)有 context,如何在 Repository 中創(chuàng)建實(shí)例呢苗踪?我們可以在構(gòu)造器中得到:
class GitRepoRepository (context: Context){
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
val netManager = NetManager(context)
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
我們之前在 ViewModel 中創(chuàng)建了 GitRepoRepository 的實(shí)例颠区,因?yàn)槲覀兊?NetManager 需要一個(gè) Context,我們?cè)鯓釉?ViewModel 中拿到徒探?你可以從生命周期感知的組件庫(kù)中拿到 AndroidViewModel瓦呼,它有一個(gè) context。這個(gè) context 是應(yīng)用的上下文测暗,而不是 Activity 的:
class MainViewModel : AndroidViewModel {
constructor(application: Application) : super(application)
var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication()))
val text = ObservableField("old data")
val isLoading = ObservableField(false)
var repositories = MutableLiveData<ArrayList<Repository>>()
fun loadRepositories() {
isLoading.set(true)
gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories.value = data
}
})
}
}
這一行:
constructor(application: Application) : super(application)
我們?yōu)?MainViewModel 定義了一個(gè)構(gòu)造器央串。這是必要的,因?yàn)?AndroidViewModel 在它的構(gòu)造器中請(qǐng)求了 Application 實(shí)例碗啄。所以在我們的構(gòu)造器中可以調(diào)用 super 方法质和,這樣被我們繼承的 AndroidViewModel 的構(gòu)造器就會(huì)被調(diào)用。
注意:我們可以用一行代碼來(lái)表示:
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
}
現(xiàn)在稚字,我們?cè)?GitRepoRepository 中有了 NetManager 實(shí)例饲宿,我們就可以檢查網(wǎng)絡(luò)連接了:
class GitRepoRepository(val netManager: NetManager) {
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
netManager.isConnectedToInternet?.let {
if (it) {
remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
override fun onRemoteDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
} else {
localDataSource.getRepositories(object : OnRepoLocalReadyCallback {
override fun onLocalDataReady(data: ArrayList<Repository>) {
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
如果我們連接了網(wǎng)絡(luò)厦酬,我們就獲取遠(yuǎn)程數(shù)據(jù)然后保存到本地。否則瘫想,我們就從本地拿數(shù)據(jù)仗阅。
Kotlin 筆記:let 操作符會(huì)檢查是否為空并返回一個(gè) it 值。
接下來(lái)的文章中国夜,我會(huì)介紹依賴注入减噪,為什么在 ViewModel 中創(chuàng)建 repository 實(shí)例是不好的,以及如何避免使用 AndroidViewModel车吹。
英文原文:Modern Android development with Kotlin (Part 2)
舊文推薦:
用 Kotlin 開(kāi)發(fā)現(xiàn)代 Android 項(xiàng)目 Part 1
Kotlin 讓使用 Android API 變得輕松
“Effective Java” 可能對(duì) Kotlin 的設(shè)計(jì)造成了怎樣的影響——第一部分