当我们开发一个新项目,常常需要先搭建一个项目结构,就好像建房子,你把地基和骨架搭建好了,搭建完善了,你在根据不同的业务需求,大部分情况下只是在基本的架构上面做开发而已,这样大大的提高的开发效率,同时如果前期搭建了一个好的架构,会对项目的持续迭代更新和项目的代码质量有着至关重要的作用,所以了解并知道如何搭建一个通用的项目架构是非常必要了。
知识点汇总:
一:项目简介
二:MVVMHabitComponent的项目框架疑问与解答
三:MVVMHabit的项目框架疑问与解答
四:项目中可以优化的点汇总
五:项目知识点举一反三
六: 扩展阅读
一:项目简介
MVVMHabit是以谷歌DataBinding+LiveData+ViewModel框架为基础,整合Okhttp+RxJava+Retrofit+Glide等流行模块,加上各种原生控件自定义的BindingAdapter,让事件与数据源完美绑定的一款容易上瘾的实用性MVVM快速开发框架,而MVVMHabitComponent是基于MVVMHabit的基础上实现的组件化方案架构,高内聚,低耦合,代码边界清晰,每一个组件都可以拆分出来独立运行。所有组件寄托于宿主App,加载分离的各个组件,各自编译自己的模块,有利于多人团队协作开发。
项目地址:
1、https://github.com/goldze/MVVMHabit
2、https://github.com/goldze/MVVMHabitComponent
二:MVVMHabitComponent的项目框架疑问与解答
2.1、组件化项目的架构图
2.2、组件化项目的优点
2.3、如何单独运行Module项目和整体运行项目,单独运行项目时build.gradle需要配置什么?
2.4、如何执行单个Module项目和完整项目的初始化(Application初始化模块)
2.5、互不依赖的Module之间,相互跳转Activity的方式有哪些,都有什么优缺点
2.6、LiveData 与 ObservableField的区别
2.7、路由框架ARouter如何实现不同组件Activity和Fragment的项目跳转
2.8、路由框架Arouter如何实现不同组件间函数的相互调用
2.1、组件化项目的架构图
解析:组件化的架构一般分为三层:应用层,组件层,基础层。
应用层:一般实现很少的业务逻辑,通常被称为壳应用。
组件层:随着项目迭代和业务扩展,组件层的组件模块会越来越多,各个组件Module的代码也会越来越多。
基础层:如果刚开始搭建项目的架构时,最需要完善的就是基础层的代码,基础层的功能模块越完善,组件层的开发
效率就会越高,后续改的也相对较少。
2.2、组件化项目的优点
优点一:业务解耦
各个业务模块通过组件化架构,实现了解耦,不同的模块相互独立。
优点二:并行开发
在开发项目时,各个不同的组件模块可以分开开发,并且每个模块可以独立运行。
优点三:提高开发效率
由于项目模块的基础模块已经实现,各个组件的开发人员只需要针对自己的模块开发,并独立运行自己模块的代码,
,通过路由和服务外发接口,实现与其他组件的联系,大大提高了开发效率。
2.3、如何单独运行Module项目和整体运行项目,单独运行项目时build.gradle需要配置什么?
1、在gradle.properties中,重新设置isBuildModule=false。
2、if (isBuildModule.toBoolean()) {
//作为独立App应用运行
apply plugin: 'com.android.application'
} else {
//作为组件运行
apply plugin: 'com.android.library'
}
3、defaultConfig {
//如果是独立模块,则使用当前组件的包名
if (isBuildModule.toBoolean()) {
applicationId 组件的包名
}
}
4、sourceSets {
main {
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/alone/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
resources {
exclude 'src/main/alone/*'
}
}
}
}
2.4、如何执行单个Module项目和完整项目的初始化(Application初始化模块)
解析:项目中主要实现思路是:接口定义、接口实现、反射调用不同组件的接口实现类,执行初始化工作。
第一步:在基础层定义初始化接口
第二步:各个组件实现接口的实现类
第三部:在应用层通过反射调用各个组件的实现类函数。(通过单例+静态类路径封装)
2.5、互不依赖的Module之间,相互跳转Activity的方式有哪些,都有什么优缺点
方式一:隐式跳转
这是一种解决方法,但是一个项目中不可能所有的跳转都是隐式的,这样Manifest文件会有很多过滤配置,而且非常不利于后期维护。
方式二:反射
用反射拿到Activity的class文件也可以实现跳转,但是大量的使用反射跳转对性能会有影响。
方式三:路由框架(ARouter原理)
1、在每个需要对其他module提供调用的Activity中,都会声明类似下面@Route注解,我们称之为路由地址。
2、编译期时生成路由映射,路由框架会在项目的编译期通过注解处理器apt扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中,只要拿到了映射关系便能拿到Activity.class。
3、不同module之间启动Activity
4、ARouter使用
备注:因为没有相互引用,startActivity()是实现不了的,必须需要一个协定的通信方式。
2.6、LiveData 与 ObservableField的区别
一:ObservableField只有在数据发生改变时UI才会收到通知,而LiveData不同,只要你postValue或者setValue,UI都会收到通知,不管数据有无变化。
二:LiveData能感知Activity的生命周期,在Activity不活动的时候不会触发,例如一个Activity不在任务栈顶部。
三:支持Transformations,而且可以与许多架构组件 (如Room、WorkManager) 相互配合使用。
2.7、路由框架ARouter如何实现不同组件Activity和Fragment的项目跳转
解析:Arouter通过编译时动态生成路由表的方式,实现不同界面的跳转,底层实现思路:apt注解解析器、javapoat
动态生成代码、startActivity实现界面跳转的,而Fragment的跳转更多只是通过其他模块的服务外发获取不同模块
的Fragment实例,从而实现Fragment的跳转。
2.8、路由框架Arouter如何实现不同组件间函数的相互调用
实现思路主要是:
第一步:基础层的通信接口定义(提供服务接口)
第二步:组件层的接口实现
第三步:路由实现函数调用。
三:MVVMHabit的项目框架疑问与解答
3.1、BaseActivity、BaseFragment、BaseViewModel类中分别封装了什么
3.2、使用DataBinging时,会遇到四种无法编译通过的场景,分别是什么,应该如何处理
3.3、BR自动生成的类中,有四个常量,分别代表什么意思
3.4、如何使用DataBinging定义自定义控件属性
3.5、RxAppCompatActivity和RxFragment类的作用是什么
3.6、用于ViewModel与xml之间的数据绑定,执行的命令回调的实现原理
3.7、Messager类在项目中的作用是什么,实现原理是怎么样的
3.8、CaoConfig类在项目中的作用是什么,实现原理是怎么样的
3.9、Rxbus的作用是什么,实现原理是怎么样的
3.10、ContainerActivity类作用是什么,实现原理是怎么样的
3.11、项目中是如何实现Activity和Fragment的堆栈管理的(任务栈管理),Task任务栈的作用是什么
3.12、如何给不同的界面设置不同的Repository(数据获取模块Model),实现不同界面数据获取的区分
3.13、项目中是如何封装网络请求模块的
3.14、项目中是如何封装Https相关网络请求的
3.15、项目中是如何通过Rxjava实现文件的下载和下载进度的回调
3.16、项目中是如何对网络请求中常见异常进行处理的
3.17、项目中对Cookie的处理是如何实现的
3.18、项目中引入了开源框架BindingCollectionAdapter的作用是什么,实现原理是什么
3.1、BaseActivity、BaseFragment、BaseViewModel类中分别封装了什么
一:BaseActivity
1.1、在ActivityLifecycleCallbacks中,通过AppManager管理ActivityTask任务,这里我们可以发挥一下自己的
想象,可以实现更多不同界面的封装。
1.2、通过对类的泛型,实现不同界面的DataBinging和ViewModel的初始化适配,各个界面的DataBinging需要继承
ViewDataBinding,各个不同界面的ViewModel需要实现不同的AndroidViewModel,
DataBinding的初始化代码:
binding = DataBindingUtil.setContentView(this, initContentView(savedInstanceState));
binding.setVariable(viewModelId, viewModel); //关联ViewModel
binding.setLifecycleOwner(this); //支持LiveData绑定xml,数据改变,UI自动会更新
ViewModel的初始化代码:
viewModel = initViewModel();
if (viewModel == null) {
Class modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1];
} else {
//如果没有指定泛型参数,则默认使用BaseViewModel
modelClass = BaseViewModel.class;
}
viewModel = (VM) createViewModel(this, modelClass);
}
binding.setVariable(viewModelId, viewModel); //关联ViewModel
getLifecycle().addObserver(viewModel); //让ViewModel拥有View的生命周期感应
viewModel.injectLifecycleProvider(this); //注入RxLifecycle生命周期
从上面的代码中,我们看到viewModel如果为空,里面有一堆陌生的函数调用,最后也实现了viewModel的初始化,具体我们需要了解相关函数的作用。
1、getClass().getGenericSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的直接超类的 Type,然后将其转换。ParameterizedType。
2、getActualTypeArguments()
返回表示此类型实际类型参数的 Type 对象的数组。[0]就是这个数组中第一个了。简而言之就是获得超类的泛型参数的实际类型。
通过对这两个主要函数的解析,应该就能很好的理解上面的代码了吧。
public <T extends ViewModel> T createViewModel(FragmentActivity activity, Class<T> cls) {
return ViewModelProviders.of(activity).get(cls);
}
注意:
public void refreshLayout() { // 刷新布局通过
if (viewModel != null) {
binding.setVariable(viewModelId, viewModel);
}
}
1.3、注意到BaseActivity继承RxAppCompatActivity,这是Rxjava的家庭成员rxlifecycle提供的类,方便的实现Rxjava的异步任务的解绑,防止内存泄漏,有兴趣可以看看类的源码,代码并不复杂。
1.4、通过registorUIChangeLiveDataCallBack函数,注册ViewModel与View的契约UI回调事件,把不同界面中常用的一些操作封装在ViewModel中,例如:启动对话框、Toast提示、界面跳转等操作,这里我们可以根据项目进行扩展,例如添加加载,空数据,网络异常等界面。
1.5、回调函数onDestroy中,实现常规的反注册相关函数,代码如下: Messenger.getDefault().unregister(viewModel); //解除Messenger注册
if (viewModel != null) {
viewModel.removeRxBus();
}
if(binding != null){
binding.unbind();
}
1.6、我们注意到在onDestroy()中解除Messenger注册了。
二:BaseFragment
2.1、大体上BaseFragment的初始化的东西都和BaseActivity中保持一致,只是在相关的生命周期中有一点区别和调用的初始化函数稍微有点不同。
2.2、界面的Databinding界面初始化:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, initContentView(inflater, container, savedInstanceState), container, false);
return binding.getRoot();
}
2.3、在回调函数public void onViewCreated(View view, @Nullable Bundle savedInstanceState)中执行主要的界面初始化行为。
2.4、RxFragment与RxAppCompatActivity有着异曲同工的效果。
三:BaseViewModel
3.1、通过定义对象CompositeDisposable,管理RxJava,解决异步操作造成的内存泄漏。
3.2、通过弱应用对象lifecycle,注入RxLifecycle生命周期。
3.3、通过类的泛型初始化Model层的对象初始化,实现MVVM的Model层,统一模块的数据仓库,包含网络数据和本地数据。
3.4、内部类UIChangeLiveData实现BaseAcitivty和BaseFragment的通用功能的监听数据定义,代码如下:
public final class UIChangeLiveData extends SingleLiveEvent {
private SingleLiveEvent<String> showDialogEvent;
private SingleLiveEvent<Void> dismissDialogEvent;
private SingleLiveEvent<Map<String, Object>> startActivityEvent;
private SingleLiveEvent<Map<String, Object>> startContainerActivityEvent;
private SingleLiveEvent<Void> finishEvent;
private SingleLiveEvent<Void> onBackPressedEvent;
这里需要补充一下:SingleLiveEvent类也是自定义类,继承MutableLiveData类。
3.5、BaseViewModel实现了接口Consumer<Disposable>,从而使得在非ViewModel的异步任务也可以解除异步绑定事件,防止内存泄露。
这里顺便附上MVVM的架构图:
3.2、使用DataBinging时,会遇到五种无法编译通过的场景,分别是什么,应该如何处理?
解析:使用databinding其实有个缺点,就是会遇到一些编译错误,而AS不能很好的定位到错误的位置,这对于刚开始使用databinding的开发者来说是一个比较郁闷的事,下面就汇总出相关的问题。
1、绑定错误
绑定错误是一个很常见的错误,基本都会犯。比如TextView的 android:text="" ,本来要绑定的是一个String类型,结果你不小心,可能绑了一个Boolean上去,或者变量名写错了,这时候编辑器不会报红错,而是在点编译运行的时候,在AS的Messages中会出现错误提示,如下图:
解决方法:把错误提示拉到最下面 (上面的提示找不到BR类这个不要管它),看最后一个错误 ,这里会提示是哪个xml出了错,并且会定位到行数,按照提示找到对应位置,即可解决该编译错误的问题。
注意: 行数要+1,意思是上面报出第33行错误,实际是第34行错误,AS定位的不准确 (这可能是它的一个bug)。
2、xml导包错误
在xml中需要导入ViewModel或者一些业务相关的类,假如在xml中导错了类,那一行则会报红,但是res/layout却没有错误提示,有一种场景,非常特殊,不容易找出错误位置。就是你写了一个xml,导入了一个类,比如XXXUtils,后来因为业务需求,把那个XXXUtils删了,这时候res/layout下不会出现任何错误,而你在编译运行的时候,才会出现错误日志。苦逼的是,不会像上面那样提示哪一个xml文件,哪一行出错了,最后一个错误只是一大片的报错报告。如下图:
解决方法:同样找到最后一个错误提示,找到Cannot resolve type for xxx这一句 (xxx是类名),然后使用全局搜索 (Ctrl+H) ,搜索哪个xml引用了这个类,跟踪点击进去,在xml就会出现一个红错,看到错误你就会明白了,这样就可解决该编译错误的问题。
3、build错误
构建多module工程时,如出现【4.1.1、绑定错误】,且你能确定这个绑定是没有问题的,经过修改后出现下图错误:
解决方法:这种是databinding比较大的坑,清理、重构和删build都不起作用,网上很难找到方法。经过试验,解决办法是手动创建异常中提到的文件夹,或者拷贝上一个没有报错的版本中对应的文件夹,可以解决这个异常。
4、自动生成类错误
有时候在写完xml时,databinding没有自动生成对应的Binding类及属性。比如新建了一个activity_login.xml,按照databinding的写法加入<layout> <variable>后,理论上会自动对应生成ActivityLoginBinding.java类和variable的属性,可能是as对databding的支持还不够吧,有时候偏偏就不生成,导致BR.xxx报红等一些莫名的错误。
解决方法:其实确保自己的写法没有问题,是可以直接运行的,报红不一定是你写的有问题,也有可能是编译器抽风了。或者使用下面的办法
第一招:Build->Clean Project;
第二招:Build->Rebuild Project;
第三招:重启大法。
5、gradle错误
如果遇到以下编译问题:
错误: 无法将类 BindingRecyclerViewAdapters中的方法 setAdapter应用到给定类型; 需要: RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory 找到: RecyclerView,ItemBinding,ObservableList,BindingRecyclerViewAdapter<CAP#1>,ItemIds,ViewHolderFactory 原因: 推断类型不符合等式约束条件 推断: CAP#1 等式约束条件: CAP#1,NetWorkItemViewModel 其中, T是类型变量: T扩展已在方法 setAdapter(RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory)中声明的Object 其中, CAP#1是新类型变量: CAP#1从?的捕获扩展Object。
一般是由于gradle plugin版本3.5.1造成的,请换成gradle plugin 3.5.0以下版本。
3.3、BR自动生成的类中,有四个常量,分别代表什么意思
解析:项目中生成的BR类代码如下:
public class BR {
public static final int _all = 0;
public static final int adapter = 1;
public static final int toolbarViewModel = 2;
public static final int viewModel = 3;
}
解析:BR类似Android R文件,DataBinderMapperImpl 提供了布局文件layoutid到ViewDataBinding类对象的映射,主要用于加载layout返回对应的ViewDataBinding对象。
3.4、如何使用DataBinging定义自定义控件属性
第一步:在项目中的attrs.xml文件中,定义了一些通用View控件和定义不同自定义控件或系统控件的相关自定义属性,部分定义如下:
<attr name="requestFocus" format="boolean" />
<attr name="itemView" format="reference" />
<attr name="items" format="reference" />
<attr name="adapter" format="reference" />
<attr name="onScrollChangeCommand" format="reference" />
<attr name="url" format="string" />
<attr name="onTouchCommand" format="reference" />
<attr name="onClickCommand" format="reference" />
第二步:然后为不同的控件,定义其相关的自定义绑定适配器,需要重点了解BindingAdapter注解的使用,参考代码如下,图片控件的设置。
@BindingAdapter(value = {"url", "placeholderRes"}, requireAll = false)
public static void setImageUri(ImageView imageView, String url, int placeholderRes) {
if (!TextUtils.isEmpty(url)) {
//使用Glide框架加载图片
Glide.with(imageView.getContext())
.load(url)
.apply(new RequestOptions().placeholder(placeholderRes))
.into(imageView);
}
}
view的焦点发生变化的事件绑定:
@BindingAdapter({"onFocusChangeCommand"})
public static void onFocusChangeCommand(View view, final BindingCommand<Boolean> onFocusChangeCommand) {
view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (onFocusChangeCommand != null) {
onFocusChangeCommand.execute(hasFocus);
}
}
});
}
这里涉及的的自定义类:BindingCommand,用于执行的命令回调, 用于ViewModel与xml之间的数据绑定,由于需要绑定的命令回调的类型和场景比较多,项目中实现了相关类来适配不同的情况,相关具体实现解析,请继续查看后文,实现类如下:
备注:代码实现参考开源项目https://github.com/Kelin-Hong/MVVMLight。
BindingAdapter的官方概述:
BindingAdapter is applied to methods that are used to manipulate how values with expressions are set to views. The simplest example is to have a public static method that takes the view and the value to set。
BindingAdapter注解的官方代码实现:
@Target(ElementType.METHOD)
public @interface BindingAdapter {
/**
* @return The attributes associated with this binding adapter.
*/
String[] value();
/**
* Whether every attribute must be assigned a binding expression or if some
* can be absent. When this is false, the BindingAdapter will be called
* when at least one associated attribute has a binding expression. The attributes
* for which there was no binding expression (even a normal XML value) will
* cause the associated parameter receive the Java default value. Care must be
* taken to ensure that a default value is not confused with a valid XML value.
*
* @return whether or not every attribute must be assigned a binding expression. The default
* value is true.
*/
boolean requireAll() default true;
}
第三步:在布局文件中,由于之前Databinding与ViewModel就已经绑定关联上了,所以数据源可以直接从ViewModel中获取,参考代码如下:
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@{viewModel.drawableImg}"
binding:url="@{viewModel.entity.img}" />
<EditText
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:hint="请输入用户名"
android:text="@={viewModel.userName}"
android:textColor="@color/textColor"
android:textColorHint="@color/textColorHint"
android:textSize="16sp"
binding:onFocusChangeCommand="@{viewModel.onFocusChangeCommand}" />
@BindingAdapter
这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用。
3.5、RxAppCompatActivity和RxFragment类的作用是什么?
解析:Rxjava的家庭成员rxlifecycle,该开源框架可以在Rxjava实现异步任务时,防止退出界面导致的内存泄漏问题,该开源框架的实现类如下:
3.6、用于ViewModel与xml之间的数据绑定,执行的命令回调的实现原理
项目中通过定义通用的命令相关类,实现控件的操作事件以命令的形式进行传递,主要实现类代码如下:
public class BindingCommand<T> {
private BindingAction execute;
private BindingConsumer<T> consumer;
private BindingFunction<Boolean> canExecute0;
public BindingCommand(BindingAction execute) {
this.execute = execute;
}
public BindingCommand(BindingConsumer<T> execute) { //带泛型参数的命令绑定
this.consumer = execute;
}
public BindingCommand(BindingAction execute, BindingFunction<Boolean> canExecute0) {
this.execute = execute; //触发命令
this.canExecute0 = canExecute0; //true则执行,反之不执行
}
public BindingCommand(BindingConsumer<T> execute, BindingFunction<Boolean> canExecute0) {
this.consumer = execute; //带泛型参数触发命令
this.canExecute0 = canExecute0; //true则执行,反之不执行
}
public void execute() {
if (execute != null && canExecute0()) { //执行BindingAction命令
execute.call();
}
}
public void execute(T parameter) { //执行带泛型参数的命令
if (consumer != null && canExecute0()) {
consumer.call(parameter);
}
}
private boolean canExecute0() { //是否需要执行,true则执行, 反之不执行
if (canExecute0 == null) {
return true;
}
return canExecute0.call();
}
}
项目中通过定义的类如下所示:
备注:如果发现控件需要的绑定操作命令需要在当前的绑定指令类中无法满足相关控件的实现,可以根据项目的实际需要,定义相关的命令相关类,实现相关功能,这里的思考一下触摸事件的命令相关类应该如何实现。
3.7、Messager类在项目中的作用是什么,实现原理是怎么样的?
解析:Messenger是一个轻量级全局的消息通信工具,在我们的复杂业务中,难免会出现一些交叉的业务,比如ViewModel与ViewModel之间需要有数据交换,这时候可以轻松地使用Messenger发送一个实体或一个空消息,将事件从一个ViewModel回调到另一个ViewModel中。
使用方法:
public static final String TOKEN_LOGINVIEWMODEL_REFRESH = "token_loginviewmodel_refresh"; //定义一个静态String类型的字符串token
在ViewModel中注册消息监听:
//参数1:接受人(上下文)
//参数2:定义的token
//参数3:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() { //注册一个空消息监听
@Override
public void call() { }
});
//参数1:接受人(上下文)
//参数2:定义的token
//参数3:实体的泛型约束
//参数4:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new BindingConsumer<String>() { //注册一个带数据回调的消息监听
@Override
public void call(String s) { }
});
//发送一个空消息,在需要回调的地方使用token发送消息
//参数1:定义的token
Messenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);
//参数1:回调的实体
//参数2:定义的token
Messenger.getDefault().send("refresh",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH); //发送一个带数据回调消息
token最好不要重名,不然可能就会出现逻辑上的bug,为了更好的维护和清晰逻辑,建议以aa_bb_cc的格式来定义token。aa:TOKEN,bb:ViewModel的类名,cc:动作名(功能名)。
为了避免大量使用Messenger,建议只在ViewModel与ViewModel之间使用,View与ViewModel之间采用ObservableField去监听UI上的逻辑,可在继承了BaseActivity或BaseFragment中重写initViewObservable()方法来初始化UI的监听注册了监听,当然也要解除它。在BaseActivity、BaseFragment的onDestroy()方法里已经调用 Messenger.getDefault().unregister(viewModel);解除注册,所以不用担心忘记解除导致的逻辑错误和内存泄漏。
具体代码实现关键字:
1、单例模式
2、散列表存储与读取
3、函数的重载
4、散列表的清除
备注:Messager的实现参考开源项目https://github.com/Kelin-Hong/MVVMLight。
3.8、CaoConfig类在项目中的作用是什么,实现原理是怎么样的
解析:如果你的程序出现崩溃,它会检测到(各种崩溃,比如空指针),会弹出一个页面,提示你程序崩溃,你是否要关闭程序,还是重新启动程序。
实现是基于开源项目:https://github.com/Ereza/CustomActivityOnCrash
官方的概述:This library allows launching a custom activity when the app crashes, instead of showing the hated "Unfortunately, X has stopped" dialog。
项目中的实现思路关键字:
1、建造者模式
2、Thread.setDefaultUncaughtExceptionHandler()函数的使用
3、ActivityLifecycleCallbacks函数的使用
3.9、Rxbus的作用是什么,实现原理是怎么样的?
解析:RxBus并不是一个库,而是一种模式。相信大多数开发者都使用过EventBus,对RxBus也是很熟悉。由于MVVMabit中已经加入RxJava,所以采用了RxBus代替EventBus作为事件总线通信,以减少库的依赖。
使用方法:
在ViewModel中重写registerRxBus()方法来注册RxBus,重写removeRxBus()方法来移除RxBus
private Disposable mSubscription; //订阅者
@Override
public void registerRxBus() { //注册RxBus
super.registerRxBus();
mSubscription = RxBus.getDefault().toObservable(String.class)
.subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
}
});
RxSubscriptions.add(mSubscription); //将订阅者加入管理站
}
@Override
public void removeRxBus() { //移除RxBus
super.removeRxBus();
//将订阅者从管理站中移除
RxSubscriptions.remove(mSubscription);
}
RxBus.getDefault().post(object); //在需要执行回调的地方发送
3.10、ContainerActivity类作用是什么,实现原理是怎么样的
解析:一个盛装Fragment的一个容器(代理)Activity,普通界面只需要编写Fragment,使用此Activity盛装,这样就不需要每个界面都在AndroidManifest中注册一遍。
使用方法:
在ViewModel中调用BaseViewModel的方法开一个Fragment
startContainerActivity(你的Fragment类名.class.getCanonicalName())
在ViewModel中调用BaseViewModel的方法,携带一个序列化实体打开一个Fragment
Bundle mBundle = new Bundle();
mBundle.putParcelable("entity", entity);
startContainerActivity(你的Fragment类名.class.getCanonicalName(), mBundle);
在你的Fragment中取出实体:
Bundle mBundle = getArguments();
if (mBundle != null) {
entity = mBundle.getParcelable("entity");
}
3.11、项目中是如何实现Activity和Fragment的堆栈管理的(任务栈管理),Task任务栈的作用是什么
解析:项目中通过AppManager类实现了Activity和Fragment类的任务栈管理。
代码实现关键字:
1、单例模式
2、ActivityLifecycleCallbacks与FragmentLifecycleCallbacks的回调监听
3、Stack数据结构的基本使用:增、删、查、改、遍历等操作。
3.12、如何给不同的界面设置不同的Repository(数据获取模块Model),实现不同界面数据获取的区分
解析:通过AppViewModelFactory类来初始化各个ViewModel的Model实例,不同的界面可以实现不同HttpDataSourceImpl和LocalDataSourceImpl,从而实现界面数据获取的区分。
如果不同的界面需要设置不同的Model,并且各界面的网络数据模块和本地数据模块的逻辑相互分离,可以考虑各个界面单独实现ViewModel的初始化。
代码实现:
@Override
public LoginViewModel initViewModel() {
return ViewModelProviders.of(this, new ViewModelProvider.Factory() {
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LoginViewModel.class)) {
HttpDataSource httpDataSource = HttpDataSourceImpl.getInstance(RetrofitClient.getInstance().create(DemoApiService.class));
LocalDataSource localDataSource = LocalDataSourceImpl.getInstance();
return (T) new LoginViewModel(getApplication(), DemoRepository.getInstance(httpDataSource, localDataSource));
}
throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass.getName());
}
}).get(LoginViewModel.class);
}
3.13、项目中是如何封装网络请求模块的
解析:项目中通过RetrofitClient类封装网络请求,通过建造者模式初始化对象OkHttpClient okHttpClient,
Retrofit retrofit的实现网络请求参数配置,例如:拦截器的配置、Https的配置、Gson数据格式的转换配置、cookie的相关的配置等,项目在请求返回时,返回对象Observable兼容Rxjava实例。
3.14、项目中是如何封装Https相关网络请求的
解析:项目中通过HttpsUtils类,实现对对象SSLSocketFactory sSLSocketFactory和X509TrustManager trustManager的初始化,从而实现对Https网路请求的设置,而在设置这两个对象时,主要需要设置四个参数:X509TrustManager trustManager、InputStream bksFile、String password、InputStream... certificates,所以我们需要根据项目的实际情况设置实现相关参数的设置。
3.15、项目中是如何通过Rxjava实现文件的下载和下载进度的回调
解析:项目中主要通过三个类组合实现文件的下载,三个类分别是:DownLoadManager、ProgressCallBack、
DownLoadSubscriber实现,而这里面的进度回调主要通过如下代码实现:
private Source source(Source source) {
return new ForwardingSource(source) {
long bytesReaded = 0;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
bytesReaded += bytesRead == -1 ? 0 : bytesRead;
//使用RxBus的方式,实时发送当前已读取(上传/下载)的字节数据
RxBus.getDefault().post(new DownLoadStateBean(contentLength(), bytesReaded, tag));
return bytesRead;
}
};
}
mSubscription = RxBus.getDefault().toObservable(DownLoadStateBean.class)
.observeOn(AndroidSchedulers.mainThread()) //回调到主线程更新UI
.subscribe(new Consumer<DownLoadStateBean>() {
@Override
public void accept(final DownLoadStateBean progressLoadBean) throws Exception {
progress(progressLoadBean.getBytesLoaded(), progressLoadBean.getTotal());
}
});
实现思路:通过OkHttpClient设置ProgressInterceptor()拦截器实现对下载进度的监听,并通过RxBus把进度情况发送到ProgressCallBack实现类中。
3.16、项目中是如何对网络请求中常见异常进行处理的?
解析:在工具类RxUtils中,封装函数exceptionTransformer(),继而通过observable.onErrorResumeNext(new HttpResponseFunc())和Observable.error()函数获取异常信息,然后通过自定义类ExceptionHandle的静态函数
ResponseThrowable handleException(Throwable e),实现对不同的网络错误进行处理。
代码示例:
model.demoGet()
.compose(RxUtils.schedulersTransformer()) //线程调度
.compose(RxUtils.exceptionTransformer()) //网络错误的异常转换, 这里可以换成自己的ExceptionHandle
.doOnSubscribe(this)... //请求与ViewModel周期同步
3.17、项目中对Cookie的处理是如何实现的
解析:通过在okHttpClient中设置cookieJar的实现类,设置cookie的常见操作接口,并实现持久化存储和内存存储的两种方式实现对连接cookie的处理。
3.18、项目中引入了开源框架BindingCollectionAdapter的作用是什么,实现原理是什么
解析:
官方说明:Easy way to bind collections to listviews and recyclerviews with the new Android Data Binding framework。
框架解析:其实根据前面的知识,如果想要一个控件在xml配置相应的属性去支配ViewModel,立即可以想到@BindingAdapter,而改开源框架也是这么实现的,通过在类BindingCollectionAdapters中,配置List与RecyclerView中静态初始化函数,在通过xml的属性配置和ViewModel的变量传参,从而实现对ListView与RecyclerView控件在xml布局中进行相关的初始化,项目初始化的代码就在BindingCollectionAdapters类的各个今天方法中,但是,如果我们想要添加控件的灵活性,并且适配更多的使用场景,我们需要引入泛型,抽象类,接口定义等方式,实现对复杂的控件(PageView、RecyclerView等)实现灵活性,扩展性更高的属性配置。
代码的接入实例:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
binding:adapter="@{adapter}"
binding:itemBinding="@{viewModel.itemBinding}"
binding:items="@{viewModel.observableList}"
binding:layoutManager="@{LayoutManagers.linear()}"
binding:lineManager="@{LineManagers.horizontal()}" />
框架核心代码实现:
public class BindingCollectionAdapters {
// AdapterView
@SuppressWarnings("unchecked")
@BindingAdapter(value = {"itemBinding", "itemTypeCount", "items", "adapter", "itemDropDownLayout", "itemIds", "itemIsEnabled"}, requireAll = false)
public static <T> void setAdapter(AdapterView adapterView, ItemBinding<T> itemBinding, Integer itemTypeCount, List items, BindingListViewAdapter<T> adapter, @LayoutRes int itemDropDownLayout, BindingListViewAdapter.ItemIds<? super T> itemIds, BindingListViewAdapter.ItemIsEnabled<? super T> itemIsEnabled) {
if (itemBinding == null) {
throw new IllegalArgumentException("onItemBind must not be null");
}
BindingListViewAdapter<T> oldAdapter = (BindingListViewAdapter<T>) unwrapAdapter(adapterView.getAdapter());
if (adapter == null) {
if (oldAdapter == null) {
int count = itemTypeCount != null ? itemTypeCount : 1;
adapter = new BindingListViewAdapter<>(count);
} else {
adapter = oldAdapter;
}
}
adapter.setItemBinding(itemBinding);
adapter.setDropDownItemLayout(itemDropDownLayout);
adapter.setItems(items);
adapter.setItemIds(itemIds);
adapter.setItemIsEnabled(itemIsEnabled);
if (oldAdapter != adapter) {
adapterView.setAdapter(adapter);
}
}
........
}
ViewModel中的核心代码:
//给RecyclerView添加ObservableList
public ObservableList<MultiItemViewModel> observableList = new ObservableArrayList<>();
//RecyclerView多布局添加ItemBinding
public ItemBinding<MultiItemViewModel> itemBinding = ItemBinding.of(new OnItemBind<MultiItemViewModel>() {
@Override
public void onItemBind(ItemBinding itemBinding, int position, MultiItemViewModel item) {
//通过item的类型, 动态设置Item加载的布局
String itemType = (String) item.getItemType();
if (MultiRecycleType_Head.equals(itemType)) {
//设置头布局
itemBinding.set(BR.viewModel, R.layout.item_multi_head);
} else if (MultiRecycleType_Left.equals(itemType)) {
//设置左布局
itemBinding.set(BR.viewModel, R.layout.item_multi_rv_left);
} else if (MultiRecycleType_Right.equals(itemType)) {
//设置右布局
itemBinding.set(BR.viewModel, R.layout.item_multi_rv_right);
}
}
});
开源项目:https://github.com/evant/binding-collection-adapter(MVVM之列表绑定神器BindingCollectionAdapter)
四:项目中可以优化的点汇总
1、在UIChangeLiveData中,引入loadsir开源框架,实现项目通用的加载、请求异常、空数据、正常数据界面等。
2、考虑在ActivityLifecycleCallbacks中封装更多Acitivity中通用的东西(toolbar、沉浸式等)
3、BaseFragment添加懒加载逻辑
4、各个界面的Model设置模块,应该根据不同界面实现各个不同的model。
5、网络请求模块修改为RxHttp实现网络请求的进一步封装。
6、参考使用RxErrorHandler实现对网络请求异常的处理。
五:项目知识点举一反三
知识点一:MutableLiveData与MediatorLiveData的区别
解析:MutableLiveData是LiveData的子类,它公开setValue和postValue方法(第二个方法是线程安全的),因此您可以将值分配给任何活动的观察者,MediatorLiveData是MutableLiveData的子类,MediatorLiveData可以观察其他LiveData对象(源)并对它们的onChange事件作出反应,这将使您可以控制何时传播事件或进行特定操作。
知识点二:项目中的自定义SingleLiveEvent的作用是什么
背景:只要使用过一段时间的LiveData就会发现,LiveData会经常多次回调数据。我们经常碰到的这个问题,我们的ViewModel里是给Activity持有的,并且里面有一个LiveData数据,我们A_Fragment现在获得Activity的ViewModel并且注册LiveData数据成为观察者,这个时候我们setValue()就会让前台的A_Fragment得到一次LiveData数据,接下来操作A_Fragment启动B_Fragment,在返回到A_Fragment。你会发现只要再次注册LiveData的observe(this, new Observer ...),那么A_Fragment里面又会接收到一次LiveData的数据。
问题出现原因:
1、一部分原因是LiveData的机制,就是向所有前台Fragment或者Activity发送数据。只要注册的观察者在前台就必定会收到这个数据。
2、另一部分的原因是对ViewModel理解不深刻,理论上只有在Activity保存的ViewModel它没被销毁过就会一直给新的前台Fragment观察者发送数据。我们需要管理好ViewModel的使用范围。 比如只需要在Fragment里使用的ViewModel就不要给Activity保管。而根Activity的ViewModel只需要做一下数据共享与看情况使用LiveData。
解决方法:
1、就是管理好ViewModel的范围,如果业务范围只跟某个Fragment有关,那么最好就只给这个Fragment使用。这样Fragment在销毁或者创建的时候,也会销毁ViewModel与创建ViewModel,ViewModel携带的LiveData就是全新的不会在发送之前设置的数据。
2、实现类 SingleLiveEvent,其中的机制是用一个原子AtomicBoolean记录一次setValue。在发送一次后在将AtomicBoolean设置为false,阻止后续前台重新触发时的数据发送。
六:扩展阅读
1、https://github.com/goldze/MVVMHabitComponent(组件化项目结构)
2、https://github.com/goldze/MVVMHabit(DataBinding+LiveData+ViewModel项目架构)
3、https://blog.csdn.net/ican87/article/details/86612733(阿里路由框架ARouter原理分析总结)
4、https://blog.csdn.net/yu75567218/article/details/86706290(自己实现全局通信messenger)
5、https://github.com/Kelin-Hong/MVVMLight(MVVM Light Toolkit使用指南)
6、https://www.jianshu.com/p/2fc41a310f79(如何构建Android MVVM应用程序)
7、https://zhuanlan.zhihu.com/p/133949967(两步使用 LiveData 替换 Observable Field)
8、https://github.com/Ereza/CustomActivityOnCrash(Android检测程序崩溃框架)
9、https://github.com/JessYanCoding/RxErrorHandler(RxJava的错误处理库)
10、https://github.com/KunMinX/Jetpack-MVVM-Best-Practice(难得一见的Jetpack MVVM最佳实践)
11、https://www.cnblogs.com/guanxinjing/p/12669506.html(SingleLiveEvent解决LiveData或者MutableLiveData多次回调的问题)
12、https://blog.csdn.net/yu75567218/article/details/87860020(MVVM之列表绑定神器BindingCollectionAdapter)
网友评论