最近在优化重构移动会控及企业通讯录模块,使用androidx组件对相关业务进行重构,总结了一些相关知识点及踩的一些坑,以备后查。
Android 架构组件是一组库,可帮助您设计稳健、可测试且易维护的应用。具有以下优点:
1.提供健壮的架构模型,为开发稳健的应用提供保证。
2.提供生命周期管理,在配置更改后继续有效、避免内存泄漏,以及轻松加载数据到界面中。
Android架构组件主要包括LiveData、ViewModel、Room、Lifecycle等组件。
为了开发健壮的应用,必须遵循以下原则
1.分离关注点
避免将业务代码放到Activity或者Fragment中处理,界面的类应仅包含处理界面和操作系统交互的逻辑,尽量保持界面的简单,这样可以避免许多与生命周期相关的问题。
2.通过模型驱动界面
典型的是数据驱动模型,模型是负责处理应用数据的组件。它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响。界面元素状态变化必然和某个数据结构相关联,所以可以通过维护数据的变化来驱动界面元素的更新。典型的设计模式是观察者模式。在Androidx组件中具备这种特性的是LiveData组件,一个类似RxJava的响应式编程框架。
使用androidx组件如何构建应用呢?盗一张官方架构图加以说明
这张图很清晰地阐述了基于androidx构建应用的做法,这是一个典型的MVVM架构图,View层就是Activity/Fragment, ViewModel层通过组件ViewModel和LiveData实现, Model层抽象为一个Repo,Repo包含LocalDataSource和RemoteDataSource。Repo的引入遵循了分离关注点的原则,将持久化数据放到Repo中管理,这里持久化数据不限于Db及Network Api,还可以扩展到SharePreference、与App生命周期一致的数据结构、缓存数据、状态数据结构等。
为了构建健壮可维护的应用,提供几点最佳做法的建议
详细的应用架构指南可以参考Google官方文档
https://developer.android.google.cn/jetpack/docs/guide#addendum
集成相关组件到应用,请参考集成指南
https://developer.android.google.cn/topic/libraries/architecture/adding-components
下面总结的是一些官方文档中没有的或者不够深入的,
如果之前没有接触过LiveData,请查看官方指南
https://developer.android.google.cn/topic/libraries/architecture/livedata
下面以一个简单的登录模型说明androidx组件的配合使用,示例代码如下:
public class LoginActivity extends AbsLifecycleActivity<UserViewModel>{
@Override
protected void initWidgetData() {
//1. 触发登录请求
loginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mViewModel.setLoginParams(new LoginParams(mobile, password));
}
});
}
@Override
protected void dataObserver() {
//2. 订阅登录状态
mViewModel.ownerInfo.observe(this, new Observer<Resource<Owner>>() {
@Override
public void onChanged(Resource<Owner> ownerResource) {
switch (ownerResource.status) {
case SUCCESS:
onLoginSuccess(ownerResource.data);
break;
case LOADING:
break;
case ERROR:
BsErrorCode errorCode = ownerResource.errorCode;
if (errorCode != null)
onLoginFailed(errorCode.code);
break;
}
}
});
}
}
public class UserViewModel extends AbsViewModel<ContactsRepository> {
private MutableLiveData<LoginParams> _loginParams = new MutableLiveData<>();
public LiveData<Resource<Owner>> ownerInfo = Transformations.switchMap(_loginParams, new Function<LoginParams, LiveData<Resource<Owner>>>() {
@Override
public LiveData<Resource<Owner>> apply(LoginParams loginParams) {
Logger.d(tag(), "apply loginParams to owner, loginParams = " + loginParams);
if (loginParams == null) {
return AbsentLiveData.create();
} else {
return remoteDataSource.reqLoginServer(loginParams);
}
}
});
public void setLoginParams(LoginParams loginParams) {
LoginParams curLoginParams = _loginParams.getValue();
if (curLoginParams == null || !curLoginParams.equals(loginParams)) {
_loginParams.setValue(loginParams);
}
}
}
整个过程简单来说就是更改引起变化,这里登录参数的更改会触发登录结果的变化,本例中就是LoginParams的更改引起登录结果Owner信息的变化。通过mViewModel.setLoginParams
触发更改,通过mViewModel.ownerInfo.observe
观察结果变化。
LiveData基本使用包括创建、观察、更新等,请参考官方指南
https://developer.android.google.cn/topic/libraries/architecture/livedata
LiveData类图组成如下
注意Livedata中两个重要方法setValue
、postValue
的区别,setValue
只能在主线程中调用,postValue
可以在子线程调用,结果会在主线程中回调。
LiveData变换主要有两种变换:map和switchMap,都是Transformations类提供的。类似RxJava中的map与flatMap,map变换直接修改返回的值和类型,switchMap操作需要返回一个LiveData对象。
具体实现可以参考Transformations
类。
LiveData<User> userLiveData = ...;
LiveData<String> userFullNameLiveData =
Transformations.map(
userLiveData,
user -> user.firstName + user.lastName);
});
swithMap
使用参考上面示例代码UserViewModel
类。
MediatorLiveData 是 LiveData 的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。注意,当我们完成了对原始的 LiveData 源对象的观察,一定通过removeSource方法将源移除。
参考MediatorLiveData
类。
LiveData<Integer> liveData1 = ...;
LiveData<Integer> liveData2 = ...;
MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData1, new Observer<Integer>() {
private int count = 1;
{@literal @}Override public void onChanged(@Nullable Integer s) {
count++;
liveDataMerger.setValue(s);
if (count > 10) {
liveDataMerger.removeSource(liveData1);
}
}
});
Room是官方提供的db的crud框架,使用apt技术提供简单的接口可供调用,返回多种类型的数据, 提供接口类型的API给App使用,隐藏掉对Cursor的操作。这种设计很优秀,但是缺点也是显而易见,调试起来比较费时间。
官方指南参考下面链接
使用 Room 将数据保存到本地数据库
https://developer.android.google.cn/training/data-storage/room/index.html
Room 持久性库
https://developer.android.google.cn/topic/libraries/architecture/room
Android Room library详解
http://note.youdao.com/noteshare?id=7ea9c11c7f1f62f041556c4ca35c1849
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> queryUser(String userId);
@Query("SELECT * FROM user WHERE id = :userId")
User queryUserSync(String userId);
@Query("SELECT * FROM user")
Cursor queryAllUserCursor();
Tips:
@Query("SELECT _id, id, name, mobile AS summary, pinyin, 0 AS type FROM user WHERE name LIKE '%' || :keyword || '%' OR mobile LIKE '%' || :keyword || '%' OR firstletter LIKE '%' || :keyword || '%' ORDER BY name COLLATE LOCALIZED ASC")
Cursor searchUsers(String keyword);
SQLiteDatabase
提供的相关接口进行操作。RoomDatabase
的getSupportDataBase()
方法获取SupportSQLiteDatabase
实例,SupportSQLiteDatabase
SQLiteDatabase
的包装,所以有相似的方法可以调用。@Database(
//该db中存在4张表 owner、enterprise、department 、user
entities = {Owner.class, Enterprise.class, Department.class, User.class},
version = 1 //数据库版本号
//exportSchema = true
)
public abstract class ContactsDb extends RoomDatabase {
//interface调用,apt提供实现
public abstract UserDao userDao();
//interface调用,apt提供实现
public abstract OwnerDao ownerDao();
//获取db单例
public static synchronized ContactsDb getInstance(Context context) {
if (sInstance == null) {
sInstance = Room
.databaseBuilder(context.getApplicationContext(), ContactsDb.class, "cmcc_contact.db")
//.addMigrations(MIGRATION_1_2, MIGRATION_2_10, MIGRATION_10_11, MIGRATION_11_15,MIGRATION_15_16) //提供数据库版本升级
.fallbackToDestructiveMigration() //数据库降级时候,删除table,重新创建
.build();
}
return sInstance;
}
}
//升级代码处理
private static final Migration MIGRATION_2_10 = new Migration(2, 10) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//database.execSQL("ALTER TABLE " + User.TABLE_NAME + " ADD " + User.COLUMN_STARRED + " INTEGER NOT NULL DEFAULT(0);");
Logger.d(TAG,"invoke migrate from 2->10");
}
};
考虑到通讯录首次登录后需要保存企业通讯录到本地数据库,所以考虑下面网络请求决策图设计
它首先观察资源的数据库。首次从数据库中加载条目时,NetworkBoundResource 会检查查询结果决定是分派当前结果,还是应从网络中重新获取。请注意,考虑到您可能会希望在通过网络更新数据的同时显示缓存的数据,这两种情况可能会同时发生。
如果网络调用成功完成,它会将响应保存到数据库中并重新初始化数据流。如果网络请求失败,NetworkBoundResource 会直接分派失败消息。
下面以ContactsRemoteDataSource
类中请求企业信息并保存到数据库中为例说明具体的流程。
public class ContactsRemoteDataSource extends AbsRepository implements IRemoteDataSource {
public LiveData<Resource<Enterprise>> reqEnterpriseInfo(final String enterpriseId, final String ownerMobile){
return new NetworkBoundResource<Enterprise, QueryEnterpriseResponse>(appExecutors){
@Override
protected void onFetchFailed() {
Logger.e(TAG,"fetch enterprise info failed.");
}
@Override
protected void saveCallResult(QueryEnterpriseResponse item) {
Logger.i(TAG,"fetch enterprise info success and save it.");
Enterprise enterprise = ...;
mContactsDb.enterpriseDao().insertEnterpriseSync(enterprise);
}
@Override
protected boolean shouldFetch(Enterprise data) {
return data == null;
}
@Override
protected LiveData<Enterprise> loadFromDb() {
return mContactsDb.enterpriseDao().loadEnterpriseAsyn();
}
@Override
protected LiveData<ApiResponse<QueryEnterpriseResponse>> createCall() {
return mContactsService.reqEnterpriseInfo(enterpriseId);
}
}.asLiveData();
}
}
public abstract class NetworkBoundResource<ResultType, RequestType> {
@MainThread
public NetworkBoundResource(AppExecutors appExecutors) {
mAppExecutors = appExecutors;
result.postValue(Resource.loading((ResultType) null, null));
//room中做了异步处理,这里不会阻塞主线程
final LiveData<ResultType> dbSource = loadFromDb();
//观察者模式,加入一个源数据,等待源数据变化,从而触发Observer中的回调方法
result.addSource(dbSource, new Observer<ResultType>() {
@Override
public void onChanged(ResultType data) {
if(data instanceof Owner || data instanceof Enterprise)
Log.d( TAG, "owner | enterprise info load from db = "+data);
result.removeSource(dbSource);
if(shouldFetch(data)){
fetchFromNetwork(dbSource);
}else{
result.addSource(dbSource, new Observer<ResultType>() {
@Override
public void onChanged(ResultType newData) {
setValue(Resource.success(newData));
}
});
}
}
});
}
private AppExecutors mAppExecutors;
private MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
protected void onFetchFailed(){ }
public LiveData<Resource<ResultType>> asLiveData(){
return result;
}
@WorkerThread
protected RequestType processResponse(ApiResponse.ApiSuccessResponse<RequestType> response){
return response.body;
}
@WorkerThread
protected BsErrorCode getNetErrorCode(RequestType item){
return BsErrorCode.success();
}
@WorkerThread
protected abstract void saveCallResult(RequestType item);
@MainThread
protected abstract boolean shouldFetch(ResultType data);
@MainThread
protected abstract LiveData<ResultType> loadFromDb();
@MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
}
请注意有关NetworkBoundResource
定义的以下重要细节:
ResultType
和 RequestType
),因为从 API 返回的数据类型可能与本地使用的数据类型不匹配。ApiResponse
的类。ApiResponse
是 Retrofit2.Call
类的一个简单封装容器,可将响应转换为 LiveData
实例。在上面的示例代码中,Resource是一个带有状态的结果数据,可以描述整个异步请求的过程,具体代码如下:
public class Resource<T> {
public Status status;
public T data;
public String message;
private Resource(Status status, T data, String message){
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(T data){
return new Resource<>(Status.SUCCESS, data, "load success");
}
public static <T> Resource<T> error(String msg, T data){
return new Resource<>(Status.ERROR, data, msg);
}
public static<T> Resource<T> loading(T data, String loadMsg){
String loadMessage = TextUtils.isEmpty(loadMsg)?"loading":loadMsg;
return new Resource<>(Status.LOADING, data,loadMessage);
}
@Override
public String toString() {
return "Resource{" +
"status=" + status +
", data=" + data +
", message='" + message + '\'' +
'}';
}
}
在androidx架构有一个很重要的概念,那就是repository,repository可以理解为管理持久数据的仓库,是界面数据状态的唯一可信源,无论是从磁盘加载的数据还是缓存的数据都可以在这一层管理,切忌在View及ViewModel或者Presenter创建某个数据的多个副本或者多个引用,否则造成数据管理的混乱。
repository类图设计如下:
这里使用简单的外观模式将整个模块业务抽象成一个Repo,Repo中包含本地数据源与远端数据源,本地数据源可以理解为需要持久化或者与应用生命周期一致的数据,例如Db、sharePref、App静态数据等,远端数据是指从服务器获取的或者需要提交的服务器的数据,例如Http Api接口。
Repo采用单例模式注入
public class Injection {
private static IContactsRepo mRepo;
public static IContactsRepo providerContactsRepo(@Nullable Context context){
if(mRepo == null){
synchronized (Injection.class){
if(mRepo == null){
ILocalDataSource localDataSource = ContactsLocalDataSource.getInstance();
IRemoteDataSource remoteDataSource = ContactsRemoteDataSource.getInstance();
IContactsRepo contactsRepo = ContactsRepository.getInstance(localDataSource,remoteDataSource);
contactsRepo.init(context);
mRepo = contactsRepo;
}
}
}
return mRepo;
}
public static BaseSchedulerProvider provideSchedulerProvider() {
return SchedulerProvider.getInstance();
}
}
本地数据源及远端数据源获取
IContactsRepo contactsRepo = Injection.providerContactsRepo(this);
ILocalDataSource localDataSource = contactsRepo.getLocalDataSource();
IremoteDataSource remoteDataSource = contactsRepo.getRemoteDataSource();
ViewModel的创建及销毁、Repo的创建可以封装到统一的基类中进行管理。
AbsLifecycleActivity
类
public abstract class AbsLifecycleActivity<T extends AbsViewModel> extends BaseActivity {
protected T mViewModel;
public AbsLifecycleActivity() {
}
@Override
public void initViews(Bundle savedInstanceState) {
//反射方式创建ViewModel实例
mViewModel = VMProviders(this, (Class<T>) TUtil.getInstance(this, 0));
dataObserver();
}
protected <T extends ViewModel> T VMProviders(FragmentActivity fragment, @NonNull Class modelClass) {
return (T)ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()).create(modelClass);
}
//观察ViewModel中的LiveData类型数据
protected void dataObserver() {
}
}
AbsViewModel
类
public class AbsViewModel<T extends AbsRepository> extends AndroidViewModel {
protected T mRepository;
public AbsViewModel(@NonNull Application application) {
super(application);
mRepository = providerRepo();
}
//通过反射创建实例
protected T providerRepo(){
return TUtil.getNewInstance(this, 0);
}
//onDestory时做清理工作
@Override
protected void onCleared() {
super.onCleared();
if (mRepository != null) {
mRepository.unDisposable();
}
}
}
androidx组件提供一套响应式框架,提供生命周期管理,渗入了先进的编程思想在里面,上面的内容只是主要的一部分内容,还有其他一些组件,比如WorkManager、Paging、navigation等,希望大家可以尝试使用。
Android Jetpack 使用入门
https://developer.android.google.cn/jetpack/docs/getting-started
使用生命周期感知型组件处理生命周期
https://developer.android.google.cn/topic/libraries/architecture/lifecycle
Android框架组件–LiveData的使用
https://blog.csdn.net/u011810352/article/details/81334339
使用 Room 将数据保存到本地数据库
https://developer.android.google.cn/training/data-storage/room/index.html
ViewModel 概览
https://developer.android.google.cn/topic/libraries/architecture/viewmodel
Sunflower Github示例
https://github.com/android/sunflower