Android Architecture Components(3) - ViewModel

上一篇文章中我們介紹了Architecture Components中的LifeCycle,LifeCycleOwner及LifeCycleObserver朴乖,不知道大家掌握的怎么樣吸耿?在學(xué)習(xí)編碼的路上氓癌,還是要多多實踐才可以呢绒障。
接下來我們要介紹的是ViewModel番舆。


ViewModel簡介

ViewModel是用來存儲和管理生命周期過程敏感的界面數(shù)據(jù)的一個類酝碳,用ViewModel存儲的數(shù)據(jù)可以在應(yīng)用設(shè)置項發(fā)生改變時保存下來例如當(dāng)屏幕旋轉(zhuǎn)。

Android框架層管理UI控件的生命周期比如Activity和Fragment恨狈∈杌框架層需要決定在面對用戶交互時何時銷毀或者重建UI控件,這一過程不是由開發(fā)者控制的禾怠。

如果系統(tǒng)銷毀活著重建UI控件沃斤,那么用戶的輸入數(shù)據(jù)活著你已緩存的UI數(shù)據(jù)都會丟失,例如刃宵,在你的應(yīng)用中有一個Activity展示著一個用戶列表衡瓶,當(dāng)屏幕發(fā)生旋轉(zhuǎn)時,Activity被重建牲证,那么新創(chuàng)建的Activity需要再去請求一次用戶列表數(shù)據(jù)哮针。對于一些簡單的數(shù)據(jù),我們可以使用onSavedInstanceState()方法存儲在Bundle中坦袍,然后在onCreate()函數(shù)中恢復(fù)十厢,但是這種情況只適用于少量并且可以被序列化的數(shù)據(jù),并不適用于其他數(shù)據(jù)捂齐,例如說一個用戶列表或者很多圖片蛮放。

另一個問題是UI控件需要頻繁的發(fā)起異步請求并等待返回結(jié)果。UI控件需要去管理這些異步請求并保證在它完成后被系統(tǒng)清理掉以避免內(nèi)存泄漏奠宜。這種管理需要大量的耐心和細(xì)心包颁,并且在這些對象因為設(shè)置改變而重建的情形下,造成了一種資源浪費(fèi)压真。

Activity和Fragment這種UI空間只是去展示UI數(shù)據(jù)娩嚼,響應(yīng)用戶交互或者處理系統(tǒng)交互,例如說請求權(quán)限滴肿,UI控件同時也需要負(fù)責(zé)從數(shù)據(jù)庫或者網(wǎng)絡(luò)上加載數(shù)據(jù)岳悟,我們應(yīng)該進(jìn)行責(zé)任分?jǐn)偅灰獮閁I控件添加過多的操作泼差,這樣會導(dǎo)致一個類去處理一個應(yīng)用所要處理的工作贵少,讓我們的測試工作變得更加艱難。

綜合以上幾點(diǎn)堆缘,Google推出了ViewModel滔灶,用于幫助UI控件準(zhǔn)備數(shù)據(jù),ViewModel會在設(shè)置發(fā)生變化時自動存儲數(shù)據(jù)套啤,他們所持有的數(shù)據(jù)可以立刻被新建的Fragment或者Activity復(fù)用宽气。

ViewModel的使用

假設(shè)我們需要在一個 應(yīng)用頁面內(nèi)展示一個用戶列表随常,那么我們可以將數(shù)據(jù)請求操作托管給ViewModel,代碼如下:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }
    private void loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}

實現(xiàn)一個ViewModel只需要繼承自ViewModel即可萄涯,隨后我們可以在Activity中訪問數(shù)據(jù)绪氛,方式如下:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

如果這個Activity被重建,那么他會接收到上一個Activity所創(chuàng)建的MyViewModel對象涝影,當(dāng)持有該ViewModel對象的Activity銷毀時枣察,系統(tǒng)會調(diào)用ViewModel.onCleared()方法釋放資源。

注意ViewModel對象絕不能持有View燃逻,LifeCycle或者任何持有Activity 引用的對象

