玩轉Android Jetpack系列之ViewMode

Android心路歷程發表於2019-05-07

1. 什麼是Android Jetpack?

Android Jetpack是谷歌在2018年I/O開發者大會上推出的新一代元件、工具和架構指導,旨在加快開發者的 Android 應用開發速度。 ——[官方介紹網站]1

Google爸爸老司機在2018年的I/O大會上發車了,推出了新一代的開發元件、工具和架構指導,並打包在一起,取名為“Jetpack”,顧名思義,Jetpack直譯過來的意思就是噴氣揹包,Google也使用了一個很形象的穿戴噴氣揹包的android機器人來做形象代言,如下圖。

玩轉Android Jetpack系列之ViewMode

通過圖片可以很明確的感受到谷歌爸爸的意圖:帶你飛!。簡單的概況,Jetpack推出的主要目的是為了能夠讓開發者更加快速、方便以及高質量的完成產品開發,Jetpack的主要作用可以概況為以下幾點:

  • 加速開發速度,Jetpack包含的一系列元件可以分開使用,也可以相互組合使用,同時還完美支援Kotlin語言的功能特點進一步提升開發效率
  • 聚焦開發的核心,Jetpack提供的元件可以協助管理日常繁瑣且容易出錯的地方,比如生命週期的管理,後臺任務的管理,導航的處理等等,這樣開發可以將開發的注意力放到更加核心的地方上。
  • 提升應用開發的質量,利用Jetpack元件進行開發可以有效減少記憶體溢位、崩潰的概率,提升應用開發的質量,並提供向後的相容性

Jetpack元件主要分為四個方向:基礎,架構,行為和UI。詳情見下表:

基礎架構行為UI
AppCompatData BindingDownload ManagerAnimation & transitions
Android KTXLifecyclesMedia & playbackAuto
MultidexLiveDataNotificationsEmoji
TestNavigationPermissionsFragment
-PagingSharingLayout
-RoomSlicesPalette
-ViewMode-TV
-WorkManager-Wear OS by Google

如上表格,有些內容是很早就有的,有些是最近推出的,Jetpack將這些內容打包在一起,共同組成Jetpack,本系列主要對Jetpack新推出的架構這一塊內容進行敘述。

2. ViewMode概述

ViewMode主要用來管理和儲存與UI繫結的資料,同時ViewMode還與UI的生命週期相關聯。例如ViewMode的一大特色在於:與ViewMode相關聯的UI介面如果因為某些原因需要重新繪製建立時,例如橫豎屏切換,導致Activity銷燬並重新建立時,ViewMode仍然可以保留之前讀取到的資料不會因為Activity的銷燬而丟失,這樣我們無需額外再浪費資源去再次請求資料。

有時候Activity或者Fragment的生命週期的變動是不受控制的,經常會因為各種事件的排程導致介面需要重新建立。當Activity需要重新建立的時候,之前與之繫結的資料也會丟失。比如你的應用介面通過list來展示使用者名稱單,如果介面重新建立時,之前獲取的使用者名稱單資料需要再次重新獲取,但是使用者名稱單資料相對來說是一個比較穩定的靜態資料,再次獲取一次資料顯然浪費了系統資源。

有的同學看到這裡後可能會有疑問:不對呀,Android中不是提供了onSaveInstanceState()方法來儲存資料嗎,然後重新執行OnCreate的時候,通過Bundle引數來再次獲取儲存的資料?這種方式也是可行的,但是有個限制,即這種方式只能儲存資料量較小的情況,並且資料被序列化才行。如果遇到資料量較大的時候,比如圖片資料,這種方案顯示力不從心了。

我們在日常請求資料後對UI進行繫結還有一個常見的問題,有時候我們會把資料的的請求放到非同步去操作,這樣不會因為長時間獲取資料導致UI程式的堵塞。但是隨之帶來的問題也挺多,例如我們需要管理和維護好獲取到資料後的回撥,另外在銷燬當前UI的時候,我們需要確保非同步任務中的資源有效的得到了清理,防止出現記憶體溢位。一旦我們的介面需要重新繪製的時候,我們上述所有的非同步操作需要重新建立和執行,這樣顯然浪費了系統的開銷。

