【翻译】Architecture Components - Android App应用架构组件指南

应用架构组件指南

原文请看:https://developer.android.com/topic/libraries/architecture/guide.html

本指南适用于具有构建安卓APP基础知识的开发人员,现在想知道最佳实践和建议的架构,以构建强大的生产级安卓APP。

注意:这篇文章假设你已经熟悉安卓框架,如果你是一个开发新手,请先阅读安卓开发入门指南

安卓开发者面临的常见问题


与传统的桌面应用程序不同,在大多数情况下,启动快捷方式都有一个入口点,并作为一个单独的进程运行,安卓APP的应用程序架构更为复杂,一个典型的安卓APP由多个APP组件构成,包含activities、fragments、services、content providers and broadcast receivers。

所有的这些APP组件都定义在AndroidMenifest.xml文件中,被整合到安卓操作系统作为设备整体的用户体验。然而像之前所说的,传统的桌面应用是作为一个整理的单独的进程运行,但是正确编写的安卓APP需要更灵活,因为用户通过设备上的不同APP不断切换流程和任务。

例如:思考下在你喜欢的社交网络APP中分享照片时会发生什么。APP会希望使用照相机,安卓操作系统将会启动拍照APP来完成这个请求。在这点上,用户离开了社交网络APP,但是这个体验是无缝的。拍照APP可能也会有其他的请求,比如启动文件选择器APP,最终用户会回到社交网络APP中分享照片。此外,在用户操作的任何时候都可能被电话打断,而且可以打完电话后继续分享照片。

在安卓中,这种应用程序之间相互切换的行为很常见,因此你必须正确的处理这些流程。请记住,移动设备资源有限,所以在任何时候,操作系统都有可能会杀死一些APP,为新的APP腾出资源。

所有的应用程序组件都可以单独或者无序启动,并且可在任意时候被用户或者系统销毁。因为应用程序组件是短暂的,并且它们的生命周期不受你的控制,所以不应该将任何应用程序数据和状态存存储在应用程序组件中,并且应用程序组件不应该相互依赖。

常见的架构原理


如果你不能在应用程序组件中存储数据和状态,那么该如何做呢?

你应该关注的最重要的问题是在你的APP中如何做好隔离,将所有代码都写在Activity或者Fragment是一个常见的问题。任何不处理UI或者与操作系统交互的代码都不应该写在这些类中,让他们保持最可能的干净可以避免许多与生命周期相关的问题。不要忘记你不能完全控制他们,他们只是一些操作系统和你APP的约定的合约类。在任何时候安卓操作系统可能会根据用户的交互或者其他类似低内存等因素来销毁他们。最好尽量减少对他们的依赖以提供统计坚固的体验。

第二个重要的原则是你应该通过model来驱动UI,最好是持久化的model。为什么要是持久化model有两个原因:如果操作系统因为低内存销毁你的APP来释放资源,你的用户将不会丢失数据,即使是网络也连接不上你的APP也还能继续运行。model是负责处理应用程序组件数据的组件,他们独立于APP的视图(Views)和应用程序组件,所以他们能够避免有生命周期的问题,保持UI代码尽量简单,使得代码更容易管理维护。将数据放在model类中,并明确定义好其管理数据的责任,这样可以使得APP更容易测试,并保持一致性。

推荐的APP架构


在本节中,我们将用一个例子来演示如何使用Architecture Components来构建一个APP。

注意:最佳的方案不可能适用所有场景,话虽如此,本章节推荐的架构对于大多数案例应该是个不错的选择,如果你已经有了自己一套方法,并且非常适用,你可以不用更改,继续使用你的方法。

假设我们正常制作一个显示用户资料的UI。用户资料会从一个REST API来获取。

创建UI界面

UI界面包含一个fragment UserProfileFragment.java和它对应的layout文件user_profile_layout.xml。

为了构造这个UI,我们的数据model需要包含两个元素。

  • 用户ID,用户ID标示,使用fragment参数的方式传递到fragment中,如果安卓操作系统销毁了当前进程,下次APP启动的时候,这个用户ID会被保留下来。

  • User对象,包含用户资料信息的POJO对象。

