findViewById 所有可能的 null
情况1
MainActivity.kt
package io.github.helloxmlimport android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompatprivate const val TAG: String = "MainActivity"internal class MainActivity internal constructor() : AppCompatActivity() {internal companion object {internal fun startActivity(context: Context) {context.startActivity(Intent(context, MainActivity::class.java))}}private val tvClick: TextView? by lazy {findViewById<TextView>(R.id.tv_click)}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v: View, insets: WindowInsetsCompat ->val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)Log.i(TAG, "onCreate -> v: ${v.javaClass}")insets}initListener()}private fun initListener() {tvClick?.setOnClickListener {Log.i(TAG, "initListener container: ${findViewById<FrameLayout>(R.id.container)}")}}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_click"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><io.github.helloxml.MyFrameLayoutandroid:id="@+id/container"android:layout_width="match_parent"android:layout_height="200dp"app:layout_constraintTop_toBottomOf="@id/tv_click"/></androidx.constraintlayout.widget.ConstraintLayout>
MyFrameLayout.kt
package io.github.helloxmlimport android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayoutinternal class MyFrameLayout internal constructor(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) // 1
internal class MyFrameLayout internal constructor(context: Context, attributeSet: AttributeSet) : FrameLayout(context) // 2
分别观察1和2看log是否返回MyFrameLayout,下面我们进行原理剖析。
打断点查看
AppCompatActivity.java
@NonNull
public AppCompatDelegate getDelegate() {if (mDelegate == null) {mDelegate = AppCompatDelegate.create(this, this);}return mDelegate;
}
@Override
public <T extends View> T findViewById(@IdRes int id) {return getDelegate().findViewById(id);
}
上面的findViewById返回值应该使用@Nullable标注,但是没有非常奇怪,新手可能会遇到空指针异常。
AppCompatDelegate.java
@Nullable
public abstract <T extends View> T findViewById(@IdRes int id);
查看ViewGroup.java
/*** {@hide}*/
@Override
protected <T extends View> T findViewTraversal(@IdRes int id) {if (id == mID) { // 1return (T) this;}final View[] where = mChildren;final int len = mChildrenCount;for (int i = 0; i < len; i++) {View v = where[i];if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {v = v.findViewById(id);if (v != null) {return (T) v;}}}return null;
}
情况1和情况2的唯一区别是mID一个为-1,一个不为-1;
接下来查看View.java
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {this(context);mSourceLayoutId = Resources.getAttributeSetSourceResId(attrs);// ...final int N = a.getIndexCount();for (int i = 0; i < N; i++) {int attr = a.getIndex(i);switch (attr) {// ...case com.android.internal.R.styleable.View_id:mID = a.getResourceId(attr, NO_ID);break;// ...}}
}
因为internal class MyFrameLayout internal constructor(context: Context, attributeSet: AttributeSet) : FrameLayout(context),没有走到com.android.internal.R.styleable.View_id:分支,所有findViewById为null.
解决了为什么findViewBy为null的原因1.
情况2 比较常见
错误引用另一个布局文件的id
假设存在另一个activity_main1.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_click"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><io.github.helloxml.MyFrameLayoutandroid:id="@+id/container1"android:layout_width="match_parent"android:layout_height="200dp"app:layout_constraintTop_toBottomOf="@id/tv_click"/></androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
...
Log.i(TAG, "initListener container: ${findViewById<FrameLayout>(R.id.container1)}")
...
结果一定为null.
解决方法
可以使用viewBinding
配置如下
android {buildFeatures { viewBinding = true}
}
这样几乎不会出现viewId为空的任何问题.
当然标准的自定义View一般像下面那样写。
package io.github.helloxmlimport android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayoutinternal class MyFrameLayout @JvmOverloads internal constructor(context: Context,attributeSet: AttributeSet? = null,defStyleAttr: Int = 0
) : FrameLayout(context, attributeSet, defStyleAttr)
双向数据绑定
有些公司使用数据绑定dataBinding做双向数据绑定
例如
android {buildFeatures {viewBinding = truedataBinding = true}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout><data><variablename="name"type="String" /></data><androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_click"android:text="@={name}"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><io.github.helloxml.MyFrameLayoutandroid:id="@+id/container1"android:layout_width="match_parent"android:layout_height="200dp"app:layout_constraintTop_toBottomOf="@id/tv_click"/></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MainActivity
package io.github.helloxmlimport android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.github.helloxml.databinding.ActivityMain1Binding
import io.github.helloxml.databinding.ActivityMainBinding
import kotlin.random.Randomprivate const val TAG: String = "MainActivity"internal class MainActivity internal constructor() : AppCompatActivity() {internal companion object {internal fun startActivity(context: Context) {context.startActivity(Intent(context, MainActivity::class.java))}}private val binding: ActivityMain1Binding by lazy {ActivityMain1Binding.inflate(layoutInflater)}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(binding.root)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v: View, insets: WindowInsetsCompat ->val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)Log.i(TAG, "onCreate -> v: ${v.javaClass}")insets}initListener()}private fun initListener() {binding.name = "Hello World"binding.tvClick.setOnClickListener {Log.i(TAG, "initListener container: ${binding.container1}")binding.name = Random.nextInt().toString()}}
}
还有一种使用的Kotlin synthetics
个人感觉还不如findViewById,因为各种空问题都解决不了还容易导入其他布局的id.
迁移文档