Android 应用架构组件(Architecture Components)实践

Architecture Components 是在 2017 年 Google I/O 大会上,Google 官方推出的一个构建 Android 应用架构的库。它可以帮你避免在 Android 应用开发中常见的一些问题,比如:内存泄露,管理组件生命周期等等。本文将介绍如何利用 Architecture Components 库开发一个实际的 Android 应用 ArchitecturePractice,欢迎 fork 和 star。

本文主要分为以下两部分:

  1. 介绍 Architecture Components 库;
  2. 如何使用 Architecture Components 库开发应用。

关于 Architecture Components

应用开发者所面对的问题

在资源有限的移动设备中,在任何时候,系统都有可能为新的应用杀死一些原来的应用,那么在原来应用中的组件(Activity、Fragment、Service等等)也会被销毁,这些组件的生命周期不受开发者控制,而是由系统控制的,所以不要在应用程序组件中存储任何应用数据和状态,并且应用程序组件之间相互不要依赖。

常见的构建原则

如果不可以在应用程序组件中存储应用数据和状态,那么该如何构建应用呢?这儿有两条常见的构建原则:

  • 关注点分离:一个常见的错误是在 Activity 和 Fragment 中编写所有的代码。任何和UI或者操作系统交互无关的代码都尽量不要出现在这些类中,尽量保持这些类的精简会帮助你避免很多和生命周期相关的问题。最好减少对它们的依赖以提供一个稳定的用户体验。
  • 通过 Model 驱动 UI,最好是持久化的 Model。最好使用持久化的数据有两个原因:a. 如果系统销毁应用释放资源,用户也不用担心丢失数据; b. 即使网络连接不可靠或者断网,应用仍将继续运行。Model 是负责处理应用数据的组件,Model 独立运行于应用中的 View 和应用程序中的其他组件,因此 Model 和其他应用程序组件的生命周期无关。基于 Model 构建的应用程序,其管理数据的职责明确,所以更容易测试,而且稳定性更高。

主要内容

处理生命周期

android.arch.lifecycle 包中提供了可以构建生命周期感知的组件的类和接口,这些组件可以根据 Activity/Fragment 的生命周期自动调整它的行为。

  • Lifecycle:它是一个持有 Activity/Fragment 生命周期状态信息的类,并且允许其他对象观察此状态。
  • LifecycleOwner:是一个具有单一方法的接口。如果一个类实现了此接口,则该类中需要持有一个 Lifecycle 对象,并通过LifecycleOwner#getLifecycle() 方法返回该对象。

并不是只有 Activity 和 Fragment 才可以实现 LifecycleOwner 接口的,任何和 Activity/Fragment 生命周期有关系的类都可以实现此接口。通过实现此接口,该类完全是生命周期可感知的,只需要对它进行初始化,它就可以进行自己的初始化和清理操作,而不受其 Activity/Fragment 的管理。详细可以参看官方文档说明:LifecycleOwner 实践

LiveData

LiveData 是一个数据持有类,它持有一个值并且该值可以被观察。不同于普通的可观察者,LiveData 遵从应用组件的生命周期,这样 Observer 便可以指定一个其应该遵循的 Lifecycle

如果 Observer 所依附的 Lifecycle 处于 STARTED 或者 RESUMED 状态,则 LiveData 认为 Observer 处于活跃状态。

可以感知组件生命周期的 LiveData 给我们提供了一种可能:可以在多个 ActivityFragment 之间共享它。

使用 LiveData 会有以下几个优势:

  • 避免内存泄露:因为 Observer 是绑定到 Lifecycle 对象上的,当 Lifecycle 对象被销毁的时候,LiveData 对象也会被自动清除
  • 不会因为 Activity 停止而使应用崩溃:如果 Observer 所绑定的 Lifecycle 处于闲置状态(例如:Activity 处于后台运行时),他们不会接收到改变的事件
  • 始终保持最新的数据:如果一个 Lifecycle 重新启动以后(例如:Activity 从后台重新开始运行于前台),它会接收到最新的数据(除非没有最新的数据)
  • 正确处理配置改变:如果一个 Activity 或者 Fragment 以为配置改变(例如:旋转屏幕)被重建以后,LiveData 将会接收到最新的数据
  • 资源共享:通过单例模式,可以在多个 Activity 或者 Fragment 之间共享 LiveData 数据。
  • 不再手动的处理生命周期:Fragment 只有在处于活跃的时候才会观察 LiveData 数据。由于 Fragment 提供了 Lifecycle 对象,所以 LiveData 会管理这一切。

