Android架构组件-App架构指南,你还不收藏嘛

本指南适用于那些已经拥有开发Android应用基础知识的开发人员,现在想了解能够开发出更加健壮、优质的应用程序架构。

先给大家来点小福利吧,架构全套学习资料以及高清视频教程,教你如何一步一步走上架构师之路

领取方式,关注小编私信【学】即可获取



首先需要说明的是:Android Architecture Components 翻译为 Android架构组件 并不是我自己随意翻译的,而是Google官方博客中明确称其为 Android架构组件,因此我遵循了这种叫法。

下面这张图是Android架构组件完整的架构图,其中表示了的架构组件的所有模块以及它们之间如何交互:

APP开发者面临的常见问题

与传统的桌面应用程序不同,Android应用程序的结构要复杂得多,在大多数情况下,它们只在桌面快捷启动方式中有一个入口,并且作为单个进程运行。一个典型的Android应用程序是由多个 APP组件(Android四大组件) 构成的,包括 activitiesfragmentsservicescontent providers and broadcast receivers

这些 app组件 中的大部分都是在 应用清单(AndroidManifast.xml)中声明的,Android操作系统使用这些组件将应用程序集成到设备的用户界面中。虽然,应用程序通常上是以单个进程运行的,但是一个合理的Android应用需要更加灵活,因为用户可以通过不同的应用程序,在他们的设备上不断切换流程和任务。

想象下在我们最喜爱的社交网络应用中分享照片时会发生什么情况。首先这个应用程序触发一个Camera(拍照或摄像) Intent,由Android操作系统启动一个Camera应用来处理请求。此时,用户虽然离开了这个社交网络应用,但他们的体验是无缝的。相机应用程序又可能触发其他 Intent,例如启动文件管理器,该文件管理器可以启动另一个应用程序,最终用户回到社交网络应用并分享照片。此外,用户在这个过程的任何时候都可能被电话打断,并在打完电话后回来继续分享照片。

在Android中,这种应用程序跳转行为是很常见的,所以我们的应用程序必须正确处理这些流程。请记住,移动设备是资源受限的,所以在任何时候,操作系统都可能需要杀死一些应用程序,以腾出空间给新的应用。

这一切的要点在于,我们的 app组件 可以单独和无序地启动,并且可以在任何时候由用户或系统销毁。由于 app组件 是短暂的,并且它们的生命周期(创建和销毁时)不在我们的控制之下,因此我们不应该在app组件中存储任何 app数据或状态,并且 app组件不应相互依赖。

通用架构原则

如果不使用 app组件存储app数据和状态,那该如何构造应用程序呢?

我们需要关注的最重要的事情是:如何在你的应用中分离关注点。最常见的错误是将所有的代码写入一个 Activity 或 Fragment,任何不处理 UI 或 与操作系统交互的代码都不应该出现在这些类中,我们应该尽可能保持 Activity 或Fragment 精简,这样可以避免许多生命周期相关的问题。

请记住,我们不拥有这些类,它们只是建立操作系统和我们的应用程序之间契约的胶水类。Android操作系统可能会随时根据用户交互或其他因素(如低内存)来销毁它们,最好尽可能地减少依赖他们,以提供可靠的用户体验。

第二个重要原则是: 你应该从一个 Model 驱动你的UI,最好是一个持久化的 Model。之所以说持久化是理想的 Model,原因有两个:如果操作系统销毁你的应用程序以释放资源,那么你的用户就不会丢失数据,即使网络连接不稳定或连接不上,你的应用程序也会继续工作。

Model 是负责处理应用程序数据的组件,它们独立于应用程序的 Views 和 app组件,因此 Model 与这些 app组件的生命周期问题是相隔离的。保持简洁的UI代码,以及不受约束的应用程序逻辑,可以使app的管理更加容易,基于具有明确定义的管理数据责任的模型类的应用程序,会更加具有可测试性,并使我们的应用程序状态保持前后一致。

推荐的App架构

在本节中,我们将演示如何通过使用用例来构造使用了 架构组件(Architecture Components) 的应用程序。

