Android架构组件

这篇架构指南面向有一定Android开发基础并想了解高质量、高稳定性App最佳实践及推荐架构的开发者。

注意:这篇指南假设读者熟悉Android Framework,如果你是Android开发新手,建议先去学习入门系列课程,这些课程也是作为学习本指南的基础。

App开发者面临的常见问题

移动开发不同于传统的桌面程序开发,桌面程序一般都有唯一的快捷方式入口,并且常作为单进程存在;而Android App则拥有更加复杂的结构,一个典型的Android应用通常由多个应用组件构成,包括不同数量的Activity、Fragment、Service、Content Provider、Broadcast Receiver等。

大部分的组件都会在AndroidManifest中声明,这个配置文件被Android操作系统用来决定如何将App集成到该设备整体的用户体验中去。前文已提到,一个桌面应用程序一般作为独立进程存在,而一个App则需要更加灵活,因为用户在移动设备上,需要经常随意地在App间进行切换,以便完成不同的任务及流程。

举个例子,如果想要在一个社交App上分享一张图片,思考下会是怎样的流程?首先社交App可能会使用Intent方式通过Android系统来启动一个拍照App,此时用户虽然离开了社交App,但其体验是无缝衔接的。同样的,拍照App也可能启动其他应用,如文件选择器或其他。最终用户返回社交App然后分享图片。当然,用户操作可能在上述过程中的任何时刻被电话打断,当通话结束后,再继续进行上述图片分享流程。

在Android中,类似上述App间切换非常频繁,因此我们的App需要正确处理上述流程。需要牢记的是移动设备的资源是有限的,所以在任何时候,操作系统都可能杀死一些App来为一些新的App腾出资源。

所有这些都说明我们的App组件可能被无规则地启动,也可能随时被用户或系统销毁,其生命周期并不由我们控制,因此我们不应该在App组件中存储任何数据而且组件之间也不应该互相依赖。

通用的架构原则

如果不在App组件中存储数据及状态,那应该怎样架构App呢?

第一,在App开发中,最需要的事情是职责分离( separation of concerns)。大家通常犯的一个错误是把所有代码都写到Activity或Fragment中,实际上任何跟UI处理或系统交互无关的代码都不应该写到Activity或Fragment中。尽可能保持Activity和Fragment的简洁(职责单一化)可以避免许多生命周期相关的问题。不要忘记你并不能控制这些类,它们就像粘合剂,体现了App和操作系统之间的联系,Android系统可能根据用户操作或其他原因如低内存而随时销毁它们。我们应该最小化对App组件的依赖从而提供一个稳定的用户体验。

第二,另一个重要原则是数据模型驱动UI,最好是可持久化的数据模型。持久化数据有两个好处:

  1. 当系统为了释放资源而销毁掉App时用户不会丢失数据
  2. 当网络状况差或无网络时App依然可以工作

Model(数据模型)也可以看做是一种处理App数据的组件,它们独立于视图和其他App组件,因此Model不存在其他组件生命周期相关的问题。保证UI代码简单、不掺杂业务逻辑可以方便管理,数据模型具有管理数据的职责,基于数据模型的应用程序将使它们具有可测性和应用程序一致性。

推荐的App架构

在这一节,将会通过一个示例来描述如何使用架构组件(Architecture Components )架构App。

注意:并不存在一个适用于任何场景的完美架构,也就是说,这里推荐的架构对于大部分开发案例来说只是一个好的开端。如果你已经有了一个不错的Android App架构,你可以继续使用而不用改变。

假设我们正在开发一个展示用户信息的界面,用户信息将通过REST API从后台请求数据。

构建用户界面UI

用户界面用一个Fragment实现,如UserProfileFragment.java ,其对应的布局文件为user_profile_layout.xml

为了驱动UI,数据模型需要维护两个数据元素:

  1. User ID——用户的标识符,通常使用Fragment参数将此信息传给Fragment。如果Android系统销毁了App进程,User ID可以被保存下来因此当用户下一次启动应用时User ID是可用的
  2. User object——一个POJO对象,用于维护User数据

接下来将基于ViewModel来创建一个UserProfileViewModel类来维护上述信息。

ViewModel为特定的UI组件(Fragment或Activity)提供数据并处理与业务逻辑的交互,如调用其他组件加载数据。ViewModel与View解耦且不受配置变化的影响,如方向旋转时重建Activity不会影响ViewModel。

