Android 應用架構(gòu)組件(Architecture Components)實踐

Architecture Components 是在 2017 年 Google I/O 大會上腌闯,Google 官方推出的一個構(gòu)建 Android 應用架構(gòu)的庫燎悍。它可以幫你避免在 Android 應用開發(fā)中常見的一些問題,比如:內(nèi)存泄露筹燕,管理組件生命周期等等。本文將介紹如何利用 Architecture Components 庫開發(fā)一個實際的 Android 應用 ArchitecturePractice,歡迎 fork 和 star舞丛。

本文主要分為以下兩部分:

  1. 介紹 Architecture Components 庫;
  2. 如何使用 Architecture Components 庫開發(fā)應用果漾。

關于 Architecture Components

應用開發(fā)者所面對的問題

在資源有限的移動設備中球切,在任何時候,系統(tǒng)都有可能為新的應用殺死一些原來的應用绒障,那么在原來應用中的組件(Activity吨凑、Fragment、Service等等)也會被銷毀户辱,這些組件的生命周期不受開發(fā)者控制鸵钝,而是由系統(tǒng)控制的,所以不要在應用程序組件中存儲任何應用數(shù)據(jù)和狀態(tài)焕妙,并且應用程序組件之間相互不要依賴蒋伦。

常見的構(gòu)建原則

如果不可以在應用程序組件中存儲應用數(shù)據(jù)和狀態(tài),那么該如何構(gòu)建應用呢焚鹊?這兒有兩條常見的構(gòu)建原則:

  • 關注點分離:一個常見的錯誤是在 Activity 和 Fragment 中編寫所有的代碼痕届。任何和UI或者操作系統(tǒng)交互無關的代碼都盡量不要出現(xiàn)在這些類中韧献,盡量保持這些類的精簡會幫助你避免很多和生命周期相關的問題。最好減少對它們的依賴以提供一個穩(wěn)定的用戶體驗研叫。
  • 通過 Model 驅(qū)動 UI锤窑,最好是持久化的 Model。最好使用持久化的數(shù)據(jù)有兩個原因:a. 如果系統(tǒng)銷毀應用釋放資源嚷炉,用戶也不用擔心丟失數(shù)據(jù)渊啰; b. 即使網(wǎng)絡連接不可靠或者斷網(wǎng),應用仍將繼續(xù)運行申屹。Model 是負責處理應用數(shù)據(jù)的組件绘证,Model 獨立運行于應用中的 View 和應用程序中的其他組件,因此 Model 和其他應用程序組件的生命周期無關哗讥∪履牵基于 Model 構(gòu)建的應用程序,其管理數(shù)據(jù)的職責明確杆煞,所以更容易測試魏宽,而且穩(wěn)定性更高。

主要內(nèi)容

處理生命周期

android.arch.lifecycle 包中提供了可以構(gòu)建生命周期感知的組件的類和接口决乎,這些組件可以根據(jù) Activity/Fragment 的生命周期自動調(diào)整它的行為队询。

  • Lifecycle:它是一個持有 Activity/Fragment 生命周期狀態(tài)信息的類,并且允許其他對象觀察此狀態(tài)构诚。
  • LifecycleOwner:是一個具有單一方法的接口蚌斩。如果一個類實現(xiàn)了此接口,則該類中需要持有一個 Lifecycle 對象范嘱,并通過LifecycleOwner#getLifecycle() 方法返回該對象凳寺。

并不是只有 Activity 和 Fragment 才可以實現(xiàn) LifecycleOwner 接口的,任何和 Activity/Fragment 生命周期有關系的類都可以實現(xiàn)此接口彤侍。通過實現(xiàn)此接口肠缨,該類完全是生命周期可感知的,只需要對它進行初始化盏阶,它就可以進行自己的初始化和清理操作晒奕,而不受其 Activity/Fragment 的管理。詳細可以參看官方文檔說明:LifecycleOwner 實踐

LiveData

LiveData 是一個數(shù)據(jù)持有類名斟,它持有一個值并且該值可以被觀察脑慧。不同于普通的可觀察者,LiveData 遵從應用組件的生命周期砰盐,這樣 Observer 便可以指定一個其應該遵循的 Lifecycle闷袒。

如果 Observer 所依附的 Lifecycle 處于 STARTED 或者 RESUMED 狀態(tài),則 LiveData 認為 Observer 處于活躍狀態(tài)岩梳。

可以感知組件生命周期的 LiveData 給我們提供了一種可能:可以在多個 Activity囊骤、Fragment 之間共享它晃择。

