Android Arch Comp

来源:互联网 发布:e订通软件 编辑:程序博客网 时间:2024/06/06 02:42

Guide to App Architecture(应用架构使用指南)

本指南适合应用程序开发人员,现在希望了解最佳实践和推荐的架构来构建健壮,生产应用。

注:本指南假设读者对应用框架有一定的熟悉。如果刚开始应用开发,先查看入门培训,里面涵盖了本指南提到的相关主题。

Common problems faced by app developers

应用开发者面临的共同问题
和传统的桌面开发不同,桌面开发在大多数场景中都会有一个单独的快捷方式入口,并运行在一个单独的进程中。而Android应用结构相对更加复杂。一个典型的应用程序由多个应用组件构成,包括Activities, fragments, services, content providers 和broadcast receivers.

多数应用组件都会在应用清单文件中声明,安卓操作系统通过这些组件来决定如何将应用集成到用户体验当中。然而,前面提到桌面应用运行在一个独立的进程中,一个正确编写的Android应用需要更加的灵活,保证用户在设备上使用不同的应用,不断切换流量和任务。

例如,想象下当你在你最喜欢的社交应用中分享一张图片时会发生什么。应用会发起一个调用摄像头的意图让Android操作系统运行摄像头应用响应这个意图/请求。同时,用户离开社交应用,但是这个体验是无缝承接的。反过来,摄像头应用也可能调起其他应用,比如文件选择。最后,用户又返回这个社交应用,并分享这张图片。用户也可以在这个过程中接电话,并在接完电话后继续分享照片。

在Android中,上述的行为很常见,因此你所开发的应用也必须正确的处理这样的流程。但是必须记住的一点是,移动设备的资源是有限的,在任何时候,操作系统都可能杀掉应用程序已保证有足够的资源来运行新的应用程序。

总的来说应用组件可以单独的启动,也可以无序的启动,也可以随时被用户和操作系统所杀掉。因为应用组件非常短暂,且生命周期不受控制,因此不该在应用组件中存储任何应用数据和状态组件间也不该相互依赖

Common architectural principles

通用架构原则
如果我们无法使用应用组件来保存应用数据和状态,那我们应该如何构建应用呢?

你应该关注的最重要的一个原则就是在应用中关注点的分离(解耦)。将全部的代码都写在Activity或Fragment中是一个常见的错误。任何和UI渲染操作系统交互无关的代码都不应该放在Activity和Fragment中。尽可能保证这两个组件代码整洁,避免引起生命周期相关的问题。我们并不永远这些类,像Activity、Fragment是系统提供给我们在操作系统和应用交互的类。安卓操作系统可能会在任何时候基于用户交互或低内存等其他因素销毁组件。最好是减少对它们的依赖,以提供可靠的用户体验。

第二个重要的原则是应该使用数据来驱动渲染UI界面,可能是一个持久化的数据模型。进行数据持久化有两个原因:1. 如果系统为了获取资源而干掉应用用户也不会丢掉数据;2. 即便网络链接不稳定或无法链接,应用仍然可以正常运行。模型是处理应用数据的组件,独立于视图和应用组件。因此模型是和组件生命周期相隔离的。UI代码越简单,应用逻辑越自由,耦合性越多,应用程序就会越容易管理。将应用程序建立在那些有明确的管理数据职责的模型类上,可以使它们具有可测试性和应用一致性。

推荐的应用架构
本模块将通过一个用例来展示如何使用Architecture Components来构建应用。

注:并不存在一个架构或者编写应用的方式适合所有的场景。也就是说,本文中推荐的架构在大部分的使用场景中是一个好的开始。如果你已经有一个良好的应用架构体系,并不需要去做什么改变。

假设我们正在设计一个UI页面来展示用户信息。用户相关信息将从我们私有的后台中获取数据。

Building the user interface

构建用户接口
用户页面由UserProfileFragment.java和对应的布局文件user_profile_layout组成。

要展示UI,我们需要两个数据元素:
- The User ID:用户id。要展示UI,我们需要两个数据元素:最好是使用fragment arguments将这个信息传递给fragment.如果操作系统销毁了进程,这个信息将保留,并在下次重启时获取到。
- The User object:一个POJO对象,持有用户相关数据。