有时候,也许想在 LiveData 被下发到 Observer 之前,改变 LiveData 的值,或者是基于当前的 LiveData 下发另一个不同的 LiveData 值。Lifecycle 包中的 Transformations 可以实现这样的功能。

  • Transformations.map(),使用此方法,可以将 LiveData 传递到下游
LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});
  • Transformations.switchMap(),和 map() 方法类似,使用 switchMap() 应用于 LiveData 的值并解包,然后将结果传递到下游。传递给 switchMap() 的方法必须返回一个 Lifecycle
private LiveData<User> getUser(String id) {
  ...;
}
LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );

使用这两个转换,允许在整个调用链中携带观察者的 Lifecycle 信息,这样的话,只有在观察者观察到 LiveData 的返回值时,才会运算这些转换。

当你需要在 ViewModel 中添加一个 Lifecycle 对象时,Transformations 或许是一个好的解决办法。

ViewModel

ViewModel 类是用来存储和管理 UI 相关的数据,这样在配置发生变化(例如:屏幕旋转)时,数据就不会丢失。
由于应用程序组件(例如:Activity、Fragment),具有一个由 Android Framework 管理的生命周期,Activity 或 Fragment 在某些情况下(比如:内存紧张或者屏幕旋转)会发生销毁或者重新创建的情况。这样就会带来一些问题:

  • 由于 Activity 或者 Fragment 有可能会被销毁或重新创建,所以保存于其中的数据有可能会丢失
  • 在 Activity 或者 Fragment 中会经常发起一些需要一定时间才会返回结果的异步请求调用
  • 如果把处理应用数据、完成响应用户操作、处理系统通信工作的代码都写在 Activity 或者 Fragment 中,那么 Activity 或者 Fragment 将会变得非常的臃肿,给维护工作带来一定的困难

针对以上问题,Lifecycle 提供了一个叫 ViewModel 的类,一个 UI 控制器的帮助类,用来为 UI 准备数据。

在配置更改的时候,ViewModel 会被保留,以便其保存的数据可以立即传递给重新创建的 Activity 或者 Fragment 实例中。如果 Activity 被重新创建,它将会收到由之前的 Activity 或者 Fragment 创建的 ViewModel 实例。当所有者 Activity 被销毁以后,Framework 会调用 ViewModel#onCleared() 清楚系统资源。

在多个 Fragment 之间共享数据

在同一个 Activity 中的多个 Fragment 之间进行通信是十分常见的需求,目前通常的做法是新建一个接口,并且用 Activity 将多个 Fragment 联系起来。如果需要通信的数据比较多,就会出现接口泛滥的情况。

使用 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 实例对象。这样做有以下几个优点:

  • Activity 不需要知道该通信的任何事情
  • Fragment 之间不受相互影响。除了 ViewModel 之外,Fragment 不需要了解彼此,就算一个 Fragment 被销毁了,另一个也可以正常工作。而且每个 Fragment 都有自己独立的生命周期,不受其他 Fragment 的影响。
ViewModel 的生命周期

ViewModel 对象存在于内存当中,直到传递给它的 Lifecycle 对象被完成的销毁(Activity:被完全销毁,Fragment:被完成移除)。其生命周期图如下所示:

viewmodel-lifecycle.png
ViewModel vs SavedInstanceState
  • ViewModels 提供了一种在配置更改时保存数据的简便方式,但是如果应用进程被操作系统杀死,那么数据则没有机会被恢复。
  • 通过 SavedInstanceState 保存的数据,存在于操作系统进程的内存中。当用户离开应用数个小时之后,应用的进程很有可能被操作系统杀死,通过 SavedInstanceState 保存的数据,则可以在 Activity 或者 Fragment 重新创建的时候,在其中的 onCreate() 方法中通过 Bundle 恢复数据。