使用 LiveData 會有以下幾個優(yōu)勢:

  • 避免內(nèi)存泄露:因為 Observer 是綁定到 Lifecycle 對象上的,當 Lifecycle 對象被銷毀的時候也物,LiveData 對象也會被自動清除
  • 不會因為 Activity 停止而使應用崩潰:如果 Observer 所綁定的 Lifecycle 處于閑置狀態(tài)(例如:Activity 處于后臺運行時)宫屠,他們不會接收到改變的事件
  • 始終保持最新的數(shù)據(jù):如果一個 Lifecycle 重新啟動以后(例如:Activity 從后臺重新開始運行于前臺),它會接收到最新的數(shù)據(jù)(除非沒有最新的數(shù)據(jù))
  • 正確處理配置改變:如果一個 Activity 或者 Fragment 以為配置改變(例如:旋轉(zhuǎn)屏幕)被重建以后滑蚯,LiveData 將會接收到最新的數(shù)據(jù)
  • 資源共享:通過單例模式浪蹂,可以在多個 Activity 或者 Fragment 之間共享 LiveData 數(shù)據(jù)。
  • 不再手動的處理生命周期:Fragment 只有在處于活躍的時候才會觀察 LiveData 數(shù)據(jù)告材。由于 Fragment 提供了 Lifecycle 對象坤次,所以 LiveData 會管理這一切。

有時候斥赋,也許想在 LiveData 被下發(fā)到 Observer 之前浙踢,改變 LiveData 的值,或者是基于當前的 LiveData 下發(fā)另一個不同的 LiveData 值灿渴。Lifecycle 包中的 Transformations 可以實現(xiàn)這樣的功能。

  • Transformations.map()胰舆,使用此方法骚露,可以將 LiveData 傳遞到下游
LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});
  • Transformations.switchMap(),和 map() 方法類似缚窿,使用 switchMap() 應用于 LiveData 的值并解包棘幸,然后將結(jié)果傳遞到下游。傳遞給 switchMap() 的方法必須返回一個 Lifecycle
private LiveData<User> getUser(String id) {
  ...;
}
LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );

使用這兩個轉(zhuǎn)換倦零,允許在整個調(diào)用鏈中攜帶觀察者的 Lifecycle 信息误续,這樣的話,只有在觀察者觀察到 LiveData 的返回值時扫茅,才會運算這些轉(zhuǎn)換蹋嵌。

當你需要在 ViewModel 中添加一個 Lifecycle 對象時,Transformations 或許是一個好的解決辦法葫隙。

ViewModel

ViewModel 類是用來存儲和管理 UI 相關的數(shù)據(jù)栽烂,這樣在配置發(fā)生變化(例如:屏幕旋轉(zhuǎn))時,數(shù)據(jù)就不會丟失恋脚。
由于應用程序組件(例如:Activity腺办、Fragment),具有一個由 Android Framework 管理的生命周期糟描,Activity 或 Fragment 在某些情況下(比如:內(nèi)存緊張或者屏幕旋轉(zhuǎn))會發(fā)生銷毀或者重新創(chuàng)建的情況怀喉。這樣就會帶來一些問題:

  • 由于 Activity 或者 Fragment 有可能會被銷毀或重新創(chuàng)建,所以保存于其中的數(shù)據(jù)有可能會丟失
  • 在 Activity 或者 Fragment 中會經(jīng)常發(fā)起一些需要一定時間才會返回結(jié)果的異步請求調(diào)用
  • 如果把處理應用數(shù)據(jù)船响、完成響應用戶操作躬拢、處理系統(tǒng)通信工作的代碼都寫在 Activity 或者 Fragment 中躲履,那么 Activity 或者 Fragment 將會變得非常的臃腫,給維護工作帶來一定的困難

針對以上問題估灿,Lifecycle 提供了一個叫 ViewModel 的類崇呵,一個 UI 控制器的幫助類,用來為 UI 準備數(shù)據(jù)馅袁。

在配置更改的時候域慷,ViewModel 會被保留,以便其保存的數(shù)據(jù)可以立即傳遞給重新創(chuàng)建的 Activity 或者 Fragment 實例中汗销。如果 Activity 被重新創(chuàng)建犹褒,它將會收到由之前的 Activity 或者 Fragment 創(chuàng)建的 ViewModel 實例。當所有者 Activity 被銷毀以后弛针,F(xiàn)ramework 會調(diào)用 ViewModel#onCleared() 清楚系統(tǒng)資源叠骑。

在多個 Fragment 之間共享數(shù)據(jù)

在同一個 Activity 中的多個 Fragment 之間進行通信是十分常見的需求,目前通常的做法是新建一個接口削茁,并且用 Activity 將多個 Fragment 聯(lián)系起來宙枷。如果需要通信的數(shù)據(jù)比較多,就會出現(xiàn)接口泛濫的情況茧跋。

