我所理解的 Android Architecture Components

我所理解的 Android Architecture Components

写在前面
Android Architecture Components 是 Google 2017 年 I/O 大会提出的一种架构解决方案。在此之前,Android 应用大多数以 MVC MVP MVVM 等比较常见的架构方式被构建。看到这样各自为战的情况,再加上开发者们强烈的意愿,Google 自然也坐不住了,推出了 AAC 这种架构方式,看它的命名,Android Architecture Components,是不是大有一统江湖的意思。

架构变迁

在介绍 AAC 架构之前,想和大家聊聊之前常见架构模式的变迁。
在软件行业,无论是什么形式的架构,大家都有一个共识,如果实践是检验真理的唯一标准,那可测试性就是检验架构好坏的唯一标准。好的架构会尽量让逻辑开发者关注逻辑本身,并且减少模板代码。

无架构模式 -- MVC

大家可以想象一下,我们从把所有代码写在一团到有了 MVC 架构模式,有哪些提升?

  • 可测试性显著提升(虽然我知道很多公司和开发者都没有写单元测试的习惯)
  • 数据隔离、关注点隔离

我们从没有架构,到 MVC,终于能做到「分层」了。

MVC - MVP

之后我们写着写着又发现,MVP 对比 MVC 更有优势:

  • 面向接口编程
  • 将逻辑从 UI 组件(Activity、Fragment)转移到 P 层,进一步提升可测试性

为什么逻辑放在 P 层要更合理呢?因为首先 UI 组件是连接你和 OS 的那一层,也就是说它并不完完全全属于开发者本身,会受到系统的影响,比如配置的更改、内存不足重启等等,所以尽量保证 UI 组件整洁,绝对会提升代码的可测试性和稳健性。
其次也符合 Passive View Pattern 的这种架构建议模式,其实我们也可以把 Passive View Pattern 这种模式直接类比到后台和前端的关系,这也是为什么大多数公司后台开发者的数量大于前端开发者的数量。

现存问题

无论是 MVC MVP MVVM 它们存在什么问题呢?
他们的模块通信方式,始终是持有对象(或接口)。对于 Android 系统来说,每一个你在 Manifest 声明的四大组件都有可能会突然死掉,这也造成了:

  • 持有对象可能会有 NPE
  • 会有可能内存泄漏
  • 会写很多生命周期相关的模板代码

为了掩盖或解决以上种种问题,AAC 架构就应运而生了,AAC 架构包括了一系列组件,比如 LifeCycle Room LiveData ViewModel Paging 等等。其实无论是 MVC MVP MVVM 或者是其他的架构方式,数据层的争议是最小的,AAC 也提出了一种数据层的构建方式 -- Repository,我们可以先来看看 Repository。

Repository

一个可扩展的、高内聚的数据层应该有哪些特性?

  • 涵盖多个数据来源(Network、Database、File、Cache)
  • 对数据进行必要的逻辑处理(缓存、格式转换等)
  • 对外提供单一的数据出口(对外的方法应该是 getData,而不是 getNetData/getCacheData)

举个例子?

public class TasksRepository {

    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }
}

TasksRepository 是对外提供 Task 的数据层,可以看到他持有了 tasksRemoteDataSourcetasksLocalDataSource 两个对象,这就属于我们刚才提到的,多个数据来源。并且他们都实现了 TasksDataSource 符合面向接口编程。

