自定义ViewGroup使用DataBinding报空指针异常如何解决
核心问题
运行应用时触发如下崩溃:
Unable to resume activity {...MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.appcompat.widget.AppCompatTextView.setTag(java.lang.Object)' on a null object reference
目标是在自定义ViewGroup中正确接入DataBinding,不触发绑定流程异常。
复现代码
1. 自定义ViewGroup对应布局
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <androidx.cardview.widget.CardView android:id="@+id/card" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/margin_8dp" android:minHeight="72dp" android:orientation="vertical" app:cardBackgroundColor="@color/white" app:cardCornerRadius="@dimen/margin_8dp" app:cardElevation="@dimen/margin_2dp" app:strokeColor="@color/color_divider" app:strokeWidth=".5dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/clRoot" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/margin_16dp"> <!-- 其他固定视图 --> <LinearLayout android:id="@+id/llContainer" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_16dp" android:orientation="vertical" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvHeader" tools:background="@color/all_stock_back" tools:layout_height="200dp" /> <!-- 其他固定视图 --> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView> </layout>
2. 原始自定义View实现
class CustomLayout : LinearLayout { private lateinit var mBinding: CustomLayoutBinding private lateinit var containerLayout: LinearLayout constructor(context: Context) : super(context) { initView(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { initArgs(context, attrs) initView(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { initArgs(context, attrs) initView(context) } private fun initView(context: Context) { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater mBinding = DataBindingUtil.inflate(inflater, R.layout.custom_layout, this, true) containerLayout = mBinding.llContainer } private fun initArgs(context: Context, attrs: AttributeSet?) { // 读取自定义属性逻辑 } override fun onFinishInflate() { super.onFinishInflate() mBinding.run { // 设置文本、监听器逻辑 } } override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { val id = child?.id if (id == R.id.card || id == R.id.clRoot) { // 原逻辑为判断自身布局的控件ID super.addView(child, index, params); } else { containerLayout.addView(child, index, params); } } }
3. 外部调用示例
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="vm" type="SomeViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout> <CustomLayout android:id="@+id/customid" app:collapsable="false"> <GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2" android:rowCount="4"> <androidx.appcompat.widget.AppCompatTextView android:text="@{vm.headerText}" /> <androidx.appcompat.widget.AppCompatTextView /> </GridLayout> </CustomLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
问题原因
崩溃的核心原因是重写的addView逻辑破坏了DataBinding的视图遍历绑定流程:
- DataBinding在inflate布局阶段,会按照视图树层级顺序遍历所有子View,给带绑定表达式的View调用
setTag存储绑定信息,同时建立绑定类和View实例的映射关系。 - 重写的
addView在inflate阶段就会拦截所有子View添加操作,把外部传入的业务子View直接add到llContainer中,此时外层布局的DataBinding还在遍历CustomLayout的直接子节点,被转移的View脱离了原遍历路径,导致绑定类中对应的TextView实例为null,后续调用setTag时直接触发空指针。 - 自定义View自身布局inflate阶段的
addView调用也会被该重写逻辑拦截,容易出现containerLayout还未初始化就被调用的时序问题。
解决方案
核心原则是不要在inflate流程执行过程中修改视图树结构,避免干扰DataBinding的视图映射逻辑,具体修改点:
- 移除直接重写
addView拦截所有子View添加的逻辑,避免影响系统inflate流程。 - 增加inflate完成标记位,确保自身布局加载完成前不做任何子View转移操作。
- 在
onFinishInflate回调(此时所有子View已经全部加载完成,DataBinding也完成了初始视图遍历)中,再把业务子View移动到llContainer容器中。
修正后的核心实现代码:
class CustomLayout : LinearLayout { private lateinit var mBinding: CustomLayoutBinding private lateinit var containerLayout: LinearLayout // 标记自身布局是否inflate完成 private var isViewInflated = false constructor(context: Context) : super(context) { initView(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { initArgs(context, attrs) initView(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { initArgs(context, attrs) initView(context) } private fun initView(context: Context) { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater mBinding = DataBindingUtil.inflate(inflater, R.layout.custom_layout, this, true) containerLayout = mBinding.llContainer isViewInflated = true } private fun initArgs(context: Context, attrs: AttributeSet?) { // 读取自定义属性逻辑 } override fun onFinishInflate() { super.onFinishInflate() // 所有子View加载完成后,再移除非自身布局的业务子View val needMoveViews = mutableListOf<View>() for (i in 0 until childCount) { val child = getChildAt(i) // 排除自身布局加载的根CardView if (child.id != R.id.card) { needMoveViews.add(child) } } needMoveViews.forEach { child -> removeView(child) containerLayout.addView(child) } mBinding.run { // 初始化自身的文本设置、事件监听逻辑 } } }
内容的提问来源于stack exchange,提问作者Kdaydin
相关产品推荐
相关产品推荐