我們日常使用的Activity或者Fragment,他們的主要職責就是展示UI,以及與使用者的操作行為進行互動,或者與系統的一些事件進行通訊,例如許可權管理對話方塊。如果還要求Activity對資料請求的事件進行管理和維護,這個已經超出了Activity本來的意圖,並且隨著事務的增多,Activity會越來越臃腫,一旦出了問題,需要花費大量精力去維護。

ViewMode的推出正是基於上述問題給出的解決方案,完美高效的將UI控制器和資料業務進行分離,UI控制器只負責UI展示相關的工作,資料業務只負責獲取資料的相關工作。

3. ViewMode的使用方法

通過上文的鋪墊,那麼如何使用ViewMode呢,為了方便說明,本文采用一個簡單的例子,通過Activity中的list展示一組使用者的姓名,下面詳細進行說明。ViewMode是一個抽象類,所以我們需要通過extends整合它才能夠使用,程式碼如下所示:

public class MyViewMode extends ViewModel {

    private MutableLiveData<List<User>> users;

    public LiveData<List<User>> getUsers(){
        if (users == null){
            users = new MutableLiveData<>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers(){
        String[] names = {"張三","李四","王五","John","小明","Leo","Wang","Li","Ha","Yun"};
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < names.length; i++){
            User user = new User();
            user.setName(names[i]);
            userList.add(user);
        }
        users.setValue(userList);
    }
}
複製程式碼

上述例子較為簡單,程式碼行數沒有多少,主要定義了一個型別為MutableLiveData變數:users與兩個方法。這裡的users就是我們用來存放資料的變數容器,可能有的同學注意到了,它的型別是MutableLiveData,這個型別是什麼呢?看起來很眼熟。如果各位有印象的話,我們在上文中介紹Jetpack的時候,在介紹Jetpack構成的時候,架構中有一個LiveData。恩對,這個變數型別也是Jetpack的一員。可不要小看它,它的本領很多,我們會在下一文中單獨對它進行討論。大家只要這裡記住,他是用來儲存資料的容器即可,而MutableLiveData是對LiveData的擴充套件,主要實現了set和post方法來方便更改LiveData的值。

loadUser的方法很容易理解,為了方便測試,我們這裡定義了一個字串陣列,然後通過for迴圈進行遍歷儲存到list中,最後通過LiveData提供的setValue方法將資料存放到users中。getUser是針對資料users的get方法,為了防止每次讀取user的時候都要建立一次資料,對資料進行判空處理,只有為空的情況下才呼叫loadUser去載入資料。

ViewMode的實現方式就是這麼簡單,它不需要關心UI是如何呈現的,它只關心資料如何獲取。完全與UI無關。
下面看下Activity的實現方式,主要程式碼如下:

public class MainActivity extends AppCompatActivity {

    private ListView mListView;

    private MyAdapter adapater;

    private List<User> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        MyViewMode model = ViewModelProviders.of(this).get(MyViewMode.class);
        model.getUsers().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(@Nullable List<User> users) {
                mDatas = users;
                adapater.notifyDataSetChanged();
            }
        });
    }
    .
    .
    .
複製程式碼

看完程式碼是不是發現太簡單了,沒有什麼多餘需要說明的,主要是在OnCreate方法中,首先獲取我們定義的MyViewMode,獲取的方式是通過ViewModelProviders的of方法繫結activity的例項進行初始化,然後通過get方法來獲取到MyViewMode的例項。
接著第二句通過呼叫我們在MyViewMode已經定義過的getUser方法來獲取LiveData資料,LiveData資料可以再通過observe方法進行資料回撥的返回,如上程式碼中的onChanged回撥。所以我們只要在onChange方法中做好資料重新整理UI的操作即可。

注意:ViewMode中不能引用任何View的例項,也不能引用任何持有Activity或者Context的例項。如果有些請求資料的情況必須用到Context,在繼承ViewMode的時候,可以改為繼承AndroidViewMode,這個類會返回一個帶有Context的建構函式。