再来看它的一个对数据操作的方法:

  /**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);

        Task cachedTask = getTaskWithId(taskId);

        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }

        // Load from server/persisted if needed.

        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }

            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }

                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

可以看到这个方法符合刚才所说的,只对调用者提供单一的数据出口,调用方根本不需要知道数据来自于缓存还是网络还是数据库,并且也隐藏了缓存数据的逻辑,这就是一个典型的 Repository 构建方式。这个例子来自于 google sample todo-mvp

在 AAC 架构中,Repository 中数据库为 Room,不过如果你有已经在使用的,稳定的数据库解决方案,其实是没有必要替换的,所以 Room 的使用不是本文的重点。数据库中读出来的数据,自然可以配合 LiveData,关于 LiveData 的特性,一会再谈。

所以以上所说的 Repository 应该是如图所示:


Repository

ViewModel

可以试想一下,假如我们把 Repository 直接写在 UI 组件(Activity、Fragment)里面,会造成哪些问题呢?

  • 违背了 UI 层尽量简单的原则,并不能很好的进行测试
  • 因为 Activity、Fragment 可能因为配置更改、内存不足,导致了视图销毁或者重建,此时就会产生大量的冗余逻辑代码

为了解决以上问题,需要一个数据和 UI 组件的中间层 -- ViewModel

官方文档对 ViewModel 的概括:

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

也就是说 ViewModel 是基于生命周期感知的,来持有并管理 UI 层相关数据的类,并且它可以在 app 的 config 发生改变重建时,始终存活下来。

ViewModel 生命周期

看了 ViewModel 的描述和它的生命周期图,可以总结下 ViewModel 的特性:

  • 生命周期感知。一般在 UI 组件的 onCreate 方法中由开发者手动的创建,随着 UI 组件真正销毁调用 onDestory 时,调用 onCleared 方法销毁。
  • 与 UI 组件解耦。无论由于配置更改 UI 组件反复创建了多少个实例,对 ViewModel 来说都是未知的,ViewModel 也不需要关心,UI 组件最终拿到的都是第一次创建的那个保持原有数据的 ViewModel。并且 ViewModel 中也不应该有任何 android * 包下的引用(除了 android * arch),如果需要一个 Application 的 Context,启动一些 Service 的话,使用 AndroidViewModel 这个类就可以了。

来看一个简单的例子:

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}


public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        User user = viewModel.getUser();
    }
}

也许你觉得这个 demo 看起来平平无奇,但是 ViewModel 自身的框架已经为你做了以下工作:

  • 随着 Fragment 的 onDestory,ViewModel 自动 onCleared
  • 配置更改 Fragment 重启时,ViewModel 未感知,始终携带数据存活
  • ViewModel 不持有任何 View 相关的引用。(在任何位置持有 Activity、Fragment 都是有风险的,有可能造成 NPE、内存泄漏)

肯定有同学会问,你 ViewModel 都不持有 View 的对象,那数据更改了,怎么通知 View 呢?对了就是我们接下来要说的观察者模式,观察者模式对于 MVC MVP 这种持有对象和接口的模式来说,简直就是降维打击,可以试想一下,如果我的 View 层去观察 ViewModel 数据的变化,当 View 层被杀死,那最多也就是不再去观察 ViewModel 了,ViewModel 对此也不在意,所以就会减少特别多的 NPE 和内存泄漏。

在 AAC 架构中,观察者模式是通过 LiveData 这个类配合完成的。

LiveData

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

从官方文档的描述中,可以总结出被 LiveData 包装的数据,有这样的特性:

  • 生命周期感知,LiveData 仅仅会在你 UI 组件处于 active 状态时,notify 数据,这也为我们避免了很多 NPE 和内存泄漏
  • 数据自动更新,当 UI 组件从 inactive 到 active 状态时,假设在此期间有数据更新,LiveData 会将最新的数据 notify 给 UI 组件,比如 App 回到 Home 页再切换回来。
  • 自动清除引用,当 UI 组件调用 onDestory 时,LiveData 会清除 LifeCycle 相关的引用和注册

可以来看一下 ViewModel 配合 LiveData 应该是怎样的一个效果:

public class NameViewModel extends ViewModel {

// Create a LiveData with a String
private MutableLiveData<String> mCurrentName;

    public MutableLiveData<String> getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData<String>();
        }
        return mCurrentName;
    }

// Rest of the ViewModel...
}
public class NameActivity extends AppCompatActivity {

    private NameViewModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Other code to setup the activity...

        // Get the ViewModel.
        mModel = ViewModelProviders.of(this).get(NameViewModel.class);


        // Create the observer which updates the UI.
        final Observer<String> nameObserver = new Observer<String>() {
            @Override
            public void onChanged(@Nullable final String newName) {
                // Update the UI, in this case, a TextView.
                mNameTextView.setText(newName);
            }
        };

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        mModel.getCurrentName().observe(this, nameObserver);
    }
}

mCurrentName 被 LiveData 包裹,这个时候就赋予了 mCurrentName 被观察的能力。当数据发生更改时,如果 NameActivity 是 active 状态,则会回调 onChanged 方法。
自然,我也可以主动地去更改 LiveData 中的数据的值:

mButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String anotherName = "John Doe";
        mModel.getCurrentName().setValue(anotherName);
    }
});

除此以外,还有 postValue 方法,将值直接抛到 UI 线程。setSource 方法,绑定观察数据源

上文中,提到了很多次生命周期的监测,其实是与 LifeCycle 这个组件相关的,LifeCycle 这个组件就是典型的为了减少模板代码而生的。同理如果你是在开发逻辑中,写了很多模板型的代码,这个时候你就要考虑从架构中优化,减少这些不必要的模板代码。关于 Lifecycle,其实可说的点并不是很多,有兴趣可以去官方文档查看。

到目前为止,我的 UI 层和 ViewModel 通过 LiveData 建立了可靠的观察者模式:


View - ViewModel

当发生了 View 层的销毁,会有怎样的结果呢?


View Destory

这就是观察者模式的优势所在,View 层销毁,对 ViewModel 来说只是少了一个观察者而已,我不持有你的对象,我肯定不会内存泄漏,也不会有什么 NPE。现在看起来 View 层和 ViewModel 层的通信方式已经升级为观察者模式了,那在另一侧,ViewModel 层和 Repository 也应该有观察者模式这种高级通信方式吧。

@Dao
public interface ProductDao {
    @Query("SELECT * FROM products")
    LiveData<List<ProductEntity>> loadAllProducts(); 
}

可以看到我们从 Room 数据库取出的数据直接拿 LiveData 包装后,作为返回值抛出。

public class DataRepository {

private MediatorLiveData<List<ProductEntity>> mObservableProducts;

    private DataRepository(final AppDatabase database) {
        mDatabase = database;
        mObservableProducts = new MediatorLiveData<>();

        mObservableProducts.addSource(mDatabase.productDao().loadAllProducts(),
                productEntities -> {
                    if (mDatabase.getDatabaseCreated().getValue() != null) {
                        mObservableProducts.postValue(productEntities);
                    }
                });
    }

    /**
     * Get the list of products from the database and get notified when the data changes.
     */
    public LiveData<List<ProductEntity>> getProducts() {
        return mObservableProducts;
    }

}