在設(shè)計理念上序目,ViewModel的生命周期獨(dú)立于View或者LifeCycleOwner,這種設(shè)計也意味著你可以更簡單的為ViewModel編寫測試用例以覆蓋ViewModel中的操作伯襟。ViewModel可以持有LifeCycleObservers比如說LiveData猿涨,如果ViewModel需要使用Application的引用,例如說去獲取一個系統(tǒng)服務(wù)姆怪,此時可以繼承自AndroidViewModel叛赚,該類有一個構(gòu)造函數(shù),可以接受Application的引用稽揭。

這里我再舉一個簡單的ViewModel的例子俺附,以便大家更好的理解ViewModel。這個例子是界面上有一個Button和一個TextView溪掀,TextView用于記錄Button 的點(diǎn)擊次數(shù)事镣,在屏幕旋轉(zhuǎn)時保持TextView上的次數(shù)不變。

首先編寫布局文件揪胃,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.code.archicomponentssmaples.MainActivity">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:text="Click Me"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

這里我試用了約束布局璃哟,不懂得同學(xué)可以自行百度,或者使用LinearLayout/RelativeLayout自己實現(xiàn)即可只嚣。

編寫ViewModel類沮稚,用于持有Button的點(diǎn)擊次數(shù),如下:

public class ClickCounterViewModal extends ViewModel {
    private int count = 0;
    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }
}

隨后在Activity中使用該ViewModel緩存Button點(diǎn)擊次數(shù)册舞,代碼如下:

public class LifeOwnerActivity extends AppCompatActivity {

  private TextView mTextView;
  private Button mButton;
  private ClickCounterViewModal mClickCounterViewModal;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_life_owner);
    mClickCounterViewModal = ViewModelProviders.of(this).get(ClickCounterViewModal.class);
    initView();
    initData();
  }

  private void initView(){
    mTextView = (TextView)findViewById(R.id.owner_textView);
    mButton = findViewById(R.id.owner_Button);
  }

  private void initData(){
    displayClickCount();

  mButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
      mClickCounterViewModal.setCount(mClickCounterViewModal.getCount()+1);
        displayClickCount();
      }
    });
  }

  private void displayClickCount(){
    mTextView.setText(mClickCounterViewModal.getCount()+"");
  }
}

如上述代碼ViewModel對象需要使用ViewModelProviders.of(Context)進(jìn)行初始化。

另外多說一點(diǎn)ViewModel的提出只是為了處理UI相關(guān)數(shù)據(jù)的緩存問題障般,并不代表它能完全替代onSavedInstanceState()的作用调鲸。

在這個例子中ViewModel的工作流程如下圖:

這里寫圖片描述

ViewModel生命周期

ViewModel對象的生命周期依賴于初始化時ViewModelProvider傳入的LifeCycle對象,ViewModel對象會常駐在內(nèi)存中直到與其對應(yīng)的LifeCycle被銷毀挽荡,對于Activity而言藐石,就是當(dāng)其被finish時,對于Fragment而言就是當(dāng)它被detach的時候定拟。
下圖說明了一個Activity在發(fā)生屏幕旋轉(zhuǎn)時自身的生命周期變化以及與其對應(yīng)的ViewModel的生命周期于微。

這里寫圖片描述

通常情況下逗嫡,在System調(diào)起Activity時,我們在Activity的onCreate()函數(shù)內(nèi)初始化ViewModel對象株依,隨后系統(tǒng)可能多次調(diào)用該Activity的onCreate()函數(shù)驱证,例如說發(fā)生多次屏幕旋轉(zhuǎn)。這種清醒下ViewModel來源于第一次onCreate(),直到調(diào)用Activity.finish()時恋腕,該ViewModel對象才會被銷毀并釋放資源抹锄。

使用ViewModel在Fragment間共享數(shù)據(jù)

在我們?nèi)粘W兂缮钪校珹ctivity需要與其內(nèi)部的一個或多個Fragment交互信息的需求比比皆是荠藤,假設(shè)又這樣一種情形伙单,我們有一個Activity頁面,左右各一個Fragment哈肖,左側(cè)展示列表吻育,右側(cè)展示列表中某一項的詳情。這種情形下Fragment中需要定義接口淤井,持有Fragment的Activity需要同時綁定這兩個Fragment扫沼,并且一個Fragment要處理另一個Fragment沒有創(chuàng)建或顯示的問題。

這種痛點(diǎn)可以通過ViewModel解決庄吼,這兩個Fragment可以公用一個ViewModel對象缎除,在Activity內(nèi)處理,示例代碼如下:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
    public void select(Item item) {
        selected.setValue(item);
    }
    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