现在我们有3个文件:

  • user_profile.xml:UI布局文件
  • UserProfileViewModel.java:为UI提供数据的类
  • UserProfileFragment.java:UI Controller用来展示ViewModel中的数据并响应用户交互

下面就是初步的实现(为了简单起见,忽略了布局文件):

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 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);
    }
}

注意:上面的例子继承了LifecycleFragment而不是Fragment。当架构组件中的lifecycle API稳定之后,Android Support Library中的Fragment类将实现LifecycleOwner。

现在我们有了3个代码模块,应该怎样将它们串联起来呢?毕竟我们需要当ViewModel的user字段改变后能够通知UI,此时LiveData就派上用场了。

LiveData是一个可观察的数据容器,它不需要显式地创建依赖路径就可以被App中的组件观察。LiveData可以根据App组件(Activity、Fragment、Service)的生命周期做正确的事情,从而能够有效阻止App中对象的内存泄露。

注意:如果你正在使用RxJava或Agera或其他方案,你可以继续使用它们替代LiveData,但是必须确保正确处理生命周期逻辑:当组件stopped时应该暂停数据流;当组件destroyed时也应该销毁数据流。也可以添加android.arch.lifecycle:reactivestreams来配合reactive streams library(如RxJava2)使用LiveData。

现在我们将UserProfileViewModel中的User字段用LiveData来替代,因此当数据变化时Fragment会被通知。最妙的是LiveData是生命周期可感知的,而且会自在不需要的时候自动清除引用。

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

接下来修改UserProfileFragment来观察数据并更新UI。

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

每当用户数据发生变化时,onChanged回调将会触发,从而导致UI刷新。

如果你熟悉使用被观察者回调模式的其他库,你应该意识到我们不需要重写Fragment的onStop()方法来停止观察数据。对于LiveData这是不必要的,因为它本身是生命周期可感知的,这意味着如果Fragment不在激活状态时(执行了onStart() 但没有执行onStop()),LiveData不会触发回调,同时,LiveData会在Fragment执行了onDestroy()时自动移除观察者。

我们也不需要做任何事情去处理配置改变(如用户旋转屏幕),当配置变化时,ViewModel将会自动恢复,所以当新的Fragment创建后,它会收到相同的ViewModel对象实例并且立即收到当前数据的回调。ViewModel比View的生命周期长,这就是为什么ViewModel不应该直接引用View。更多资料请看The lifecycle of a ViewModel。

获取数据

现在我们已经将ViewModel和Fragment联系起来,那么ViewModel是如何获取用户数据的呢?在这个示例中,假设后台提供了一个REST API,我们使用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对象,虽然这是行得通的,但随着App业务逻辑的增长将变得难以维护。ViewModel承担了太多的职责,这违背了前文提到的职责分离的原则,另外ViewModel绑定了Activity和Fragment的生命周期,当生命周期结束后,ViewModel会丢失所有数据,也是一种不好的用户体验。因此,我们的ViewModel将把数据请求的工作代理给一个新的模块,叫做Repository

Repository的职责是处理数据操作,为其他模块提供简洁的API。Repository知道从哪里获取数据,当数据更新时知道调用什么接口,可以把它想象成不同数据源(持久化的Model、Webservice、Cache等)之间的中介。

下面展示了UserRepository类通过Webservice类获取用户数据:

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获取的,这意味着如果有必要,我们可以将具体实现换成其他方式。

注意:为了简单,这里省略了网络异常处理逻辑。对于错误处理及loading状态的处理可以参考 原文附录:Addendum: exposing network status。

管理组件之间的依赖:

上面的UserRepository类需要一个Webservice的实例才能工作,虽然创建Webservice实例很简单,但需要创建对Webservice的依赖,这显然会使代码冗余和复杂化(e.g. 每一个需要Webservice实例的类都需要对它的依赖)。另外,UserRepository可能并非唯一需要Webservice的类,如果每一个类都创建一个Webservice,则会造成资源冗余。

有两种方式可以解决这个问题:

  • 依赖注入(Dependency Injection):依赖注入允许一个类定义其依赖而不用事先构造它们,在运行时,另一个类可以提供这些依赖。在Android开发中推荐使用Google的Dagger 2 进行依赖注入。Dagger 2会自动遍历依赖树来构建出依赖对象并能在编译时对依赖提供验证。
  • 服务定位器(Service Locator):服务定位器提供了一个注册表,从中可以获取依赖。服务定位器的实现比依赖注入相对简单一些,因此如果你对DI不熟悉,可以使用这种方式。