DataRepository 从数据库中取出数据,构建成 MediatorLiveData<List<ProductEntity>> mObservableProducts 对象,通过 getProducts 方法对 ViewModel 暴露数据。

public class ProductListViewModel extends AndroidViewModel {

    // MediatorLiveData can observe other LiveData objects and react on their emissions.
    private final MediatorLiveData<List<ProductEntity>> mObservableProducts;

    public ProductListViewModel(Application application) {
        super(application);

        mObservableProducts = new MediatorLiveData<>();
        // set by default null, until we get data from the database.
        mObservableProducts.setValue(null);

        LiveData<List<ProductEntity>> products = ((BasicApp) application).getRepository()
                .getProducts();

        // observe the changes of the products from the database and forward them
        mObservableProducts.addSource(products, mObservableProducts::setValue);
    }

    /**
     * Expose the LiveData Products query so the UI can observe it.
     */
    public LiveData<List<ProductEntity>> getProducts() {
        return mObservableProducts;
    }

}

通过代码可以看到,通过一系列 LiveData 对象,将 ViewModel 和 Repository 也建立了观察者模式:


ViewModel - Repository

作为开发者,我只需要在 ViewModel 调用 onCleared 方法时,去掉 Repository 的引用就好了。

其他

1.ViewModel 是 onSaveInstanceState 方法的替代方案吗?

ViewModel 的数据是在内存层面持有的,官方文档也反复提到过,ViewModel 也仅仅是在配置更改 UI 层重建时,可以存活的。当 App 由于系统内存不足被杀死时,ViewModel 也会销毁,这个时候就需要借助 onSaveInstanceState 来保存一些轻量的数据。所以数据持久化这块,ViewModel 解决了配置更改重启的问题,但是除此之外的,依然要依靠原有的方案。

2.ViewModel 是 Fragment.setRetainInstance(true) 的替代方案吗?

Fragment.setRetainInstance(true) 这个方法的目的就是在配置更改时,保留 Fragment 的实例,所以 ViewModel 完全可以成为这个方法的替代方案。

3.看起来 RxJava 一样可以完成 LiveData 所做的事

是的,如果你 RxJava 玩得好,一样能做出和 LiveData 一样的效果,官网也明确表明,如果你执意使用 RxJava 替代 LiveData,请保证你能处理 UI 的生命周期,使你的 data streams 配合 UI 组件状态正确地 pause destory。(何必为难自己呢,用 LiveData 多好),Google 也出了一个类可以将二者配合使用 LiveDataReactiveStreams

总结

所以完整的 AAC 架构图应为:


AAC

要想打造一个能处理繁重业务量的架构是很不容易的,我也准备在公司项目中,使用 AAC 重构一个模块,有任何心得感受或者采坑了,会更新到这个文章中。

参考资料:
https://developer.android.com/topic/libraries/architecture/
https://github.com/googlesamples/android-architecture-components

推荐阅读更多精彩内容