注意:不可能有一种编写应用程序的方法对每个场景都是最好的。对于大多数用例来说,推荐的这个架构可能是一个好的起点。如果你已经有了编写Android应用的好方法,那就不要在更改了。

假如我没正在搭建一个用来显示 用户概况的UI,该用户概况将使用 REST API 从我们自己的服务器端获取。

搭建用户界面

这个UI 将由 UserProfileFragment.java 及 Fragment 相应的 user_profile_layout.xml 布局文件组成。

为了驱动用户界面,我们的数据模型需要保存两个数据元素。

用户ID:用户的标识符。最好使用 fragment 参数(setArguments方法) 将此信息传递到 fragment 中。如果Android系统销毁了你的进程,这些信息将被保留,便于应用在下次重新启动时可用。

用户对象:保存用户数据的 POJO(简单的Java对象)

我们将创建一个基于ViewModel 的 UserProfileViewModel 类来保存这些信息。

一个 ViewModel 提供了一个特定 UI 组件中的数据,如一个 fragment 或 activity, 并且负责与数据处理业务的通信,例如调用其他 app组件 来加载数据或转发用户信息的修改。ViewModel不知道View,并且不受配置更改的影响,例如由于屏幕旋转而重新创建 Activity。

现在我们有3个文件。

user_profile.xml:定义屏幕上的 UI。

UserProfileViewModel.java:为 UI 准备数据的类。

UserProfileFragment.java:显示 ViewModel 中的数据并对用户交互作出响应的 UI 控制器。

接下来我们将开始实现(为了简单起见,省略了布局文件):

publicclassUserProfileViewModelextendsViewModel{

privateString userId;

privateUser user;

publicvoidinit(String userId){

this.userId = userId;

}

publicUsergetUser(){

returnuser;

}

}12345678910111213

publicclassUserProfileFragmentextendsFragment{

privatestaticfinalString UID_KEY ="uid";

privateUserProfileViewModel viewModel;

@Override

publicvoidonActivityCreated(@Nullable Bundle savedInstanceState){

super.onActivityCreated(savedInstanceState);

String userId = getArguments().getString(UID_KEY);

viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);

viewModel.init(userId);

}

@Override

publicViewonCreateView(LayoutInflater inflater,

                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){

returninflater.inflate(R.layout.user_profile, container,false);

}

}123456789101112131415161718

现在,我们如何将它们联系在一起呢?当给 ViewModel 的 User 字段设值后,我们需要一种方法来通知用户界面,这就是 LiveData类的作用。

LiveData 是一个可观察的数据持有者。它允许应用程序中的组件观察 LiveData 对象的更改,但不会在它们之间创建明确的和严格的依赖关系路径。 LiveData 还会关联 app组件(activities, fragments, services) 的生命周期状态,并做出合适的事情来防止内存泄漏。

注意:如果你已经在使用类似 RxJava 或 Agera 的库 ,则可以继续使用它们而不是LiveData。但是,当你使用它们或其他方式时,请确保正确处理生命周期,以便在相关的LifecycleOwner 停止时暂停数据流,并在销毁 LifecycleOwner 时销毁数据流。你还可以添加android.arch.lifecycle:reactivestreams 以将 LiveData 与其他的响应流库(例如RxJava2)一起使用。

现在我们用 LiveData 替换 UserProfileViewModel 中的 User 字段,以便在数据更新时通知 Fragment。最主要的是:LiveData是生命周期感知的,并且在不在需要时,它将自动清理引用。

publicclassUserProfileViewModelextendsViewModel{

...

privateUser user;

privateLiveData user;

publicLiveDatagetUser(){

returnuser;

}

}123456789

现在我们修改 UserProfileFragment 以便观察数据并更新 UI。

@Override

publicvoid onActivityCreated(@NullableBundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);

viewModel.getUser().observe(this, user -> {

// update UI

    });

}1234567

每次更新用户数据时, 都会调用 onChanged 回调,并刷新 UI。