使用 ViewModel 可以解決這個痛點慰丛。在同一個 Activity 中的 Fragment 可以使用此 Activity 限定的 ViewModel 來處理該通訊。比如如下代碼所示:

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 onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends LifecycleFragment {
    public void onActivityCreated() {
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // 更新 UI
        });
    }
}

在上面示例代碼中瘾杭,獲取 ViewModelProvider 時兩個 Fragment 都使用 getActivity() 方法诅病,這就意味著,它們會收到同一個 Activity 限制的同一個 ViewModel 實例對象粥烁。這樣做有以下幾個優(yōu)點:

  • Activity 不需要知道該通信的任何事情
  • Fragment 之間不受相互影響贤笆。除了 ViewModel 之外,F(xiàn)ragment 不需要了解彼此讨阻,就算一個 Fragment 被銷毀了芥永,另一個也可以正常工作。而且每個 Fragment 都有自己獨立的生命周期钝吮,不受其他 Fragment 的影響恤左。
ViewModel 的生命周期

ViewModel 對象存在于內(nèi)存當中,直到傳遞給它的 Lifecycle 對象被完成的銷毀(Activity:被完全銷毀搀绣,F(xiàn)ragment:被完成移除)飞袋。其生命周期圖如下所示:

viewmodel-lifecycle.png
ViewModel vs SavedInstanceState
  • ViewModels 提供了一種在配置更改時保存數(shù)據(jù)的簡便方式,但是如果應用進程被操作系統(tǒng)殺死链患,那么數(shù)據(jù)則沒有機會被恢復巧鸭。
  • 通過 SavedInstanceState 保存的數(shù)據(jù),存在于操作系統(tǒng)進程的內(nèi)存中麻捻。當用戶離開應用數(shù)個小時之后纲仍,應用的進程很有可能被操作系統(tǒng)殺死呀袱,通過 SavedInstanceState 保存的數(shù)據(jù),則可以在 Activity 或者 Fragment 重新創(chuàng)建的時候郑叠,在其中的 onCreate() 方法中通過 Bundle 恢復數(shù)據(jù)夜赵。

Room Persistence Library

Room 在 SQLite 之上提供了一個抽象層,以便在利用 SQLite 全部功能的同時也可以流暢發(fā)訪問數(shù)據(jù)庫乡革。
在 Room 中有非常重要的三個類:

  • Database:可以使用此組件創(chuàng)建一個數(shù)據(jù)庫持有者寇僧。使用注解定義實體列表,通過類的內(nèi)容定義數(shù)據(jù)庫中數(shù)據(jù)訪問對象列表沸版。它也是底層連接的主要切入點嘁傀。

注解的類應該是一個繼承了 RoomDatabase 的抽象類。在運行時视粮,可以通過 Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder() 方法獲取單例细办。

  • Entity:該組件代表了一個表示數(shù)據(jù)庫中某個數(shù)據(jù)表某一行的類。對于每個 Entity 類蕾殴,都會在數(shù)據(jù)庫中創(chuàng)建一個數(shù)據(jù)表保存該類的對象笑撞。必須通過 Database 中的 entities 字段引用 Entity 類。Entity 類中的每個字段都會持久化到數(shù)據(jù)中钓觉,除非使用 @Ignore 注解修飾
  • DAO:該組件表示一個數(shù)據(jù)訪問對象(DAO)的類或者接口茴肥。DAO 是 Room 的主要組件,其職責是定義方法來訪問數(shù)據(jù)庫议谷。被 @Database 注解修飾的類必須包含一個沒有參數(shù)的抽象方法,該方法的返回值是被 @Dao 注解的類堕虹。在編譯時生成代碼時卧晓,Room 創(chuàng)建該類的實現(xiàn)。

Room 中的三大組件與應用程序中其他部分的關系如下圖所示:

room_architecture.png

關于 Room Persistence Library 更加詳細的內(nèi)容赴捞,請參閱 Room Persistence Library 官方說明文檔逼裆。

Architecture Components 的使用

添加組件到項目

添加 Google Maven 倉庫

在應用工程的 build.gradle 文件中添加依賴對 Google Maven 的依賴:

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

添加 Architecture Components 組件

在應用或者模塊的 build.gradle 文件中添加對 Architecture Components 的依賴,如下所示:

  // Lifecycles, LiveData 和 ViewModel
  compile "android.arch.lifecycle:runtime:1.0.0-alpha5"
  compile "android.arch.lifecycle:extensions:1.0.0-alpha5"
  annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha5"

  // Room
  compile "android.arch.persistence.room:runtime:1.0.0-alpha5"
  annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha5"

  // 對 RxJava 的支持
  compile "android.arch.persistence.room:rxjava2:1.0.0-alpha5"

  // 對測試 Room 的支持
  testCompile "android.arch.persistence.room:testing:1.0.0-alpha5"

具體應用

