该手册针的目标人群是早已晓得怎样建构简单的app,而且希望了解建立强壮的产品级app的最佳实践和推荐构架。
app开发者面临的困局
不同于大部份的传统桌面应用只有一个入口,但是作为一个整体的进程运行,Androidapp有愈加复杂的结构。一个典型的app由多种组件构成,包括activity,fragment,service,contentprovider和broadcastreceiver。
几乎所有的组件都在app清单上面进行申明,这样Android系统就才能决定怎样将该app整合到整体的用户体验中来。一个优良的app须要在用户的工作流或则任务切换中应对自如。
当你想要在社交网路app中分享一张合照时会发生哪些?app触发了一个拍照机intent,之后系统启动单反app来响应。这个时侯用户离开了社交网路app,但是体验没有被打断。单反app也可能触发其他的intent,例如打开了一个文件选择器。最终用户返回社交app完成了相片分享。其实分享的中途可能会进来一个电话打断了这个过程,但是在通话结束后还是继续。
Android中应用跳转是很常见的,因此你的app要能正确应对。仍然要记住的是,联通设备是资源有限的,因此操作系统可能在任何时侯杀害一些app便于给其他app挪位子。
这儿的要点是app中的组件可能被单独启动而且没有固定次序,还可能在任何时侯被用户或则系统销毁。由于app组件是朝生暮死的,它们的生命周期不受你的控制,为此你不应当把数据或则状态存到app组件上面而且组件之间不应当互相依赖。
构架的通常原则
假如app组件不能拿来储存数据和状态,这么app应当怎样组织?
首要的一点就是要做到分离关注点(separationofconcerns)。一个常见的错误做法是将所有的代码都讲到Activity或则Fragment中。任何与UI无关的或则不与系统交互的代码都不应当放在这种类上面。尽量保持这一层代码很薄能防止好多跟生命周期相关的问题。请记住你并不拥有这种类,它们仅仅是系统和你的app沟通的胶带类(glueclass)。系统按照用户交互或则低显存的情况在任意时刻销毁它们。因而坚实的用户体验绝不应当依赖于此。
其次你应当使用model来驱动UI,最好是持久化的model。首推持久化的缘由有二,app在任何时侯都不会遗失用户数据虽然是在系统销毁app释放资源的时侯,但是app在网路状况不佳的时侯也能工作正常。model是app上面负责处理数据的组件,与app的view和其他组件独立,因此不受这种组件生命周期的影响。保持UI的代码简单而且不包含应用逻辑促使代码愈发容易管理。基于定义良好的model来创建app,这样促使app更具有可测性和一致性。
推荐的app构架
接出来我们将通过一个用例来展示怎样使用构架组件来组织一个app。
假定我们要创建一个UI拿来展示用户信息,用户信息通过RESTAPI从后台获取。
创建用户界面
UI由UserProfileFragment.java和布局文件user_profile.xml构成。
要驱动UI,我们的数据model要持有两个数据元素。
-用户ID:用户的标示符。最好是使用fragment参数传递该信息。假如系统销毁了进程,该信息会被保留,当app重启后再度可用。
-用户对象:一个持有用户数据的POJO。
我们将基于ViewModel类创建一个类UserProfileViewModel用于保持这种信息。
一个ViewModel用于给特定的UI组件提供数据,比如fragment或则activity,而且负责和数据处理的业务逻辑部份沟通,如通知其它组件加载数据或则转发用户更改。ViewModel并不晓得特定的View而且也不受配置修改的影响,例如旋转设备造成的activity重启。
如今我们有三个文件。
-user_profile.xml
-UserProfileViewModel.java
-UserProfileFragment.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 );
}
}
注意:上例中继承了LifecycleFragment而不是Fragment类,当构架组件中的lifecycleAPI稳定后,Android支持库中的Fragment将会实现LifecycleOwner。
怎么联接三个代码模块呢?虽然当ViewModel的user域的数据打算妥当后,我们须要一种方法来通知UI。这就轮到LiveData类出场了。
LiveData是可观测数据(observabledata)的持有者。app中的组件订阅LiveData的状态变化,不须要显示的定义依赖。LiveData同样能应付app组件(activity,fragment,service)的生命周期因而杜绝显存泄露。
注意:假如你已然使用了RxJava或则Agera这样的库,可以继续作为LiveData的取代使用。假如使用这种取代库,请确保在相关LifecycleOwner停止的时侯暂停数据流但是在LifecycleOwner被销毁时销毁数据流。你同样可以通过添加android.arch.lifecycle:reactivestreams依赖将LiveData和其他的响应式流库配合使用(如RxJava2)。
如今将UserProfileViewModel中的User域替换成LiveData,这样fragment能够在数据变化时收到通知。LiveData的用处是它能应对生命周期的变化,在引用不再使用时手动清除。
public class UserProfileViewModel extends ViewModel { ... private User user; private LiveData user; public LiveData getUser () { return user;
}
}
之后更改UserProfileFragment便于订阅数据和更新UI
@Override public void onActivityCreated (@Nullable Bundle savedInstanceState) { super .onActivityCreated(savedInstanceState);
viewModel.getUser().observe( this , user -> { // update UI });
}
用户数据一旦变化,onChanged都会被反弹,UI才能得到刷新。
假如你熟悉其他库中的可订阅式反弹,你可能还会意识到我们没有在fragment的onStop方式中停止订阅数据。对于LiveData这是不须要的,由于它能处理生命周期,这意味着只有在fragment处于活动状态时(onStart和onStop之间)就会被反弹。假如fragment被销毁(onDestroy),LiveData会手动将之从订阅列表中移除。
我们同样也不须要处理系统配置变化(比如旋转屏幕)。ViewModel会在配置变化后手动恢复,新的fragment会得到相同的一个ViewModel实例而且会通过反弹得到现有的数据。这就是ViewModel不应当直接引用View的缘由,它们不受View生命周期的影响。
获取数据
如今早已将ViewModel联接到了fragment,之后ViewModel从那里获取数据呢?本例中,我们假定后台提供一个RESTAPI,我们使用Retrofit库来访问后台。
下边是retrofit拿来和后台沟通的Webservice插口:
public interface Webservice { @GET ( "/users/{user}" )
Call getUser( @Path ( "user" ) String userId);
}
简单的做法就是ViewModel直接通过Webservice获取数据之后形参给user对象。这样做其实没错,但是app会显得不容易维护,因而这违背了单一职责原则。除此之外,ViewModel的作用域紧密联系到了activity或则fragment生命周期,生命周期结束后数据也丢掉虽然不是好的用户体验。为此,我们的ViewModel会将这项工作委托给一个新的模块Repository。
Repository模块负责处理数据操作。它向app提供了简约的API。它们负责到各类数据源获取数据(持久化数据,web服务,缓存等)。
public class UserRepository { private Webservice webservice; // ... public LiveData getUser ( int userId) { // This is not an optimal implementation, we'll fix it below final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue( new Callback() { @Override public void onResponse (Call call, Response response) { // error case is left out for brevity data.setValue(response.body());
}
}); return data;
}
}
repository模块看似没有必要,但是它给app提供了各类数据源的具象层。现今ViewModel并不晓得数据是来自Webservice,也就意味着这种实现在必要时可以替换。
管理各组件之间的依赖
UserRepository类须要一个Webservice实例来完成工作。可以直接创建,但是Webservice也有依赖。并且UserRepository可能不是惟一使用Webservice的类,假如每位类都创建一个Webservice,这么就导致了代码重复。
有两种形式可以解决这个问题:
-依赖注入:依赖注入可以促使类申明而不创建依赖。在运行时,另外的一个类负责提供这种以来。我们推荐使用Dagger2来完成依赖注入。Dagger2在编译时审查依赖树因而手动创建依赖。
-服务定位:服务定位提供了一个注册点,须要任何依赖都可以索要。实现上去比依赖注入简单,因此假如不熟悉DI,可以使用servicelocator取代。
这种模式都对代码的扩充提供了便利,既对依赖进行了清晰的管理又没有引入重复代码和额外的复杂度。还有另外一项用处就是易于测试。
在本例中,我们使用Dagger2进行依赖管理。
联接ViewModel和repository
public class UserProfileViewModel extends ViewModel { private LiveData 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 getUser () { return this .user;
}
}
缓存数据
里面的repository实现仅仅具象了webservice的调用,依赖单一的数据源,因此不是很实用。
问题在于UserRepository获取了数据并且没有暂存上去,这样当用户离开UserProfileFragment再回到该界面时,app要重新获取数据。这样做有两个缺点:浪费了网路带宽和用户时间。为了解决这个问题,我们给UserRepository添加了另一个数据源拿来在显存中缓存User对象。
@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 getUser (String userId) {
LiveData cached = userCache.get(userId); if (cached != null ) { return cached;
} final MutableLiveData 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() { @Override public void onResponse (Call call, Response response) {
data.setValue(response.body());
}
}); return data;
}
}
持久化数据
在当前的实现中,假如用户旋转了屏幕或则暂停并再度步入app,因为有缓存的存在,UI会立刻渲染出数据。但是当用户离开app后几个小时系统杀害了该进程,这个时侯用户返回到app会发生哪些呢?
根据当前的实现,app会再度从网路上获取数据。这不仅仅是糟糕的用户体验,并且也浪费联通数据流量。最合适的做法就是持久化model。如今就轮到Room持久化库上场了。
Room是一个对象映射库,提供本地数据的持久化。可以在编译期检测sql句子。准许将数据库数据的变化通过LiveData对象的方式曝露出去,并且对数据库的访问做了线程限制(不能再主线程访问)。
要使用Room,首先定义schema。将User类通过@Entity注解成数据库中的表
@Entity class User { @PrimaryKey private int id; private String name; private String lastName; // getters and setters for fields }
之后创建一个数据库类
@Database (entities = {User.class}, version = 1 ) public abstract class MyDatabase extends RoomDatabase { }
请注意MyDatabase是具象类,Room会手动提供实现。
如今我们要将用户数据插入到数据库。创建一个dataacessobject(DAO)。
@Dao public interface UserDao { @Insert (onConflict = REPLACE) void save(User user); @Query ( "SELECT * FROM user WHERE id = :userId" )
LiveData load(String userId);
}
之后在数据库类中引用DAO
@Database (entities = {User.class}, version = 1 ) public abstract class MyDatabase extends RoomDatabase { public abstract UserDao userDao ();
}
请注意,load方式返回了一个LiveData。在数据库有数据变化时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 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的数据源,UserProfileViewModel和UserProfileFragment却毫不知情,这就是具象带来的益处。因为我们可以伪造UserRepository,对UserProfileViewModel的测试也愈发便捷。
如今我们的代码完成了。虽然是用户几天后返回到同一个UI,用户信息也是即刻呈现的,由于我们早已做了持久化。与此同时,repository也会在后台更新数据。其实按照应用场景,太旧的数据你可能不会选择呈现。
在一些应用场景中,如下拉刷新,假如有网路操作正在进行,这么提示用户是很有必要的。将UI操作和实际的数据分开是有用处的,虽然数据可能由于多种缘由更新。
有两种方式处理这些状况:
-修改getUser方式返回一个包含网路操作状态的LiveData,详情见附表。
-在repository中提供一个公共方式返回User的刷新状态。假如仅仅只想在用户的显示动作(如下拉刷新)后显示状态,这么这些方法更好。
真理的惟一持有者(Singlesourceoftruth)
不同的RESTAPI端点返回相同的数据是很常见的。诸如,假如后台有另一个端点返回一个同学列表,这么同一个用户对象可能来自两个不同的API端点,仅仅是细度不同而已。假如UserRepository将Webservice的恳求结果直接返回这么UI上呈现的数据可能没有一致性,虽然来自后台的数据可能在前后两次恳求时不同。这也就是为什么在UserRepository的实现中,webservice的反弹仅仅将数据存到数据库中。之后数据库的变化会反弹LiveData对象的活动订阅者。
在这个模型中,数据库充当了真理的惟一持有者,app的其他部份通过repository访问它。
测试
之前我们提过,分离关注点的一个用处就是可测性。让我们分别瞧瞧怎样测试各个模块。
-用户插口和交互:这是惟一须要AndroidUIInstrumentationtest的地方。测试UI的最好方法就是创建一个Espresso测试。你可以创建一个fragment之后提供一个模拟的ViewModel。虽然fragment仅仅与ViewModel交互,模拟ViewModel才能完整的测试该UI。
-ViewModel:ViewModel可以使用Junit。仅仅须要模拟UserRepository。
-UserRepository:同样使用JUnit进行测试。你须要模拟Webservice和DAO。你可以测试是否进行了正确的webservice恳求,之后将恳求结果存入数据库而且在本地数据有缓存的情况下不进行多余的恳求。由于Webservice和UserDao都是插口,你可以随便模拟它们进行更复杂的测试用例。
-UserDao:推荐使用instrumentation测试。由于这种instrumentation测试不须要UI,所有会运行得很快。对于每位测试你都可以创建一个显存中的数据库。
-Webservice:测试应当与外界独立,因而Webservice的测试也不应当恳求实际的后台。诸如,MockWebServer库可以用于模拟一个本地服务器便于进行测试。
-测试套件:构架组件提供了两个JUnit规则(InstantTaskExecutorRule和CountingTaskExecutorRule),都包含在android.arch.core:core-testing这个mavenartifact中。
最终的构架图示
附表:曝露网路状态
//a generic class that describes a data with a status public class Resource {
@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 Resource success (@NonNull T data) { return new Resource<>(SUCCESS, data, null );
} public static Resource error (String msg, @Nullable T data) { return new Resource<>(ERROR, data, msg);
} public static Resource loading (@Nullable T data) { return new Resource<>(LOADING, data, null );
}
}
// ResultType: Type for the Resource data // RequestType: Type for the API response public abstract class NetworkBoundResource { private final MediatorLiveData> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading( null ));
LiveData 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 dbSource) {
LiveData> 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 response) { new AsyncTask() {
@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();
} // 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 loadFromDb (); // Called to create the API call. @NonNull @MainThread protected abstract LiveData> 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> getAsLiveData () { return result;
}
}
最终的UserRepository是这样的:
class UserRepository {
Webservice webservice;
UserDao userDao; public LiveData> loadUser ( final String userId) { return new NetworkBoundResource() { @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 loadFromDb () { return userDao.load(userId);
} @NonNull @Override protected LiveData> createCall () { return webservice.getUser(userId);
}
}.getAsLiveData();
}
}