继承ViewModel创建UserProfileViewModel类来保存数据。
ViewModel为指定的UI组件提供数据,比如fragment, Activity,处理数据相关业务的交互,比如调起其他组件加载数据或者转发用户修改。ViewModel并不知道具体的视图,且不受配置变化的影响,比如Activity的旋转。

创建三个文件:
- user_profile.xml : 界面定义
- UserProfileViewModel.java : 为UI准备数据
- UserProfileFragment.java : UI控制器:显示ViewModel中的数据,响应用户交互。

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {    private String userId;    private User user;    public void init(String userId) {        this.userId = userId;    }    public User getUser() {        return user;    }}

UserProfileFragment

public class UserProfileFragment extends LifecycleFragment {    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);    }}

注:UserProfileFragment继承自LifecycleFragment代替Fragment。当架构组件的API稳定后,Support包中的Fragment将实现LifecycleOwner接口。

创建这三个文件后,当ViewModle中的user被设置后,我们需要通知UI。这里就需要LiveData。

LiveData是一个可观察的数据持有者对象。允许应用中的组件观察LiveData对象的变化,而不需要显示的依赖。LiveData也保留了当前组件的生命周期状态,正确处理数据避免内存泄露。

注:如果在开发中已经使用了RxJava或Agera,可以不使用LiveData。但是如果使用LiveData,确保正确的处理什么周期,这样当相关组件的生命周期暂停时,数据流也会暂停,组件被销毁时数据流也能正确的关闭。或者也可以添加android.arch.lifecycle:reactivestreams依赖来使用LiveData配合其他响应式的库。

现在使用UserProfileViewModel和LiveData替换User对象,这样Fragment就能接受到数据变化的通知。LiveData是能感知生命周期,及时的清除无效的引用。

public class UserProfileViewModel extends ViewModel {    ...    private User user;    private LiveData<User> user;    public LiveData<User> getUser() {        return user;    }}

接着,修改UserProfileFragment来注册数据变化通知,并及时更新UI