4. ViewMode的生命週期

ViewMode在其生命週期的範圍內會一直儲存在記憶體中,當依附的Activity被finish後才會銷燬,或者當依附的Fragment detached後進行銷燬。為了驗證,我們在上述的例子中,每個生命週期中打出相應的log,然後在MyViewMode的loadUser和getUser也打出相應的log。然後程式執行後執行橫豎屏切換來讓生命週期重新Oncreate,然後檢視得到的log如下:

2018-07-31 10:34:48.573 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:34:48.607 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:34:48.607 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel loadUsers
2018-07-31 10:34:48.612 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:34:51.044 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:34:51.045 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy
2018-07-31 10:34:51.102 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:34:51.142 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:34:51.148 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:35:01.437 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:35:01.438 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy
2018-07-31 10:35:01.485 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:35:01.509 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:35:01.519 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:35:04.550 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:35:04.551 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel onCleared
2018-07-31 10:35:04.551 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy
複製程式碼

當開啟應用後,執行onCreate的時候,ViewMode會通過呼叫getUsers和loadUsers來獲取資料,當切換手機橫豎屏後,MainActivity會destroy並重新onCreate來重構當前介面,所以我們在log中會看到生命週期再次重新觸發onCreate,但是ViewMode僅僅呼叫了getUsers來返回資料,並沒有呼叫loadUsers,我們再回頭看上面getUsers的邏輯,當判斷users資料為空的情況下,才會去執行loadUsers,這樣也就意味著,我們切換橫豎屏後,activity被銷燬並重建後,user的資料並沒有丟失,所以並沒有重新執行獲取資料的操作。

細心的讀者可能發現了,在日誌的最後,ViewMode會執行onCleared操作,這個是ViewMode的一個回撥,表明當前Activity要徹底關閉,ViewMode需要做一些回收清理的操作,如下程式碼:

@Override
    protected void onCleared() {
        super.onCleared();
        /**
         * 這裡可以執行一些資源釋放、資料清理的操作
         */
    }
複製程式碼

下面用一張圖來標註ViewMode的生命週期與Activity的生命週期的關聯,如下圖所示:

玩轉Android Jetpack系列之ViewMode

5. 應用舉例:利用ViewMode進行Fragment之間的資料互動

我們在平日裡會遇到這樣的場景,比如檔案管理器這個應用,檔案管理器的主介面通過一個MainFragment進行封裝實現,主要呈現各個檔案類別的入口,比如有音樂、視訊、圖片、文件等的入口,點選對應的入口項,會進入對應的詳情頁,而詳情頁是由另外一個DetailFragment實現。我們傳統的實現方案是在DetailFragment中抽象暴露出一些回撥介面,然後當在MainFragment進行點選不同的入口時,執行DetailFragment中的回撥介面來展示不同的Detail詳情內容。

同樣,我們也可以利用ViewMode來實現上述場景,關鍵程式碼如下所示:

public class SharedViewModel extends ViewModel {
    //selected儲存的是被選中的item的狀態或者資料
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    //主要通過masterFragment進行呼叫互動,用來更新selected中的值
    public void select(Item item) {
        selected.setValue(item);
    }

    //主要給detailFragment進行回撥,用來通知selected的值的更新情況
    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 -> {
            //當點選某一個item的時候,更新viewmode中的selected的值
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //在onCreate中繫結ViewMode的selected的值,當有更新時通知DetailFragment
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, item -> {
           // Update the UI.
        });
    }
}
複製程式碼

上述程式碼的邏輯很簡單,MasterFragment與DetailFragment並不直接進行互動,而是各自與ViewMode進行互動,MasterFragment用來更新維護ViewMode中的資料,DetailFragment可以收到來自ViewMode中資料更新的通知。這樣便達到了兩個frangment之間的資料通訊。

6. 後記

ViewMode其實還有很多應用場景,但是需要和LiveData、Room等其他的JetPack構件一起使用效果更佳,所以這裡先賣個關子,等後續內容將LiveData、Room等內容都涉及到後,我們統一利用這些構件來嘗試做一些更牛叉的事情。


相關文章