假如在項目中有類似于下面知乎列表這樣的一個頁面:

zhihu_list.jpg

關于該頁面有如下兩個接口:

  // 請求最新的知乎列表(下拉刷新)
  https://news-at.zhihu.com/api/4/news/latest

  // 上拉加載歷史列表(上拉加載更多)
  https://news-at.zhihu.com/api/4/news/before/{date}

根據(jù)上面兩個接口和 “UI設計稿”赦政,再根據(jù) Architecture Components 組件中提供的類胜宇,我們一步步完成此頁面。首先我們分三步:

  • UI 界面的實現(xiàn)
  • 中間層 ViewModel(UI 界面和數(shù)據(jù)層連接的橋梁)
  • 數(shù)據(jù)層(本地持久化數(shù)據(jù)和服務器端的網(wǎng)絡數(shù)據(jù))

View 界面

此頁面使用 Fragment 控制并顯示恢着,將其命名為 ZhihuListFragment.java桐愉,其布局文件 fragment_zhihu_list.xml 如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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:id="@+id/rl_zhihu_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_zhihu"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_zhihu_list"
            android:name="com.lijiankun24.architecturepractice.fragment.GirlFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="LinearLayoutManager"
            tools:context=".ui.fragment.GirlListFragment"
            tools:listitem="@layout/fragment_girl_list_item"/>
    </android.support.v4.widget.SwipeRefreshLayout>

    <!-- ProgressBar顏色更改  http://www.voidcn.com/blog/dongbeitcy/article/p-5781104.html -->
    <ProgressBar
        android:id="@+id/bar_load_more_zhihu"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:indeterminate="true"
        android:indeterminateTint="@color/colorPrimaryDark"
        android:indeterminateTintMode="src_atop"/>
</RelativeLayout>

fragment_zhihu_list.xml 布局文件比較簡單,不需要多講掰派。

ZhihuListFragment.java 代碼如下所示:

/**
 * ZhihuListFragment.java
 * <p>
 * Created by lijiankun on 17/7/30.
 */

public class ZhihuListFragment extends LifecycleFragment {

    // ZhihuListFragment 所對應的 ViewModel 類的對象
    private ZhihuListViewModel mListViewModel = null;

    private SwipeRefreshLayout mRefreshLayout = null;

    private ZhihuListAdapter mAdapter = null;

    private ProgressBar mLoadMorebar = null;

    private View mRLZhihuRoot = null;

    // 自定義接口从诲,將 RecyclerView 的 Adapter 對其中每個 Item 的點擊事件會傳到 ZhihuListFragment 中。
    private final OnItemClickListener<ZhihuStory> mZhihuOnItemClickListener =
            new OnItemClickListener<ZhihuStory>() {
                @Override
                public void onClick(ZhihuStory zhihuStory) {
                    if (Util.isNetworkConnected(MyApplication.getInstance())) {
                        ZhihuActivity.startZhihuActivity(getActivity(), zhihuStory.getId(),
                                zhihuStory.getTitle());
                    } else {
                        Util.showSnackbar(mRLZhihuRoot, getString(R.string.network_error));
                    }
                }
            };

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_zhihu_list, container, false);
        initView(view);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        subscribeUI();
    }

    /**
     * 將 ZhihuListFragment 對應的 ZhihuListViewModel 類中的 LiveData 添加注冊監(jiān)聽到
     * 此 ZhihuListFragment
     */
    private void subscribeUI() {
        // 通過 ViewModelProviders 創(chuàng)建對應的 ZhihuListViewModel 對象
        ZhihuListViewModel.Factory factory = new ZhihuListViewModel
                .Factory(MyApplication.getInstance()
                , Injection.getDataRepository(MyApplication.getInstance()));
        mListViewModel = ViewModelProviders.of(this, factory).get(ZhihuListViewModel.class);
        mListViewModel.getZhihuList().observe(this, new Observer<List<ZhihuStory>>() {
            @Override
            public void onChanged(@Nullable List<ZhihuStory> stories) {
                if (stories == null || stories.size() <= 0) {
                    return;
                }
                L.i("size is " + stories.size());
                mAdapter.setStoryList(stories);
            }
        });
        mListViewModel.isLoadingZhihuList().observe(this, new Observer<Boolean>() {
            @Override
            public void onChanged(@Nullable Boolean aBoolean) {
                if (aBoolean == null) {
                    return;
                }
                L.i("state " + aBoolean);
                mRefreshLayout.setRefreshing(false);
                mLoadMorebar.setVisibility(aBoolean ? View.VISIBLE : View.INVISIBLE);
            }
        });
        mListViewModel.refreshZhihusData();
    }

    /**
     * 初始化頁面 UI
     *
     * @param view Fragment 的 View
     */
    private void initView(View view) {
        if (view == null) {
            return;
        }
        LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
        mAdapter = new ZhihuListAdapter(getContext(), mZhihuOnItemClickListener);
        RecyclerView recyclerView = view.findViewById(R.id.rv_zhihu_list);
        recyclerView.setAdapter(mAdapter);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.addOnScrollListener(new ZhihuOnScrollListener());

        mRefreshLayout = view.findViewById(R.id.srl_zhihu);
        mRefreshLayout.setOnRefreshListener(new ZhihuSwipeListener());
        mRefreshLayout.setColorSchemeResources(
                android.R.color.holo_blue_bright,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light,
                android.R.color.holo_red_light);

        mLoadMorebar = view.findViewById(R.id.bar_load_more_zhihu);
        mRLZhihuRoot = view.findViewById(R.id.rl_zhihu_root);
    }

    /**
     * ZhihuSwipeListener 用于 SwipeRefreshLayout 下拉刷新操作
     */
    private class ZhihuSwipeListener implements SwipeRefreshLayout.OnRefreshListener {
        @Override
        public void onRefresh() {
            mAdapter.clearStoryList();
            mListViewModel.refreshZhihusData();
        }
    }

    /**
     * ZhihuOnScrollListener 用于 RecyclerView 下拉到最低端時的上拉加載更多操作
     */
    private class ZhihuOnScrollListener extends RecyclerView.OnScrollListener {

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            LinearLayoutManager layoutManager = (LinearLayoutManager)
                    recyclerView.getLayoutManager();
            int lastPosition = layoutManager
                    .findLastCompletelyVisibleItemPosition();
            if (lastPosition == mAdapter.getItemCount() - 1) {
                // 上拉加載更多數(shù)據(jù)
                mListViewModel.loadNextPageZhihu();
            }
        }
    }
}

