[简译]Google 官方 App 架构 Guide

本文将介绍Google 官方 App 架构——Architecture Components,并展示了一个简单的使用场景。

App 开发者面临的问题

相比 desktop app,写 Android apps 更加复杂,需要涉及更多组件交互(activities, fragments, services, content providers and broadcast receivers),同时还要考虑各个app、task之间的切换。

比如考虑这样一个例子——在社交App中调起相机应用拍照,然后相机调起文件选择器选择刚刚拍的照片,最终回到社交App分享刚刚完成的照片。

在保证这一串数据流正确的同时,你还得担心App随时可能会被系统回收。

总结:

  • system random kill
  • out-of-order launched
  • out-of-control lifecycle

通用的架构设计理念

  1. 合理的拆分
    不要在Activity,Fragment中写非UI操作或非系统交互的代码,避免LifeCycle依赖。make it clean

  2. 尽量通过 Model 驱动 UI
    比如一个支持持久化的Model,以防止data lose。此外还能让业务代码和View拆分开。

推荐架构

下面是通过实现一个用户主页的场景,来介绍Architecture Components

1. 构建UI

UserProfileFragment.java

user_profile_layout.xml

UserProfileViewModel

ViewModel

  • 提供数据给UI component
  • 处理业务逻辑交互, 比如调用dataHandler进行数据加载.
  • ViewModel不知道View实现细节,也不受Phone Config改变影响
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 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);
}
}

Note: LifecycleFragment 是实现了 LifecycleOwner接口的Fragment
(据说稳定之后support包的Fragment会默认实现)

新概念——LiveData,用来通知UI,ViewModel的数据发生来更新。

LiveData 可以理解为自带Lifecycle管理的RxJava Observerable,一旦离开生命周期,会自己clean相关引用。

Note: 如果使用 RxJava 或者 Agera, 你得自己管理 LifecycleOwener 的Observerable生命周期。

或者你可以使用
android.arch.lifecycle:reactivestreams 来集成LiveData 和其他Stream库 (for example, RxJava2).

public class UserProfileViewModel extends ViewModel {
...
- private User user;
+ private LiveData<User> user;
+ public LiveData<User> getUser() {
+ return user;
+ }
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
+ viewModel.getUser().observe(this, user -> {
+ // update UI
+ });
}

LiveData 会保证在 fragment 处于 active 状态时[onStart,onStop] 才调用onChanged,并在onDestroy()时移除observer。

在configuration changes(旋转屏幕后),重建的 fragment 会恢复同一个 ViewModel 实例(从这个意义上说ViewModel独立于View生命周期)
SeeThe lifecycle of a ViewModel.

so far so good, but how to init LiveData through net api ?

2. 获取网络数据

接下来使用 Retrofit 为例从后端获取数据。

Webservice

public interface Webservice {
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}

不要直接在ViewModel中调用Webservice,Why?

  1. 随着代码增长,不利于后继维护
  2. 职责划分不明,ViewModel 应该只负责 drive UI change
  3. ViewModel 在Activity finished后,也会销毁,不适合存储数据。

Repository

Repository 负责数据处理, 相当于一个从各个数据源读取数据的中间层。

比如这里,在UserRepository类里调用 WebService 获取用户数据。

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) {
// error case is left out for brevity
data.setValue(response.body());
}
});
return data;
}
}

尽管看起来 repository module 没有存在的意义, 但是对ViewModel而言,Webservice是透明的,所以你可以很方便的跟换获取数据的实现代码。

Note: error status 处理,seeAddendum: exposing network status.

处理 components 之间的依赖:

这里使用Dagger 2处理依赖

3. 连接 ViewModel 和 repository

UserProfileViewModel

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;
}
}

4. 数据缓存

为UserRepository 添加一个缓存,避免重复的网络请求。

@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;
}
}

5. 数据持久化

为了防止app被kill后重复请求数据,我们需要数据持久化。

为了处理数据持久化过程中的数据不一致的问题,新轮子出现了——Room

Room
一个ORM库,旨在提供访问持久层能力的同时减少一些boilerplate code. 另外,它可以在编译期验证query的正确性。 Room 隐藏了SQL表和查询的底层细节。同时,还通过暴露LiveData 提供了观察数据库数据改动的能力。除此之外,它还限制了在主线程访问数据的这种常见失误。

Note: 没必要特地更换项目里的ORM库,你可以继续使用SQLite ORM 或者Realm

要使用Room, 我们先要定义本地Schema.

@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 {
}

MyDatabase 这里被声明为 abstract. Room 会自动的提供各种实现. See Room

为了插入UserData,还得创建一个DAO(data access object)

@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);

@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}

然后在 database 类中引用 DAO。

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

load()方法返回的LiveData<User>可以方便的通知observers数据发生了变动。

Note: Room还处于 alpha 1 release, 由于 Room 是基于数据库的修改来检查脏数据,所以它有可能会误报.

UserRepository

@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);
// return a LiveData directly from the database.
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());
}
});
}
}

由于Repository的抽象,你不需要更改UserProfileViewModel的实现,而且在测试UserProfileViewModel时,你可以提供一个FakeUserRepository

到此代码就完成了。

Single source of truth

确保App内部的数据来源是统一的,避免数据冲突

6. 如何测试

如何分别测试各个Module(略)

7. 最终架构图

Guiding principles


这里是 google 官方总结的一些建议

  • manifest 里定义的入口 - activities, services, broadcast receivers, etc. - 不应该保存数据源. 他们的角色应该是一个协调者,这些组件生命周期随着用户交互可能很短。
  • 定义各个modules的职责时一定不要仁慈。不要在一个类里面做过多的事。
  • module之间尽可能少的暴露,不要为了一时的方便暴露内部实现细节,最终一切技术债都会加倍的偿还的。
  • 在设计modules之间的交互时,考虑下如何让modules可以方便的隔离、测试.
  • 一个App的核心竞争力是它与众不同的地方,不要试图重新发明轮子,不要写重复的代码。集中精力让app变得唯一。剩下的交给现成的框架、轮子。
  • 尽可能的持久化数据,保证离线可用,考虑下低网速的用户体验。
  • repository 应该总是使用唯一的数据来更新。

附录: exposing network status


这一章讨论如何处理network status

Resource 封装了data和state。

Below is a sample implementation:

//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是一个Helper类。负责决策数据来源于disk还是network。

  1. 观察database的数据变动,并判断数据库的数据是否足够好来决定dispatch数据、以及fetch from network。

  2. 如果network返回数据成功,首先将数据存储到database,然后重新初始化stream,重新开始读取流程。如果网络失败,直接dispatch 失败。

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
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}

注意:

  1. ResultTypeRequestType全部泛型化,是为了灵活的处理Api返回的数据类型和本地的数据类型
  2. ApiResponse只是Retrofit2.Call的简单封装,用来将response转化为LiveData
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();
}
}

最后,在UserRepository中使用NetworkBoundResource

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();
}
}