如何在自定义View里使用ViewModel
前言
ViewModel只能在Activty和Fragment里使用吗,能不能在View里使用呢?
假如我要提供一个View
,它包含一堆数据和状态,比如一个新闻列表、时刻表等。我是否可以再这个这个自定义View
里使用ViewModel
去管理数据呢?
在View中使用ViewModel
答案是肯定的!那么我们说干就干,看看到底怎么使用。
为了确保与宿主Avtivity/Fragment发生管理和便于宿主管理,我们需要使用ViewModelProvider
去创建ViewModel
,典型的使用方法如下:
ViewModelProvider(this,).get(CustomModel::class.java)
但这时就遇到了麻烦,ViewModelStoreOwner
去哪里弄,不仅没有ViewModelStoreOwner
,也没有ViewModelStore
啊。当然,你也可以打破规则,什么都不管,直接创建ViewModel
,但是我并不建议你这么做。这里我讲解一下如何老老实实的按照“规则”去使用它。
首先要获取到ViewModelStoreOwner
,有两种方法:
- 在你自定义View中实现它,并按照
ComponentActivity
的逻辑实现一遍; - 使用承载你自定义View的Activity或者Fragment的
ViewModelStoreOwner
在开始使用ViewModel之前,我们先准备一个自定义View,就弄一个简单的组合View:
class CustomView : RelativeLayout {constructor(context: Context) : super(context) {initView(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {initView(context)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {initView(context)}private fun initView(context: Context) {mViewModelStore = ViewModelStore()LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)}}
布局文件:
<RelativeLayoutandroid:gravity="center"xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv"android:layout_width="wrap_content"android:layout_height="wrap_content"/>
</RelativeLayout>
就是一个继承自RelativeLayout
的CustomView
,里面一个TextView展示一段文字。
在自定义View中实现ViewModelStoreOwner
接下来,让我们为上文中的CustomView
升级一下,为他加入ViewModelStoreOwner
。按照ComponentActivity
里的方法,我们需要实现ViewModelStoreOwner
接口,然后定义一个ViewModelStore
变量,并在销毁时清理掉所有的ViewModel
。实现后的代码如下:
/*** 实现ViewModelStoreOwner接口*/
class CustomViewWithStoreOwner : RelativeLayout, ViewModelStoreOwner {//定义ViewModelStore变量private lateinit var mViewModelStore: ViewModelStoreconstructor(context: Context) : super(context) {initView(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {initView(context)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {initView(context)}private fun initView(context: Context) {mViewModelStore = ViewModelStore()LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)val customModel = ViewModelProvider(this).get(CustomViewModel::class.java)customModel.data = "我是一个自定义控件啊"findViewById<TextView>(R.id.tv).text = customModel.data;}override fun getViewModelStore(): ViewModelStore {//接口方法实现return mViewModelStore;}override fun onDetachedFromWindow() {//View移除时清理所有的viewModelviewModelStore.clear()super.onDetachedFromWindow()}}
代码很简单,三两行注释就能理解
这是一个超级简化的实现方法。当然,如果有需求,也可以按照JetPackComponentActivity
中的方法去实现:你可以定义一个实现ViewModelStore
的父类,并持有ViewModelStore
变量。然后去继承这个父类实现你的自定义View就行了。但这个方法也有不少缺点:首先代码量变多了,其次,因为无法多继承,你的自定义View没法随心所欲的去继承其他父类了。
这种方法有个巨大的优点:自定义View销毁时,ViewModel便会立刻被销毁。但是我很不喜欢这种方法的,因为它需要每次都去实现ViewModelStoreOwner
接口,还要去管理ViewModelStore
,确实很麻烦。
使用ViewTreeViewModelStoreOwner
怎么拿到ViewModelStoreOwner
呢?贴心的JetPack早已想好了办法——ViewTreeViewModelStoreOwner
。并且Kotlin也提供了View
的扩展函数方便我们获取ViewModelStoreOwner
:
public fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =ViewTreeViewModelStoreOwner.get(this)
那么接下来,看一下我们的自定义View:
class CustomViewWithoutStoreOwner : RelativeLayout {constructor(context: Context) : super(context) {initView(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {initView(context)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {initView(context)}private fun initView(context: Context) {LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)val customModel = findViewTreeViewModelStoreOwner()?.let {ViewModelProvider(it).get(CustomViewModel::class.java)}customModel!!.data = "我是一个自定义控件啊"findViewById<TextView>(R.id.tv).text = customModel.data;}}
你以为这就完了?跑起来看一下:
完美的空指针异常!!!为什么会这样呢?我们去看一下ViewTreeViewModelStoreOwner
的源码:
public class ViewTreeViewModelStoreOwner {private ViewTreeViewModelStoreOwner() {// No instances}public static void set(@NonNull View view, @Nullable ViewModelStoreOwner viewModelStoreOwner) {view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);}@Nullablepublic static ViewModelStoreOwner get(@NonNull View view) {ViewModelStoreOwner found = (ViewModelStoreOwner) view.getTag(R.id.view_tree_view_model_store_owner);if (found != null) return found;ViewParent parent = view.getParent();while (found == null && parent instanceof View) {final View parentView = (View) parent;found = (ViewModelStoreOwner) parentView.getTag(R.id.view_tree_view_model_store_owner);parent = parentView.getParent();}return found;}
}
代码很简单,一个set和一个get。set方法就是在View里添加一个Tag,而get方法会从View的Tag里寻找ViewModelStoreOwner
,并且会不断的向上遍历所有的父View,直到发现ViewModelStoreOwner
或者没有父View为止。所以,上文中我们没有set,当然get不到ViewModelStoreOwner
,最终导致无法创建ViewModel了。
查看源码,我们发现androidx.activity.ComponentActivity
默认就实现了该方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {initViewTreeOwners();super.setContentView(layoutResID);
}private void initViewTreeOwners() {// Set the view tree owners before setting the content view so that the inflation process// and attach listeners will see them already presentViewTreeLifecycleOwner.set(getWindow().getDecorView(), this);ViewTreeViewModelStoreOwner.set(getWindow().getDecorView(), this);ViewTreeSavedStateRegistryOwner.set(getWindow().getDecorView(), this);
}
所以我们只需要继承androidx.activity.ComponentActivity
或者照葫芦画瓢,重写一些我们的setContentView
方法:
override fun setContentView(layoutResID: Int) {ViewTreeViewModelStoreOwner.set(window.decorView,this)super.setContentView(layoutResID)
}
在androidx.fragment.app.DialogFragment
和androidx.fragment.app.Fragment
也是用了ViewTreeViewModelStoreOwner.set方法。Fragment里的使用可以类比Activity参看这两个类里的官方实现。
如果此时你觉得就大功告成了,那么你就打错特错了——ViewTreeViewModelStoreOwner
是通过getParent
获取View的父类向上遍历的,如果我们的View还没有添加到View树中。我们肯定是拿不到任何东西的。所以CustomViewWithoutStoreOwner
还需要做一下调整:
class CustomViewWithoutStoreOwner : RelativeLayout {constructor(context: Context) : super(context) {initView(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {initView(context)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {initView(context)}private fun initView(context: Context) {LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)}override fun onAttachedToWindow() {super.onAttachedToWindow()val customModel = findViewTreeViewModelStoreOwner()?.let {ViewModelProvider(it).get(CustomViewModel::class.java)}customModel!!.data = "我是一个自定义控件啊"findViewById<TextView>(R.id.tv).text = customModel.data;}}
我们需要等View挂载到View Tree上之后再获取ViewModelStoreOwner
。到这里一切就大工高成了。
总结
总结一下在VIew里使用ViewModel有两种方法:
让View实现
ViewModelStoreOwner
,并由它自己管理。该方法的优点在于可以将ViewModel的生命周期和View绑定在一起,但是实现相对负责。
通过
ViewTreeViewModelStoreOwner
,使用承载View的Activity/Fragment里的ViewModelStoreOwner
。该方法的优点在于使用简单方便,而且释放了View的职责,无需View去管理ViewModel。缺点是小坑偏多,要留意Activity的生命周期和View的创建过程。不然就是竹篮打水一场空。避免以下几点会导致空指针的异常的情况:
- Activity/Fragment或者它们的父类里没有调用
ViewTreeViewModelStoreOwner.set
方法; - View还没有挂载到树上就开始调用
ViewTreeViewModelStoreOwner.get
方法(在onAttachedToWindow
之后)。
上面两种情况都会造成
findViewTreeViewModelStoreOwner
总是返回null的问题。- Activity/Fragment或者它们的父类里没有调用
如何在自定义View里使用ViewModel相关推荐
- iOS 自定义view里实现控制器的跳转
1.view里实现控制器的modal 拿到主窗口的根控制器,用根控制器进行modal需要的modal的控制器 场景:点击自定义view里的按钮实现控制器的modal UIViewController ...
- 初学Kotlin——在自定义View里的应用
什么是Kotlin Kotlin,它是JetBrains开发的基于JVM的面向对象的语言.2017年的时候被Google推荐Android的官方语言,同时Android studio 3.0正式支持这 ...
- android使用自定义,Android 自定义View的使用
在Android开发中,很多自带的View满足不了我们的要求,所有我们可以自定义View来达到自己想要的效果 自定义View其实很简单也很好学,话不多说现在开始. 第一步:我们需要新建一个JAVA类 ...
- Android初级教程初谈自定义view自定义属性
有些时候,自己要在布局文件中重复书写大量的代码来定义一个布局.这是最基本的使用,当然要掌握:但是有些场景都去对应的布局里面写对应的属性,就显得很无力.会发现,系统自带的控件无法满足我们的要求,这个时候 ...
- Autolayout_自定义View
Autolayout笔记:自定义View 如果你想在自定义View里用Autolayout进行布局的话,有下面几个点需要注意: 指定Intrinsic Content Size 区分frame和ali ...
- 自定义 View 之联系人字母索引及定位效果
博主声明: 转载请在开头附加本文链接及作者信息,并标记为转载.本文由博主 威威喵 原创,请多支持与指教. 本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/ ...
- Android开发自定义View实现数字与图片无缝切换的2048
本博客地址:http://blog.csdn.net/talentclass_ctt/article/details/51952378 最近在学自定义View,无意中看到鸿洋大神以前写过的2048(附 ...
- android 自定义viewgroup onmeasure,Android进阶——自定义View之View的绘制流程及实现onMeasure完全攻略...
引言 Android实际项目开发中,自定义View不可或缺,而作为自定义View的一种重要实现方式--继承View重绘尤其重要,前面很多文章基本总结了继承View的基本流程:自定义属性和继承View重 ...
- 自定义View之仿QQ运动步数进度效果
前言 今天接着上一篇来写关于自定义View方面的东西,我是近期在学习整理这方面的知识点,所以把相关的笔记都放到这个Android自定义View的专栏里了,方便自己下次忘记的时候能回来翻翻,今天的内容是 ...
最新文章
- 小程序生成网址链接,网址链接跳转小程序
- js 创建一条通用链表
- php %3c%3c%3c 解析常量,PHP基础知识小结1
- FlashMapManager
- 3.0的USB,我们都用错了。
- springcloud 之服务注册与发现Eureka Server
- 第3章 Python 数字图像处理(DIP) - 灰度变换与空间滤波9 - 直方图处理 - 直方图匹配(规定化)灰度图像,彩色图像都适用
- java导出数据到excel模板_springboot+jxls 根据Excel模板 填写数据并导出
- 12999元!小米MIX FOLD致敬未来尊享礼盒上线:限量100套 想买先抽签
- 移动端适配的基础知识
- 高并发架构系列:Kafka、RocketMQ、RabbitMQ的优劣势比较
- HDU 6390 GuGuFishtion(莫比乌斯反演 + 欧拉函数性质 + 积性函数)题解
- ubuntu18.4 中 mysql5.7 全完卸载与安装
- 图片文字混排的垂直居中、inline-block块元素和行内元素混排的垂直居中问题
- 汽车之家口碑数据的爬虫
- Android Tips 8
- 如何搜索英文文献综述?
- excel2007/2010中独立显示窗体的方式
- 游戏CG音效制作技巧
- Maven五分钟入门
热门文章
- 原码、反码、补码(8位二进制数)
- 魔性手游《刀剑大乱斗》源码-H5+安卓+IOS三端源码
- Java 源码学习(String)
- 机器学习笔记——学习曲线的绘制
- vue实现点击按钮,弹出对话框
- 【贴片SD Card介绍】贴片SD Card (LEILONG雷龙科技)
- DDS(Date-Distribution Service)协议解读和测试解决方案
- “我,今年32岁,存款为0“:三十不立,才是真实的人生
- 常见矩阵:对称矩阵、Hermite矩阵、正交矩阵、酉矩阵、奇异矩阵、正规矩阵、幂等矩阵
- 基于安卓Android的健身app系统