【译】Google官方推出的Android架构组件系列文章(一)App架构指南

PS: 2018.06.24按照官网最新文档更新本文翻译

系列文章导航

  1. 【译】Google官方推出的Android架构组件系列文章(一)App架构指南
  2. 【译】Google官方推出的Android架构组件系列文章(二)将Architecture Components引入工程
  3. 【译】Google官方推出的Android架构组件系列文章(三)处理生命周期
  4. 【译】Google官方推出的Android架构组件系列文章(四)LiveData
  5. 【译】Google官方推出的Android架构组件系列文章(五)ViewModel
  6. 【译】Google官方推出的Android架构组件系列文章(六)Room持久化库

原文地址:https://developer.android.com/jetpack/docs/guide

这篇指南适用于熟悉构建app基础,并且想要了解构建强大的生产级应用的最佳实践和推荐架构的开发人员。

App开发人员面临的常见问题

不像传统的桌面应用,大部分情况下从一个单一的快捷启动器启动,之后作为一个单一进程运行,Android应用程序结构更复杂。一个典型的Android应用由多个应用组件构建而成,包含activityfragmentservicecontent provider以及broadcast receiver

这些应用组件大多数声明于app manifest文件中,Android操作系统通过这个文件来决定如何将你的应用程序集成到设备的整体用户体验中。如前所述,桌面应用一般是运行在一个单独的进程中,而一个编写正确的Android应用则需要更加灵活。因为,用户可以不断切换流程和任务而任意使用设备上的不同应用程序。

举个例子,思考一下当你在喜欢的社交应用中分享照片时会发生什么。该应用触发一个camera intent,Android系统根据这个Intent启动相机应用来处理这个请求。在这个时间点,用户离开了这个社交应用,但是他们的体验却是无缝的。接下来,相机应用可能会触发别的Intent,比如启动文件选择框,这可能会启动另一个应用。最后用户返回到社交应用,然后分享这张照片。在这个过程的任意时间点,用户也可能会被一个电话中断,在打完电话后才会回来分享照片。

在Android中,这种应用间跳跃行为是很常见的,因此你的应用必须能够正确得处理这些流程。牢记一点,移动设备的资源是有限的,因此在任何时候,操作系统可能需要杀掉某些应用来为新的应用腾空间。

所有这些归纳为一点:你的应用组件可以被单独、无序地启动,并且在任意时间可以被用户或者系统销毁。因为应用组件生存时间是短暂的,并且他们的生命周期(创建和销毁)不受你的控制,所以你不应该将任何应用程序数据或状态存储在应用程序组件中,并且应用组件不应该相互依赖。

常用架构准则

如果不能用应用组件来存储应用数据和状态,那么应用应该如何架构呢?

你应该聚焦的最重要的事情,是你应用中的关注点分离(separation of concerns)。一个常犯的错误是把所有代码都写在Activity或者Fragment中。任何不是用来处理UI或者操作系统交互相关的代码都不应该放到这些类中。尽可能让这些类保持简洁能够让你避免很多生命周期相关的问题。请记住,你并不拥有这些类,它们仅仅是将你的应用和操作系统黏贴在一起的合约类。任何时候,Android系统可能会根据用户交互或者其他因素(如低内存)而销毁它们。最好尽量减少对它们的依赖,从而提供一个坚实的用户体验。

第二条重要原则是,你应该根据模型来驱动UI,最好是持久化模型。有两个原因来说明持久化是理想的:如果操作系统销毁了你的应用来释放资源,你的用户将不会丢掉数据,并且当网络抖动或者没有连接时,你的应用仍然可以继续工作。模型(Model)是那些负责处理应用数据的组件。它们独立于View(视图)和应用组件,因此它们与这些组件的生命周期问题隔离。保持UI代码简单,远离应用逻辑将会更容易管理。将你的应用程序构建在那些数据管理责任定义良好的模型类之上,将会使它们可测试,并具备应用一致性。

