写点什么

2020 年,我这样在项目中使用 MVVM,BATJ 等企业 Android 面试知识分享

用户头像
Android架构
关注
发布于: 15 小时前


也就是说,除了我的编写的图片压缩库 [Compressor](


) 以及一个工具类库 [Android-utils](


) 之外,引入它并不会为你的项目引入更多的库。对于 EventBus,除了我们在项目中提供的一个封装之外,也不会为你引入任何类库。至于友盟,我们只不过是为你在顶层的 View 层里注入了一些事件追踪方法,也不会强迫你在项目中添加友盟依赖。也就是说,除了那两个必选的库之外,其它都是可选的。该框架的主要目的是赋能,是不是要在项目中应用完全取决于用户。


好了,接下来就详细介绍下这个类库以及正确的使用 MVVM 的姿势。

2.2 MVVM 和 Databinding 的暧昧关系

说到 MVVM,总是绕不开 Databinding. 我之前也了解过许多 MVVM 框架,很多都将 Databinding 作为了项目必需的方案。事实上,MVVM 和 Databinding 之间没有半毛钱的关系。


Databinding 提供了双向的绑定功能,因此很多框架直接将 ViewModel 注入到了 xml 里面。我个人是比较反感这种做法的。或许是早起 Databinding 不成熟的时候经常带来莫名其妙的编译错误 xxx.BR not found 以及 can't find variable 等,在使用 Databinding 的时候,我更多地通过它来获取控件并在代码中完成赋值。此外,xml 中的代码缺少编译时的检查机制,比如当你在 xml 中将 int 类型赋值给 String 类型的时候,它不会在编译期间报错。另外,在 xml 中能完成的业务逻辑有限,一个三目运算符 ?: 就已经超出了代码的宽度限制。很多时候你不得不将一部分业务放在 Java 代码里,一部分代码放在 xml 里。出现问题的时候,这就增加了排查问题的难度。记得之前在项目中使用 Databinding 的时候还出现过 UI 没有及时刷新的问题,由于年代久远,具体原因我已经记不大清。最后,使用 Databinding 还可能会拖慢项目编译的速度,如果项目比较小的话或许问题不大,但对于一个模块过百的大型项目而言,这无疑是雪上加霜。


就一个框架而言,Android-VMLib 在 Databinding 上所做的工作是赋能。我们为你提供了应用 Databinding 的能力,同时也为你提供了完全排除 Databinding 的方案。以 Activity 为例,我们提供了 BaseActivity 和 CommonActivity 两个抽象类。假如你想在项目中使用 Databinding,仿造下面的类这样传入布局的 DataBinding 类,以及 ViewModel,然后通过 binding 获取控件并使用即可:


