目录:
.移动APP用户体验
.常见的架构原则
.关注点分离
.从模型中驱动UI
.推荐应用架构
.概述
.构建用户接口
.获取数据
.连接ViewModel和仓库
.缓存数据
.永久数据
.显示正在进行的操作
.测试组件
.最佳实践
.附录:公开网络状态
原文地址:https://developer.android.google.cn/jetpack/docs/guide#addendum
本指南是为那些已经具备了构建应用程序的基础,现在想知道如何构建健壮的、高质量的应用程序的开发人员编写的,包含了最佳实践和推荐体系结构
假设您已经熟悉Android框架。如果您是Android应用程序开发的新手,请参阅我们的开发人员指南,它是本指南学习的前提条件。
移动APP用户体验
在大多数情况下,桌面应用程序有一个来自桌面或启动程序的单一入口点,然后作为一个单一的进程独自运行。
另一方面,Android应用程序的结构要复杂得多。一个典型的Android应用程序包含多个应用程序组件,包括Activity、Fragment、Service、ContentProvider和BroadcastReceiver。
您可以在应用程序清单(AndroidManifest.xml)中声明这些应用程序组件。然后,Android操作系统使用这个文件来决定如何将应用程序集成到设备的整体用户体验中。由于一个编写良好的Android应用程序包含多个组件,而且用户经常在短时间内与多个应用程序交互,因此应用程序需要适应不同类型的用户驱动工作和任务。
例如:考虑一下,当在你最喜欢的社交网络应用程序中分享一张照片时会发生什么?
1: 这款应用程序会触发摄像头的意图。然后,Android操作系统会启动一个相机应用程序来处理这个请求。此时,用户已经离开了社交网络应用,但他们的体验是无缝的
2: 相机应用程序可能会触发其他意图,比如启动文件选择器,文件选择器可能会启动另一个应用程序。
3: 最终,用户返回到社交网络应用程序并分享照片。
在此过程中的任何时刻,用户都可能被电话或通知打断。在此中断之后,用户希望能够返回并恢复此照片共享过程。这种应用程序跳跃行为在移动设备上很常见,所以您的应用程序必须正确处理这些事情。
请记住,移动设备也是资源受限的,所以在任何时候,操作系统可能会终止一些应用程序进程,为新的进程腾出空间。
考虑到这种环境的条件,您的应用程序组件有可能单独或无序地启动,操作系统或用户可以在任何时候销毁它们。因为这些事件不在你的控制之下,你不应该在你的应用组件中存储任何应用数据或状态,你的应用组件不应该相互依赖。
常见的架构原则
如果你不应该使用app组件存储app数据和状态,你应该如何设计你的app?
关注点分离:
最重要的原则是关注的分离。将所有代码编写到Activity或Fragment中是一个常见的错误。这些基于UI的类应该只包含处理UI和操作系统交互的逻辑。通过保持这些类尽可能的精简,您可以避免许多与生命周期相关的问题
请记住,您并不拥有Activity和Fragment的实例 ,相反,它们只是代表Android操作系统和你的应用程序之间的链接类。操作系统可以根据用户交互或系统条件(如内存不足)在任何时候销毁它们。为了提供令人满意的用户体验和更易于管理的应用程序维护体验,最好尽量减少对它们的依赖
从模型中驱动UI
另一个重要的原则是,您应该从模型(最好是持久模型)中驱动UI。模型是负责处理应用程序数据的组件。它们独立于应用程序中的视图对象和应用程序组件,因此不受应用程序生命周期和相关问题的影响
持久性是理想的,原因如下:
.如果Android操作系统销毁您的应用程序来释放资源,您的用户不会丢失数据
.当网络连接不稳定或不可用时,您的应用程序将继续工作。
通过基于模型类和定义良好的数据管理职责,您的应用程序更加方便测试和保质一致性
推荐的应用架构
在本节中,我们将通过端到端用例演示如何使用体系结构组件构建应用程序
注意:不可能有一种方法来编写适用于所有场景的应用程序。尽管如此,这个推荐的体系结构对于大多数情况和工作流都是一个很好的起点。如果您已经有了一种很好的方式来编写遵循通用架构原则的Android应用程序,那么您就不需要更改它。
假设我们正在构建一个显示用户配置文件的UI。我们使用私有后端和REST API来获取给定概要文件的数据
概述
首先,考虑下面的图,它显示了所有模块在设计完app后应该如何相互交互
总体架构注意,每个组件只依赖于它下面一级的组件。例如,Activity和Fragment仅依赖于视图模型。仓库是唯一依赖于多个其他类的类 ; 在本例中,仓库依赖于持久数据模型和远程后端数据源
这种设计创造了一致性和愉快的用户体验。不管用户是在关闭应用程序几分钟后还是几天后回到应用程序,他们都会立即看到应用程序在本地保存的用户信息。如果该数据过时,应用程序的仓库模块将开始在后台更新数据
构建用户接口
UI由一个UserProfileFragment及其对应的布局文件user_profile_layout.xml组成
为了驱动UI,我们的数据模型需要保存以下数据元素:
.用户ID: 用户的标识符。传递信息到Fragment中,最好的方式是使用 Fragment arguments 。如果Android操作系统销毁了我们的进程,这些信息就会被保存下来,这样下次重启应用程序时ID就可用了
.用户对象:保存用户详细信息的数据类。
我们使用一个UserProfileViewModel,它是基于ViewModel 架构组件,来保存这些信息
ViewModel对象为特定UI组件(如Fragment或Activity)提供数据,并包含与模型通信的数据处理业务逻辑。例如,ViewModel可以调用其他组件来加载数据,它还可以转发用户修改数据的请求。ViewModel不知道UI组件,因此它不受配置更改的影响,比如在旋转设备时重新创建Activity
我们现在已经定义了以下文件:
.user_profile.xml : UI布局定义
.UserProfileFragment : 显示数据的UI控制器
.UserProfileViewModel : 该类为查看UserProfileFragment 准备数据,且对用户交互作出响应
下面的代码片段显示了这些文件的初始内容。(为了简单起见,省略了布局文件。)
UserProfileViewModel :
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
UserProfileFragment :
public class UserProfileFragment extends Fragment {
private static final String UID_KEY = "uid";
private UserProfileViewModel viewModel;
@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
String userId =getArguments().getString(UID_KEY);
viewModel= ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);}
@Overridepublic View onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile, container, false);
}}
现在我们有了这些代码模块,我们如何连接它们?毕竟,当User字段在UserProfileViewModel类中设置时,我们需要一种通知UI的方法。这就是LiveData体系结构组件的用途。
LiveData是一个可观察的数据持有者。应用程序中的其他组件可以使用这个>保持器监视对象的更改,而无需在它们之间创建显式和严格的依赖路径。LiveData组件还关注应用程序组件(如Activity、Fragment和Service)的生命周期状态,并包括清理逻辑,以防止对象泄漏和过度内存消耗。
注意:如果您已经在使用RxJava或Agera之类的库,那么可以继续使用它们,而不是使用LiveData。但是,当您使用这些库和方法时,请确保正确地处理应用程序的生命周期。特别是,当关联的生命周期被停止或者被销毁时,确保暂停你的数据流或销毁这些数据流。你也可以添加 android.arch.lifecycle:reactivestreams 将LiveData与另一个响应流库(如RxJava2)一起使用。
为了将LiveData组件合并到我们的应用程序中,我们将UserProfileViewModel中的字段类型更改为LiveData<User>。现在,当数据更新时,UserProfileFragment被通知。此外,由于这个LiveData字段是生命周期敏感的,它会在不再需要引用之后自动清理它们
UserProfileViewModel:
public class UserProfileViewModel extends ViewModel {
...
private User user;
private LiveData<User> user;
public LiveData<User> getUser() {return user;}
}
现在我们修改UserProfileFragment来观察数据并更新UI :
UserProfileFragment :
@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {// Update UI.});}
每次更新用户配置文件数据时,都会调用onChanged()回调,并刷新UI。
如果您熟悉使用可观察回调的其他库,您可能已经意识到我们没有覆盖Fragment的onStop()方法来停止观察数据。对于LiveData,这一步是不必要的,因为它是生命周期敏感的,这意味着它不会调用onChanged()回调,除非Fragment处于活动状态;也就是说,它已经收到onStart(),但是还没有收到onStop()。当调用Fragment的onDestroy()方法时,LiveData还会自动删除观察者
我们也没有添加任何逻辑来处理配置更改,比如用户旋转设备的屏幕。当配置更改时,UserProfileViewModel将自动恢复,一旦创建了新的Fragment,它就会接收到相同的ViewModel实例,并立即使用当前数据调用回调。鉴于ViewModel对象的意图比它们更新的相应视图对象更持久,您不应该在ViewModel的实现中包含对视图对象的直接引用。有关与UI组件的生命周期相对应的视图模型的生命周期的更多信息,请参见视图模型的生命周期。
获取数据
既然我们已经使用LiveData将UserProfileViewModel连接到UserProfileFragment,那么我们如何获取用户配置文件数据呢?
对于本例,我们假设后台提供了一个REST API。我们使用了Retrofit库来访问我们的后端,但是您可以自由地使用不同的库来实现相同的目的。
以下是我们对与后台通信的Webservice的定义:
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来获取数据并将该数据分配给我们的LiveData对象。这种设计是可行的,但是随着它的使用,我们的应用程序变得越来越难以维护。它给UserProfileViewModel类太多的责任,这违反了关注点分离原则。此外,视图模型的范围与Activity或Fragment生命周期相关联,这意味着当关联的UI对象的生命周期结束时,来自web服务的数据将丢失。这种行为会造成不受欢迎的用户体验
相反,我们的ViewModel将数据获取过程委托给一个新的模块,一个存储仓库
存储仓库模块处理数据操作。它们提供了一个干净的API,以便应用程序的其余部分可以轻松检索这些数据。他们知道从哪里获取数据,以及更新数据时要进行哪些API调用。您可以将存储仓库看作是不同数据源(如持久模型、web服务和缓存)之间的中介
我们的UserRepository类,如下面的代码片段所示,使用WebService的一个实例来获取用户的数据:
UserRepository :
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This isn't an optimal implementation. We'll fix it later.
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());
}
// Error case is left out for brevity.
});
return data;}
}
尽管存储库模块看起来没有必要,但它有一个重要的用途:它从应用程序的其余部分抽象数据源。现在,我们的UserProfileViewModel不知道如何获取数据,所以我们可以向视图模型提供从几个不同的数据获取实现中获得的数据
注意:为了简单起见,我们省略了网络错误情况。有关公开错误和加载状态的替代实现,请参见附录:公开网络状态
管理组件之间的依赖关系 :
上面的UserRepository类需要一个Webservice实例来获取用户数据。它可以简单地创建实例,但是要做到这一点,它还需要知道Webservice类的依赖关系。此外,UserRepository可能不是唯一需要web服务的类。这种情况需要我们复制代码,因为每个需要引用Webservice的类都需要知道如何构造它和它的依赖关系。如果每个类都创建一个新的web服务,我们的应用程序可能会变得非常占用资源
您可以使用以下设计模式来解决这个问题:
.依赖注入(DI):依赖注入允许类定义它们的依赖而无需构造它们。在运行时,另一个类负责提供这些依赖项。我们推荐Dagger 2库在Android应用程序中实现依赖注入。Dagger 2通过遍历依赖关系树自动构造对象,并对依赖关系提供编译时保证
.服务定位器:服务定位器模式提供了一个注册中心,在那里类可以获取它们的依赖项,而不是构造它们。
实现服务注册中心比使用DI更容易,因此如果您不熟悉DI,可以使用服务定位器模式。
这些模式允许您扩展代码,因为它们为管理依赖关系提供了清晰的模式,而无需重复代码或增加复杂性。此外,这些模式允许您在测试和生产数据获取实现之间快速切换。
我们的示例应用程序使用Dagger 2管理Webservice对象的依赖关系。
连接ViewModel和仓库
现在,我们修改UserProfileViewModel来使用UserRepository对象:
UserProfileViewModel:
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
// Instructs Dagger 2 to provide the UserRepository parameter.
@Inject
public UserProfileViewModel(UserRepository userRepo) {this.userRepo = userRepo;}
public void init(int userId) {
if (this.user != null) {
// ViewModel is created on a per-Fragment basis, so the userId
// doesn't change.
return;
}
user= userRepo.getUser(userId);}
public LiveData<User> getUser() {return this.user;}}
缓存数据
UserRepository实现抽象了对Webservice对象的调用,但由于它只依赖于一个数据源,因此不够灵活。
UserRepository实现的关键问题是从后端获取数据后,它不会将数据存储在任何地方。因此,如果用户离开UserProfileFragment,然后返回给它,我们的app必须重新取回数据,即使数据没有改变
这种设计不是最优的,原因如下:
.它浪费了宝贵的网络带宽。
.它强制用户等待新查询完成。
为了解决这些缺点,我们向UserRepository添加了一个新的数据源,它将用户对象缓存在内存中
UserRepository :
// Informs Dagger that this class should be constructed only once.
@Singleton
public class UserRepository {
private Webservice webservice;
// Simple in-memory cache. Details omitted for brevity.
private UserCache userCache;
public LiveData<User> getUser(int userId) {
LiveData<User> cached = userCache.get(userId);
if (cached != null) {return cached;}
final MutableLiveData<User> data = new MutableLiveData<>();
userCache.put(userId, data);
// This implementation is still suboptimal but better than before.
// A complete implementation also handles 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立即变得可见,因为存储库从内存缓存中检索数据
但是,如果用户离开应用程序,在Android操作系统终止进程数小时后返回,会发生什么情况呢?在这种情况下,依靠我们当前的实现,我们需要再次从网络中获取数据。这个重取过程不仅仅是一个糟糕的用户体验;这也是一种浪费,因为它会消耗有价值的移动数据
您可以通过缓存web请求来解决这个问题,但是这会产生一个关键的新问题:如果从另一种类型的请求中出现相同的用户数据,例如获取好友列表,会发生什么情况?该应用程序将显示不一致的数据,这是最令人困惑的。例如,如果用户在不同时间发出好友列表请求和单用户请求,我们的应用程序可能会显示同一用户数据的两个不同版本。我们的应用程序需要找出如何合并这些不一致的数据
处理这种情况的正确方法是使用持久模型。这就是Room持久性库可以提供帮助的地方
Room是一个对象映射库,它以最少的样板代码提供本地数据持久性。在编译时,它对每个针对数据模式的查询进行验证,因此损坏的SQL查询会导致编译时错误,而不是运行时错误。Room 为原始的SQL表和查询 抽象出了一些底层的实现工作细节。它还允许您观察数据库数据的变化,包括集合和连接查询,使用LiveData对象公开这些变化。它甚至显式定义了处理常见线程问题的执行约束,例如在主线程上的访问存储。
注意:如果您的应用程序已经使用了另一个持久性解决方案,例如SQLite对象关系映射(ORM),则不需要用Room替换现有的解决方案。但是,如果您正在编写一个新的应用程序或重构一个现有的应用程序,我们建议使用Room来保存应用程序的数据。这样,您就可以利用库的抽象和查询验证功能
要使用Room,我们需要定义本地模式。首先,我们将@Entity注释添加到用户数据模型类中,并将@PrimaryKey注释添加到类的id字段中。这些注释将User标记为数据库中的表,id标记为表的主键
User :
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// Getters and setters for fields.
}
然后,通过实现RoomDatabase,为我们的应用程序创建数据库类
注意,UserDatabase是抽象的。Room自动提供了它的一个实现。有关详细信息,请参阅Room文档
我们现在需要一种将用户数据插入数据库的方法。对于这个任务,我们创建一个数据访问对象(DAO)。
UserDao :
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(int userId);}
注意,load方法返回类型为LiveData的对象,Room知道何时数据库被修改,并在数据更改时自动通知所有Activity观察者,它只在至少有一个活动观察者时更新数据
注:Room根据表修改情况进行失效检查,可能会发出假的确定通知。
定义了UserDao类之后,我们从数据库类引用DAO
UserDatabase:
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
现在我们可以修改UserRepository来合并Room数据源:
@Singletonpublic
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);
// Returns a LiveData object directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
// Runs in a background thread.
executor.execute(() -> {
// Check if user data was fetched recently.
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// Refreshes the data.
Response<User> response = webservice.getUser(userId).execute();
// Check for errors here.
// Updates the database. The LiveData object automatically
// refreshes, so we don't need to do anything else here.
userDao.save(response.body());}});}}
注意,即使我们更改了数据来自UserRepository的位置,我们也不需要更改UserProfileViewModel或UserProfileFragment。这个小范围的更新展示了我们的应用架构提供的灵活性。它对于测试也很有用,因为我们可以提供一个伪用户存储库,同时测试我们的产品UserProfileViewModel。
如果您使用了这套架构,有可能会让用户在一段时间内,看到的都是陈旧的数据,直到仓库管理类能够获取最新的数据更新。根据您的使用场景,您可能不想显示这些过时的信息。替代方案,您可以显示占位符数据,它显示虚拟值,并指示您的应用程序当前正在获取和加载最新信息。
资源唯一性
不同的REST API端点返回相同的数据是很常见的。例如,如果我们的后端有另一个端点返回朋友列表,那么相同的用户对象可能来自两个不同的API端点,甚至可能使用不同的粒度级别。如果用户存储库按原样返回来自Webservice请求的响应,而不检查一致性,我们的ui可能会显示令人混淆的信息,因为存储库数据的版本和格式将取决于最近调用的端点。
因此,我们的UserRepository实现将web服务响应保存到数据库中。对数据库的更改然后触发对Activity关联LiveData对象的回调。使用这个模型,数据库作为真实的单一来源,应用程序的其他部分使用我们的UserRepository访问它。无论您是否使用磁盘缓存,我们都建议您的存储仓库将数据源指定为应用程序其余部分的唯一真实源
显示正在进行的操作
在某些用例中,例如下拉刷新,UI向用户显示当前正在进行网络操作是很重要的。将UI操作与实际数据分离是一种很好的实践,因为数据可能由于各种原因而更新。例如,如果我们获取好友列表,可能会以编程方式再次获取相同的用户,从而触发LiveData< user >更新。从UI的角度来看,正在运行的请求只是另一个数据点,类似于用户对象本身中的任何其他数据块。
我们可以使用以下策略之一在UI中显示一致的数据更新状态,而不管更新数据的请求来自哪里:
.更改getUser()以返回LiveData类型的对象。这个对象将包括网络操作的状态。
例如,请参阅android-architecture-components GitHub项目中的
NetworkBoundResource实现
.当从一个显式的用户行为(例如,下拉刷新)获取数据的过程中,如果您只是希望在你的UI中显示网络状态,这是一种很好的方式.当从一个显式的用户行为(例如,下拉刷新)获取数据的过程中,如果您只是希望在你的UI中显示网络状态,这是一种很好的方式
测试组件
在关注点分离一节中,我们提到遵循这个原则的一个关键好处是可测试性
下面的列表显示了如何从我们的扩展示例中测试每个代码模块:
1: 用户接口和交互:使用 Android UI instrumentation 测试。创建此测试的最佳方法是使用Espresso库。您可以创建片段并为其提供模拟UserProfileViewModel。因为片段只与UserProfileViewModel通信,所以模拟这个类就足以完全测试应用程序的UI。
2: ViewModel:您可以使用JUnit测试来测试UserProfileViewModel类。您只需要模拟一个类,UserRepository
3: UserRepository:您还可以使用JUnit测试来测试UserRepository。您需要模拟web服务和UserDao。在这些测试中,验证以下行为:
A: 存储库执行正确的web服务调用。
B: 存储库将结果保存到数据库中。
C: 如果数据被缓存并且是最新的,存储库不会发出不必要的请求
因为Webservice和UserDao都是接口,所以您可以模拟它们,或者为更复杂的测试用例创建伪实现
4: UserDao:使用 instrumentation 测试DAO类。因为这些 instrumentation 测试不需要任何UI组件,所以它们运行得很快
对于每个测试,在内存中创建一个数据库,以确保测试没有任何副作用,例如更改磁盘上的数据库文件
注意:Room允许指定数据库实现,因此可以通过基于SupportSQLiteOpenHelper的JUnit实现来测试DAO。但是,不推荐这种方法,因为在设备上运行的SQLite版本可能与开发机器上的SQLite版本不同
5: Webservice:在这些测试中,避免对后端进行网络调用。所有的测试,尤其是基于web的测试,独立于外部世界是非常重要的。
一些库,包括MockWebServer,可以帮助您为这些测试创建一个伪本地服务器
6: 测试架构:架构组件提供了一个maven的构件来控制它的后台线程。android.arch.core:core-testing 构件 包含以下JUnit规则
A : InstantTaskExecutorRule:使用此规则立即在调用线程上执行任何后台操作
B : CountingTaskExecutorRule:使用该规则等待架构组件的后台操作。您还可以将此规则与Espresso关联作为空闲资源
最佳实践
编程是一个创造性的领域,构建Android应用程序也不例外。有许多方法可以解决问题,无论是在多个活动或片段之间通信数据、检索远程数据并在本地将其保存为脱机模式,还是其他许多重要应用程序遇到的常见场景
虽然下面的建议不是强制性的,但是根据我们的经验,从长远来看,遵循这些建议可以使您的代码库更加健壮、可测试和可维护
1: 避免指定应用程序的入口点(例如Activity、Service和Broadcast)作为数据源
相反,它们应该只与其他组件协调以检索与该入口点相关的数据子集。每个应用程序组件都是短暂的,这取决于用户与设备的交互以及系统当前的整体健康状况
2: 在应用程序的各个模块之间创建定义明确的职责边界。
例如,不要将从网络加载数据的代码分散到代码库中的多个类或包中。同样,不要将多个不相关的职责(例如数据缓存和数据绑定)定义到同一个类中
3: 尽可能少地从每个模块公开。
不要尝试从一个模块中创建一个快捷方式,来暴漏一个内部的实现细节。你可能会在短期内节约一些时间,但之后你会为你的代码结构产生的技术债务付出更多的时间
4: 考虑如何使每个模块单独测试
举例,一个定义良好的从网络获取数据的api会比在本地数据库中保留数据更容易测试。相反,如果你把这2个模块的逻辑混合在同一个地方,或者分散你的网络代码到你的整个基础代码中,那将会变得很复杂,几乎不可能完成测试
5: 专注于应用程序的独特核心突出于其他应用程序
不要通过一遍又一遍的编写相同的模版代码来重新发明创造,相反的,聚焦你的时间和精力,来为你的应用程序制作独特性,让Android的架构组件和其他的推荐库来处理重复的模版。
6: 尽可能多的保持相关的最新的数据
这样,用户可以享受你的应用的功能,即使他们的设备在离线模式。记住,并不是所有的用户享受常规,高速连接
7: 指派一个数据源作为单一源是唯一正确的
当应用程序需要访问这一块的数据,它应该总是源自这一唯一的源泉。
附录:暴漏网络状态
在上面的推荐应用架构部分,我们省略了网络错误和加载状态保持简单的代码片段
本节演示了如何使用一个资源类封装数据和它的状态。
下面的代码片段提供了一个示例,Resource的实现:
// A generic class that contains data and status about loading this data.
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<>(Status.SUCCESS, data, null);}
public static <T> Resource<T> error(String msg, @Nullable T data) {return new Resource<>(Status.ERROR, data, msg);}
public static <T> Resource<T> loading(@Nullable T data) {return new Resource<>(Status.LOADING, data, null);}
public enum Status { SUCCESS, ERROR, LOADING }}
因为它是常见的加载数据从网络显示磁盘拷贝数据时,最好创建一个helper类,您可以在多个地方重用。在这个例子中,我们创建一个名为NetworkBoundResource的类
下图显示了NetworkBoundResource的决策树:
它从一个观察中的数据库资源开始。当首次从数据库加载时,NetworkBoundResource 检查结果是否正确或者应该从网络重新获取。注意,这两种情况可能发生在同一时间,考虑到你可能想要显示缓存数据而从网络上更新数据
如果网络调用成功完成,它保存响应到数据库中并重新初始化。如果网络请求失败,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 to fetch
// potentially updated data 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.
@MainThreadprotected void onFetchFailed();
// Returns a LiveData object that represents the resource that's implemented
// in the base class.
public final LiveData<Resource<ResultType>> getAsLiveData();}
注意关于这个类的这些重要的细节定义
1: 它定义了两种类型参数,ResultType RequestType,因为从API返回的数据类型可能与在本地使用数据类型不匹配。
2: 为了网络请求,它使用了一个类ApiResponse。ApiResponse是基于Retrofit2.Call类的简单封装,以用来作为LiveData的实例来适配响应结果
完整的实现NetworkBoundResource类作为android-architecture-components GitHub工程的一部分存在
创建NetworkBoundResource之后,在UserRepository类中,我们可以使用它将用户写磁盘和实现网络绑定
UserRepository
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final int userId) {
return new NetworkBoundResource<User,User>() {
@Overrideprotected 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();}}
网友评论