androidx组件使用详解

1 背景

最近在优化重构移动会控及企业通讯录模块,使用androidx组件对相关业务进行重构,总结了一些相关知识点及踩的一些坑,以备后查。

2 AndroidX组件简介

Android 架构组件是一组库,可帮助您设计稳健、可测试且易维护的应用。具有以下优点:
1.提供健壮的架构模型,为开发稳健的应用提供保证。
2.提供生命周期管理,在配置更改后继续有效、避免内存泄漏,以及轻松加载数据到界面中。
Android架构组件主要包括LiveData、ViewModel、Room、Lifecycle等组件。

2.1 应用架构原则

为了开发健壮的应用,必须遵循以下原则
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生命周期一致的数据结构、缓存数据、状态数据结构等。

2.2 最佳做法

为了构建健壮可维护的应用,提供几点最佳做法的建议

  1. 避免将应用的入口点(如 Activity、Service 和广播接收器)指定为数据源。
  2. 在应用的各个模块之间设定明确定义的职责界限。
  3. 尽量少公开每个模块中的代码。
  4. 考虑如何使每个模块可独立测试。
  5. 专注于应用的独特核心,以使其从其他应用中脱颖而出。
  6. 保留尽可能多的相关数据和最新数据。
  7. 将一个数据源指定为单一可信来源。

详细的应用架构指南可以参考Google官方文档
https://developer.android.google.cn/jetpack/docs/guide#addendum

3.组件集成

集成相关组件到应用,请参考集成指南
https://developer.android.google.cn/topic/libraries/architecture/adding-components

4.androidx组件使用

下面总结的是一些官方文档中没有的或者不够深入的,
如果之前没有接触过LiveData,请查看官方指南
https://developer.android.google.cn/topic/libraries/architecture/livedata

4.1 MVVM架构示例

下面以一个简单的登录模型说明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观察结果变化。

4.2 LiveData基本使用

LiveData基本使用包括创建、观察、更新等,请参考官方指南
https://developer.android.google.cn/topic/libraries/architecture/livedata
LiveData类图组成如下
在这里插入图片描述
注意Livedata中两个重要方法setValuepostValue的区别,setValue只能在主线程中调用,postValue可以在子线程调用,结果会在主线程中回调。

4.3 LiveData变换操作map与switchMap

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类。

4.4 合并多个LiveData源

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

4.5 LiveData结合Room使用

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:

  1. Room查询操作
    如果方法返回数据类型不是LiveData类型,需要在子线程中调用,否则会报错,room框架自带db查询操作的线程处理模型。
  2. Room模糊查询
    room模糊查询语句如下,需要带"||"处理参数
@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);
  1. Room与ContentProvider
    Room提供的接口对ContentProvider操作不是很友好,对于条件查询处理比较弱,所以建议还是采用SQLiteDatabase提供的相关接口进行操作。
    通过RoomDatabasegetSupportDataBase()方法获取SupportSQLiteDatabase实例,SupportSQLiteDatabase
    是对SQLiteDatabase的包装,所以有相似的方法可以调用。
  2. Room对数据库升降级的处理
    创建Room单实例时加入数据库升降级的处理
@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");

        }
    };

4.6 网络请求框架NetworkBoundResource

考虑到通讯录首次登录后需要保存企业通讯录到本地数据库,所以考虑下面网络请求决策图设计
在这里插入图片描述
它首先观察资源的数据库。首次从数据库中加载条目时,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定义的以下重要细节:

  • 它定义了两个类型参数(ResultTypeRequestType),因为从 API 返回的数据类型可能与本地使用的数据类型不匹配。
  • 它对网络请求使用了一个名为 ApiResponse 的类。ApiResponseRetrofit2.Call 类的一个简单封装容器,可将响应转换为 LiveData 实例。

4.7 带有状态的结果数据

在上面的示例代码中,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 + '\'' +
                '}';
    }
}

4.8 数据仓库设计

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

4.9 MVVM组件管理

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Calvin880828

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值