推荐的应用架构

在这一节,我们通过一个用例来演示如何使用Architecture Components架构应用程序。

注意:没有哪一种编写app的方式能够最佳满足所有场景。话虽如此,这个推荐的架构对于大多数场景都是一个好的开端。如果你已经拥有一种好的编写Android应用的方式,你可以不需要改变。

假设我们在构建一个展示用户信息的UI。用户信息可以使用REST API从我们的私有后端获取到。

构建界面

UI包括一个fragment UserProfileFragment.java,以及相应的布局文件user_profile_layout.xml

为了驱动UI,我们的数据模型需要持有两个数据元素。

  • User ID: 用户ID。最好通过fragment参数来传递这个数据。如果系统销毁了进程,该信息会被保留,当app重启后再次可用
  • User Object:持有用户数据的一个POJO对象。

我们创建一个继承自ViewModelUserProfileViewModel类,该类将持有上面的信息。

ViewModel向具体的UI组件(比如fragmentactivity)提供数据,并且处理与数据处理业务部分的通信,例如调用别的组件加载数据或转发用户修改。ViewModel并不知道View,并且不受配置改变的影响,比如因为旋转而重新创建Activity。

现在,我们有3个文件:

  • user_profile.xml: 定义UI界面
  • UserProfileViewModel.java: 为UI提供数据的类
  • UserProfileFragment.java: UI控制器。展示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 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);
    }
}

现在,我们有了这三个代码模块,怎么将他们连接起来呢?毕竟,当ViewModeluser字段设置以后,我们需要一种方式来通知UI。所以,该LiveData类登场了。

LiveData是一个可观察的数据持有者(data holder)。它允许应用组件观察LiveData对象的改变,而不需要在它们之间创建显式和刚性的依赖路径。LiveData还尊重应用程序组件(ActivityFragmentService)的生命周期状态,并且做正确的事情以防止对象泄漏, 从而使你的应用程序不消耗更多的内存。

注意:如果你已经在使用像RxJavaAgrea这样的库,你可以继续使用而不必替换为LiveData。但是当你在使用这些库或其他方法的时候,请确保能正确处理生命周期,比如说当相关的LifecycleOwner停止时候你的数据流应该暂停,而LifecycleOwner销毁的时候,你的数据流也应该被销毁。你也可以添加android.arch.lifecycle:reactivestreams库,将LiveData和另一个响应式流库配合使用(比如RxJava2)。

现在我们将UserProfileViewModel中的User域替换成LiveData<User>,这样当数据更新的时候,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将会刷新。

如果你熟悉别的使用可观察回调的库,则可能已经意识到我们不必重写FragmentonStop()方法来停止观察数据。使用LiveData是不必这么做的,因为它是生命周期感知的,也就是说只有Fragment在激活状态(收到onStart(),但是没有收到onStop())的时候,LiveData才会调用回调。当Fragment收到onDestroy()时,LiveData会自动移除观察者。

我们也不需要做任何特殊的事情来处理配置改变(比如,用户旋转屏幕)。当配置改变时,一旦新的Fragment创建,ViewModel会自动还原,它将收到同一个ViewModel实例,并且将立即使用当前的数据调用回调。这也是为啥 ViewModels不应该直接引用Views。它们可以超越View的生命周期。参见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对象。尽管这个可用,但随着你应用的迭代后续将很难维护。它赋予了ViewModel类太多责任,这违反了我们之前提到的关注点分离原则。此外,ViewModel的范围被绑到了ActivityFragment的生命周期上,因此在生命周期结束时将会丢失所有的数据,这是一个很糟糕的用户体验。相反,我们的ViewModel将把这项工作委托给一个新的Repository模块。

Repository模块负责处理数据操作。他们向app的其他部分提供一个干净整洁的API。他们知道从哪里去获取数据,知道当数据更新时候需要调用哪些API。你可以把他们看作不同数据源(持久化数据,Web服务,缓存等)之间的中间人。