ZhihuListFragment.java 中注釋已經(jīng)比較清楚靡羡,關于 UI 方面的不多講系洛,其中最重要的一個方法是 subscribeUI() 方法俊性。在該方法中,創(chuàng)建 ZhihuListViewModel 對象之后描扯,對 ZhihuListViewModel 中兩個重要的數(shù)據(jù)進行注冊觀察并更新 UI定页,兩個重要的數(shù)據(jù)分別是:LiveData<List<ZhihuStory>>LiveData<Boolean>

  • LiveData<List<ZhihuStory>>:表示從數(shù)據(jù)層獲取到的知乎列表的數(shù)據(jù)
  • LiveData<Boolean>:表示是否正在獲取數(shù)據(jù)的狀態(tài),以控制界面中加載動畫的顯示和隱藏

ViewModel 控制層

ZhihuListViewModel 類中需要三個 LiveData 類型的屬性绽诚。LiveData 類型的數(shù)據(jù)和 RxJava 中的 Observables 工作模式類似典徊,當 LiveData 持有的數(shù)據(jù)發(fā)生變化時,通知觀察者憔购。如下面代碼所示:

public class ZhihuListViewModel extends AndroidViewModel {

    // 請求接口中查詢的日期參數(shù)
    private MutableLiveData<String> mZhihuPageDate = new MutableLiveData<>();

    // Zhihu 列表的數(shù)據(jù)
    private final LiveData<List<ZhihuStory>> mZhihuList;

    // 是否正在進行網(wǎng)絡請求的狀態(tài)參數(shù)
    private final LiveData<Boolean> mIsLoadingZhihuList;

    ......

    private ZhihuListViewModel(Application application) {
        super(application);
        // 使用 Transformations.switchMap() 方法宫峦,表示當 View 改變 mZhihuPageDate 參數(shù)的值時,則進行 zhihu 列表數(shù)據(jù)的請求
        mZhihuList = Transformations.switchMap(mZhihuPageDate, new Function<String, LiveData<List<ZhihuStory>>>() {
            @Override
            public LiveData<List<ZhihuStory>> apply(String input) {
                ......
            }
        });
    }

    public LiveData<List<ZhihuStory>> getZhihuList() {
        return mZhihuList;
    }

    public LiveData<Boolean> isLoadingZhihuList() {
        return mIsLoadingZhihuList;
    }

    /**
     * 下拉刷新玫鸟,獲取最新的 Zhihu 列表數(shù)據(jù)
     */
    public void refreshZhihusData() {
          mZhihuPageDate.setValue("today");
    }

    /**
     * 上拉加載更多時导绷,獲取 Zhihu 歷史列表數(shù)據(jù)
     *
     * @param positon 表示列表滑動到最后一項
     */
    public void loadNextPageZhihu(int positon) {
        if (!Util.isNetworkConnected(MyApplication.getInstance())) {
            return;
        }   
        mZhihuPageDate.setValue(String.valueOf(positon));
    }

