學習記錄(4) - ViewModel

前言

學習記錄系列是通過閱讀學習《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ù)

image.png

ViewModel 的生命周期

ViewModel 生命周期是貫穿整個 activity 生命周期型雳,包括 Activity 因旋轉(zhuǎn)造成的重創(chuàng)建当凡,直到 Activity 真正意義上銷毀后才會結(jié)束山害。既然如此,用來存放數(shù)據(jù)再好不過了沿量。

image.png

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将塑。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蝌麸,隨后出現(xiàn)的幾起案子点寥,更是在濱河造成了極大的恐慌,老刑警劉巖祥楣,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件开财,死亡現(xiàn)場離奇詭異,居然都是意外死亡误褪,警方通過查閱死者的電腦和手機责鳍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兽间,“玉大人历葛,你說我怎么就攤上這事。” “怎么了恤溶?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵乓诽,是天一觀的道長。 經(jīng)常有香客問我咒程,道長鸠天,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任帐姻,我火速辦了婚禮稠集,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饥瓷。我一直安慰自己剥纷,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布呢铆。 她就那樣靜靜地躺著晦鞋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棺克。 梳的紋絲不亂的頭發(fā)上悠垛,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音逆航,去河邊找鬼鼎文。 笑死,一個胖子當著我的面吹牛因俐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播周偎,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼抹剩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蓉坎?” 一聲冷哼從身側(cè)響起澳眷,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛉艾,沒想到半個月后钳踊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡勿侯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年拓瞪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片助琐。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡祭埂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兵钮,到底是詐尸還是另有隱情蛆橡,我是刑警寧澤舌界,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站泰演,受9級特大地震影響呻拌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睦焕,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一藐握、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧复亏,春花似錦趾娃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至耕突,卻和暖如春笤成,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眷茁。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工炕泳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人上祈。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓培遵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親登刺。 傳聞我的和親對象是個殘疾皇子籽腕,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361