根据Google官网Android Jetpack翻译
Android Jetpack最重要的原则就是关注点分离原则。要竟可能地精简Activity和Fragment,让他们处理UI和与操作系统的交互。这样可以避免很多生命周期引起的问题。
因为操作系统随时又能销毁Activity跟Fragment,所以已经尽量地减少依赖。
另一个重要的原则就是从模型(最好是持久型model)驱动UI。model是负责处理数据的组件,独立于视图对象和应用程序组件,因此不受程序的生命周期影响。原因如下:
1. 如果系统销毁App来释放资源,用户不会丢失数据。
2. 程序可以在网络不可用或者出现问题的时候继续工作。
可以使用架构组件来构建AppArchitecture Components
这里除了Repository以外,所有的模块都只是依赖下面的一个模块。为了有一个好的用户体验,应该先将本地的数据展示给用户,如果本地数据已经过时了,就从远程获取数据。
UI包含了fragment UserProfileFragment,布局文件user_profile_layout.xml。同时为了驱动UI,我们的数据层需要包含下面的这些元素:
1) User ID:用户的标识,最好使用fragment的argument将userid传递到fragment中。如果系统销毁了我们的进程,那么这个信息会被保存下来,下次App重启是就可以使用ID。
2)User Object:保存用户信息的class类。
当我们使用UserProfileViewModel时,我们需要使用ViewModel的架构组件来保存这些信息
ViewModel为activity或者fragment提供数据,并且用于模型通信的业务数据处理逻辑。
例如ViewModel可以调用其他组件来加载数据,并且可以转发用户请求来修改数据。
因为ViewModel并不会持有activity或者fragment,因此他不回受到生命周期的影响
现在我们创建了三个文件:
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;
@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);
}
}
现在我们已经有了代码了,但是我们要怎么将这两者联系起来呢?当我们设置在UserProfileViewModel中设置user的时候,我们需要去通知UI的方法,这时候就需要使用到LiveData架构组件了。
LiveDat是一个可观测的数据保持器。应用程序中的其他组件可以通过使用它来监视对象的更改,而不需要在对象之间创建依赖。
Livedata组件还遵循应用程序组件的生命周期状态,例如activity、fragment和service,并且包括清理逻辑,以防止对象泄漏和过度内存消耗。
tips:当程序中已经使用了RxJava或者Agera。你可以不使用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 -> {
// Update UI.
});
}
每次数据有改动时,都会调用OnChange()方法来刷新UI
如果你对其他第三方的观察回调库熟悉的话,你会发现LiveData没有在onStop()方法中去停止观察数据。
对于LiveData来说这是没有必要的,因为LIveData已经遵循了生命周期,只有在活跃状态才会调用onChange()方法,也就是说在Fragment中会在onStart()接收但不会在onStop()接收。LiveData会在Fragment destroy的时候自动移除。
现在我们已经通过LiveData将UserProfileFragment与UserProfileViewModel联系到一起了,那么我们如何获取用户数据呢?
在这里我们假设已经有后台的REST API服务
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太多的职责,违背了关注分离原则。此外ViewModel与Activity和Fragment的生命周期相关联,在UI对象的生命周期结束时,WebService获取到的数据将会丢失。这样会产生非常差的用户体验。
我们的ViewModel将数据获取的过程全部委托给了一个新的model,Repository
Repository Model处理数据操作。它提供程序其他部分检索数据简便的API。我们可以将Repository当做是数据源的中介,例如持久模型、webService和缓存。
我们的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
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之后,用户需要重新获取数据,即使数据没有发生改变。
这种设计不是最好的原因如下:
1、他浪费了宝贵的带宽。
2、他迫使用户等待新数据的查询。
为了解决这些问题,我们想UserRepository添加了一个将User保存在内存的新数据源
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会立刻显示,且不会发生改变,因为Repository可以从内存中直接读取数据。
但是当用户在系统杀死这个进程之后再返回程序会发生什么呢?在我们之前的实现中,我们需要去再次获取网络数据。这不仅仅是一种不好的用户体验,而且还是浪费了移动流量。
或许你可以通过缓存web请求来解决这个问题,但同时这也会导致一个新的问题:如果同样的用户数据通过不一样的请求,比如获取好友列表,会发生什么呢?程序将会展示不一样的数据,这是最混乱的。例如,同一个用户在不同的时间请求用户列表和单用户,我们的应用程序将会展示用户的两个版本的数据。我们的程序需要知道如何合并这些数据。
处理这种情况的方法是使用持久化数据模型,这时候我们就需要使用Room了
Room是一个对象映射库,它使用最少的样板代码提供本地数据持久性。在编译时,他将会根据数据模式验证每个查询,因此终端的SQL查询会导致编译时错误而不是运行时错误。Room抽象了处理原始SQL表和查询的一些基础实现细节。它还允许您观察对数据库数据的更改,包括集合和连接查询,并使用LiveData对象公开这些更改。它甚至显式地定义了解决常见线程问题的执行约束,例如访问主线程上的存储
为了使用Room,我们需要定义我们的局域模式。首先在User模型类中使用注解@Entity,在id上使用注解@PrimaryKey。这些注解在数据库中,为我们创建了一张User表以及主键。
User
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// Getters and setters for fields.
}
然后我们创建一个数据库类通过继承RoomDatabase
UserDatabase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
}
注意这个UserDatabase类是一个抽象类,Room会自动帮我们实现他,详情查看Room
我们需要将数据插入到数据库中的方法,为此我们创建了一个数据访问对象
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知道何时修改数据库,并在数据更改时自动通知所有活动观察者。因为Room使用LiveData,所以此操作是有效的;它只在至少有一个活动观察者时才更新数据。
现在我们已经已经创建了UserDao了,我们需要去修改UserDatabase
UserDatabase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
现在我们可以在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);
// 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());
}
});
}
}
在某些用例中,如下拉刷新,显示正在进行的网络操作是很重要的。由于数据会因为各种原因更新,将UI操作与实际数据分离是一种很好的实践。例如,如果我们获取好友列表,同一个用户可能会以编程方式再次获取,从而触发LiveData
更新。从UI的角度来看,请求正在传输的事实只是另一个数据点,类似于User对象本身中的任何其他数据段。
我们可以使用以下策略之一在UI中显示一致的数据更新状态,而不管更新数据的请求来自何处:
1.更改getUser()返回LiveData类型的对象。此对象将包括网络操作的状态。
例如GitHub上的android-architecture-components项目中的NetworkBoundResource实现
2.在UserRepository类中提供另一个公共函数,该函数可以返回User的刷新状态。如果仅当数据获取过程源自显式用户操作(如下拉刷新)时,才希望在UI中显示网络状态,则此选项更好。
尽管以下建议不是强制性的,但我们的经验是,遵循这些建议可以使您的代码库从长远来看更加健壮、可测试和可维护:
1.避免将应用程序的入口(如activity、services和broadcastReceiver)指定为数据源。
相反,它们应该只与其他组件协调以检索与该入口相关的数据子集。每个应用程序组件都相当短暂,这取决于用户与设备的交互以及系统的总体当前健康状况。
2.在应用程序的各个模块之间创建明确定义的职责边界。
例如,不要将从网络加载数据的代码分散到代码库中的多个类或包中。同样,不要定义多个无关的职责,例如数据缓存和数据绑定到同一个类中。
3.尽可能地少暴露每个模块
不要试图创建“仅此一个”快捷方式,以公开来自一个模块的内部实现细节。您可能在短期内获得一些时间,但是随着代码库的发展,您会多次招致技术债务。
4.考虑如何使每个模块在隔离状态下可测试。
例如,使用定义良好的API从网络获取数据,可以更容易地测试将数据持久化在本地数据库中的模块。相反,如果在一个地方混合来自这两个模块的逻辑,或者将网络代码分布在整个代码库中,那么测试就变得非常困难(如果不是不可能的话)。
5.专注于你的应用程序的独特核心,让它从其他应用程序中脱颖而出。
不要反复编写相同的样板代码来重新发明轮子。相反,把时间和精力集中在是什么让你的应用程序独特,让Android架构组件和其他推荐的库处理重复的样板
6.尽可能多地保持相关和新的数据。
这样,即使用户的设备处于脱机模式,用户也可以享受应用程序的功能。记住,不是所有的用户都喜欢持续、高速的连接。
7.指定一个数据源作为真实的唯一来源。
每当你的应用程序需要访问这段数据时,它应该总是源自于这个单一的真实来源。
在这之前,我们忽视了网络错误和加载状态来保持代码的简洁。
在这里我们通过Resource类来处理分装数据和网络状态。
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 }
}
由于加载网络数据和本地数据是一个公共方法,所以我们最好创建一个可以再多处重用帮助类。这里我们创建一个NetworkBoundResource类。
下图展示了NetworkBoundResource的决策树
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 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.
@MainThread
protected 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实例。
具体的实现可以参照android-architecture-components GitHub project
然后就可以完善UserRepository类了
UserRepository
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final int 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();
}
}