原文地址:https://developer.android.com/topic/libraries/architecture/guide.html#addendum
现在按照google官方发布的android architecture来一步步的深入使用。
想象一下,我们构建一个用来显示用户信息的程序,用户信息是使用REST API从我们自己私人的服务器上获取的。
构建用户界面
程序中包含一个UserProfileFragment.java和他的布局文件user_profile_layout.xml.
为了用户界面的显示,我们的数据model需要有两个数据字段。
- User ID 用户的标识,最好使用传递参数的方式将这些参数传递给fragment,如果Android操作系统销毁您的进程,这些信息将被保留,以便在您的应用下次重新启动时使用。
- User object 保存用户数据的实体类
我们将会创建一个继承自ViewModel的UserProfileViewModel来保存这些信息。
ViewModel为特定的UI组件(如Activity或Fragment)提供数据,并处理与数据处理业务部分的通信,例如调用其他组件来加载数据或转发用户修改操作。 ViewModel不需要知道UI,并且不受配置更改的影响,例如由于旋转而重新创建活动。
现在我们有3个文件。
- user_profile.xml 用户信息的显示界面
- UserProfileViewModel.java 为UI准备数据的类
- UserProfileFragment.java 在ViewModel中显示数据并对用户交互作出反应的UI控制器。
现在我们开始用代码实现:
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是一个可观察的数据持有者,它允许应用程序中的组件观察LiveData对象的更改,而不会在它们之间创建明确的和严格的依赖关系路径。 LiveData还遵循应用程序组件(活动,片段,服务)的生命周期状态,并做正确的事情来防止对象泄漏,使您的应用程序不会消耗更多的内存。
注意:如果你已经使用了例如RxJava和Agrea的库的话,你可以继续使用他们用来代替LiveData,但是,当你使用它们或其他方法时,请确保正确处理生命周期,以便在相关的LifecycleOwner停止时停止数据流,并在销毁LifecycleOwner时销毁数据流。您还可以添加android.arch.lifecycle:reactivestreams组件以将LiveData与Rxjava2一起使用。
现在我们将UserProfileViewModel中的User字段替换成LiveData
public class UserProfileViewModel extends ViewModel{
...
private LiveData user;
public LiveData getUser(){
return user;
}
}
现在来修改UserProfileFragment用来观察数据从而通知UI
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this,user -> {
//更新UI
});
}
只要用户数据一更新,UI就会被刷新。
如果你熟悉使用可观察回调的其他库,您可能已经意识到我们不必重写片段的onStop()方法来停止观察数据。这对于LiveData来说是不必要的,因为它是生命周期感知的,这意味着它不会调用回调,除非片段处于活动状态(收到onStart(),但没有收到onStop())。当片段收到onDestroy()时,LiveData也会自动移除观察者。
我们也不需要为配置更改(例如,用户旋转屏幕)而做一些额外的工作。当配置更改的时候,ViewModel会自动保存数据,所以当新的fragment出现的时候,它将接收到同一个ViewModel的实例,回调将被立即调用当前数据。这就是ViewModel不能直接引用Views的原因。他们可以超越View的生命周期。请参阅ViewModel的生命周期。
获取数据
现在我们已经将Fragment和ViewModel连接到一起了,但是ViewModel如何获取用户数据,下面我们使用Retrofit来获取我们用户数据。
public interface Webservice{
@GET("/users/{user}")
Call getUser(@Path("user") String userId);
}
我们需要一个新的模块,存储库模块负责处理数据操作。他们提供了一个干净的API到应用程序的其余部分。他们知道从何处获取数据以及在更新数据时调用哪些API。您可以将它们视为不同数据源(持久模型,Web服务,缓存等)之间的中介。
UserRepository用来获取用户数据
public clas UserRepository{
private Webservice webservice;
public LiveData getUser(int userId){
final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback(){
@Override
public void onResponse(Call call,Response response){
data.setValue(response.body());
}
});
return data;
}
}
即使存储库模块看起来不必要,它也有一个重要的作用。它从应用程序的其余部分提取数据源。现在我们的ViewModel不知道数据是由Webservice获取的,这意味着我们可以根据需要可以从其他地方提取数据。
管理组件之间的依赖关系
上面的UserRepository类需要Webservice的一个实例来完成它的工作。他可以创建WebService的实力,这会使代码复杂化和复制(例如,每个需要Webservice实例的类将需要知道如何用它的依赖关系来构造它)。此外,UserRepository可能不是唯一需要Web服务的类。如果每个类创建一个新的WebService,这将会造成资源依赖严重。
可以使用依赖注入来解决这个问题,比如Dagger
连接ViewModel和repository
public class UserProfileViewModel extends ViewModel{
private LiveData user;
private UserRepository userRepo;
@Inject
public UserProfileViewModel(UserRepository userRepo){
this.userRepo = userRepo;
}
public void init(String userId){
if(this.user != null){
return;
}
user = userRepo.getUser(userId);
}
public LiveData getUser(){
return this.user;
}
}
缓存数据
上面的存储库实现对抽象调用Web服务是有好处的,但是因为它只依赖于一个数据源,所以它不是很实用。 上面的UserRepository实现的问题是,在获取数据之后,它不保留在任何地方。如果用户离开UserProfileFragment并返回到它,应用程序将重新获取数据。这是不好的,原因有两个:浪费宝贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到我们的UserRepository中,它将把用户对象缓存在内存中。
@Singleton
public class UserRepository{
private Webservice webservice;
private UserCache userCache;
public LiveData getUser(String userId){
LiveData cached = userCache.get(userId);
if(cached != null){
return cached;
}
final MutableLiveData data = new MutableLiveData<>();
userCache.put(userId,data);
webservice.getUser(userId).enqueue(new Callback{
@Override
public void onResponse(Call call, Response response){
data.setValue(response.body());
}
});
return data;
}
}
数据持久化
在我们当前的实现中,如果用户旋转屏幕或离开并返回到应用程序,则现有UI将立即可见,因为存储库从内存中高速缓存中检索数据。但是,如果用户离开应用程序,并被系统杀死该进程后数小时后回来,会发生什么?
此时我们就需要对数据进行持久化操作,从而避免重复从网络中获取数据而造成的资源浪费。
处理这个问题的正确方法是使用持久化模型。这时就需要使用Room来帮忙了。
Room是一个对象映射库,提供本地数据持久化的功能。在编译时,它会根据模式验证每个查询,以便断开的SQL查询导致编译时错误。还抽象出一些使用原始SQL表和查询的底层实现细节。它还允许观察对数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改。另外,它明确定义了解决常见问题的线程约束,例如访问主线程上的存储。
如果你的app已经使用了其他的持久化解决方案例如ORM,你就不需要替换现有的方案为Room。
使用Room,我们需要定义我们自己的本地模式,首先,需要在你的User类上添加@Entity注解标记为数据库中的一个表。
@Entity
class User{
@PrimaryKey
private int id;
private String name;
private String lastname;
}
接着,创建一个数据库类集成RoomDatabase
@Database(entities={User.class},version =1)
public abstract class MyDatabase extends RoomDatabase{
}
注意到MyDatabase是抽象的,Room会自动实现它。
现在我们需要插入数据到数据库中的方法,所以我们需要创建一个数据库访问实体类
@Dao
public interface UserDao{
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id=:userId")
LiveData load(String userId);
}
然后在数据库类中引用DAO
public abstract class MyDatabase extends RoomDatabase{
public abstract UserDao userDao();
}
注意load方法返回一个LiveData
现在我们需要修改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 = weservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData getUser(String userId){
refreshUser(userId);
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。 现在我们的代码是完整的。如果用户以后回到相同的用户界面,他们会立即看到用户信息,因为我们队对数据进行了持久化的操作。同时,如果数据陈旧,我们的仓库将在后台更新数据。当然,根据您的使用情况,如果数据太旧,您可能不希望显示持久化数据。
下图展示的就是所有模块之间是如何进行交互的。
[图片上传失败...(image-ab6985-1511161885983)]
附录:暴露网络状态
//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) {
return new Resource<>(SUCCESS, data, null);
}
public static Resource error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static Resource loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}
从网络加载数据然后从磁盘显示数据是通用的方式,所以我们需要创建一个可以在多个地方重复使用的帮助类,NetworkBoundResource,一下是决策树:
[图片上传失败...(image-fa9dfd-1511161885983)]
下面是NetworkBoundResource的代码
// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource {
// 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 loadFromDb();
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData> 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> getAsLiveData();
}
请注意上面定义了两种数据类型(ResultType,RequestType),因为从Api返回的数据类型可能与本地使用的数据类型不匹配。
另外请注意,上面的代码使用ApiResponse进行网络请求。 ApiResponse是Retrofit2.Call类的一个简单包装,将其响应转换为LiveData。
以下是NetworkBoundResource类的rest的实现:
public abstract class NetworkBoundResource {
private final MediatorLiveData> result = new MediatorLiveData<>();
@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)));
}
});
}
private void fetchFromNetwork(final LiveData 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 response) {
new AsyncTask() {
@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> getAsLiveData() {
return result;
}
}
现在我们可以使用NetworkBoundResource来在repository中编写我们自己的磁盘和网络用户信息获取实现:
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData> loadUser(final String userId) {
return new NetworkBoundResource() {
@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 loadFromDb() {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData> createCall() {
return webservice.getUser(userId);
}
}.getAsLiveData();
}
}