上述方法可以清晰地管理依赖而不会产生冗余代码,同时不会增加复杂度。

在这个示例中,我们使用Dagger 2来管理依赖。

关联ViewModel和Repository

现在来修改UserProfileViewModel类,让其使用Repository:

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;
    }
}

缓存数据

上文实现的Repository对Webservice调用的抽象非常好,但由于它只是依赖于单一的数据源,因此并不是很实用。

上文中实现的UserRepository存在的问题是当获取到数据之后,并没有做任何的持久化工作,如果用户离开了UserProfileFragment然后再次回来,App将会重新获取数据,这很糟糕,主要表现在:

  1. 浪费了流量
  2. 强制用户重新等待请求完成

为了解决上述问题,我们来添加一个新的数据源用来在内存中缓存User数据。

@Singleton  // informs Dagger that this class should be constructed once
public 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;
    }
}

持久化数据

在当前的实现中,如果用户旋转了屏幕或者离开再次回来,UI将会立即显示数据因为Repository会从内存缓存中获取数据。但是如果用户离开App几个小时之后再次回来,如果Android系统销毁了应用进程,将会发生什么呢?

在这种情况下,将会重新通过网络获取数据,这不仅是一个糟糕的用户体验而且还浪费了流量。解决方案是缓存网络请求的结果。

合理的缓存方法是使用一个持久化的模型,于是Room登场了。

Room是一个提供本地数据存储的对象关系映射库。在编译时,它可以校验每个查询,因此无效的SQL查询将导致编译错误而不是在运行时报错。Room抽象了底层原生的SQL表查询操作,同时允许观察数据库数据(包括集合和关联查询)的变化,并通过LiveData暴露出来。此外,Room还显式地定义了线程约束来处理常见问题,比如在主线程上访问数据。

注意:如果你熟悉其他的SQLite ORM持久化解决方案或不同的库如 Realm,你并不需要将其替换为Room,除非Room的一些特性更适合你的应用场景。

为了使用Room,需要使用@Entity注解来标记User类,此注解会将User作为一张表。

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

然后,通过继承 RoomDatabase为App创建一个数据库类:

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

注意到MyDatabase是一个抽象类,Room自动为其提供实现。可以阅读 Room文档以了解更多。

接下来需要提供一个方法将User数据插入到数据库,因此我们创建了一个DAO(data access object)对象。

@Dao
public 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,在至少有一个处于激活状态的观察者时才会更新数据,这将更加高效。

注意:Room还处于alpha 1版本,Room checks invalidations based on table modifications which means it may dispatch false positive notifications。

接下来为UserRepository集成Room数据源:

@Singleton
public 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中的数据源,UserProfileViewModelUserProfileFragment也不需要修改,这就是抽象提供的灵活性。对于测试来说也很有用,因为你可以使用模拟的UserPrository来测试UserProfileViewModel

现在我们的代码完成了,如果用户几天后再次返回页面,由于做了数据持久化,因此仍然可以立即看到用户数据。同时,如果数据变陈旧了,Repository将会在后台更新数据。当然,根据你的应用场景,也可以选择不显示太陈旧的持久化数据。

在一些需求中,如下拉刷新,将当前的网络操作进度显示给用户很重要。将UI操作与实际数据分开是一种很好的做法,因为它可能出于各种原因进行更新(例如,如果获取好友列表,相同User的获取将导致 LiveData 的更新)。

对于这个案例有两种解决方案:

  • 修改getUser,使返回的LiveData中包含网络操作状态。示例实现可参考原文附录一节: Addendum: exposing network status。
  • 在Repository类中提供另一个公有方法,用于返回用户的刷新状态。如果你希望在用户显式操作(如下拉刷新)下展示UI中的网络状态,则此方法更好一些。
单一数据源

不同的REST API返回相同数据的情况很常见,例如,另一个后台接口返回了好友列表数据,那么相同的User对象将来自两个不同粒度的API。如果UserRepository按照Webservice的请求返回响应结果,UI层则可能出现潜在的数据不一致问题,因为服务端的不同数据接口可能改变。这就是为什么在UserRepository的实现中将Webservice的callback返回的数据直接存到数据库,然后数据库数据的改变则触发LiveData的回调。