如上所示总寻,兩個Fragment只需要監(jiān)聽ViewModel中值的變化更新UI即可器罐。
需要注意的是這兩個Fragment在ViewModelProvider內(nèi)傳入了getActivity(), 此時該ViewModel對象的生命周期完全依賴于持有兩個Fragment的Activity。

這種模式有如下幾點(diǎn)好處:

  • Activity不需要關(guān)注交互中的任何事渐行;
  • Fragments彼此不需要知道對方的狀態(tài)轰坊,一個Fragment消失了,另一個仍然可以正常工作祟印;
  • 每個Fragment有其獨(dú)立的生命周期肴沫,不會彼此影響,如果一個Fragment被另一個Fragment替換了蕴忆,UI仍然能正常顯示颤芬;

使用ViewModel代替Loaders

類似于CursorLoader的加載類經(jīng)常被頻繁的用于異步維護(hù)界面和數(shù)據(jù)庫的數(shù)據(jù)一致,現(xiàn)在你也可以用ViewModel和一些其他的輔助類來實現(xiàn)這種功能了套鹅,使用ViewModel可以使我們的界面控制與數(shù)據(jù)加載解耦站蝠。
一種常見的使用CursorLoader監(jiān)聽數(shù)據(jù)庫變化的結(jié)構(gòu)圖如下,當(dāng)一個數(shù)據(jù)庫值發(fā)生改變時卓鹿,加載器會自動觸發(fā)一個重新加載事件菱魔,完成后更新UI。


這里寫圖片描述

當(dāng)ViewModel與Room以及LiveData結(jié)合使用時吟孙,就可以完全替代加載器的功能澜倦,ViewModel保證數(shù)據(jù)在設(shè)置發(fā)生改變的過程中不清空聚蝶,當(dāng)數(shù)據(jù)發(fā)生改變時,Room通知LiveData藻治,隨后使用新的數(shù)據(jù)更新UI碘勉。


這里寫圖片描述

當(dāng)數(shù)據(jù)變得越來越復(fù)雜時,你或許會使用一個單獨(dú)的類去加載數(shù)據(jù)栋艳,ViewModel的目的就在于保證數(shù)據(jù)在生命周期變化過程中不被清空恰聘,關(guān)于LiveData,Room相關(guān)的更多詳細(xì)內(nèi)容吸占,我們會在下一篇推文中詳細(xì)介紹晴叨,感謝大家閱讀。更多詳細(xì)內(nèi)容歡迎大家關(guān)注我的公眾號
[圖片上傳失敗...(image-feea9f-1512483891026)]
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矾屯,一起剝皮案震驚了整個濱河市兼蕊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌件蚕,老刑警劉巖孙技,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異排作,居然都是意外死亡牵啦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門妄痪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哈雏,“玉大人,你說我怎么就攤上這事衫生∩驯瘢” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵罪针,是天一觀的道長彭羹。 經(jīng)常有香客問我,道長泪酱,這世上最難降的妖魔是什么派殷? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮西篓,結(jié)果婚禮上愈腾,老公的妹妹穿的比我還像新娘。我一直安慰自己岂津,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布悦即。 她就那樣靜靜地躺著吮成,像睡著了一般橱乱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上粱甫,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天泳叠,我揣著相機(jī)與錄音,去河邊找鬼茶宵。 笑死危纫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的乌庶。 我是一名探鬼主播种蝶,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瞒大!你這毒婦竟也來了螃征?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤透敌,失蹤者是張志新(化名)和其女友劉穎盯滚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酗电,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魄藕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撵术。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片背率。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖荷荤,靈堂內(nèi)的尸體忽然破棺而出退渗,到底是詐尸還是另有隱情,我是刑警寧澤蕴纳,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布会油,位于F島的核電站,受9級特大地震影響古毛,放射性物質(zhì)發(fā)生泄漏翻翩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一稻薇、第九天 我趴在偏房一處隱蔽的房頂上張望嫂冻。 院中可真熱鬧,春花似錦塞椎、人聲如沸桨仿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽服傍。三九已至钱雷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吹零,已是汗流浹背罩抗。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灿椅,地道東北人套蒂。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像茫蛹,于是被迫代替她去往敵國和親操刀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容