    public static class Factory extends ViewModelProvider.NewInstanceFactory {

        @NonNull
        private final Application mApplication;

        public Factory(@NonNull Application application) {
            mApplication = application;
        }

        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            return (T) new ZhihuListViewModel(mApplication);
        }
    }
}

注:由于 ViewModel 存活的時間可能會比個別的 activity 和 fragment 實例更長,所以它決不能引用 View屎飘,或任何持任何 activity(context)妥曲。如果 ViewModel 需要 Application 的 context(如:調(diào)用系統(tǒng)服務),可以繼承 AndroidViewModel 類钦购,可以在構(gòu)造函數(shù)中接受 Application檐盟。

上面示例的 ZhihuListViewModel 類是功能并不完整的 ViewModel 類,因為它只是向 View 層提供了操作 Zhihu 列表數(shù)據(jù)和監(jiān)聽數(shù)據(jù)請求狀態(tài)的接口押桃,那么 ZhihuListViewModel 該從哪里獲取數(shù)據(jù)呢葵萎?換句話說,數(shù)據(jù)源在哪里唱凯?
ArchitecturePractice 項目中羡忘,封裝了 DataRepository 類,表示所有數(shù)據(jù)的源頭磕昼。
那 ZhihuListViewModel 應該持有一個 DataRepository 對象卷雕,來獲取數(shù)據(jù)。完整的 ZhihuListViewModel 類如下所示:


/**
 * ZhihuListViewModel.java
 * <p>
 * Created by lijiankun on 17/7/30.
 */

public class ZhihuListViewModel extends AndroidViewModel {

    // 請求接口中查詢的日期參數(shù)
    private MutableLiveData<String> mZhihuPageDate = new MutableLiveData<>();

    // Zhihu 列表的數(shù)據(jù)
    private final LiveData<List<ZhihuStory>> mZhihuList;

    // 數(shù)據(jù)源
    private DataRepository mDataRepository = null;

    private ZhihuListViewModel(Application application, DataRepository dataRepository) {
        super(application);
        mDataRepository = dataRepository;
        // 使用 Transformations.switchMap() 方法票从,當 View 改變 mZhihuPageDate 參數(shù)的值時漫雕,則進行 zhihu 列表數(shù)據(jù)的請求
        mZhihuList = Transformations.switchMap(mZhihuPageDate, new Function<String, LiveData<List<ZhihuStory>>>() {
            @Override
            public LiveData<List<ZhihuStory>> apply(String input) {
                return mDataRepository.getZhihuList(input);
            }
        });
    }

    /**
      * 獲取 Zhihu 列表數(shù)據(jù)
      *
      * @return Zhihu 列表數(shù)據(jù)
      */
    public LiveData<List<ZhihuStory>> getZhihuList() {
        return mZhihuList;
    }

    /**
      * 數(shù)據(jù)請求狀態(tài)由 DataRepository 控制堵未,包括下拉刷新和上拉加載更多
      *
      * @return 是否在進行數(shù)據(jù)請求
      */
    public LiveData<Boolean> isLoadingZhihuList() {
        return mDataRepository.isLoadingZhihuList();
    }

    /**
     * 下拉刷新捌刮,獲取最新的 Zhihu 列表數(shù)據(jù)
     */
    public void refreshZhihusData() {
        mZhihuPageDate.setValue("today");
    }

    /**
     * 上拉加載更多時,獲取 Zhihu 歷史列表數(shù)據(jù)
     *
     * @param positon 表示列表滑動到最后一項
     */
    public void loadNextPageZhihu(int positon) {
        if (!Util.isNetworkConnected(MyApplication.getInstance())) {
            return;
        }
        mZhihuPageDate.setValue(String.valueOf(positon));
    }

    public static class Factory extends ViewModelProvider.NewInstanceFactory {

        @NonNull
        private final Application mApplication;

        private final DataRepository mGirlsDataRepository;

        public Factory(@NonNull Application application, DataRepository girlsDataRepository) {
            mApplication = application;
            mGirlsDataRepository = girlsDataRepository;
        }

        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            return (T) new ZhihuListViewModel(mApplication, mGirlsDataRepository);
        }
    }
}

Model 數(shù)據(jù)層

正如在 ViewModel 控制層中介紹的钞支,ArchitecturePractice 中所有的數(shù)據(jù)均由 DataRepository 類中獲取吟榴。在 DataRepository 中有兩個數(shù)據(jù)源:本地數(shù)據(jù)庫和遠端服務器发框,如果有網(wǎng),則從服務器獲取最新數(shù)據(jù),并保存在本地數(shù)據(jù)庫中梅惯;如果沒有網(wǎng)宪拥,則從本地數(shù)據(jù)庫中加載數(shù)據(jù)并顯示。則 DataRepository 的初步實現(xiàn)是這樣的:


/**
 * DataRepository.java
 * <p>
 * Created by lijiankun on 17/7/7.
 */

public class DataRepository {

    private static DataRepository INSTANCE = null;

    // 從服務器獲取數(shù)據(jù)
    private final DataSource mRemoteDataSource;

    // 從本地數(shù)據(jù)庫獲取數(shù)據(jù)
    private final DataSource mLocalDataSource;

    private static Application sApplication = null;

    private DataRepository(@NonNull DataSource remoteDataSource,
                           @NonNull DataSource localDataSource) {
        mRemoteDataSource = remoteDataSource;
        mLocalDataSource = localDataSource;
    }

    static DataRepository getInstance(@NonNull DataSource remoteDataSource,
                                      @NonNull DataSource localDataSource,
                                      Application application) {
        if (INSTANCE == null) {
            synchronized (DataRepository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new DataRepository(remoteDataSource, localDataSource);
                    sApplication = application;
                }
            }
        }
        return INSTANCE;
    }

    public LiveData<List<ZhihuStory>> getZhihuList(@NonNull String date) {
        if (Util.isNetworkConnected(sApplication.getApplicationContext())) {
            if (date.equals("today")) {
                return mRemoteDataSource.getLastZhihuList();
            } else {
                return mRemoteDataSource.getMoreZhihuList(date);
            }
        } else {
            if (date.equals("today")) {
                return mLocalDataSource.getLastZhihuList();
            } else {
                return mLocalDataSource.getMoreZhihuList(date);
            }
        }
    }

    public LiveData<Boolean> isLoadingZhihuList() {
        if (Util.isNetworkConnected(sApplication.getApplicationContext())) {
            return mRemoteDataSource.isLoadingZhihuList();
        } else {
            return mLocalDataSource.isLoadingZhihuList();
        }
    }
}

其中的 DataSource 表示獲取數(shù)據(jù)的抽象層铣减,如下所示:

public interface DataSource {

    ......

    /**
     * Zhihu 相關方法
     */
    LiveData<List<ZhihuStory>> getLastZhihuList();

    LiveData<List<ZhihuStory>> getMoreZhihuList(String date);

    LiveData<Boolean> isLoadingZhihuList();
}

此外還有兩個非常重要的類:RemoteDataSourceLocalDataSource她君,這兩個類分別實現(xiàn)了 DataSource 接口。
RemoteDataSource 類代碼如下所示葫哗,從遠端服務器獲取數(shù)據(jù)使用的是 Retrofit缔刹,并且對網(wǎng)絡請求進行簡單封裝,由 ApiManager 統(tǒng)一向外提供網(wǎng)絡請求接口:


/**
 * RemoteDataSource.java
 * <p>
 * Created by lijiankun on 17/7/7.
 */

public class RemoteDataSource implements DataSource {

    private static RemoteDataSource INSTANCE = null;

    private final MutableLiveData<Boolean> mIsLoadingZhihuList;

    private final MutableLiveData<List<ZhihuStory>> mZhihuList;

    private final ApiZhihu mApiZhihu;

    private String mZhihuPageDate;

    {
        mIsLoadingZhihuList = new MutableLiveData<>();
        mZhihuList = new MutableLiveData<>();
    }

    private RemoteDataSource() {
        mApiZhihu = ApiManager.getInstance().getApiZhihu();
    }

    public static RemoteDataSource getInstance() {
        if (INSTANCE == null) {
            synchronized (RemoteDataSource.class) {
                if (INSTANCE == null) {
                    INSTANCE = new RemoteDataSource();
                }
            }
        }
        return INSTANCE;
    }

    @Override
    public LiveData<List<ZhihuStory>> getLastZhihuList() {
        mIsLoadingZhihuList.setValue(true);
        mApiZhihu.getLatestNews()
                .enqueue(new Callback<ZhihuData>() {
                    @Override
                    public void onResponse(Call<ZhihuData> call, Response<ZhihuData> response) {
                        if (response.isSuccessful()) {
                            mZhihuList.setValue(response.body().getStories());
                            refreshLocalZhihuList(response.body().getStories());
                            mZhihuPageDate = response.body().getDate();
                        }
                        mIsLoadingZhihuList.setValue(false);
                    }

                    @Override
                    public void onFailure(Call<ZhihuData> call, Throwable t) {
                        mIsLoadingZhihuList.setValue(false);
                    }
                });
        return mZhihuList;
    }