下面的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拉取的,这也就意味着如果有需要的话我们可以将它替换成其他的实现。

注意:为了简单起见,我们去掉了网络异常情况。有关暴露错误和加载状态的替代实现版本,请参阅附录:暴露网络状态

管理组件间依赖关系

上面的UserRepository类需要一个Webservice实例来完成它的工作。它可以简单创建一个实例,但是这样做它需要知道构建Webservice的依赖关系。这将会使代码复杂化并且重复(比如,每个需要一个Webservice实例的类都需要知道如何使用它的依赖来构造它)。此外,UserRepository可能不是唯一一个需要Webservice的类。如果每个类都创建一个新的WebService,这将会是资源浪费。

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

  • 依赖注入:依赖注入允许类定义他们的依赖关系而不创建他们。在运行时,另外的类负责提供这些依赖。我们推荐Google的Dagger2库来实现Android应用的依赖注入。
  • 服务定位器(Service Locator):服务定位器提供了一个注册表,其中类可以获取它们的依赖关系,而不是构造它们。与依赖注入相比,实施起来相对容易,因此如果你不熟悉依赖注入,请改用Service Locator

这些模式允许你扩展代码,因为它们为管理依赖关系提供了明确的模式,而会重复代码或增加复杂性。这两种方式都允许交换实现方式进行测试,这也是采用它们的主要好处之一。

在这个例子里,我们将使用Dagger2来管理依赖。

连接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 实现对于抽象Web服务的调用是很好的,但是由于它仅仅依赖于一个数据源,它并不是非常有用。

上面的UserRepository实现的问题是,在拉取数据后,它并没有保存在任何地方。如果用户离开了UserProfileFragment 并返回,app将重新拉取数据。这个很糟糕,因为两个原因:它浪费了宝贵的网络带宽,并迫使用户等待新的查询完成。为了解决这个问题,我们将向我们的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 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可以从内存缓存中拉取数据。但是如果用户离开应用并在Android系统杀掉进程几个小时候后 重新启动,会发生什么?

按现在的实现,我们需要从网络中再次拉取数据。这不仅是一个糟糕的用户体验,也是非常浪费的,因为它将使用移动数据来重新拉取相同的数据。你可以通过缓存Web请求来简单解决这个问题,但它将会产生新的问题。如果相同的用户数据从另一种类型的请求(例如,获取一个朋友列表)出现,会发生什么情况?那么,你的应用程序可能会显示不一致数据,这是最令人困惑的用户体验。举个栗子,相同的用户数据可能展现不同因为朋友列表请求和用户请求可以在不同的时间执行。你的应用程序需要合并他们来避免展示不一致的数据。

处理这种问题的正确办法是使用一个持久化模型。这就是Room持久化库来拯救的地方!

Room是一个对象映射库,可以使用最少的样板代码提供本地数据持久性。在编译时,它根据模式验证每个查询,这样损坏的SQL查询导致编译时错误而不是运行时故障。Room抽象了使用原始SQL表和查询的基本实现细节。它还允许观察对数据库数据(包括集合和连接查询)的改变,通过LiveData对象暴露这些更改。另外,它明确定义了解决常见问题的线程约束,例如在主线程上访问存储。

注意:如果你熟悉其他持久化方案像SQLite ORM或其他的数据库比如Realm,则无需将其替换为Room,除非Room的功能集与你的用例更相关。如果你在编写新的app或者重构老的app,我们建议使用Room来做数据持久化。

要使用Room,我们需要定义我们的本地协议(schema)。首先,在User类上增加@Entity注解,将其标识为数据库的一个表。

@Entity
class 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文档。

现在,我们需要一种方式把user数据插入到数据库中。为了满足这个,我们创建一个DAO类。