Room Persistence Library

Room 在 SQLite 之上提供了一个抽象层,以便在利用 SQLite 全部功能的同时也可以流畅发访问数据库。
在 Room 中有非常重要的三个类:

  • Database:可以使用此组件创建一个数据库持有者。使用注解定义实体列表,通过类的内容定义数据库中数据访问对象列表。它也是底层连接的主要切入点。

注解的类应该是一个继承了 RoomDatabase 的抽象类。在运行时,可以通过 Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder() 方法获取单例。

  • Entity:该组件代表了一个表示数据库中某个数据表某一行的类。对于每个 Entity 类,都会在数据库中创建一个数据表保存该类的对象。必须通过 Database 中的 entities 字段引用 Entity 类。Entity 类中的每个字段都会持久化到数据中,除非使用 @Ignore 注解修饰
  • DAO:该组件表示一个数据访问对象(DAO)的类或者接口。DAO 是 Room 的主要组件,其职责是定义方法来访问数据库。被 @Database 注解修饰的类必须包含一个没有参数的抽象方法,该方法的返回值是被 @Dao 注解的类。在编译时生成代码时,Room 创建该类的实现。

Room 中的三大组件与应用程序中其他部分的关系如下图所示:

room_architecture.png

关于 Room Persistence Library 更加详细的内容,请参阅 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}

根据上面两个接口和 “UI设计稿”,再根据 Architecture Components 组件中提供的类,我们一步步完成此页面。首先我们分三步:

  • UI 界面的实现
  • 中间层 ViewModel(UI 界面和数据层连接的桥梁)
  • 数据层(本地持久化数据和服务器端的网络数据)

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 添加注册监听到
     * 此 ZhihuListFragment
     */
    private void subscribeUI() {
        // 通过 ViewModelProviders 创建对应的 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) {
                // 上拉加载更多数据
                mListViewModel.loadNextPageZhihu();
            }
        }
    }
}

ZhihuListFragment.java 中注释已经比较清楚,关于 UI 方面的不多讲,其中最重要的一个方法是 subscribeUI() 方法。在该方法中,创建 ZhihuListViewModel 对象之后,对 ZhihuListViewModel 中两个重要的数据进行注册观察并更新 UI,两个重要的数据分别是:LiveData<List<ZhihuStory>>LiveData<Boolean>

  • LiveData<List<ZhihuStory>>:表示从数据层获取到的知乎列表的数据
  • LiveData<Boolean>:表示是否正在获取数据的状态,以控制界面中加载动画的显示和隐藏

ViewModel 控制层

ZhihuListViewModel 类中需要三个 LiveData 类型的属性。LiveData 类型的数据和 RxJava 中的 Observables 工作模式类似,当 LiveData 持有的数据发生变化时,通知观察者。如下面代码所示:

public class ZhihuListViewModel extends AndroidViewModel {

    // 请求接口中查询的日期参数
    private MutableLiveData<String> mZhihuPageDate = new MutableLiveData<>();

    // Zhihu 列表的数据
    private final LiveData<List<ZhihuStory>> mZhihuList;

    // 是否正在进行网络请求的状态参数
    private final LiveData<Boolean> mIsLoadingZhihuList;

    ......

