app开发者遇到的普遍问题
桌面应用大部分情况下都只有单一的入口,作为一整个进程运行。然而Android应用的组成更复杂,包含了activities、fragments、services、content providers、broadcast receivers等组件,因此要求app在用户来回切换界面和任务的时候考虑周全。
关键点是app的组件可能不按顺序地单独启动 ,可能在任何时候被系统或者用户终止掉。因为app组件拥有短暂的、开发者不可控的生命周期,因此不能把任何数据或者状态保存在app组件中。
基本结构原则
SoC原则(Separation of Concerns):关注点分离原则。常见的一种错误是把所有的代码都写到activity中或者fragment中。没有处理UI或者系统交互的代码不应该出现在这些类当中。保持activity和fragment的代码尽可能少可以减少生命周期的相关问题。记住:你并不拥有activity或者fragment这些类,他们只是用来维系操作系统和你的app之间的粘合剂。Android系统可能在用户操作或者系统低内存之下把系统组件终止掉。所以应该尽可能减少对于系统组件的依赖。
第二条重要原则是使用model来控制UI,特别是持久化的model。持久化可以保证数据不会丢失,应用正常响应。Model就是用来操作app的数据的,独立于app的Views或者组件,也就独立于这些组件的生命周期。保持UI代码的简单、不涉及逻辑,会更好掌控。基于model类去管理数据,可以让app保持可测性和持久性。
导入Architecture库
在project的build.gradle的allprojects闭包中把Google的Maven仓库添加进去:
allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}
如果下载依赖库不成功,可以考虑把https://maven.google.com更换为
https://dl.google.com/dl/android/maven2/,实际访问第一个网址会跳到第二个。
然后在module的build.gradle中根据需要声明依赖
// Lifecycle、LiveData和ViewModel
compile "android.arch.lifecycle:runtime:1.0.0-alpha8"
compile "android.arch.lifecycle:extensions:1.0.0-alpha8"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha8"
// Room
compile "android.arch.persistence.room:runtime:1.0.0-alpha8"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha8"
// Room的RxJava支持
compile "android.arch.persistence.room:rxjava2:1.0.0-alpha8"
推荐的app结构
使用Architecture Components来搭建app。注意不可能有一种搭建方法对每种场景都最佳,但是使用Architecture Components是不错的选择。下面以用户简介场景为例。
搭建用户界面
要驱动UI,数据模型需要包括两个数据元素
1. User ID:用户id,最好使用fragment的arguments来传递参数,即使进程被终止,id仍然会被保存下来。
2. User object:用户对象。
基于ViewModel类创建UserProfileViewModel类来保存信息。
ViewModel为一个特定的UI组件提供数据,比如一个activity或者fragment,并处理与业务数据部分的交流,比如调用其他组件加载数据,或者转发用户的修改。ViewModel完全独立于View。
ViewModel类的声明如下:
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
在Fragment中获取ViewModel:
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);
}
}
上述Fragment继承于实现了LifeCycleOwner接口的LifeCycleFragment,在Architecture库正式发布之后,support包的Fragment就会实现LifeCycleOwner了。
接下来就轮到LiveData出场了,LiveData是一个可观测的数据持有者,可以让app组件观测到LiveData所包含对象的变化,又不用创建强耦合关系。LiveData还可以很好地配合应用组件的生命周期避免内存泄露。可以在其他响应流的基础上使用LiveData。
所以这时候就改成使用LiveData< User>来代替User
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
Fragment中就可以根据LiveData的回调来更新UI了:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, new Observer<User>() {
@Override
public void onChanged(@Nullable User user) {
viewModel.setProduct(productEntity); // 调用ViewModel中ObservableField的方法
}
});
}
ViewModel获取数据
假设后台提供了REST API,使用Retrofit进行网络请求
public interface Webservice {
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
注意ViewModel不能直接调用这个Webservice,ViewModel的作用范围仅限于Activity或者Fragment的生命周期内,可能造成数据丢失,此时应该使用一个Repository模块来获取数据。
Repository模块是用来操作数据的,可以用来协调网络请求、数据库、缓存。
下面创建一个UserRepository,用来把ViewModel与数据获取方式抽离开:
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) {
// 省略错误情况
data.setValue(response.body());
}
});
return data;
}
}
UserRepository包含一个Webservice对象,因此也就需要知道Webservice的创建细节,而且可能还有其他Repository需要用到Webservice对象,此时有两种方式来处理对象依赖关系。
1. 依赖注入:依赖注入可以定义依赖关系,而且不用去创建所依赖对象,在运行的时候是由另外一个类负责创建。可以使用Google的Dagger2。
2. Service Locator:提供一种方式来获取所依赖对象,不需要去创建他们。
现在我们使用定义的repository给ViewModel传递数据:
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每次都从服务器获取数据,效率不是很高,可以加上缓存:
@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;
}
}
但是缓存在应用退出后也会消失,最好再加上一个数据持久化。
数据持久化
Room是一个对象映射数据库。
使用@Entity定义一个表格
@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 {
}
定义数据库操作对象DAO,load可以返回LiveData对象,当数据库内容改变时,如果有活动的观测者则会通知其更新:
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
然后数据库中就可以引用DAO了:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
在Repository就可以加上数据库方式了:
@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);
// 直接从数据库返回一个LiveData对象
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// 在子线程中执行
// 检查是否有可用数据
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// 没有就请求网络
Response response = webservice.getUser(userId).execute();
// TODO check for error etc.
// 更新数据库,LiveData会自动更新
userDao.save(response.body());
}
});
}
}