    @Override
    public LiveData<List<ZhihuStory>> getMoreZhihuList(String date) {
        mIsLoadingZhihuList.setValue(true);
        mApiZhihu.getTheDaily(mZhihuPageDate)
                .enqueue(new Callback<ZhihuData>() {
                    @Override
                    public void onResponse(Call<ZhihuData> call, Response<ZhihuData> response) {
                        if (response.isSuccessful()) {
                            mZhihuList.setValue(response.body().getStories());
                            refreshLocalZhihuList(response.body().getStories());
                            mZhihuPageDate = response.body().getDate();
                        }
                        mIsLoadingZhihuList.setValue(false);
                    }

                    @Override
                    public void onFailure(Call<ZhihuData> call, Throwable t) {
                        mIsLoadingZhihuList.setValue(false);
                    }
                });
        return mZhihuList;
    }

    @Override
    public MutableLiveData<Boolean> isLoadingZhihuList() {
        return mIsLoadingZhihuList;
    }

    private void refreshLocalZhihuList(List<ZhihuStory> zhihuStoryList) {
        if (zhihuStoryList == null || zhihuStoryList.isEmpty()) {
            return;
        }
        AppDatabaseManager.getInstance().insertZhihuList(zhihuStoryList);
    }
}

LocalDataSource 類代碼如下所示劣针,從本地數(shù)據(jù)庫獲取數(shù)據(jù)使用的是 Architecture Components 中的 Room 庫校镐,簡單封裝為 AppDatabaseManager ,向外提供統(tǒng)一的方法:

/**
 * LocalDataSource.java
 * <p>
 * Created by lijiankun on 17/7/7.
 */

public class LocalDataSource implements DataSource {

    private static LocalDataSource INSTANCE = null;

    private LocalDataSource() {
    }

    public static LocalDataSource getInstance() {
        if (INSTANCE == null) {
            synchronized (LocalDataSource.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LocalDataSource();
                }
            }
        }
        return INSTANCE;
    }

    ......

    @Override
    public LiveData<List<ZhihuStory>> getLastZhihuList() {
        return AppDatabaseManager.getInstance().loadZhihuList();
    }

    @Override
    public LiveData<List<ZhihuStory>> getMoreZhihuList(String date) {
        return null;
    }

    @Override
    public LiveData<Boolean> isLoadingZhihuList() {
        return AppDatabaseManager.getInstance().isLoadingZhihuList();
    }
}

使用到的 ApiManager 用于統(tǒng)一管理 Retrofit 網(wǎng)絡請求捺典,AppDatabaseManager 則是對 Room 數(shù)據(jù)庫的統(tǒng)一管理鸟廓,關于 Retrofit 和 Room 的使用就不再多說。這樣一個向外提供干凈可靠 API 的數(shù)據(jù)源 DataRepository 模塊則完成了襟己,DataRepository 主要負責處理數(shù)據(jù)的獲取引谜。


至此,關于 Architecture Components 組件的介紹和實踐都全部介紹完畢擎浴,本文中用于舉例的 ArchitecturePractice 在 GitHub 上员咽,歡迎 star 和 fork,也歡迎通過下面二維碼下載 APK 體驗贮预,如果有什么問題歡迎指出贝室。我的工作郵箱:jiankunli24@gmail.com

QR.png

參考資料:

Android Architecture Components 官方文檔

Google 官方推出應用開發(fā)架構(gòu)指南 -- Hevin

譯 Architecture Components 之 Guide to App Architecture -- zly394

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市仿吞,隨后出現(xiàn)的幾起案子滑频,更是在濱河造成了極大的恐慌,老刑警劉巖茫藏,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件误趴,死亡現(xiàn)場離奇詭異霹琼,居然都是意外死亡务傲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門枣申,熙熙樓的掌柜王于貴愁眉苦臉地迎上來售葡,“玉大人,你說我怎么就攤上這事忠藤⌒铮” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵模孩,是天一觀的道長尖阔。 經(jīng)常有香客問我贮缅,道長,這世上最難降的妖魔是什么介却? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任谴供,我火速辦了婚禮,結(jié)果婚禮上齿坷,老公的妹妹穿的比我還像新娘桂肌。我一直安慰自己,他們只是感情好永淌,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布崎场。 她就那樣靜靜地躺著,像睡著了一般遂蛀。 火紅的嫁衣襯著肌膚如雪谭跨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天答恶,我揣著相機與錄音饺蚊,去河邊找鬼。 笑死悬嗓,一個胖子當著我的面吹牛污呼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播包竹,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼燕酷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了周瞎?” 一聲冷哼從身側(cè)響起苗缩,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎声诸,沒想到半個月后酱讶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡彼乌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年泻肯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰照。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡灶挟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毒租,到底是詐尸還是另有隱情稚铣,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站惕医,受9級特大地震影響耕漱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜抬伺,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一孤个、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沛简,春花似錦齐鲤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捧灰,卻和暖如春淆九,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背毛俏。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工炭庙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人煌寇。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓焕蹄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親阀溶。 傳聞我的和親對象是個殘疾皇子腻脏,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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