我们会创建一个基于ViewModel类型的UserProfileViewModel对象来保存上面的信息。

ViewModel为特定的UI组件提供数据,比如:Activity、Fragment等,并处理与业务部门的通讯,比如调用其他组件加载数据。ViewModel与View之间相互隔离,并且不受activity屏幕旋转导致activity重新创建的影响。

现在我们有三个文件了。

  • user_profile.xml: layout文件。
  • UserProfileViewModel.java: 为UI提供数据的viewmodel对象。
  • UserProfileFragment.java: UI控制器,用来显示ViewModel中的数据,还有处理用户和UI的交互。

以下是我们的初始实现(简单起见,我们先忽略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 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。在Architecture Components 的lifecycles API变稳定后,Android Support Library的Fragment class会实现LifecycleOwner接口。

现在我们有三个代码模块,我们怎么把他们关联起来?毕竟,当ViewModel的用户字段被设置时,我们需要通知到UI,所以我们现在需要一个新的对象LiveData。

LiveData是一个可观察的数据持有者,他可以让APP中的组件观察LiveData是否发生改变,而且不需要他们之前有严格的相互依赖关系。LiveData还会遵循应用程序组件(Activity,Fragment, Service)的生命周期状态来避免内存泄露,从而是你的APP不会消费太多的内存。

注意:如果你已经在使用像RxJava或者Agera这类的类库,你可以继续使用他们。但是请确认自己需要正确的处理生命周期。你还可以添加android.arch.lifecycle:reactivestreams artifact,将LiveData与RxJava结合起来使用。

现在我们把UserProfileViewModel的User字段替换成LiveData<User>,这样的话,当user被更新时,fragment可以被通知到。LiveData能正确的处理生命周期,所以它会自动清理references,当它已经不需要时。

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

现在我们修改UserProfileFragment来观察user发生变化时更新UI。

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

每当user字段被更新后,onChanged回调就会被调用,然后UI就会被更新。

如果你非常熟悉其他的观察回调模式的话,你会发现我们没有覆盖fragment的onStop()方法来停止观察数据。这个是不必须的,因为LiveData是生命周期感知的。这意味着除非fragment是激活状态(onStart但是还没有onStop),要不然是不会发起回调的。当fragment调用onStop后, LiveData还会自动删除观察者。

我们没有做任何事情来处理configuration changes(当用户旋转屏幕)。当发生configuration changes事件,新的fragment被产生时,ViewModel会自动被恢复。新的fragment会关联上之前ViewModel的相同实例,LiveData会被瞬间回调。这就是为什么ViewModel不应该直接关联到View,他们应该独立于View的生命周期。请查看:The lifecycle of a ViewModel

获取数据

现在我们已经关联了fragment和ViewModel,但是ViewModel如何获取用户数据呢?在这个例子中,我们假设后端提供了一个REST接口。我们将会使用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会将获取数据这个工作委托给一个新的Repository模块。

Repository模块负责处理数据操作,他们为APP剩下的部分提供了一个干净的API。他们知道从哪里获取数据,知道数据有更新时调用什么API。你可以把他们看成是不同数据源(持久化模型,web service,缓存等)之间的中介者。

下面的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;
    }
}

看起来Respository模块不太重要,但是它确实起了很大的作用,它抽象了data source这层,现在ViewModel不知道数据是通过WebService获取的,这样就以为着我们可以替换这个实现。

注意:为了简单起见,我们忽略了处理相关的网络错误等逻辑。有关这块的内容请看:Addendum: exposing network status 最后一章

组件之间依赖配置(IOC)

上面的UserRepository类需要Webservice的一个实例,你可以简单的直接创建这个类,但是你还需要知道Webservice的依赖。这将会显著的复杂化代码。(例如,需要Webservice实例的每个类将需要知道如何使用它的依赖来构造它)。此外,UserRepository可能不是唯一需要Webservice的类,如果每个类都创建一个WebService,这会非常耗费资源。

