前言
學習記錄系列是通過閱讀學習《Android Jetpack應用指南》對書中內(nèi)容學習記錄的Blog痒芝,《Android Jetpack應用指南》京東天貓有售,本文是學習記錄的第四篇陨帆。
誕生
在頁面(Activity/Fragment)功能較為簡單的情況下,通常會將UI交互、與數(shù)據(jù)獲取等相關(guān)的業(yè)務邏輯全部寫在頁面中坡锡。但是在頁面功能復雜的情況下猴蹂,這樣做是不合適的院溺,因為它不符合“單一功能原則”。頁面只應該負責處理用戶與UI控件的交互磅轻,并將數(shù)據(jù)展示到屏幕上珍逸。與數(shù)據(jù)相關(guān)的業(yè)務邏輯應該單獨處理和存放。
單一功能原則:在維基百科中關(guān)于“單一功能原則”的定義聋溜。在面向?qū)ο缶幊填I(lǐng)域中谆膳,單一功能原則(Single responsibility principle)規(guī)定每個類都應該有一個單一的功能,并且該功能應該由這個類完全封裝起來撮躁。這個類的所有服務都應該嚴密地和該功能平行(功能平行漱病,意味著沒有依賴)
簡介
ViewModel專門用于存放在應用程序頁面所需的數(shù)據(jù)。ViewModel 是介于 View(視圖)和 Model(數(shù)據(jù)模型)之間的一個東西把曼。它起到了橋梁的作用杨帽,使視圖和數(shù)據(jù)既能夠分離開,也能夠保持通信祝迂。
如圖所示睦尽,ViewModel將頁面所需的數(shù)據(jù)從頁面中剝離出來,頁面只需要處理用戶交互和展示數(shù)據(jù)
ViewModel 的生命周期
ViewModel 生命周期是貫穿整個 activity 生命周期型雳,包括 Activity 因旋轉(zhuǎn)造成的重創(chuàng)建当凡,直到 Activity 真正意義上銷毀后才會結(jié)束山害。既然如此,用來存放數(shù)據(jù)再好不過了沿量。
ViewModel 的基本使用方法
1.在 app 的 build.gradle 中添加依賴浪慌。
dependencies {
添加ViewModel依賴
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.2.0'
}
2.寫一個繼承自 ViewModel 的類,將其命名為 TimerViewModel
public class TimerViewModel extends ViewModel {
@Override
protected void onCleared() {
super.onCleared();
}
}
ViewModel 是一個抽象類朴则,其中只有一個 onCleared()方法权纤。當 ViewModel 不再被需要,即與之相關(guān)的 Activity 都被銷毀時乌妒, 該方法會被系統(tǒng)調(diào)用汹想。可以在該方法中執(zhí)行一些資源釋放的相關(guān)操作撤蚊。注意古掏,由于屏幕旋轉(zhuǎn)而導致的 Activity 重建,并不會調(diào)用該方法侦啸。
3.前面提到槽唾,ViewModel 最重要的作用時將視圖與數(shù)據(jù)分離荷鼠,并獨立與 Activity 的重建航背。為了驗證這一點,在 ViewModel 中創(chuàng)建一個計時器 Timer佑刷,每隔 1s忘闻,通過接口 OnTimerChangeListener 通知它的調(diào)用者钝计。
public class TimerViewModel extends ViewModel {
private Timer timer;
private int currentSecond;
/**
* ViewModel最重要的作用是將視圖與數(shù)據(jù)分離,并獨立于Activity的重建服赎。
* 為了驗證這樣一點葵蒂,在ViewModel中創(chuàng)建一個計時器Timer,每隔1s通過接口
* OnTimerChangeListener通知它的調(diào)用者
*/
public void startTiming() {
if (timer == null) {
currentSecond = 0;
timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
currentSecond ++;
if (onTimerChangeListener != null) {
onTimerChangeListener.onTimeChanged(currentSecond);
}
}
};
timer.schedule(timerTask, 1000, 1000);
}
}
/**
* 通過接口的方式完成對調(diào)用者的通知
*/
public interface OnTimerChangeListener {
void onTimeChanged(int currentSecond);
}
private OnTimerChangeListener onTimerChangeListener;
public void setOnTimerChangeListener(OnTimerChangeListener onTimerChangeListener) {
this.onTimerChangeListener = onTimerChangeListener;
}
/**
* ViewModel是一個抽象類重虑,其中只有一個onCleared()方法践付。
* 當ViewModel不再被需要,即與之相關(guān)的Activity都被銷毀時缺厉,
* 該方法會被系統(tǒng)調(diào)用永高。可以在該方法中執(zhí)行一些資源釋放相關(guān)操作
*/
@Override
protected void onCleared() {
super.onCleared();
timer.cancel();
}
}
4.在 TimerActivity 中監(jiān)聽 OnTimerChangeListener 發(fā)來的通知提针,并根據(jù)通知更新 UI 界面命爬。ViewModel 的實例化過程,是通過 ViewModelProvider 來完成的辐脖。ViewModelProvider 會判斷 ViewModel 是否存在饲宛,若存在則直接返回,否則它會創(chuàng)建一個 ViewModel嗜价。
public class TimerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
initComponent();
}
private void initComponent() {
final TextView tvTimer = findViewById(R.id.tv_timer);
// 實例化ViewModel
TimerViewModel testViewModel = new ViewModelProvider(this).get(TimerViewModel.class);
testViewModel.setOnTimerChangeListener(currentSecond -> {
// 更新UI界面
runOnUiThread(new Runnable() {
@Override
public void run() {
tvTimer.setText("Timer: " + currentSecond);
}
});
});
testViewModel.startTiming();
}
}
運行程序并旋轉(zhuǎn)屏幕艇抠,當旋轉(zhuǎn)屏幕導致 Activity 重建時幕庐,計時器沒有停止。這意味著在橫/豎屏狀態(tài)下的 Activity 所對應的 ViewModel 是同一個家淤,它并沒有被銷毀异剥,它所持有的數(shù)據(jù)也一直到存在著。
ViewModel 的原理
在頁面中通過 ViewModelProvider 類來實例化 ViewMdeol
TestViewModel testViewModel = new ViewModelProvider(this).get(TestViewModel.class);
ViewModelPrivider 接收一個 ViewModelStoreOwner 對象作為參數(shù)絮重。在以上示例代碼中該參數(shù)是 this 冤寿,指代當前的 Activity。這是因為 Activity 繼承自 FragmentActivity青伤,而在 androidx 依賴包中督怜,F(xiàn)ragmentActivity 默認實現(xiàn) ViewModelStoreOwner 接口。
public class FragmentActivity extends ComponentActivity implements
ActivityCompat.OnRequestPermissionsResultCallback,
ActivityCompat.RequestPermissionsRequestCodeValidator {
...
@NonNull
@Override
public ViewModelStore getViewModelStore() {
return FragmentActivity.this.getViewModelStore();
}
...
}
接口方法 getViewModelStore() 所定義的返回類型為 ViewModelStore潮模。
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}
從 ViewModelStore 的源碼可以看出亮蛔,ViewModel 實際是以 HashMap<String,ViewModel>的形式被緩存起來了擎厢。ViewModel 與頁面之間沒有直接的關(guān)聯(lián),它們通過 ViewModelProvider 進行關(guān)聯(lián)辣吃。當頁面需要 ViewModel 時动遭,會向 ViewModelProvider 索要,ViewModelProvider 檢查該 ViewModel 是否已經(jīng)存在于緩存中神得,若存在厘惦,則直接返回,若不存在哩簿,則實例化一個宵蕉。因此,Activity 由于配置變化導致的銷毀重建并不會影響 ViewModel 节榜,ViewModel 是獨立于頁面存在的羡玛。也正因為此,在使用 ViewModel 時需要特別注意宗苍,不需要向 ViewModel 中傳入任何類型的 Context 或 帶有 Context 引用的對象稼稿,這可能會導致頁面無法被銷毀,從而引發(fā)內(nèi)存泄漏讳窟。
需要注意的是让歼,除了 Activity,androidx 依賴包中的 Fragment 也默認實現(xiàn)了 ViewModelStoreOwner 接口丽啡。因此谋右,也可以在 Fragment 中正常使用 ViewModel。
ViewModel 與 AndroidViewModel
ViewModel 中不能將任何類型和 Context 或 含有 Context引用的對象傳入到 ViewModel 中补箍,因為這可能會導致內(nèi)存泄漏改执。如果希望在 ViewModel 中使用 Context浦徊,可以使用 AndroidViewModel 類,它繼承自 ViewModel天梧,并接收 Application 作為 Context盔性。這意味著,它的生命周期和 Application 是一樣的呢岗,那么這就不算是一個內(nèi)存泄漏了冕香。
ViewModel 與 onSaveInstanceState()方法
1.onSaveInstanceState()方法只能保存少量的、能支持序列化的數(shù)據(jù)后豫。ViewModel沒有這個限制
2.ViewModel 能支持頁面中所有的數(shù)據(jù)悉尾。ViewModel 不支持數(shù)據(jù)的持久化,當頁面被徹底銷毀時挫酿,ViewModel 及持有的數(shù)據(jù)就不存在了构眯。onSaveInstanceState()方法可以持久化頁面的數(shù)據(jù)。
3.二者不可混淆
總結(jié)
ViewModel 可以幫助我們更好地將頁面與數(shù)據(jù)從代碼層間上分離開來早龟。更重要的是惫霸,依賴于 ViewModel 的生命周期特性,我們不再需要關(guān)心屏幕旋轉(zhuǎn)帶來的數(shù)據(jù)丟失的問題葱弟,進而也不需要重新獲取數(shù)據(jù)壹店。
需要注意的是,在使用 ViewModel 的過程中芝加,千萬不要將任何類型的 Context 或 含有 Context引用的對象傳入到 ViewModel 硅卢,這可能會引起內(nèi)存泄漏。如果一定要在 ViewModel 中使用 Context藏杖,那么建議使用 ViewModel 的子類 AndroidViewModel将塑。