【翻译】App Architecture (Android架构组件) 指南

【翻译】App Architecture (Android架构组件) 指南

译者:Android的新出架构系列指南还是很有意义的,在API层为MVVM架构提供了支持。也为追求更清晰的项目架构提供了更低门槛的指导。正好国庆无事可做,因为特别喜欢这几篇指南,所以抽几天时间翻译一下,英文水平不是很好,各位就将就着看,欢迎指正。以下是正文。

本系列其他翻译

  1. 为你的项目引入组件

==正文==

本篇指南适用于有过一些开发基础,但现在想了解用更好架构来构建健壮的,高质量的app的人。

​ 注:本篇指南假设读者已经熟悉Android Framework. 如果你是Android新手,那你应该先 Getting Started 系列文章,那里有本篇指南的预备知识。

app开发者常面临的问题

在大部分情况下,传统桌面应用只一个入口(在Launcher中的快捷方式中),并且只运行在一个进程中。但Android app 不一样,它有着更复杂的架构。一个典型的Android app 是由多个app components 组成,包括多个activity,fragment,service,content provider 以及 broadcast receiver.

这些app组件大部分都在 manifest 中声明,Android 系统 通过 manifest 中的信息来决定如何将app整合到设备中,以便统一用户体验。然而正如上文提到的,传统桌面应用只在单个进程中运行,但一个优秀规范的Android App需要更加的灵活,因为用户经常在各个app之间跳转、切换工作流和任务。

举个例子,你想在最喜欢的社交app上分享一张照片,这个app 发起一个打开相机的 Intent,Android 系统接收并处理 这个Intent请求,然后打开相机应用。 在这时,虽然用户离开了该社交app,但是仍然保证了无缝衔接的用户体验。反过来,相机也可能触发其他Intent,比如打开图片选择器,这又可能打开其他app。 最后的最后,用户又可以回到社交app并分享照片。这个过程中,如果来了一个电话,用户可能被迫中断去接电话,接完电话,然后再返回app分享图片。

在Android 中,这种App之间跳转的行为很常见,所以你的app要能正确地处理这种流程。而且我们要记住,手机app的资源是有限的,所以系统随时都可能杀掉一些app,为打开新的app提供空间。

说到底就是,你的app组件能够单独被启动,不应有顺序限制,并且能够随时被系统或用户安全地销毁。因为app组件生命是短暂的,它们的生命周期(何时创建、何时销毁)并不受你的控制,所以不应该在app组件中存储任何的数据和状态 ,且你的app组件不应该彼此依赖。

通用架构原则

如果不能在 app components 中保存数据和状态,那应该怎样构建呢?

注意,最重要的原则是分离关注点, 举一个常见的反例,把所有的代码都写在Activity和Fragment中。其实,所有非UI操作的代码、系统交互的代码都不应该写在这些类中。让Activity、Fragment尽可能地保持简洁,这样能避免很多生命周期相关的问题。因为你没有真正“拥有”这些类(因为Activity 和Fragment 属于Android系统的),它们只是衔接类,用来连接OS和你App代码,Android OS 可能基于用户的操作和其他因素(比如内存情况)来销毁它们。所以最好减少对这些类的依赖,以便为用户提供稳定的用户体验。

第二个重要原则是用Model 驱动UI,最好是可持久化的model。为什么要持久化呢?有两个原因: 1. 如果系统杀死了你的app,用户不会丢失数据;2. 就算网络环境很差,你的app也可以正常运行。 Model负责处理App的数据,它应独立于app 组件的生命周期,这样才能避免生命周期导致的数据问题。而且,让UI相关代码保持简洁且与app的逻辑分离,这样将更容易管理。如果App 的基础Models 有清晰的数据管理职责,那你的app也会易于测试,并且更加稳健。

推荐app架构

在本节,我们会通过一个用例来展示如何使用 Architecture Components 来构建app。

注:世界上没有什么架构是适用于任何场景的。也就是说,这个推荐的架构应该只是一个开始,它适用于常见的应用场景。如果你已经有一个好的架构了,那就无须再改了。

想象一下,我们将要构建UI来展示一个用户的基本信息。并通过REST接口从我们私有的后台获取用户基本信息。

构建用户界面

UI将会由一个fragment(UserProfileFragment.java) 和 它相关的layout文件(user_profile_layout.xml)组成。

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

  • The User ID: 用户的标识。 通过fragment arguments 将一个用户传入fragment,用ID是最好的方式。
  • The User Object: 一个保存用户数据的POJO

我们将会创建基于ViewModelUserProfileViewModel 类来保持这些信息。

