原文地址: 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<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 -> { //更新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<User> getUser(@Path("user") String userId); }
我们需要一个新的模块,存储库模块负责处理数据操作。他们提供了一个干净的API到应用程序的其余部分。他们知道从何处获取数据以及在更新数据时调用哪些API。您可以将它们视为不同数据源(持久模型,Web服务,缓存等)之间的中介。
UserRepository用来获取用户数据
public clas 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; } }
即使存储库模块看起来不必要,它也有一个重要的作用。它从应用程序的其余部分提取数据源。现在我们的ViewModel不知道数据是由Webservice获取的,这意味着我们可以根据需要可以从其他地方提取数据。
管理组件之间的依赖关系
上面的UserRepository类需要Webservice的一个实例来完成它的工作。他可以创建WebService的实力,这会使代码复杂化和复制(例如,每个需要Webservice实例的类将需要知道如何用它的依赖关系来构造它)。此外,UserRepository可能不是唯一需要Web服务的类。如果每个类创建一个新的WebService,这将会造成资源依赖严重。
可以使用依赖注入来解决这个问题,比如 Dagger
连接ViewModel和repository
public class UserProfileViewModel extends ViewModel{ private LiveData<User> 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<User> getUser(){ return this.user; } }
缓存数据
上面的存储库实现对抽象调用Web服务是有好处的,但是因为它只依赖于一个数据源,所以它不是很实用。 上面的UserRepository实现的问题是,在获取数据之后,它不保留在任何地方。如果用户离开UserProfileFragment并返回到它,应用程序将重新获取数据。这是不好的,原因有两个:浪费宝贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到我们的UserRepository中,它将把用户对象缓存在内存中。
@Singleton 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); webservice.getUser(userId).enqueue(new Callback<User>{ @Override public void onResponse(Call<User> call, Response<User> 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<User> load(String userId); }
然后在数据库类中引用DAO
public abstract class MyDatabase extends RoomDatabase{ public abstract UserDao userDao(); }
注意load方法返回一个LiveData <User>。Room知道数据库何时被修改,当数据改变时它会自动通知所有活动的观察者。因为它使用的是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<User> 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<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,一下是决策树:
[图片上传失败...(image-fa9dfd-1511161885983)]
下面是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, implemented // in the base class. public final LiveData<Resource<ResultType>> getAsLiveData(); }
请注意上面定义了两种数据类型(ResultType,RequestType),因为从Api返回的数据类型可能与本地使用的数据类型不匹配。
另外请注意,上面的代码使用ApiResponse进行网络请求。 ApiResponse是Retrofit2.Call类的一个简单包装,将其响应转换为LiveData。
以下是NetworkBoundResource类的rest的实现:
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 来在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(); } }
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。