这里有两个方案可以解决这个问题:

  • Dependency Injection:依赖注入允许类定义他们的依赖关系而不必创建他们,在运行时,另一个类负责提供这些依赖关系。我们建议在Android APP中使用Google的Dagger 2 来实现依赖注入。Dagger 2通过依赖树自动创建对象,并在依赖关系上提供编译时保证。

  • Service Locator:Service Locator提供一个类似注册表的东西,其中类可以获取他们的依赖而不是每次都创建他们,与依赖注入(DI)相比,实现起来相对容易,因此如果你不熟悉DI,请使用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的实现对于抽象接口调用是很好的,但是因为他仅仅依赖一个数据源,它还不算完美。

上面的UserRepository实现的问题是在获取数据之后并不会保存下来,如果用户离开UserProfileFragment然后在回来,我们就需要再获取数据,这样会有两个坏处:浪费了宝贵的网络带宽,迫使用户等待来完成请求。为解决这个问题,我们在UserRepository增加一个新的数据源,内存缓存。用户数据将会缓存在这里。

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

持久化数据

在目前的实现上,如果用户旋转屏幕或者后退再进入这个页面,因为Repository从内存缓存中获取数据,所以UI可以瞬间出来。但是如果用户退出APP过1,2个小时Android系统杀掉APP进程后再进入APP会怎样呢?

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

正确的方法是使用持久化Model来处理,这就是Room持久化库使用的场景。

Room是一个O/R Mapping库(类似Hibernate),使用最精简的代码提供本地数据持久化。在编译期,它会按照Schema验证你的每条查询,如果发现有问题的SQL语句,它会提示有编译错误,而不是在运行中才暴露问题。Room隐藏了执行原始SQL语句的细节。它还允许观察数据库中数据变化(包括集合和连接查询)。另外,它明确定义线程约束,比如一些常见的问题,在主线程中访问数据库。

注意:如果你已经熟悉其他的SQLite ORM或者Realm数据库持久化方案,你不必替换陈Room,除非你觉得Room更合适。

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

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

现在我们需要把用户数据插入数据库,所以我们创建一个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);
}

然后在MyDatabase中关联这个DAO。

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

请注意,load方法一个LiveData<User>。Room知道数据库何时被修改,当发生数据变化时,他会自动通知所有观察者。这非常方便,因为只要有一个观察者它都会更新数据。