@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<User>Room知道数据库什么时候被修改,并且当数据变化的时候,它会自动通知所有处于激活状态的观察者。因为它使用的是LiveData,这将是很高效的,因为它只会在至少有一个激活观察者时更新数据。

注意:Room基于数据库表修改来检查无效修改,这意味着它可能会发送假的正面通知。( 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的数据来源,我们也不需要改变我们的 UserProfileViewModel或者UserProfileFragment。这就是抽象提供的灵活性。这对于测试来说也是极好的,因为当测试你的UserProfileViewModel的时候,你可以提供一个假的UserRepository

现在我们的代码是完整的。如果用户几天后返回到同一个页面,他们可以立即看到用户信息因为我们将这部分数据持久化保存了。同时,如果数据过时,我们的reposity将在后台更新数据。当然,这取决于你的使用场景,如果持久化数据太旧,你可能不希望对其展示。

在某些使用场景,比如下拉刷新,当进行网络操作时候,对于UI来说向用户展示操作进度是非常重要的。把UI操作与实际数据分离是很好的做法,因为数据可能可能由于各种原因被更新(例如,如果我们获取一个好友列表,则可能再次获取相同的用户,触发LiveData<User>更新)。从UI的角度来说,一个正在进行中的请求只是一个数据点这样一个事实,和别的数据片(比如User对象)没啥区别。

对于这种场景,有两种常见的解决办法:

  • 更改getUser返回一个包含网络操作异常的LiveData。附录:暴露网络状态一节提供了一个实现例子。
  • reposity类中提供一个可以返回User刷新状态的接口。如果要仅在UI中显示网络状态(为了响应用户操作,比如下拉刷新),这个选项会更好。

真相的唯一来源

不同的REST API端返回相同的数据,这种情况是很常见的。举个栗子,如果我们的后台有一个端点返回一个朋友列表,那么同一个user对象可能来自不同的API端点,也可能是不同的粒度。如果UserRepository按原样从WebService请求然后返回响应,那么我们的UI可能会显示不一致的数据,因为数据可能会在这些请求之间的服务端发生更改。这也是为什么在UserRepository的实现里,Web服务回调仅仅将数据保存到数据库中。然后,对于数据库的改变会触发激活的LiveData的回调。

在这个模型里面,数据库扮演着真相的唯一来源角色,app的其他部分通过reposity来访问它。不管你是否使用磁盘缓存,我们推荐你的reposity指定某个数据源作为应用程序的其余部分的唯一真实来源。

测试

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

  • UI和交互:这将是你唯一需要Android UI Instrumentation 测试的时间。测试UI代码的最佳途径是创建一个Espresso测试。你可以创建一个fragment,然后给它提供一个Mock的ViewModel。因为这个fragment仅仅与ViewModel通信,对其进行Mock将足以完全测试这个UI。

  • ViewModel:ViewModel可以通过JUnit来测试。你只需要Mock UserRepository就可以测试它。

  • UserRepository: 你也可以用JUnit来测试UserRepository。你需要mock Webservice 和DAO。你可以测试它是否执行了正确的Web服务调用,将结果保存到数据库中,如果数据已经被缓存和更新,则不会发生任何不必要的请求。

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

    Room还允许指定数据库实现,因此你可以通过提供SupportSQLiteOpenHelper的JUnit实现来测试它。通常不推荐使用此方法,因为运行在设备上的SQLite版本可能与你主机上的SQLite版本不同。

  • WebService:使测试独立于外部世界是很重要的,甚至你的WebService测试应该避免执行对后台的网络请求。有很多库可以解决这个问题。例如,MockWebServer是一个很好的库,它可以帮助你创建一个假的本地服务器用于测试。

  • 测试ArtifactArchitecture Components提供一个maven artifact 来控制它的后台线程。在android.arch.core:core-testing artifact里面,有两个JUnit规则:

    • InstantTaskExecutorRule:该规则可用于强制Architecture Components在调用线程上立即执行任何后台操作。
    • CountingTaskExecutorRule:该规则可用于instrumentation 测试,以等待Architecture Components的后台操作或连接到Espresso作为闲置资源。

最终架构

下面的图显示了我们推荐架构的所有模块,以及它们之间如何交互。

final-architecture.png

指导原则

编程是一个创意领域,构建Android应用程序也不例外。有很多办法去解决一个问题,无论是在多个Activity或Fragment之间传递数据,检索远程数据并将其保存到本地用于离线模式,还是任何其他常用应用遇到的常见场景。

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

  • 你定义在manifest文件中的入口点——activity,service,broadcast recevier等等,不是数据源。相反,它们应该只是与该入口点相关的数据自己的协调者。因为每个应用组件的寿命相当短,取决于用户与设备的交互以及运行时的整体状况,你不希望任何的这些入口点变成数据源。
  • 坚决在你的应用程序各个模块之间创建明确定义的责任边界。比如说,不要将从网络中加载数据的代码散布到各个类或者包中。同样,不要将无关责任比如数据缓存和数据绑定杂糅到同一个类中。
  • 每个模块尽可能少的向外暴露。不要试图创建一个从一个模块暴露内部实现细节的万能快捷方式。你可能会在短期内节约一点时间,但随着代码库的发展,你将多付出很多技术债务。
  • 当你定义模块之间的交互时,请考虑如何让每个模块分离成可测试的。例如,拥有一个定义良好的从网络获取数据的API将使得更容易测试在本地数据库中持久化该数据的模块。相反,如果将这两个模块的逻辑杂糅到一起,或者将网络代码散布在你整个代码库中,将会非常难以测试。
  • 你的应用程序的核心是能够让它脱颖而出的那部分东西。不要花时间重复造轮子,或者一遍遍写模板代码。相反,你应该将精力集中到让你的应用独一无二,处理重复模板代码的事情就交给Android Architecture Component和其他推荐的库吧。
  • 相对多一点持久化数据,尽可能更新数据,以便当设备处于离线状态时,你的应用仍然可用。虽然你可能享受稳定和高速的连接,但是你的用户可能不会。
  • 你的repository 应该指定一个数据源作为唯一真相数据源。每当你的应用程序需要访问数据片时,它应该始终源自这个唯一的真相数据源。有关更多信息,请参考唯一真相源。

附录:暴露网络状态

在上面推荐的应用架构这一节,我们有意省略了网络错误和加载状态来让样例代码简单。在这一节,我们演示一种使用Resource类封装数据和状态的暴露网络状态的方法。

下面是样例实现:

//a generic class that describes a data with a status
public class Resource<T> {
    @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 <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

因为无论是从磁盘还是网络加载数据都是一个常见的使用场景,我们将创建一个可以在多个地方重复使用的帮助类NetworkBoundResource。下面是NetworkBoundResource的决策树:

network-bound-resource.png

它从观察资源的数据库开始。当数据条目第一次从数据库加载的时候,NetworkBoundResource检查结果足够好以便被分派,或者应该从网络中获取。请注意,这两个可能同时发生,因为你可能希望在向网络拉取数据的同时展示缓存数据。

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

注意:在把新数据保存到磁盘后,我们从数据库重新初始化流,尽管通常我们不需要那么做,因为数据库会分派变化。另一方面,依赖数据库分派变化将依赖于不利的副作用,因为如果数据没有变化,数据库可以避免分发变化。我们也不想分派从网络达到的结果,因为这将违反唯一真相来源(也许在数据库中有触发器会改变打算保存的值)。我们也不想在没有新数据的情况下发送success,因为它可能会向客户端发送错误的信息。

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

// 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
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

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

也请注意,上面的代码使用ApiResponse做网络请求。ApiResponseRetrofit2.Call类的的简单包装,用于将其响应转换为LiveData

下面是NetworkBoundResource类的剩余实现:

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> 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();
    }
}

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

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

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

推荐阅读更多精彩内容