    private ZhihuListViewModel(Application application) {
        super(application);
        // 使用 Transformations.switchMap() 方法,表示当 View 改变 mZhihuPageDate 参数的值时,则进行 zhihu 列表数据的请求
        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 列表数据
     */
    public void refreshZhihusData() {
          mZhihuPageDate.setValue("today");
    }

    /**
     * 上拉加载更多时,获取 Zhihu 历史列表数据
     *
     * @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(如:调用系统服务),可以继承 AndroidViewModel 类,可以在构造函数中接受 Application。

上面示例的 ZhihuListViewModel 类是功能并不完整的 ViewModel 类,因为它只是向 View 层提供了操作 Zhihu 列表数据和监听数据请求状态的接口,那么 ZhihuListViewModel 该从哪里获取数据呢?换句话说,数据源在哪里?
ArchitecturePractice 项目中,封装了 DataRepository 类,表示所有数据的源头。
那 ZhihuListViewModel 应该持有一个 DataRepository 对象,来获取数据。完整的 ZhihuListViewModel 类如下所示:


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

public class ZhihuListViewModel extends AndroidViewModel {

    // 请求接口中查询的日期参数
    private MutableLiveData<String> mZhihuPageDate = new MutableLiveData<>();

    // Zhihu 列表的数据
    private final LiveData<List<ZhihuStory>> mZhihuList;

    // 数据源
    private DataRepository mDataRepository = null;

    private ZhihuListViewModel(Application application, DataRepository dataRepository) {
        super(application);
        mDataRepository = dataRepository;
        // 使用 Transformations.switchMap() 方法,当 View 改变 mZhihuPageDate 参数的值时,则进行 zhihu 列表数据的请求
        mZhihuList = Transformations.switchMap(mZhihuPageDate, new Function<String, LiveData<List<ZhihuStory>>>() {
            @Override
            public LiveData<List<ZhihuStory>> apply(String input) {
                return mDataRepository.getZhihuList(input);
            }
        });
    }

    /**
      * 获取 Zhihu 列表数据
      *
      * @return Zhihu 列表数据
      */
    public LiveData<List<ZhihuStory>> getZhihuList() {
        return mZhihuList;
    }

    /**
      * 数据请求状态由 DataRepository 控制,包括下拉刷新和上拉加载更多
      *
      * @return 是否在进行数据请求
      */
    public LiveData<Boolean> isLoadingZhihuList() {
        return mDataRepository.isLoadingZhihuList();
    }

    /**
     * 下拉刷新,获取最新的 Zhihu 列表数据
     */
    public void refreshZhihusData() {
        mZhihuPageDate.setValue("today");
    }

    /**
     * 上拉加载更多时,获取 Zhihu 历史列表数据
     *
     * @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 数据层

正如在 ViewModel 控制层中介绍的,ArchitecturePractice 中所有的数据均由 DataRepository 类中获取。在 DataRepository 中有两个数据源:本地数据库和远端服务器,如果有网,则从服务器获取最新数据,并保存在本地数据库中;如果没有网,则从本地数据库中加载数据并显示。则 DataRepository 的初步实现是这样的:


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

public class DataRepository {

    private static DataRepository INSTANCE = null;

    // 从服务器获取数据
    private final DataSource mRemoteDataSource;

    // 从本地数据库获取数据
    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 表示获取数据的抽象层,如下所示:

public interface DataSource {

    ......

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

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

    LiveData<Boolean> isLoadingZhihuList();
}

此外还有两个非常重要的类:RemoteDataSourceLocalDataSource,这两个类分别实现了 DataSource 接口。
RemoteDataSource 类代码如下所示,从远端服务器获取数据使用的是 Retrofit,并且对网络请求进行简单封装,由 ApiManager 统一向外提供网络请求接口:


/**
 * 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 类代码如下所示,从本地数据库获取数据使用的是 Architecture Components 中的 Room 库,简单封装为 AppDatabaseManager ,向外提供统一的方法:

/**
 * 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 用于统一管理 Retrofit 网络请求,AppDatabaseManager 则是对 Room 数据库的统一管理,关于 Retrofit 和 Room 的使用就不再多说。这样一个向外提供干净可靠 API 的数据源 DataRepository 模块则完成了,DataRepository 主要负责处理数据的获取。


至此,关于 Architecture Components 组件的介绍和实践都全部介绍完毕,本文中用于举例的 ArchitecturePractice 在 GitHub 上,欢迎 star 和 fork,也欢迎通过下面二维码下载 APK 体验,如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com

QR.png

参考资料:

Android Architecture Components 官方文档

Google 官方推出应用开发架构指南 -- Hevin

译 Architecture Components 之 Guide to App Architecture -- zly394

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容