class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {


override fun getLayoutResId(): Int = R.layout.activity_main


override fun doCreateView(savedInstanceState: Bundle?) {// 通过 binding 获取控件 setSupportActionBar(binding.toolbar)}}


假如你不想在项目中使用 Databinding,那么你可以像下面的类这样继承 BaseActivity,然后通过传统的 findViewById 来获取控件并使用:


class ContainerActivity : BaseActivity<EmptyViewModel> {


override fun getLayoutResId(): Int = R.layout.vmlib_activity_container


override fun doCreateView(savedInstanceState: Bundle?) {// 通过 findViewById 来获取控件// 或者,引入 kotlin-android-extensions 之后直接通过 id 使用控件}}


也许你看到了,我在使用 Databinding 的时候更多地将当作 ButterKinfe 来使用。我专门提供了不包含 Databinding 的能力,也是处于另一个考虑——使用 kotlin-android-extensions 之后,可以直接在代码中通过控件的 id 来使用它。如果只是想通过 Databinding 来获取控件的话,那么就没有必要使用 Databinding 的必要了。而对于确实喜欢 Databinding 的数据绑定功能的同学可以在 Android-VMLib 之上个性化封装一层。当然了,我并不是排斥 Databinding。Databinding 是一个很好的设计理念,只是对于将其大范围应用到项目中,我还是持观望态度的。

2.3 统一数据交互格式

有过后端开发经验的同学可能会知道,在后端代码中,我们通常会将代码按照层次分为 DAO、Service 和 Controler 层,各个层之间进行数据交互的时候就需要对数据交互格式做统一的封装。后端和前端之间的交互的时候也要对数据格式进行封装。我们将其推广到 MVVM 中,显然,ViewModel 层和 View 层之间进行交互的时候也应该进行一层数据包装。下面是我看到的一段代码,


final private SingleLiveEvent<String> toast;final private SingleLiveEvent<Boolean> loading;


public ApartmentProjectViewModel() {toast = new SingleLiveEvent<>();loading = new SingleLiveEvent<>();}


public SingleLiveEvent<String> getToast() {return toast;}


public SingleLiveEvent<Boolean> getLoading() {return loading;}


public void requestData() {loading.setValue(true);ApartmentProjectRepository.getInstance().requestDetail(projectId, new Business.ResultListener<ProjectDetailBean>() {@Overridepublic void onFailure(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {toast.setValue(s);loading.setValue(false);}


@Overridepublic void onSuccess(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {data.postValue(dealProjectBean(projectDetailBean));loading.setValue(false);}});}


这里为了通知 View 层数据的加载状态定义了一个 Boolean 类型的 LiveData 进行交互。这样你需要多维护一个变量,显得代码不够简洁。实际上,通过对数据交互格式的规范,我们可以更优雅地完成这个任务。


在 Android-VMLib 当中,我们通过自定义枚举来表示数据的状态,


public enum Status {// 成功 SUCCESS(0),// 失败 FAILED(1),// 加载中 LOADING(2);


public final int id;


Status(int id) {this.id = id;}}


然后,将错误信息、数据结果、数据状态以及预留字段包装成一个 Resource 对象,来作为固定的数据交互格式,


public final class Resources<T> {// 状态 public final Status status;// 数据 public final T data;// 状态,成功或者错误的 code 以及 messagepublic final String code;public final String message;// 预留字段 public final Long udf1;public final Double udf2;public final Boolean udf3;public final String udf4;public final Object udf5;


// ...}


解释下这里的预留字段的作用:它们主要用来作为数据补充说明。比如进行分页的时候,如果 View 层不仅想获取真实的数据,还想得到当前的页码,那么你可以将页码信息塞到 udf1 字段上面。以上,我对各种不同类型的基础数据类型只提供了一个通用的选择,比如整型的只提供了 Long 类型,浮点型的只提供了 Double 类型。另外,我们还提供了一个无约束的类型 udf5.


除了数据交互格式的封装,Android-VMLib 还提供了交互格式的快捷操作方法。如下图所示,



那么,使用了 Resource 之后,代码会变成什么样呢?


// View 层代码 class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {


override fun getLayoutResId(): Int = R.layout.activity_main


override fun doCreateView(savedInstanceState: Bundle?) {addSubscriptions()vm.startLoad()}


private fun addSubscriptions() {vm.getObservable(String::class.java).observe(this, Observer {when


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


(it!!.status) {Status.SUCCESS -> { ToastUtils.showShort(it.data) }Status.FAILED -> { ToastUtils.showShort(it.message) }Status.LOADING -> {/* temp do nothing / }else -> {/ temp do nothing */ }}})}}


// ViewModel 层代码 class MainViewModel(application: Application) : BaseViewModel(application) {


fun startLoad() {getObservable(String::class.java).value = Resources.loading()ARouter.getInstance().navigation(MainDataService::class.java)?.loadData(object : OnGetMainDataListener{override fun onGetData() {getObservable(String::class.java).value = Resources.loading()}})}}


这样对数据交互格式封装之后,代码是不是简洁多了呢?至于让你的代码更加简洁,Android-VMLib 还为你提供了其它的方法,请继续阅读。

2.4 进一步简化代码,优化无处不在的 LiveData

之前在使用 ViewModel+LiveData 的时候,为了进行数据交互,每个变量我都需要定义一个 LiveData,于是代码变成了下面这个样子。甚至我在有的同学那里看到过一个 ViewModel 中定义了 10+ 个 LiveData. 这让代码变得非常得难看,


public class ApartmentProjectViewModel extends ViewModel {


final private MutableLiveData<ProjectDetailBean> data;final private SingleLiveEvent<String> toast;final private SingleLiveEvent<Boolean> submit;final private SingleLiveEvent<Boolean> loading;


public ApartmentProjectViewModel() {data = new MutableLiveData<>();toast = new SingleLiveEvent<>();submit = new SingleLiveEvent<>();loading = new SingleLiveEvent<>();}


// ...}


后来我的一个同事建议我考虑下如何整理一下 LiveData,于是经过不断的推广和演化,如今这个解决方案已经比较完善——即通过 HashMap 统一管理单例的 LiveData。后来为了进一步简化 ViewModel 层的代码,我将这部分工作交给了一个 Holder 来完成。于是如下解决方案就基本成型了,


public class BaseViewModel extends AndroidViewModel {


private LiveDataHolder holder = new LiveDataHolder();


// 通过要传递的数据类型获取一个 LiveData 对象 public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) {return holder.getLiveData(dataType, false);}}


这里的 Holder 实现如下,


public class LiveDataHolder<T> {


private Map<Class, SingleLiveEvent> map = new HashMap<>();


public MutableLiveData<Resources<T>> getLiveData(Class<T> dataType, boolean single) {SingleLiveEvent<Resources<T>> liveData = map.get(dataType);if (liveData == null) {liveData = new SingleLiveEvent<>(single);map.put(dataType, liveData);}return liveData;}}


原理很简单吧。使用了这套方案之后你的代码将会变得如下面这般简洁优雅,


// ViewModel 层 class EyepetizerViewModel(application: Application) : BaseViewModel(application) {


private var eyepetizerService: EyepetizerService = ARouter.getInstance().navigation(EyepetizerService::class.java)


private var nextPageUrl: String? = null


fun requestFirstPage() {getObservable(HomeBean::class.java).value = Resources.loading()eyepetizerService.getFirstHomePage(null, object : OnGetHomeBeansListener {override fun onError(errorCode: String, errorMsg: String) {getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)}


override fun onGetHomeBean(homeBean: HomeBean) {nextPageUrl = homeBean.nextPageUrlgetObservable(HomeBean::class.java).value = Resources.success(homeBean)// 再请求一页 requestNextPage()}})}


fun requestNextPage() {eyepetizerService.getMoreHomePage(nextPageUrl, object : OnGetHomeBeansListener {override fun onError(errorCode: String, errorMsg: String) {getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)}


override fun onGetHomeBean(homeBean: HomeBean) {nextPageUrl = homeBean.nextPageUrlgetObservable(HomeBean::class.java).value = Resources.success(homeBean)}})}}


// View 层 class EyepetizerActivity : CommonActivity<EyepetizerViewModel, ActivityEyepetizerBinding>() {

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
2020 年,我这样在项目中使用 MVVM,BATJ等企业Android面试知识分享