2017年的Google IO 上,Google 官方推出了一套架构组件类库,并给出了一份 App 架构指南。这个组件的主要目的就是帮助开发者方面处理Android应用组件生命周期的问题,并提供了一个SQlite 对象映射类库。利用这些组件,可以构建一个 APP 基本的架构骨架。
长期以来,Android应用层架构的选择没有约束,MVC、MVP、MVVM等各种架构方式都有开发者使用,Google 官方也没有推荐的架构模型和应用指南。开发者在处理Activity、Fragment、Service的生命周期时,需要处理很多资源回收或者状态、数据持久化的问题,而这些case在项目中也没有通用的解决方式。
通用架构准则
第一准则是『关注点隔离(Separation of concerns)』,就是依赖解耦。最常见的错误是把所有代码都写在Activity或者Fragment里。任何与操作UI和系统交互无关的代码,都不应该在Activity或Fragment类里。
第二条准则:通过Model驱动UI,最好是可持久化的Model。
持久化有两个原因:
- 当系统由于资源紧张回收掉你的应用时,或者有网络连接等问题时,用户不会丢失数据。
- Model 是负责处理应用程序数据的组件,独立于UIView和应用程序组件,因此是和应用程序组件的生命周期隔离的。这样,UI代码保持简洁,逻辑更易管理。
推荐 APP 架构
Google 通过新引入的架构组件(Architecture Components)演示构建一个App。
构建用户界面
一个显示个人信息的界面,用户数据通过一个 REST API 获取。
UI 由一个Fragment UserProfileFragment.java 文件和 layout user_profile_layout.xml 组成。
为了驱动UI,DataModel 需要持有两种数据元素:
- User ID:用户标识id,通过Fragment的参数传入Fragment,当应用被系统回收时,会被保存,下次应用重启时,id 仍是可用的。
- User object:持有User数据的POJO。
为此我们创建一个 UserProfileViewModel(继承自 ViewModel类)。
ViewModel 为特定的UI组件(Activity、Fragment)提供数据,也会处理业务数据的交互,诸如调用其他组件加载数据,转发用户的修改。ViewModel并不了解View,也不会受到 cofiguration changes的影响(例如Activity 横竖屏切换)。
3个文件:
- user_profile_layout.xml
- UserProfileFragment.java
- UserProfileViewModel.java
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);
}
}
有了ViewModel 和 Fragment 类,怎么联系在一起呢?当ViewModel中的user属性设置或者修改时,我们需要用一种方式通知UI。此时,LiveData
就派上用场了。
LiveData
是一个可被观察的数据持有者(观察者模式)。可以让 Activity, Fragment 等应用程序组件对其进行观察,并且不会在它们之间创建强依赖,LiveData 还能够自动响应各组件的声明周期事件,防止内存泄漏,从而使应用程序不会消耗更多的内存。
使用LiveData修改ViewModel类:
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
修改UserProfileFragment类,使其能够观察user和更新UI:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
每次数据变化时,onChanged
回调会被调用,更新UI。
获取数据
使用Retrofit库,访问后端的User REST API。
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
获取数据,但是这样违反了依赖解耦的第一原则。
我们引入一个Repository模块管理数据。Repository 模块负责数据处理,为应用的其他部分提供干净可靠的 API。你可以将其考虑为不同数据源(Web,缓存或数据库)和应用之间的中间层。
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;
}
}
管理组件之间的依赖
UserRepository
中需要一个WebService
实例进行数据获取的工作,不要直接new一个WebService
对象,这很容易产生重复代码,并且使复杂度上升。另外,UserRepository
可能并不是唯一一个需要WebService
的类,如果每个类都创建一个WebService
,也会造成资源浪费。
有两种方式解决这个问题:
- 依赖注入(Dependency Injection):推荐Dagger2
- 服务定位期模式(Service Locator):通过注册表的方式获取某个class 的依赖,而不是新构建一个。使用比DI简单。
这里我们使用Dagger2管理依赖。
连接ViewModel 和 Repository
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;
}
}
数据持久化
UserRepository
中有一个web数据源,一个内存缓存,当用户杀死进程后,重新启动时,会再次从网络获取数据,这可能不是一个好的体验,因此我们还需要能够持久化。
我们使用 Room
这个组件来完成持久化的任务。
Room
是一个 SQLite object 映射类库,可以方便进行数据持久化。Room
抽象出了很多常用的数据库操作,并且在编译时会验证每个查询,从而损坏的 SQL 查询只会导致编译时错误,而不是运行时崩溃。还能和LiveData
一起工作,并帮开发者处理了很多线程问题。
要使用Room,我们用@Entity
注解把User
类标识成数据库中的一张表。
@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
会自动添加实现。
现在,我们需要把数据插入数据库,创建DAO
对象:
@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();
}
load
方法会返回LiveData<User>
,Room
会知道什么时候数据库发生了变化,并自动通知所有的观察者。
修改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);
// 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());
}
});
}
}
可以看到,即便我们修改了 UserRepository
中的数据源,我们也完全不需要修改 ViewModel
和 Fragment
,这就是抽象的灵活性。同时还非常易于测试,我们可以在测试 UserProfileViewModel 时提供测试用的 UserRepository。
至此,我们的代码完成了。当用户几天后回到相同的UI页面时,也会马上看到用户信息,因我我们已经把数据持久化了。同时,Repository也会在后台更新数据。当然UI层也会根据需求决定是否显示过期的数据。
在一些场景下,例如下拉刷新,如果正在有网络请求时UI显示用户信息是很重要的。分离UI操作和实际数据是很有用处的,因为数据可以有各种原因触发更新。
测试
- UI:使用 Espresso
- ViewModel:JUnit,需要mock UserRepository
- UserRepository:JUnit,需要mock WebService和DAO
- UserDao:推荐使用instrumenttation测试,Room可以允许指定数据库实现,因此可以提供一个JUnit实现的SupportSQLiteOpenHelper
- WebServiceMockWebServer
最终架构
指导原则
- 在 manifest中定义的组件(activity、service、broadcast receiver等),都不是数据源。因为每个组件的生命周期都很短,而且取决于当前用户和设备的交互和系统的运行状况。这些都不能作为应用的数据源。
- 确保 APP 中的模块职责清晰,建立明确的责任边界。简单来说,不要把不相关的功能职责放到同一个类里实现(例如数据缓存和数据binding)。
- 模块尽量少暴露内部实现。不要为了一时方便而把不应暴露的实现暴露出去,在后期这会导致越来越多的技术债。
- 在定义模块直接的交互时,考虑如何将每个模块尽量隔离。通过良好设计的API进行交互。例如,把网络和数据库的数据逻辑放在一起也能使代码正常工作,但是,这很难测试。
- 你应用的核心是那些能脱颖而出的东西,不要浪费时间重复造轮子或一次次编写同样的模板代码。把精力集中在使你的应用独一无二上,把一些重复性的工作交给Android Architecture Components和其他优秀的第三方库。
- 尽可能的持久化,使应用能够在脱机模式下可用。
- Repository应该指定一个数据源作为单一的信任数据源。
原文:https://developer.android.com/topic/libraries/architecture/guide.html
GitHub: https://github.com/googlesamples/android-architecture-components