如果你熟悉其他 可观察回调的库,你可能已经意识到,我们没有重写 fragment 的 onStop() 方法来停止观察数据。这对于 LiveData来说是没有必要的,因为它是生命周期感知的,这意味着它不会调用回调,除非 Fragment 处于 活动状态(已收到 onStart() 但未收到 onStop())。当 fragment 收到 onDestroy() 时,LiveData也将自动移除观察者 。

对于配置变化(例如,用户旋转屏幕)我们也没有做任何特殊的处理。当配置改变时,ViewModel 会自动恢复,所以一旦新的 Fragment 生效,它将接收到相同的 ViewModel实例,并且 ViewModel 的回调将立即被当前数据调用,这就是 ViewModels 为什么不应该直接引用 Views 的原因,他们可以比 View的生命周期更持久。想了解更多信息的请查看 The lifecycle of a ViewModel 。

获取数据

现在我们已经将 ViewModel 关联到了 Fragment,但是 ViewModel 如何获取用户数据呢?在这个例子中,我们假设服务器端提供了一个 REST API。我们将使用 Retrofit 库来访问我们的服务器端,虽然你可以自由使用不同的库来达到同样的目的。

下面是retrofit 的 Webservice ,负责与服务器端进行通信:

publicinterfaceWebservice{

/**

    * @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 getUser(@Path("user") String userId);

}123456789

ViewModel 的一个简单实现是直接调用 Webservice 来获取数据并将其 赋值给 user 对象,虽然这样是可行的,但是我们的应用程序以后将很难维护。它赋予了 ViewModel 类太多的职责,违背了我们前面提到的关注点分离原则。此外,ViewModel 的作用域与一个 Activity 或一个 Fragment 生命周期相关联,当他们的生命周期完成时将丢失所有的数据,这是非常糟糕的用户体验。因此,我们将 ViewModel 的这个工作委托给了一个新的模块 Repository 。

Repository 模块负责数据处理操作。他们为应用的其余部分提供了一个干净的API,他们知道从何处获取数据以及在更新数据时调用哪些API。你可以将它们视为不同数据源 (持久化模型, web服务, 缓存, etc.)之间的中介。

UserRepository 类使用 WebService 来获取用户数据项,如下:

publicclassUserRepository{

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

    }

}1234567891011121314151617

虽然 repository 模块看起来没有必要,但是它有一个重要的目的,它从应用程序的其余部分提取数据源。现在我们的 ViewModel 不知道数据是从 Webservice 获取到的,这意味着我们可以根据需要,将它(Webservice)替换为其他的实现。

注意:为了简单起见,我们忽略了网络错误的情况。对于暴露错误和加载状态的另一个实现,请查看 附录:暴露网络状态

管理组件之间的依赖关系:

上面的 UserRepository 类需要一个 Webservice 的实例来工作,UserRepository 可以简单地创建Webservice ,但要做到这一点,它必须需要知道 Webservice 类的依赖关系来构造它,这会使代码显著和成倍的复杂化(例如,每个需要 Webservice实例的类 都需要知道如何用它的依赖来构造它)。另外,UserRepository 可能不是唯一需要 Webservice 的类。如果每个类创建一个新的 WebService,这将是非常沉重的资源。

现在我们有两种模式可以用来解决这个问题:

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

服务定位器:服务定位器提供了一个注册表,这个类可以获得它们的依赖 而不是 构建它们。实现起来比依赖注入(DI)更容易,所以如果你不熟悉DI,可以使用 Service Locator。

这些模式允许您扩展代码,因为它们提供了用于管理依赖关系的清晰模式,无需复制代码或增加复杂性。这两个模式也允许交换实现测试, 这是使用它们的主要好处之一。

在这个例子中,我们将使用 依赖注入 来管理依赖关系。

关联ViewModel和repository

现在我们修改 UserProfileViewModel 使用的 repository。

publicclassUserProfileViewModelextendsViewModel{

privateLiveData user;

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

    }

}1234567891011121314151617181920212223

缓存数据

上面的 repository 实现 对抽象调用 Web服务是有好处的,但是因为它只依赖于一个数据源,所以它不是很有用。

UserRepository 实现的问题是,在获取数据之后,它不保存在任何地方。如果用户离开 UserProfileFragment 并返回,应用程序将重新获取数据。这是不好的,原因有两个:浪费宝贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到 UserRepository ,这个数据源可以将 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 LiveDatagetUser(String userId) {

LiveDatacached = userCache.get(userId);

if (cached != null) {

return cached;

}

final MutableLiveDatadata = 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() {

@Override

public void onResponse(Callcall, Responseresponse) {

data.setValue(response.body());

}

});

return data;

}

}123456789101112131415161718192021222324

持久化数据

在我们当前的实现中,如果用户旋转屏幕或离开并返回到应用,现有UI将立即可见,因为 repository 从内存中检索缓存的数据。但是,如果用户离开应用程序并且数小时后回来,或Android 系统杀死该进程后,会发生什么?

在目前的实现中,我们将需要从网络上重新获取数据。这不仅是一个糟糕的用户体验,而且会浪费资源,因为它会使用移动数据重新获取相同的数据。你可以简单地通过缓存Web请求来解决这个问题,但是这会产生新的问题。如果相同的用户数据从另一种类型的请求中显示出来(例如,获取朋友列表),会发生什么情况?那么你的应用程序可能会显示不一致的数据,这是一个混乱的用户体验。例如,由于好友列表请求和用户请求可以在不同的时间执行,所以相同用户的数据可能会以不同的方式显示。您的应用需要合并它们以避免显示不一致的数据。

处理这个问题的正确方法是使用 持久化模型。这就是 Room 持久化库可以拯救的地方。

Room 是一个对象映射库,使用最小的模板代码来提供本地数据持久化。在编译时,它会根据 Schema 验证每个查询,因此,有问题的SQL查询会导致编译时出错,而不是运行时失败。Room 抽象了处理原始SQL表和查询的一些底层实现细节。它还允许观察对数据库数据(包括集合和 join 查询)的更改,通过 LiveData对象 公开这些更改 。另外,它明确定义了解决常见问题的线程约束,例如在主线程上的访问存储。

注意:如果你的应用程序已经使用另一个持久化解决方案(如SQLite对象关系映射(ORM)),则不需要使用 Room 替换现有的解决方案。但是,如果你正在编写新的应用程序或重构现有的应用程序,我们建议使用 Room 来保存应用程序的数据。这样,你可以利用库的抽象和查询 验证功能。

要使用 Room,我们需要定义我们的本地 Schema。首先,使用 @Entity 注解 User 类 以将其标记为数据库中的表。

@Entity

classUser {

@PrimaryKey

privateint id;

privateStringname;

privateStringlastName;

// getters and setters for fields

}12345678

然后,为我们的 app 创建一个数据库类继承于 RoomDatabase

@Database(entities = {User.class},version=1)

publicabstractclassMyDatabaseextendsRoomDatabase{

}123

注意 MyDatabase 是抽象的。Room 自动提供一个它的实现。有关详细信息,请查看 Android架构组件- Room数据库的使用

现在我们需要一种将用户数据插入数据库的方法。为此,我们将创建一个数据访问对象(DAO: data access object)

@Dao

publicinterfaceUserDao{

@Insert(onConflict = REPLACE)

voidsave(User user);

@Query("SELECT * FROM user WHERE id = :userId")

LiveDataload(String userId);

}1234567

然后,从我们的数据库类中引用 DAO (Data Access Object)

@Database(entities = {User.class},version=1)

publicabstractclassMyDatabaseextendsRoomDatabase{

publicabstractUserDaouserDao();

}1234

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

注意:Room 根据 table 的修改来检查失效,这意味着它可能发送误报的通知。

现在我们可以修改 UserRepository 来包含 Room 数据源。

@Singleton

publicclassUserRepository{

privatefinalWebservice webservice;

privatefinalUserDao userDao;

privatefinalExecutor executor;

@Inject

publicUserRepository(Webservice webservice, UserDao userDao, Executor executor) {

this.webservice = webservice;

this.userDao = userDao;

this.executor = executor;

}

publicLiveData 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());

            }

        });

    }

}1234567891011121314151617181920212223242526272829303132333435

请注意,尽管我们改变了 来自于 UserRepository 的数据,我们并不需要改变我们 UserProfileViewModel 或 UserProfileFragment。这是抽象提供的灵活性。这对于测试来说有好处的,因为你可以在测试你的UserProfileViewModel 的时候提供一个假的 UserRepository。

现在我们的代码是完整了。如果用户以后回到相同的用户界面,他们会立即看到用户信息,因为我们持久化了。同时,如果数据过期了,我们的仓库将在后台更新数据。当然,根据您的使用情况,如果数据太旧,您可能不希望显示持久化数据。

在一些使用情况下,如 下拉刷新,UI 显示用户是否正在进行网络操作是非常重要的。将UI 操作与实际数据分开是一个很好的做法,因为它可能因各种原因而导致更新(例如,如果我们获取朋友列表,同一用户可能会再次触发 LiveData 更新)。站在UI 的角度,事实上,当有一个请求执行的时候,另一个数据点,类似于任何其他的数据 (比如 User 对象)。

这个用例有两种常见的解决方案:

更改 getUser 为返回包含网络操作状态的 LiveData 。附录中提供了一个示例实现:公开网络状态部分。

在 repository 类中提供另一个可以返回用户刷新状态的公共函数。如果只想响应显式的用户操作(如下拉刷新)来显示网络状态,则此选项更好。

单一的真相来源:

不同的 REST API 端点通常返回相同的数据。例如,如果我们的服务器端拥有另一个返回 朋友列表的端点,则同一个用户对象可能来自两个不同的API 端点,也许粒度不同。如果 UserRepository 从 Webservice请求返回原本的响应,我们的UI可能会显示不一致的数据,因为在这些请求过程中数据可能已经在服务器端发生了改变。这就是为什么在 UserRepository 实现中,Web服务回调只是将数据保存到数据库中。然后,对数据库的更改将触发回调给 活跃状态的 LiveData 对象。

在这个模型中,数据库充当了 单一的真相来源,应用程序的其他部分通过 Repository 访问它。无论你是否使用磁盘缓存,我们都建议将你的 Repository 指定为应用程序其余部分唯一的真相来源。

测试

我们已经提到分离的好处之一就是可测试性,让我们看看如何测试每个代码模块。

用户界面和交互:你唯一需要花费时间的是 Android UI Instrumentation 。测试UI 代码的最好方法是创建一个 Espresso测试。您可以创建 Fragment 并为其提供一个模拟的ViewModel。由于该 Fragment 只与 ViewModel 联系,所以伪造它足以完全测试这个UI。

ViewModel:ViewModel 可以使用 JUnit 来测试 。你只需要模拟 UserRepository 来测试它。

UserRepository:你同样也可以使用 JUnit 来测试 UserRepository。你需要模拟 Webservice 和 DAO。你可以测试它是否做出了正确的Web服务调用,并将结果保存到数据库中,如果数据已缓存且最新,则不会发出任何不必要的请求。因为 Webservice 和 UserDao 都是接口,你可以模拟它们,或者为更复杂的测试用例创建伪造的实现…

UserDao:测试 DAO 类的推荐方法是使用 instrumentation 测试。由于这些 instrumentation 测试不需要任何用户界面,他们将会运行得很快。对于每个测试,您可以创建一个处于内存中的数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。 Room 也允许指定数据库的实现,所以你可以通过提供 JUnit 来测试 SupportSQLiteOpenHelper 的实现。通常不建议使用这种方法,因为设备上运行的SQLite版本可能与主机上的SQLite版本不同。

Webservice:使测试独立于外界是很重要的,所以你的 Webservice 测试也应该避免对后端进行网络调用。有很多库可以帮助你,例如, MockWebServer 是一个强大的库,可以帮助你为测试创建一个伪造的本地服务器。

Testing Artifacts 架构组件提供了一个Maven artifact 来控制其后台线程。在android.arch.core:core-testing artifact 内部 ,有2个 JUnit 规则:InstantTaskExecutorRule:此规则可用于强制架构组件立即在调用线程上执行任何后台操作。CountingTaskExecutorRule:此规则可用于检测测试,以等待架构组件的后台操作或将其作为闲置资源连接到 Espresso。

最终的架构

下图显示了我们推荐的架构中的所有模块以及它们如何相互交互:

指导原则

编程是一个创造性的领域,开发Android应用也不例外。解决问题的方法有很多种,可以在多个Activity 或 Fragment 之间传递数据,检索远程数据并将其保存在本地以进行离线模式,也可以使用其他常见应用程序遇到的情况。

虽然以下建议不是强制性的,但是根据我们的经验,从长远来看遵循这些建议将使您的代码更加健壮,变得可测试和可维护。

你在 AndroidManifest 中定义的入口点(activities, services, broadcast receivers, 等等)不是数据的来源。相反,他们只应该协调与该入口点相关的数据子集。由于每个 app组件的 存活相当短,这取决于用户与设备的交互以及当前运行时的状况,因此你不希望这些入口点中的任何一个成为数据源。

在应用程序的各个模块之间建立明确的职责界限时要毫不留情。例如,不要将从网络中加载数据的代码分散到多个类或包中。同样,不要把不相关的职责 - 比如数据缓存和数据绑定 - 放到同一个类中。

尽可能少地暴露每个模块。不要试图创建“只有那一个”的快捷方式,从一个模块公开其内部实现细节。你可能会在短期内获得一些时间,但随着代码库的不断发展,你将会花费更多时间付出技术代价。

在定义模块之间的交互时,请考虑如何使每个模块独立地进行测试。例如,如果有一个定义良好的 API 从网络中获取数据,测试将数据保存在本地数据库中的模块会变得更容易。相反,如果将这两个模块的逻辑混合在一起,或者在整个代码库中分散网络请求代码,那么要测试就会更加困难。

你的APP的核心是让它从其他APP中脱颖而出。不要花费时间重新造轮子,或者一次又一次地写出相同的样板代码。相反,将精力集中在可以让你的应用独特的东西上,让Android 架构 和其他推荐的库处理重复的样板代码。

持久化尽可能多的相关和最新的数据,以便当设备处于离线模式时你的APP依然可用。虽然你可能喜欢恒定的高速连接,但你的用户可能并不会。

你的 repository 应该指定一个数据源作为单一的事实来源。无论你的应用程序何时需要访问这些数据,都应始终从单一的事实源头发起。有关更多信息,请查看 单一的真相来源

附录:暴露网络状态

在上面推荐的App架构部分,我们故意省略了网络错误和加载状态,以保持示例的简单。在本节中,我们演示了如何使用 Resource 类来暴露网络状态以及封装数据及其状态。

以下是一个实现的例子:

//a generic class that describes a data with a status

public class Resource {

@NonNull public final Status status;

@Nullable public final T data;

@Nullable public final String message;

private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {

this.status = status;

this.data = data;

this.message = message;

}

public static Resource success(@NonNull T data) {

returnnew Resource<>(SUCCESS, data, null);

}

public static Resource error(String msg, @Nullable T data) {

returnnew Resource<>(ERROR, data, msg);

}

public static Resource loading(@Nullable T data) {

returnnew Resource<>(LOADING, data, null);

}

}1234567891011121314151617181920212223

因为从网络加载数据,而从磁盘显示数据是一个常见的用例,我们将创建一个辅助类 NetworkBoundResource ,它可以在多个地方重复使用。以下是 NetworkBoundResource 的决策树:

它首先通过对资源的数据库进行观察。当第一次从数据库加载条目时,**NetworkBoundResource**0 会检查结果是否足够好以便被分发,或者它应该从网络中获取。请注意,这两种情况可能同时发生,因为你可能希望在从网络获取数据时显示缓存的数据。

如果网络调用成功完成,则将响应保存到数据库中并重新初始化流。如果网络请求失败,我们直接发送失败。

注意:在将新数据保存到磁盘之后,我们会重新初始化数据库中的数据流,但通常我们不需要这样做,因为数据库将会发送更改。另一方面,依靠数据库来发送更改,将产生依赖副作用,因为如果数据没有变化,数据库可以避免发送变化,那么它可能会中断。我们也不希望发送从网络返回的结果,因为这将违背单一的真相来源(也许数据库中会有触发机制可以改变保存的值)。我们也不想在没有新数据的情况下发送 SUCCESS,因为它会向客户发送错误的信息。

下面是 公开API 是为 NetworkBoundResource 的子类提供的 :

// ResultType: Type for the Resource data

// RequestType: Type for the API response

public abstract class NetworkBoundResource<ResultType, RequestType> {

    // Called to save the result of the API response into the database

    @WorkerThread

    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be

    // fetched from the network.

    @MainThread

    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database

    @NonNull @MainThread

    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.

    @NonNull @MainThread

    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components

    // like rate limiter.

    @MainThread

    protected void onFetchFailed() {

    }

    // returns a LiveData that represents the resource, implemented

    // in the base class.

    public final LiveData<Resource<ResultType>> getAsLiveData();

}123456789101112131415161718192021222324252627282930

请注意,上面的类定义了两个类型参数(ResultType, RequestType),因为从 API 返回的数据类型可能与本地使用的数据类型不匹配。

另请注意,上面的 ApiResponse 代码用于网络请求。 ApiResponse是一个简单的Retrofit2.Call类包装,将其响应转换为 LiveData。

以下是该 NetworkBoundResource 类的其余部分:

publicabstractclassNetworkBoundResource{

privatefinalMediatorLiveData> result =newMediatorLiveData<>();

@MainThread

NetworkBoundResource() {

result.setValue(Resource.loading(null));

LiveData dbSource = loadFromDb();

result.addSource(dbSource, data -> {

result.removeSource(dbSource);

if(shouldFetch(data)) {

fetchFromNetwork(dbSource);

}else{

result.addSource(dbSource,

newData -> result.setValue(Resource.success(newData)));

}

});

}

privatevoidfetchFromNetwork(finalLiveData dbSource){

LiveData> apiResponse = createCall();

// we re-attach dbSource as a new source,

        // it will dispatch its latest value quickly

        result.addSource(dbSource,

                newData -> result.setValue(Resource.loading(newData)));

        result.addSource(apiResponse, response -> {

            result.removeSource(apiResponse);

            result.removeSource(dbSource);

            //noinspection ConstantConditions

            if (response.isSuccessful()) {

                saveResultAndReInit(response);

            } else {

                onFetchFailed();

                result.addSource(dbSource,

                        newData -> result.setValue(

                                Resource.error(response.errorMessage, newData)));

            }

        });

    }

    @MainThread

    private void saveResultAndReInit(ApiResponse<RequestType> response) {

        new AsyncTask<Void, Void, Void>() {

            @Override

            protected Void doInBackground(Void... voids) {

                saveCallResult(response.body);

                return null;

            }

            @Override

            protected void onPostExecute(Void aVoid) {

                // we specially request a new live data,

                // otherwise we will get immediately last cached value,

                // which may not be updated with latest results received from network.

                result.addSource(loadFromDb(),

                        newData -> result.setValue(Resource.success(newData)));

            }

        }.execute();

    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {

        return result;

    }

}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

现在,我们可以使用 NetworkBoundResource 将在 repository 中绑定 User 实现 写入我们的磁盘和网络。

classUserRepository{

Webservice webservice;

UserDao userDao;

publicLiveData> loadUser(finalString userId) {

returnnew NetworkBoundResource() {

@Override

protectedvoid saveCallResult(@NonNullUser item) {

userDao.insert(item);

}

@Override

protectedboolean shouldFetch(@NullableUserdata) {

returnrateLimiter.canFetch(userId) && (data==null|| !isFresh(data));

}

@NonNull@Override

protectedLiveData loadFromDb() {

returnuserDao.load(userId);

}

@NonNull@Override

protectedLiveData> createCall() {

returnwebservice.getUser(userId);

}

}.getAsLiveData();

}

}

喜欢的给小编点个小关注啦,感谢!

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