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 toast; final private SingleLiveEvent loading;
public ApartmentProjectViewModel() { toast = new SingleLiveEvent<>(); loading = new SingleLiveEvent<>(); }
public SingleLiveEvent getToast() { return toast; }
public SingleLiveEvent getLoading() { return loading; }
public void requestData() { loading.setValue(true); ApartmentProjectRepository.getInstance().requestDetail(projectId, new Business.ResultListener() { @Override public void onFailure(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) { toast.setValue(s); loading.setValue(false); }
@Override
public 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 { // 状态 public final Status status; // 数据 public final T data; // 状态,成功或者错误的 code 以及 message public 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 还提供了交互格式的快捷操作方法。如下图所示,
![快捷方法](https://user-gold-cdn.xitu.io/2020/5/23/1723f99e54a89a43?imageView2/0/w/1280/h/960/ignore-error/1)
那么,使用了 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(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 {
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.nextPageUrl
getObservable(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.nextPageUrl
getObservable(HomeBean::class.java).value = Resources.success(homeBean)
}
})
}
}
// View 层 class EyepetizerActivity : CommonActivity<EyepetizerViewModel, ActivityEyepetizerBinding>() {
private lateinit var adapter: HomeAdapter
private var loading : Boolean = false
override fun getLayoutResId() = R.layout.activity_eyepetizer
override fun doCreateView(savedInstanceState: Bundle?) {
addSubscriptions()
vm.requestFirstPage()
}
private fun addSubscriptions() {
vm.getObservable(HomeBean::class.java).observe(this, Observer { resources ->
loading = false
when (resources!!.status) {
Status.SUCCESS -> {
L.d(resources.data)
val list = mutableListOf<Item>()
resources.data.issueList.forEach {
it.itemList.forEach { item ->
if (item.data.cover != null
&& item.data.author != null
) list.add(item)
}
}
adapter.addData(list)
}
Status.FAILED -> {/* temp do nothing */ }
Status.LOADING -> {/* temp do nothing */ }
else -> {/* temp do nothing */ }
}
})
}
// ...
}
这里我们通过 `getObservable(HomeBean::class.java)` 获取一个 ViewModel 和 View 层之间交互的 `LiveData<HomeBean>`,然后通过它进行数据传递。这种处理方式的好处是,你不需要在自己代码中到处定义 LiveData,将 Holder 当作一个 LiveData 池,需要数据交互的时候直接从 Holder 中获取即可。
有的同学可能会疑问,将 Class 作为从“池”中获取 LiveData 的唯一标记,会不会应用场景有限呢?Android-VMLib 已经考虑到了这个问题,下文踩坑部分会为你讲解。
### 2.5 共享 ViewModel,配置可以更简单
如果多个 ViewModel 在同一 Activity 的 Fragment 之间进行共享,那么该如何获取呢?
如果是不使用 Android-VMLib,你只需要在 Fragment 中通过 Activity 获取 ViewModel 即可,
ViewModelProviders.of(getActivity()).get(vmClass)
使用了 Android-VMLib 之后这个过程可以变得更加简洁——直接在 Fragment 上声明一个注解即可。比如,
@FragmentConfiguration(shareViewModel = true) class SecondFragment : BaseFragment() {
override fun getLayoutResId(): Int = R.layout.fragment_second
override fun doCreateView(savedInstanceState: Bundle?) {
L.d(vm)
// Get and display shared value from MainFragment
tv.text = vm.shareValue
btn_post.setOnClickListener {
Bus.get().post(SimpleEvent("MSG#00001"))
}
}
}
Android-VMLib 会读取你的 Fragment 的注解并获取 shareViewModel 字段的值,并决定使用 Activity 还是 Fragment 获取 ViewModel,以此来实现 ViewModel 的共享。是不是更加简洁了呢?
### 2.6 Android-VMLib 另一优势,强大的工具类支持
我看过很多框架,它们通常会将一些常用的工具类与框架打包到一起提供给用户使用。Android-VMLib 与之相反,我们将工具类作为独立项目进行支持。这样的目的是,1). 希望工具类本身可以摆脱对框架的依赖,独立应用到各个项目当中;2). 作为单独的模块,单独进行优化,使功能不断完善。
截至目前,工具类库 [Android-Utils]( ) 已经提供了 22 个独立的工具类,涉及从 IO、资源读取、图像处理、动画到运行时权限获取等各种功能,对于该库我会在以后的文章里进行说明。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWUrCz6t-1630493681313)(https://user-gold-cdn.xitu.io/2020/5/23/1723f9a7ba5a6530?imageView2/0/w/1280/h/960/ignore-error/1)]
需要说明的是,该库在开发的过程中参考了很多其它的类库,当然我们也开发了自己的特色工具类,比如运行时权限获取、主题中属性获取、字符串拼接等等。
### 尾声
最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。
当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。
**[CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](https://codechina.csdn.net/m0_60958482/android_p7)**
**进阶学习视频**
![](https://img-blog.csdnimg.cn/img_convert/4f1488144bf0e4b377a004e20d7afd84.png)
**附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题**?(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
知道以后的路怎么走了,理论看多了总要实践的。
**[CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](https://codechina.csdn.net/m0_60958482/android_p7)**
**进阶学习视频**
[外链图片转存中...(img-hT34pmGW-1630493681315)]
**附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题**?(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
![](https://img-blog.csdnimg.cn/img_convert/4751e4d1c7abe41f7e66927ee4fbee1c.png)
|