在上述模型中,数据库作为单一数据源,App通过Repository来访问数据。无论是否使用磁盘缓存,我们建议Repository需要为App指定一个单一数据源。

测试

上文提到了职责分离的一大好处是可测性,来看一下怎么对每一个代码模块进行测试。

  • User Interface & Interactions:这里需要使用Android UI Instrumentation test,测试UI代码最好的方法是使用Espresso。你可以创建一个Fragment并给他提供一个模拟的ViewModel,因为Fragment只与ViewModel交互,所以模拟ViewModel可以非常高效地测试UI。

  • ViewModel:ViewModel可以使用 JUnit test测试,只需要mock UserRepository即可。

  • UserRepository:对于UserRepository也可以使用JUnit test,需要mock Webservice和DAO,需要测试它是否正确调用了web服务、是否将结果数据保存到数据库、如果数据被缓存了是否还会进行不必要的请求。因为Webservice和UserDAO都是接口,可以mock它们或实现这些接口以模拟更复杂的测试场景。

  • UserDao:对于UserDao,推荐的方法是instrumentation测试,因为instrumentation测试不需要任何UI,运行速度很快。对于每一个测试,可以创建一个内存数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。

    Room允许指定数据库实现,因此可以通过提供一个SupportSQLiteOpenHelper实现来测试。这种方法并不推荐,因为运行在设备上的SQLite版本可能跟主机上的SQLite版本不一致。

  • Webservice:保持测试模块之间的独立非常重要,Webservice测试应该避免进行网络调用,有许多类库可以帮我们实现。例如,MockWebServer 是一个不错的库,可用来为测试需求创建一个模拟的本地服务器。

  • Testing Artifacts:Android架构组件提供了一个库来控制后台线程。在android.arch.core:core-testing 中,有两条单元测试规则:

    • InstantTaskExecutorRule:这条规则可用来强制架构组件在调用线程立即执行后台操作
    • CountingTaskExecutorRule:This rule can be used in instrumentation tests to wait for background operations of the Architecture Components or connect it to Espresso as an idling resource。

最终的架构

下图展示了建议架构中的所有模块以及它们之间的交互:

Android架构组件_第1张图片

指导原则

编程是一个创造性的领域,构建Android应用也不例外。解决问题的方法有很多,下面推荐的一些原则并非强制性的,根据我们的经验,如果遵循下列原则将会使你的应用变得稳定、可测试、可维护。

  • AndroidManifest中定义的入口——如Activity,services,broadcast receiver等不能作为数据源,它们仅应该用来协调与其相关的数据子集,因为App组件生命周期很短,且依赖于用户的交互操作及系统的运行状况,可能随时被销毁。
  • 严格明确App各模块的职责范围,如不要让从网络加载数据的代码分布在多个类或包中。简单来讲, 不要将不相关的职责放到同一个类中。
  • 各个模块尽可能暴露最少的接口,以降低耦合。不要为了方便就将一个模块的内部实现暴露出来,短期来看可能会节省一些时间,但随着代码的迭代增长,将会花费更多的时间来维护。
  • 定义模块之间的交互逻辑时,需要考虑到如何使每个模块可独立测试。例如,一个良好定义的从网络获取数据的API可以使本地数据库持久化模块的测试变得容易。相反,如果将这两个模块的逻辑混合到一起,甚至将网络请求代码分布到各个地方,将导致测试非常困难,甚至无法测试。
  • App的核心是如何做到使它脱颖而出,不要重复造轮子,也不要一次又一次地在模板代码上浪费时间。如何使App做到独一无二才是值得花费精力的地方,其他的重复工作就让Android架构组件和其他推荐库来做吧。
  • 尽可能为你的App持久化更多的新鲜数据,这样应用在离线状态下也是可用的,因为虽然你的网络状况可能非常好,但用户却不一定。
  • Repository应该指定单一数据源,无论何时当App需要访问一些数据,都应该从单一数据源获取,具体在前文“单一数据源”一节已经讲过。

原文地址

我的微信公众号「不混青年」,id「buhunqingnian」,技术之外的分享:
Android架构组件_第2张图片

你可能感兴趣的:(Android,Architecture,android,架构)