ViewModel为指定的UI组件提供数据(比如一个fragment或者一个activity),并且负责和业务数据处理的交互,比如调用其他组件加载数据或者传递用户数据的修改。ViewModel不与View接触,也不受configuration 变化的影响,比如旋转屏幕导致的Activity重新创建。

现在我们有3个文件:

  • user_profile.xml :屏幕的UI定义
  • UserProfileViewModel.java: 这个类为UI准备数据
  • UserProfileFragment.java :UI控制器,用于展示ViewModel中的数据并且响应用户交互。

下面是我们的实现(为了简化,layout 文件就不写在下面了)

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

现在,我们有这三个代码模块,我们应该怎么样关联它们呢?毕竟,当ViewModel的数据发生改变,我们需要用某种方式通知UI。 这就是LiveData 类发挥作用的时候啦。

LiveData 就是一个被观察的数据Holder。app 组件可以观察LiveData中的数据变化。而且不需要为它们建立显式的、死板的依赖关系。LiveData 自动适配app组件的生命周期(activities,fragments,services),并且会做一些操作来防止对象泄漏,这样你的app就不会消耗更多的内存。

注: 如果你已经用了RxJava 或者Agera这样的库,你可以继续使用他们而不是LiveData。但是,当你使用它们或者其他类似的方法时,请保证你合适地处理生命周期,这样在相关的LifecycleOwner 停止(stop)时,数据流也会暂停。当它销毁(destroyed)时,数据流也应该销毁。你可以添加android.arch.lifecycle:reactivestreams工具结合这些响应库使用(比如RxJava2)。

现在我们将UserProfileViewModel中User成员替换为LiveData<User> 这样当数据更新时,就能通知Fragment显示。LiveData还有个优势就是生命周期敏感,在数据无用时会自动清除引用。

public class UserProfileViewModel extends ViewModel {
    ...
    //private User user;
    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 -> {
      // 更新 UI
    });
}

每次user 更新,onChange方法都会调用,然后UI会刷新

如果熟悉其他的有使用观察回调的库,你可能已经注意到,我们并没有重写fragment的 onStop()方法来终止对数据的观察。因为使用LiveData不需要这么做,因为它本身就是生命周期相关的,也就是说它只会在Fragment处于active state(即在onStartonStop之间的状态)时才会回调onChange方法。当Fragment调用onDestroy后,LiveData也会自动移除外部的观察者。

不仅如此,我们也不需要处理configuration changes(比如屏幕旋转)的情况,因为ViewMode会自动重置数据。只要新的Fragment重新初始化,它会接收一个相同的ViewModel实例,并且立即回调方法,将当前的数据传入。这也是ViewModel不应该直接引用View 的原因。因为ViewModel可以在View的生命周期之外存活。详细请看The lifecycle of a ViewModel

获取数据

现在,我们已经将Fragment和ViewModel联系起来了,但是ViewModel该如何获取user 数据呢?在这个例子中,假设后台提供一个REST 接口,我们使用Retrofit库来获取后台数据(你也可以使用其他库,只要行得通就可以)。

这是和我们后台交互的retrofit Webservice