注意:从alpha 1版本开始,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(() -> {
            // 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。这是抽象层带来的灵活性。这样对测试也带来了极大的好处,因为你可以提供一个fake的UserRepository来测试UserProfileViewModel。

现在我们完成了代码,如果用户几天后再回到同一个界面,他们会立即看到用户信息,因为我们持久化了用户信息。同时,如果数据过期,Repository会在后台更新数据。当然,如果持久化的数据太旧了,你可能不希望显示出来。

在某些情况下,例如下拉刷新时,在UI上显示告诉用户当前正在进行网络请求操作是非常重要的。将UI操作和实际数据分开是很好的做法,因为可能由于各种原因更新(例如,我们获取朋友列表,则可能获取相同的用户触发LiveData<User>更新)。从UI的角度看,事实上只是同时有另外一个数据请求,就像其他的数据(这里没翻译好)。

这里有两个解决方案:

  • 修改getUser方法以返回一个包含网络返回状态的LiveData对象,附录中提供了一个示例实现:Addendum: exposing network status
  • 在Repository中提供一个可以刷新用户数据的公共函数。如果你想在UI上显示网络状态,则响应明确的用户操作(例如下拉刷新)

单一的数据来源(Single source of truth)

不同的REST API通常返回相同的数据。 例如,如果我们的后端有另一个端点返回一个朋友列表,那么同一用户对象可能来自两个不同的REST API,也可能是不同的维度。 如果UserRepository按原样从Webservice请求中返回响应,则我们的UI可能会显示不一致的数据,因为这些请求之间的服务器端的数据可能会更改。 这就是为什么在UserRepository实现中,Web服务回调只是将数据保存到数据库中。 然后,对数据库的更改将触发活动的LiveData对象上的回调。

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

测试

我们说过分成隔离的好处可测试性,我们来看看怎样测试每个模块的代码。

  • UI界面和交互:这里是唯一一个需要Android UI Instrumentation test的地方,测试UI代码最好的方式是创建一个Espresso测试,你可以创建一个fragment,然后提供一个Mock的ViewModel,因为fragment只关联ViewModel,mock ViewModel可以很方便的完全测试UI。
  • ViewModel:ViewModel可以使用JUnit test来测试,你只需要mock UserRepository对象。
  • UserRepository:你可以使用JUnit来测试,当然你需要mock WebService和DAO。您可以测试它是否进行正确的Web服务调用,将结果保存到数据库中,如果数据被缓存并且是最新的,则不会发生任何不必要的请求。 既然Webservice和UserDao都是接口,那么你可以mock它们,或为更复杂的测试用例创建假的实现。
  • UserDao:推荐测试DAO的方法是使用 instrumentation tests,因为 instrumentation tests不需要任务UI,而且还算比较快。每个测试,你可以创建一个内存数据库,确保他们不会有任何副作用(比如,修改磁盘上的数据库)
    Room也允许指定数据库实现,所以你可以提供一个SupportSQLiteOpenHelper的是实现来测试,通常不推荐使用此方法,因为在设备上运行的SQLite版本可能与主机上的SQLite版本不同。
  • Testing Artifacts:架构组件提供一个maven artifact来控制后台线程,android.arch.core:core-testing artifact有两个JUnit规则:
    • InstantTaskExecutorRule:这个规则用来强制架构组件在当前调用线程马上执行后台操作。
    • CountingTaskExecutorRule:这个规则可以用于 instrumentation tests等待架构组件的后台操作,或者作为空闲资源连接到Espresso。

最终的架构

下图展示了我们推荐的架构中的所有模块,以及他们之前如何相互交互的:


final-architecture.png

指导原则

编程属于创意的领域,构建Android APP也不例外,通常有很多方式来解决问题,无论是在多个Fragment或者Activity之前传递数据,获取远程数据并存储在本地用户离线模式,还是其他常见的一些问题。

虽然一下的建议不是强制的,当时按照我们的经验,遵循这些建议将使你的代码更健壮,更加可测试的和可维护性。

  • 在你的manifest文件中定义的入口点 - activities、services、broadcast receivers等,不是你的数据源,相反,他们应该只是协调该入口的相关数据子集。由于每个应用程序组件生命周期非常短暂,取决于用户与设备的交互以及当时设备的整体运行状态,所以你不希望这些入口点成为数据源。

  • 在您的应用程序的各个模块之间创建明确的责任界限。 例如,不要将代码中加载数据和访问网络放在多个类或包中。 同样,不要将与数据缓存和数据绑定无关的责任放在同一个类中。

  • 每个模块应该尽可能少的暴露方法,不要试图为了快速实现从而暴露内部的是实现细节。你可能短时间内会赢得一些时间,但是随着代码量的增加,你将会多次付出技术债务。

  • 当你定义模块之间交互时,请思考如何使每个模块相互隔离。例如,从网络获取数据时定义良好的API将使得更容易测试。相反,如果你把这两个模块的逻辑混在一起,或者把从网络获取数据的代码散落在你整个代码库的各个地方,那么测试就会非常难以实现。

  • 不要花时间重复造轮子或者一次又一次的编写相同的代码。相反,将精力放在如何使你的APP独一无二的同时,让Android架构组件和其他推荐的类库处理重复的劳动。

  • 尽量考虑持久化尽可能多的相关的不过时的数据,这样你的APP可以处于脱机时使用。要时刻想着,你的用户可能没有像你一样稳定、高速的网络。

  • Repository应该指定一个单一的数据源,当你的应用程序需要访问这些数据时,他应该始终源于真实的单一来源。(这里的意思是:获取网络的数据,但是只保存到数据库,然后再通过数据库的LiveData来通知UI)

附录:暴露网络状态

在上面的“推荐的APP架构”章节,我们故意忽略网络错误和加载状态以便我们阅读。在本节中,我们演示一种使用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类的公开方法:

// 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作为网络请求。 ApiResponse是Retrofit2.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();
    }
}

现在,我们可以在Repository中使用NetworkBoundResource来写入磁盘和网络调用。

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阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容