@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState) {    super.onActivityCreated(savedInstanceState);    viewModel.getUser().observe(this, user -> {      // update UI    });}

当数据发生变化时,就会收到通知,并及时更新UI。

和其他观察监听回调不同,我们并没有重写fragment.onStop()方法来取消观察注册。对于LiveData不是必须的,因为LiveData可以感知生命周期,除非相关的生命周期组件没有成对出现生命周期。在Fragment.onDestroy()中会移除所有的数据观察者。

在配置发生变化时也无需做特殊处理。当配置发生变化时,ViewModel会自动保存,一旦新的Fragment创建,会使用相同的ViewModel并回调当前的数据。这也是为什么不需要绑定具体的View的原因。

Fetching data

获取数据
上述步骤连接了ViewModel和Fragment,接下来将获取数据,可以使用Retrofit库。

retrofit WebService接口,连接后台:

public interface Webservice {    /**     * @GET declares an HTTP GET request     * @Path("user") annotation on the userId parameter marks it as a     * replacement for the {user} placeholder in the @GET path     */    @GET("/users/{user}")    Call<User> getUser(@Path("user") String userId);}

一个简单的ViewModel实现就是直接使用WebService请求来获取User对象,但是即便这种方式能实现,应用也会因此变得难以维护,也和最初提到的解耦相违背。此外,当ViewModel和Activity/Fragment的生命周期绑定后,当生命周期结束,丢掉所有数据是非常不好的一种用户体验。相反,ViewModel的获取数据的业务将代理给Repository模块完成。

Repository模块负责处理数据相关操作,为应用的其他模块提供简单的API,当数据更新是调用其他API,相当与数据源的中间层、中介。

UserRepository

public class UserRepository {    private Webservice webservice;    // ...    public LiveData<User> getUser(int userId) {        // This is not an optimal implementation, we'll fix it below        final MutableLiveData<User> data = new MutableLiveData<>();        webservice.getUser(userId).enqueue(new Callback<User>() {            @Override            public void onResponse(Call<User> call, Response<User> response) {                // error case is left out for brevity                data.setValue(response.body());            }        });        return data;    }}

尽管Repository模块看起来没有必要,但它有一个重要的目的:它从应用程序的其余部分提取数据源,ViewModel不知道数据是由Webservice获取的,这意味着可以根据需要将其交换到其他实现。

注:为了简单起见,我们省略了网络错误的例子。对于暴露错误和加载状态的另一个实现,Addendum:exposing network status

Managing dependencies between components

管理组件间的相互依赖
UserRepository需要Webservice对象完成对应的工作。它可以简单地创建它,但是要这样做,它还需要知道Webservice类的依赖项来构造它。这会是使代码复制化。(例如,需要一个Webservice实例的每个类都需要知道如何使用它的依赖项来构造它)。此外,UserRepository可能不是唯一需要web服务的类。如果每个类都创建一个新的web服务,那么将耗费过多的资源。

有两种办法来处理这个问题:

  • Dependency Injection : 依赖注入允许类在不构造它们的情况下定义它们的依赖项。在运行时,另一个类负责提供这些依赖项。我们推荐Google的Dragger2库用于在Android应用中实现依赖注入。Dragger2通过使用依赖树来自动构造对象,并提供依赖项的编译时间保证。

    • Service Locator : 服务定位器提供了一个注册表,在这个注册表中,类可以获得它们的依赖,而不是构建它们。它比依赖项注入(DI)要容易得多,所以如果您不熟悉DI,那么使用服务定位器代替。

以上两个模式允许您扩展您的代码,因为它们为管理依赖关系提供了清晰的模式,而不需要复制代码或增加复杂性。它们都允许交换实现进行测试,这也是他们的优点之一。

以下的例子将使用Dagger2来管理依赖

Connecting ViewModel and the repository

连接ViewModel和repository
UserProfileViewModel

public class UserProfileViewModel extends ViewModel {    private LiveData<User> user;    private UserRepository userRepo;    @Inject // UserRepository parameter is provided by Dagger 2    public UserProfileViewModel(UserRepository userRepo) {        this.userRepo = userRepo;    }    public void init(String userId) {        if (this.user != null) {            // ViewModel is created per Fragment so            // we know the userId won't change            return;        }        user = userRepo.getUser(userId);    }    public LiveData<User> getUser() {        return this.user;    }}

Caching data

缓存数据
上面的repository实现很好地提取了对web服务的调用,但是因为它只依赖于一个数据源,所以它不是很有用。
但是问题是,UserRepository获取数据后没有进行保存,如果用户离开UserProfileFragment后再返回,又要重新获取数据。一方面浪费网络带宽,一方面增加用户等待获取数据的时间。为了解决这个问题,在UserRepository 添加一个新的数据源,该数据源将在内存中缓存User对象。

@Singleton  // informs Dagger that this class should be constructed oncepublic class UserRepository {    private Webservice webservice;    // simple in memory cache, details omitted for brevity    private UserCache userCache;    public LiveData<User> getUser(String userId) {        LiveData<User> cached = userCache.get(userId);        if (cached != null) {            return cached;        }        final MutableLiveData<User> data = new MutableLiveData<>();        userCache.put(userId, data);        // this is still suboptimal but better than before.        // a complete implementation must also handle the error cases.        webservice.getUser(userId).enqueue(new Callback<User>() {            @Override            public void onResponse(Call<User> call, Response<User> response) {                data.setValue(response.body());            }        });        return data;    }}

Persisting data

持久化数据
在上述的实现中,如果用户旋转屏幕或者离开并返回应用,页面可以立即显示,因为数据直接从内存中获取。但是如果应用退出过一个小时后在返回,并且进程被操作系统杀掉。又会怎么样呢?

我们需要重新从网络上获取数据,这也是一个不好的用户体验,并且重新获取相同的数据也是一个很大的浪费。可以简单地通过缓存web请求来解决这个问题,但是它会产生新的问题。如果相同的用户数据来自另一种类型的请求,会发生什么情况(例如:获取一个朋友列表)?然后,应用程序可能会显示不一致的数据,这是最令人困惑的用户体验。例如,相同用户的数据可能会以不同的方式出现,因为好友请求和用户请求可以在不同的时间执行。应用需要将它们合并,以避免显示不一致的数据。

处理这个问题的正确方法是使用一个持久化模型。这就是room持久化库的作用所在。

Room是一个对象关系映射库,为本地数据持久化提供了最小的样板代码。在编译时,根据模式验证每个查询语句,因此,损坏的SQL查询会导致编译时错误,而不是运行时失败。Room抽象了处理原始SQL表和查询的一些底层实现细节。它还允许对数据库数据进行更改(包括集合和连接查询),通过LiveData对象公开这些更改.此外,它还显式地定义了解决常见问题的线程约束,例如访问主线程上的存储。

注:如果您熟悉另一个持久性解决方案,比如SQLite ORM或类似Realm的数据库,那么您不需要将其替换为Room,除非Room的特性集与您的用例更相关。

要使用Room,我们需要定义我们的本地模式。首先,用@entity注释User,将其标记为数据库中的一个表。

@Entityclass User {  @PrimaryKey  private int id;  private String name;  private String lastName;  // getters and setters for fields}

然后继承RoomDatabase创建一个数据库。

@Database(entities = {User.class}, version = 1)public abstract class MyDatabase extends RoomDatabase {}

注意:MyDatabase是抽象的,Room会自动提供一个实现类。详情可参考Room文档。

接下来创建一个data access object(DAO)对象类对数据库进行操作。

@Daopublic interface UserDao {    @Insert(onConflict = REPLACE)    void save(User user);    @Query("SELECT * FROM user WHERE id = :userId")    LiveData<User> load(String userId);}

通过数据库提供DAO引用

@Database(entities = {User.class}, version = 1)public abstract class MyDatabase extends RoomDatabase {    public abstract UserDao userDao();}

请注意,load方法返回一个LiveData。Room知道数据库何时被修改,当数据发生变化时,它会自动通知所有的活动观察者。因为它使用LiveData,所以这将是有效的,因为只有在至少有一个活动观察者的情况下,它才会更新数据。

注:在alpha 1版本中,Room检查基于表修改的失效,这意味着它可能会发送错误的正向通知。

现在,可以修改UserRepository 来合并Room 数据源。

@Singletonpublic class UserRepository {    private final Webservice webservice;    private final UserDao userDao;    private final Executor executor;    @Inject    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {        this.webservice = webservice;        this.userDao = userDao;        this.executor = executor;    }    public LiveData<User> getUser(String userId) {        refreshUser(userId);        // return a LiveData directly from the database.        return userDao.load(userId);    }    private void refreshUser(final String userId) {        executor.execute(() -> {            // running in a background thread            // check if user was fetched recently            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);            if (!userExists) {                // refresh the data                Response response = webservice.getUser(userId).execute();                // TODO check for error etc.                // Update the database.The LiveData will automatically refresh so                // we don't need to do anything else here besides updating the database                userDao.save(response.body());            }        });    }}

注意,即便我们改变了UserRepository中的数据来源,也无需修改UserProfileViewModel和UserProfileFragment代码。这就是抽象带来的灵活性,低耦合。对于测试来说,即便我们提供本地UserRepository也不会影响UserProfileViewModel的测试。

到此为止,所有的代码都已经完成了。即便用户几天后再重新访问用户界面,也能看到用户信息,因为这些用户信息已经持久化了。与此同时,如果数据已经过时,我们的数据仓库也会在后台进行数据更新。当然,根据使用场景,也许你不会暂时过时的持久化数据。

在某些使用场景中,比如下拉刷新,如果当前正在进行网络操作,及时将界面暂时给用户也是很有重要的。将UI操作和实际数据分开是一个很好的习惯(例如,如果我们获取一个好友列表,那么同一个用户可能会再次刷新,从而触发一个LiveData的更新)。从UI的角度来看,在下拉中存在请求的事实只是另一个数据点,类似于任何其他的片段数据(比如用户对象)。

以下两个解决方案将适用于这个使用场景:
1. 将getUser返回修改为LiveData对象,这样可以包含网络操作状态。在Addendum中提供了一个实现示例: Addendum: exposing network status部分。
2. 在Repository类中提供另一个公共功能,可以返回用户的刷新状态。如果您想在UI中显示网络状态,仅在对显式用户操作的响应中显示网络状态(比如下拉刷新),则该选项会更好。

Single source of truth
保证数据来源的唯一性,单一数据来源原则
相同的接口在不同服务器节点返回相同的数据是很正常的。比如说,上述后台接口中的另一个服务器节点返回了一个好友列表,同样的User对象来至两个不同的API服务器,可能也不一样。如果UserRepository按原样返回来自Webservice请求的响应,那么我们的ui可能会显示不一致的数据,因为数据可能会在这些请求之间发生变化。这就是为什么在UserRepository实现中,web服务回调只是将数据保存到数据库中。然后,对数据库的更改将触发对活动的LiveData对象的回调。

在这个模型中,数据库作为唯一的正确的数据来源,并且应用程序的其他部分通过存储库访问它。不管您是否使用磁盘缓存,我们建议您的repository 将数据源指定为您应用程序的其余部分的唯一来源。

Testing

测试
我们已经提到分离的好处之一是可测试性。让我们看看如何测试每个代码模块。
- User Interface & Interactions:用户接口和交互。这将是你唯一需要一个 Android UI Instrumentation test的时候。测试UI代码的最佳方法是创建一个Espresso测试。可以创建片段并提供一个模拟视图模型。由于片段只与ViewModel进行对话,因此对其进行模拟将足以充分测试此UI。可以创建Fragment并提供一个模拟ViewModel。由于Fragment只与ViewModel进行交互,因此对其进行模拟将足以充分测试此UI。

  • ViewModel : 可以使用JUnit test对ViewModel进行测试。您只需要模拟UserRepository来测试它。

  • UserRepository : 也可以使用JUnit来测试UserRepository。模拟创建一个WebService和DAO。可以测试它是否提供了正确的接口调用,将结果保存到数据库中,并且如果数据被缓存和更新,就不会产生任何不必要的请求。由于Webservice和UserDao都是接口,所以可以对它们进行模拟,或者为更复杂的测试用例创建模拟实现。

  • UserDao : 测试DAO类推荐的方法是使用instrumentation测试. 由于这些测试不需要任何UI,运行得很快。对于每个测试,创建内存数据库以确保测试没有任何副作用(如更改磁盘上的数据库文件)。
    Room还可以指定数据库的实现,可以通过实现supportsqliteopenhelper进行JUnit测试。通常是不推荐这种方法,因为SQLite版本上运行的装置可能不同于主机版本的SQLite。

  • Webservice : 进行WebService测试时,很重要的一点就是和服务器请求相互独立,避免链接后台接口服务器。有很多库可以帮助我们实现这个功能。例如,mockwebserver是一个非常棒的库,可以创建本地服务器来测试接口。

The final architecture

最终架构
以下这张图显示了推荐的架构中所有的模块和模块间的交互:
这里写图片描述

Guiding principles

本指南所遵循的一些原则
编程是一个创造性的领域,构建Android应用程序也不例外。解决问题的方法有很多种,例如,在多个活动或片段之间传递数据,检索远程数据,并在脱机模式下本地保存数据,或者任何大型应用程序遇到的其他常见场景。

以下建议不是强制性的,经验告诉我们,遵循这些规则将使你的代码库在长期内更加健壮、可测试和可维护。

  • 在清单活动、服务、广播接收器等中定义的入口点不是数据源。相反,它们只应该协调与该入口点相关的数据子集。由于每个应用程序组件生命周期都很短,这取决于用户与设备的交互以及运行时的当前运行状况,所以不希望这些入口点成为数据源。
  • 强制在应用程序的各个模块之间建立明确的职责界限。例如,不要将从网络中加载数据的代码分散到代码库中的多个类或包中。类似地,不要将无关的职责(如数据缓存和数据绑定)填充到同一个类中。
  • 每个模块都尽可能的封装好。不要试图创建从一个模块中公开内部实现细节的接口。短期内你会获得一些方便,但是将产生很多历史遗留问题并且阻碍代码的发展与维护。
  • 在定义模块之间的交互时,考虑如何使每个模块进行单独的测试。例如,对于定义良的从网络中获取数据的API,可以很容易地测试在本地数据库中持久化数据。相反,如果你把这两个模块的逻辑混合在一个地方,或者在整个代码库中穿插很多网络请求代码,并不是说不能测试,只是说测试起来会更困难。
  • 应用的核心,是能在其他应用中脱颖而出。不要把时间不断重复发明轮子或写同样的样板代码上。相反,集中你的精力让你的应用程序的更加独特,让Android的架构组件和其他推荐的图书馆处理重复的样板。
  • 持久化尽可能多的用户相关和实时的数据,以便当设备处于脱机模式下也能正常使用。你不能保证用户和你一样享受这高速网络链接。
  • 数据仓库应该指定一个唯一来源。每当你的应用程序需要访问这段数据时,不至于显示错乱的数据。

Addendum: exposing network status

// TODO