public interface Webservice {
    /**
     * @GET 声明此Http请求为Get请求
     * @Path("user") 注解: userId参数上的标的注解表明,userId将替换 @GET 路径中的{user}
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

不成熟的ViewModel实现可能会直接调用WebService来获取数据,然后通过回调给user 对象赋值。尽管这样能实现功能,但是随着app业务的成长,这样会很难维护。因为ViewModel承担了太多的职责,而这违反了之前提到的“分离关注点”原则。而且,ViewModel的作用域与ActivityFragment生命周期关联,这样一旦生命周期结束数据就会丢失,用户体验不好。 我们的ViewModel应当将获取数据的逻辑代理给一个新的Repository module.

Repository modules负责处理数据操作。它们为app其他部分提供纯粹的API。它们知道从哪儿获取数据,当数据更新时应该调用什么API。你可以将它们视为不同数据源(持久化模块,web service,缓存等)之间的调解器。

下面的UserRepository 类使用WebService来获取user data item.

java
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// 这不是最优的实现,我们下面会修复它
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// 为了简洁,这里忽略了错误情况
data.setValue(response.body());
}
});
return data;
}
}

尽管repository module看起来多余,但是它存在有着重要目的。它将app的数据源抽象处理,独立于其他模块。现在我们ViewModel不依赖于WebService,这样的好处就是,必要时可以替换成另外一个实现。

注:为了简化例子,我们忽略了网络错误的情况。提供一个暴露网络错误和加载状态的实现,see Addendum: exposing network status.

管理组件之间的依赖

上面的UserRepository 类需要一个Webservice实例,如果简单地创建它,那也需要知道Webservice的依赖。这会显著地让代码变复杂且重复(e.g. 每个需要Webservice的类需要知道如何创建它,以及它的依赖),并且,可能除了UserRepository之外,还有其他类需要Webservice。如果每个类都创建一个新的WebService那会很耗资源。

解决这个问题,你可以使用以下两种模式:

  • Dependency Injection 依赖注入,它可以定义一个类的依赖,却无需创建它们。在运行时,另一个类来负责提供这些依赖。我们推荐谷歌的 Dagger 2 库来实现Android app 的依赖注入。 Dagger 2 通过遍历依赖树自动构建对象,并且在编译时就能保证依赖。
  • Service Locator Service Locator 提供了一个注册关系,其他类可以从这个注册关系中获取他们的依赖,而不是通过构造方法创建它们。相比Dependency Injection (DI) 来说,Service Locator更容易实现。所以,如果你不熟悉Dependency Injection (DI),那就使用Service Locator。

这些模式让你的代码更容易扩展,因为它们提供一种清晰的依赖管理模式,这种模式避免了重复代码和 增加复杂性。使用这两种模式,就可以为了测试更改实现,这是使用它们的主要的好处。

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

连接 ViewModel 和 repository

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

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository 参数由 Dagger 2 提供
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // 每个Fragment都会创建一个ViewModel
            // 所以我们知道userId不会改变
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

缓存数据

上面的repository 实现利于将web 服务的调用抽象出来,但是由于它只依赖一个数据源,所以看起来并不是特别有作用。

上面UserRepository 的实现的问题在于在获取数据后,它没有保存数据。 如果用户离开UserProfileFragment 然后再返回,那app会重新获取数据。这有两个坏处:1.浪费了宝贵的网络带宽。 2.用户被强制等待新的网络查询完成。为了解决这个,我们会加一个新的数据源到UserRepository中 ,这个数据源会在内存中缓存User对象 。

@Singleton  // 告诉 Dagger 这个类应该值被创建一次
public class UserRepository {
    private Webservice webservice;
    // 简单的内存缓存实现,细节就略过了
    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;
    }
}

数据持久化

在就当前实现而言,如果用户旋转屏幕或者离开再返回app,现有的UI将会立即展示数据,因为repository 是从内存cache中获取数据。但是如果用户离开app数小时,然后Android OS杀死进程,这时用户才返回app,那会发生什么呢?

在当前的实现中,我们需要重新联网获取数据。这样不仅用户体验不好,而且还浪费用户手机流量。虽然简单地将Web请求缓存起来也可以解决这个问题,但是它会引入新的问题。你想,如果同一个user数据从另一个接口(eg 请求好友列表)请求下来呢?那可能会让用户感到迷惑,甚至更严重的问题。比方说:我这会儿请求好友列表,过会儿又请求user的数据,这样由于请求时间的不同,同一个user的数据可能有差异(比如被修改)。

针对上面的问题,有一个比较解决方案:就是使用一个持久化模块。这就轮到Room持久化库的show time了。

Room是一个对象映射库,只需要编写很少的模板代码,就能完成据持久化功能。早在编译期间,它就会将每一个query 和 schema做检查,所以错误的SQL查询在编译时就会不通过,而不是到运行时才发现问题。Room将SQL表操作和查询底层的实现细节抽象出来,同时,它也允许外部观察数据库中的变化(包括集合和联表查询),并通过LiveData对象暴露这些变化。而且,它显式的约束了执行线程,这样就解决了一些常见问题。比如在主线程获取内部存储数据。

注: 如果你熟悉其他的持久化解决方案(比如SQLite ORM) 或者其他不同的数据库(比如 Realm),也没必要用Room替换它们,除非Room的特性更适合你的情况。

为了使用Room,我们需要定义本地的数据模板。首先,给User类添加 @Entity 注解,代表着这个类映射到一个数据库表

@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对象到数据库的方式,下面我们创建一个 data access object (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 方法返回一个LiaveData<User>。Room知道数据库何时被修改,并且会通知所有的观察者。因为使用LiveData,所以效率比较高,因为至少要有一个观察者处于active状态,才会更新数据。

注:Room根据表操作来检查数据是否过期,这意味它可能发出误报修改通知(即明明没有修改数据,但是却触发了数据更新的通知)。

现在,我们修改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(() -> {
            // 在工作线程中执行
            // 检查user是否最近被获取过
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // 刷新数据
                Response response = webservice.getUser(userId).execute();
                // TODO 检查错误等.
                // 更新数据库. LiveData会自动刷新,只需要更新数据库就行了,其他什么都不用做
                userDao.save(response.body());
            }
        });
    }
}

注意,尽管我们改变了UserRepository中数据的来源,但是我们不需要修改UserProfileViewModelUserProfileFragment这两个类。这就体现了抽象化的灵活之处。这样也有利于测试,比如你在测试UserProfileViewModel时,你可以写一个假的UserRepository

现在代码已经完成了。如果用户几天后重返相同的界面上,用户信息会立即显示出来,因为我们做了持久化存储。同时,如果数据变化,repository会在后台更新数据。当然这都取决于你的业务场景,也有可能持久化保存的数据太旧了,你不想要显示它们。

在一些场景中,比如下拉刷新,如果有网络操作在进行,需要通过UI告诉用户。这也是一个分离UI行为与实际数据的实践场景,因为数据可能因为多种原因被更新(比如,下拉刷新好友列表,user信息可能会重新获取从而触发LiveData\更新)。从UI的角度,一个正在进行的请求只是一个数据点,和其他的数据点相似(比如 user 对象)

针对这种情况,有2个常见的解决方案:

  • 修改getUser方法,让它返回一个包含网络操作状态的LiveData。 在 Addendum: exposing network status这一节中提供了一个实现示例。
  • 提供其他能返回User刷新状态的public方法。如果只为了响应用户的行为(比如下拉刷新)而显示网络状态,那这种方案会更好。

单一数据源(source of truth)

不同的REST API端点返回相同的数据,这种现象很常见。以此举例,如果我们的后台有另一个端点(endPoint)也返回好友列表,那同一个用户数据就可能来自这两个不同的API端点,可能只会有细微的差别。如果UserRepository 只是原样返回Webservice请求的响应,我们不同UI界面就可能显示不一致的数据,因为这两次请求有时间差,服务器的数据可能已经改变。这就是为什么在UserRepository的实现中,web service的回调只将数据保存至数据库,然后,数据库一旦被修改,就会触发LiveData的回调 。

在这个模型中,数据库作为单一数据源,并且app的其他部分通过repository获取数据。不管是否用到磁盘缓存,我们建议你的repository指定一个数据源作为单一数据源。

最终架构

下面的示例图展示了我们推荐的架构,包括所有的Modules以及彼此交互。

img

指导原则

编程是一个创造性的领域,编写Android app 也不例外。解决同一个问题有许多方式,不管是多个activities 或者 fragments中间的数据交互,还是远程获取数据并存入本地,或者稍微重量级的app会遇到任何常见的场景。

当下面的建议并不是强制性的,它是我们经验的结晶,从长远看来,按照这些建议编程会让你的代码更加稳健,方便测试和维护。

  • manifest 中定义的入口-activities, fragments, services, broadcast-receiver等- 都不是数据源。相反,它们应该只协调和这个入口相关的数据子集。因为每个app组件的存活都是相当短暂的,取决于用户与设备的交互和整体的运行状况,你肯定不会想任何一个入口变成数据源的。
  • 严格定义各个Module的职责范围。例如,不要将请求网络数据的代码散落在多个类和包中。同样,不要把不相干的功能都塞入同一个类中(比如数据缓存和数据绑定)。
  • 每个module都应该尽可能向外少暴露类和接口。不要被所谓的”就这一次” 的捷径所诱惑, 从而暴露一个module的内部实现。 你可能在短期节省了时间,但是在长期的代码演变过程中,你会为此付出多次代价。
  • 正如定义你Modules之间的交互一样,想想,怎么样才能让每一个Module易于单独测试。举个栗子,如果有一个定义得很好的获取网络数据的API,那将数据保存到数据库的持久化Module会更加容易测试。相反,如果你将这两个module的逻辑混在一起,或者将你的网络请求代码散落在你整个代码基础上,那将会很难测试,甚至不可测试。
  • app的核心应该是那些让它出类拔萃的东西。不要浪费时间重新造轮子或者一遍又一遍地写模板代码。相反,集中经历在那些让你app独一无二的地方,让Android 架构组件 和 其他推荐的库来做重复的模块代码。
  • 尽可能地将最新的,有意义的数据持久化保存,这样你的app在离线模式也能用。你可能享受着稳定快速的网络连接,但是你的用户可能并没有。
  • 你的repository应该指定一个数据源作为单一数据源(source of truth)。不论什么时候你的app需要获取这些数据,这些数据都应该来源于这个单一数据源。更多信息请看 Single source of truth

Addendum: 暴露网络状态

在上面推荐app架构一节,为了让示例简洁,我们故意省略网络错误和加载状态的处理。在这一节中,我们展示一种暴露网络数据的方法:用Resource 来将数据和网络状态封装起来。

下面是一个示例:

//一个描述数据和状态的泛型类
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的决策树

img

它从观察一个作为源数据库开始。当实体第一次从数据库加载出来,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, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

注意上面这个类定义了两个泛型参数(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();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

现在,我们可以使用NetworkBoundResource 来实现我们的磁盘和网络绑定user 了。

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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值