Android 應(yīng)用架構(gòu)組件(Architecture Components)實(shí)踐

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

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

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

關(guān)于 Architecture Components

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

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

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

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

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

主要內(nèi)容

處理生命周期

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

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

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

LiveData

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

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

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

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

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

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

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

使用這兩個(gè)轉(zhuǎn)換,允許在整個(gè)調(diào)用鏈中攜帶觀察者的 Lifecycle 信息,這樣的話,只有在觀察者觀察到 LiveData 的返回值時(shí),才會運(yùn)算這些轉(zhuǎn)換。

當(dāng)你需要在 ViewModel 中添加一個(gè) Lifecycle 對象時(shí),Transformations 或許是一個(gè)好的解決辦法。

ViewModel

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

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

針對以上問題,Lifecycle 提供了一個(gè)叫 ViewModel 的類,一個(gè) UI 控制器的幫助類,用來為 UI 準(zhǔn)備數(shù)據(jù)。

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

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

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

使用 ViewModel 可以解決這個(gè)痛點(diǎn)。在同一個(gè) 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 時(shí)兩個(gè) Fragment 都使用 getActivity() 方法,這就意味著,它們會收到同一個(gè) Activity 限制的同一個(gè) ViewModel 實(shí)例對象。這樣做有以下幾個(gè)優(yōu)點(diǎn):

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

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

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

Room Persistence Library

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

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

注解的類應(yīng)該是一個(gè)繼承了 RoomDatabase 的抽象類。在運(yùn)行時(shí),可以通過 Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder() 方法獲取單例。

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

Room 中的三大組件與應(yīng)用程序中其他部分的關(guān)系如下圖所示:

room_architecture.png

關(guān)于 Room Persistence Library 更加詳細(xì)的內(nèi)容,請參閱 Room Persistence Library 官方說明文檔。

Architecture Components 的使用

添加組件到項(xiàng)目

添加 Google Maven 倉庫

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

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

添加 Architecture Components 組件

在應(yīng)用或者模塊的 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"

具體應(yīng)用

假如在項(xiàng)目中有類似于下面知乎列表這樣的一個(gè)頁面:

zhihu_list.jpg

關(guān)于該頁面有如下兩個(gè)接口:

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

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

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

  • UI 界面的實(shí)現(xiàn)
  • 中間層 ViewModel(UI 界面和數(shù)據(jù)層連接的橋梁)
  • 數(shù)據(jù)層(本地持久化數(shù)據(jù)和服務(wù)器端的網(wǎng)絡(luò)數(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 所對應(yīng)的 ViewModel 類的對象
    private ZhihuListViewModel mListViewModel = null;

    private SwipeRefreshLayout mRefreshLayout = null;

    private ZhihuListAdapter mAdapter = null;

    private ProgressBar mLoadMorebar = null;

    private View mRLZhihuRoot = null;

    // 自定義接口,將 RecyclerView 的 Adapter 對其中每個(gè) Item 的點(diǎn)擊事件會傳到 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 對應(yīng)的 ZhihuListViewModel 類中的 LiveData 添加注冊監(jiān)聽到
     * 此 ZhihuListFragment
     */
    private void subscribeUI() {
        // 通過 ViewModelProviders 創(chuàng)建對應(yī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 下拉到最低端時(shí)的上拉加載更多操作
     */
    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)比較清楚,關(guān)于 UI 方面的不多講,其中最重要的一個(gè)方法是 subscribeUI() 方法。在該方法中,創(chuàng)建 ZhihuListViewModel 對象之后,對 ZhihuListViewModel 中兩個(gè)重要的數(shù)據(jù)進(jìn)行注冊觀察并更新 UI,兩個(gè)重要的數(shù)據(jù)分別是:LiveData<List<ZhihuStory>>LiveData<Boolean>

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

ViewModel 控制層

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

public class ZhihuListViewModel extends AndroidViewModel {

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

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

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

    ......

    private ZhihuListViewModel(Application application) {
        super(application);
        // 使用 Transformations.switchMap() 方法,表示當(dāng) View 改變 mZhihuPageDate 參數(shù)的值時(shí),則進(jìn)行 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");
    }

    /**
     * 上拉加載更多時(shí),獲取 Zhihu 歷史列表數(shù)據(jù)
     *
     * @param positon 表示列表滑動到最后一項(xiàng)
     */
    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 存活的時(shí)間可能會比個(gè)別的 activity 和 fragment 實(shí)例更長,所以它決不能引用 View,或任何持任何 activity(context)。如果 ViewModel 需要 Application 的 context(如:調(diào)用系統(tǒng)服務(wù)),可以繼承 AndroidViewModel 類,可以在構(gòu)造函數(shù)中接受 Application。

上面示例的 ZhihuListViewModel 類是功能并不完整的 ViewModel 類,因?yàn)樗皇窍?View 層提供了操作 Zhihu 列表數(shù)據(jù)和監(jiān)聽數(shù)據(jù)請求狀態(tài)的接口,那么 ZhihuListViewModel 該從哪里獲取數(shù)據(jù)呢?換句話說,數(shù)據(jù)源在哪里?
ArchitecturePractice 項(xiàng)目中,封裝了 DataRepository 類,表示所有數(shù)據(jù)的源頭。
那 ZhihuListViewModel 應(yīng)該持有一個(gè) 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() 方法,當(dāng) View 改變 mZhihuPageDate 參數(shù)的值時(shí),則進(jìn)行 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 是否在進(jìn)行數(shù)據(jù)請求
      */
    public LiveData<Boolean> isLoadingZhihuList() {
        return mDataRepository.isLoadingZhihuList();
    }

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

    /**
     * 上拉加載更多時(shí),獲取 Zhihu 歷史列表數(shù)據(jù)
     *
     * @param positon 表示列表滑動到最后一項(xiàng)
     */
    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 中有兩個(gè)數(shù)據(jù)源:本地?cái)?shù)據(jù)庫和遠(yuǎn)端服務(wù)器,如果有網(wǎng),則從服務(wù)器獲取最新數(shù)據(jù),并保存在本地?cái)?shù)據(jù)庫中;如果沒有網(wǎng),則從本地?cái)?shù)據(jù)庫中加載數(shù)據(jù)并顯示。則 DataRepository 的初步實(shí)現(xiàn)是這樣的:


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

public class DataRepository {

    private static DataRepository INSTANCE = null;

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

    // 從本地?cái)?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 相關(guān)方法
     */
    LiveData<List<ZhihuStory>> getLastZhihuList();

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

    LiveData<Boolean> isLoadingZhihuList();
}

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


/**
 * 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 類代碼如下所示,從本地?cái)?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)絡(luò)請求,AppDatabaseManager 則是對 Room 數(shù)據(jù)庫的統(tǒng)一管理,關(guān)于 Retrofit 和 Room 的使用就不再多說。這樣一個(gè)向外提供干凈可靠 API 的數(shù)據(jù)源 DataRepository 模塊則完成了,DataRepository 主要負(fù)責(zé)處理數(shù)據(jù)的獲取。


至此,關(guān)于 Architecture Components 組件的介紹和實(shí)踐都全部介紹完畢,本文中用于舉例的 ArchitecturePractice 在 GitHub 上,歡迎 star 和 fork,也歡迎通過下面二維碼下載 APK 體驗(yàn),如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com

QR.png

參考資料:

Android Architecture Components 官方文檔

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

譯 Architecture Components 之 Guide to App Architecture -- zly394

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容