Android自定义View重写addView()触发NullPointerException问题排查
问题
自定义View中重写addView(child: View?)后,使用View Binding加载布局时抛出NullPointerException,即使xml中TestView没有子View也会触发。尝试用findViewById替代Binding调用,错误情况一致,迁移到View Binding前代码正常运行。
相关代码
TestView.kt
package com.example.testandroid import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import com.example.testandroid.databinding.ViewTestBinding class TestView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { var binding: ViewTestBinding init { binding = ViewTestBinding.inflate(LayoutInflater.from(context), this, true) binding.vTitle.text = "test title 2" } override fun addView(child: View?) { binding.vContainer.addView(child) } }
view_test.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/vTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="test title"/> <LinearLayout android:id="@+id/vContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"/> </LinearLayout>
FirstFragment.kt
package com.example.testandroid import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import com.example.testandroid.databinding.FragmentFirstBinding /** * A simple [Fragment] subclass as the default destination in the navigation. */ class FirstFragment : Fragment() { private var _binding: FragmentFirstBinding? = null private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentFirstBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) } override fun onDestroyView() { super.onDestroyView() _binding = null } }
fragment_first.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".FirstFragment"> <com.example.testandroid.TestView android:id="@+id/vTest" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/vText1" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="text 1"/> <TextView android:id="@+id/vText2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="text 2"/> </com.example.testandroid.TestView> </LinearLayout>
错误信息
Caused by: java.lang.NullPointerException: Attempt to read from field 'android.widget.LinearLayout com.example.testandroid.databinding.ViewTestBinding.vContainer' on a null object reference
完整错误栈
Process: com.example.testandroid, PID: 5681 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.testandroid/com.example.testandroid.MainActivity}: android.view.InflateException: Binary XML file line #23 in com.example.testandroid:layout/activity_main: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Error inflating class com.example.testandroid.TestView at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3271) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3410) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2017) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7397) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935) Caused by: android.view.InflateException: Binary XML file line #23 in com.example.testandroid:layout/activity_main: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Error inflating class com.example.testandroid.TestView Caused by: android.view.InflateException: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Error inflating class com.example.testandroid.TestView Caused by: android.view.InflateException: Binary XML file line #10 in com.example.testandroid:layout/fragment_first: Error inflating class com.example.testandroid.TestView Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at android.view.LayoutInflater.createView(LayoutInflater.java:854) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084) at android.view.LayoutInflater.inflate(LayoutInflater.java:682) at android.view.LayoutInflater.inflate(LayoutInflater.java:534) at com.example.testandroid.databinding.FragmentFirstBinding.inflate(FragmentFirstBinding.java:54) at com.example.testandroid.FirstFragment.onCreateView(FirstFragment.kt:23) at androidx.fragment.app.Fragment.performCreateView(Fragment.java:3104) at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:524) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261) at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1424) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2968) at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2879) at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3129) at androidx.fragment.app.FragmentStateManager.ensureInflatedView(FragmentStateManager.java:394) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:260) at androidx.fragment.app.FragmentLayoutInflaterFactory.onCreateView(FragmentLayoutInflaterFactory.java:142) 2023-08-09 13:44:26.820 5681-5681/com.example.testandroid E/AndroidRuntime: at androidx.fragment.app.FragmentController.onCreateView(FragmentController.java:136) at androidx.fragment.app.FragmentActivity.dispatchFragmentsOnCreateView(FragmentActivity.java:248) at androidx.fragment.app.FragmentActivity.onCreateView(FragmentActivity.java:227) at android.view.LayoutInflater.tryCreateView(LayoutInflater.java:1069) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:997) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084) at android.view.LayoutInflater.parseInclude(LayoutInflater.java:1263) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1119) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084) at android.view.LayoutInflater.inflate(LayoutInflater.java:682) at android.view.LayoutInflater.inflate(LayoutInflater.java:534) at com.example.testandroid.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:50) at com.example.testandroid.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:44) at com.example.testandroid.MainActivity.onCreate(MainActivity.kt:22) at android.app.Activity.performCreate(Activity.java:7825) at android.app.Activity.performCreate(Activity.java:7814) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1307) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3246) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3410) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2017) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7397) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935) Caused by: java.lang.NullPointerException: Attempt to read from field 'android.widget.LinearLayout com.example.testandroid.databinding.ViewTestBinding.vContainer' on a null object reference at com.example.testandroid.TestView.addView(TestView.kt:21) at com.example.testandroid.databinding.ViewTestBinding.inflate(ViewTestBinding.java:51) at com.example.testandroid.TestView.<init>(TestView.kt:16) ... 53 more
原因分析
从错误栈可以明确问题根源:
- 调用
ViewTestBinding.inflate(..., this, true)时,第三个参数attachToRoot为true,会触发inflate内部立即将布局根View添加到TestView实例,也就是调用addView方法。 - 此时
binding变量还未完成赋值(binding = ...代码未执行完毕),重写的addView中访问binding.vContainer时,binding仍为null,直接抛出NPE。 - 用
findViewById替代也无效,因为布局还未完成attach,vContainer尚未加入View树,findViewById会返回null。
解决方案
方案一:延迟attach,手动添加布局
修改ViewTestBinding.inflate的attachToRoot参数为false,手动将布局根View添加到TestView中,避开inflate过程中调用重写的addView:
init { binding = ViewTestBinding.inflate(LayoutInflater.from(context), this, false) addView(binding.root) // 手动添加布局根View到当前View binding.vTitle.text = "test title 2" }
方案二:区分初始化阶段的addView调用
在重写的addView中判断,如果是添加布局根View则调用父类逻辑,否则再添加到vContainer:
private lateinit var binding: ViewTestBinding init { binding = ViewTestBinding.inflate(LayoutInflater.from(context), this, true) binding.vTitle.text = "test title 2" } override fun addView(child: View?) { // 初始化时添加的是布局根View,调用父类逻辑 if (child == binding.root) { super.addView(child) } else { // 加空安全调用避免意外 binding.vContainer?.addView(child) } }
方案三:放弃重写addView,提供自定义方法
去掉重写的addView,给TestView添加公开方法来添加子View到vContainer,避免和系统布局初始化流程冲突:
// 移除override fun addView(child: View?) fun addContainerChild(child: View?) { binding.vContainer.addView(child) }
之后需要添加子View时,调用该方法即可,不再依赖XML中直接嵌套子View的方式。
内容的提问来源于stack exchange,提问作者coreorb
相关产品推荐
相关产品推荐

