Why `findViewById` is returning null? (using `by lazy`)

Viewed 1309

I have that small project as example:

MainActivity.kt

package com.example.scratch

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
    }
}

layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.scratch.CustomViewWithContent
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

CustomViewWithContainer.kt

package com.example.scratch

import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout

open class CustomViewWithContainer : ConstraintLayout {

    private val contentContainer by lazy {
        findViewById<FrameLayout>(R.id.content_container)
    }

    constructor(context: Context?) : super(context) {
        commonInit(context)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        commonInit(context)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        commonInit(context)
    }

    protected open fun commonInit(context: Context?) {
        if (context == null) {
            return
        }

        inflate(context, R.layout.custom_view_with_container, this)

        if (isInEditMode) {
            return
        }
    }

    protected fun setContent(contentView: View) {
        contentContainer.removeAllViews()
        contentContainer.addView(contentView)

        (contentView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
    }
}

layout/custom_view_content.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is a text view" />

</FrameLayout>

CustomViewWithContent.kt

package com.example.scratch

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.TextView

class CustomViewWithContent : CustomViewWithContainer {

    private lateinit var contentView: View

    private val textView by lazy {
        contentView.findViewById<TextView>(R.id.text_view)
    }

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun commonInit(context: Context?) {
        super.commonInit(context)

        if (context == null) {
            return
        }

        contentView = inflate(context, R.layout.custom_view_content, null)

        if (isInEditMode) {
            return
        }

        setContent(contentView)

        Log.d(
            CustomViewWithContent::class.java.simpleName,
            "TextView id: ${textView.id}"
        )
    }
}

layout/custom_view_with_container

<?xml version="1.0" encoding="utf-8"?>
<merge 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"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <FrameLayout
        android:id="@+id/content_container"
        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" />

</merge>

But when I run it, it crashes, displaying the following log: log of the crash

And I don't know why it is crashing. The id of the view that I am fetching is exactly equals to the one that can be found in the layout .xml.

I would love to know the reason why the lazy is returning null instead of the view itself, because it makes no sense :/


screenshot

Through that gif, we can see that:

  • textView (which is a lazy val) is considered as null;
  • notNullTextView is not null, and it runs the same code found inside the text view lazy val initialization block;
  • running the same initilization code of the textView in the Watches of the IDE, it also returns null.
2 Answers

Actually, your problem is not related to findViewById at all. You can replace your lazy with

private val textView by lazy {
    TextView(contentView.context)
}

and you'll get the same error! I think it's related to lazy implementation and the fact that you passing not fully created object to it and then trying to invoke. Unfortunately, I cannot say what exactly wrong.

You can easily fix you crash with lateinit for example:

class CustomViewWithContent : CustomViewWithContainer {

    private lateinit var contentView: View

    private lateinit var textView: TextView

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun commonInit(context: Context?) {
        super.commonInit(context)

        if (context == null) {
            return
        }

        contentView = inflate(context, R.layout.custom_view_content, null)
        textView = contentView.findViewById(R.id.text_view)

        if (isInEditMode) {
            return
        }

        setContent(contentView)

        Log.d(
            CustomViewWithContent::class.java.simpleName,
            "TextView id: ${textView.id}"
        )
    }
}

The code inside the lazy lambda cannot be executed. Apparently it is not within the reachable scope when commonInit is called by the system. Your code works if you move the initialization code inside the commonInit function (without the private keyword).

class CustomViewWithContent : CustomViewWithContainer {

    private lateinit var contentView: View


    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun commonInit(context: Context?) {
        super.commonInit(context)

        val textView by lazy {
            contentView.findViewById<TextView>(R.id.text_view)
        }



        if (context == null) {
            return
        }

        contentView = inflate(context, R.layout.custom_view_content, null)

        if (isInEditMode) {
            return
        }

        setContent(contentView)

        Log.d(
            CustomViewWithContent::class.java.simpleName,
            "TextView id: ${textView.id}"
        )
    }
}

This issue is not related to the actual values and functions you are using, the construct is correct language-wise and succeeds in other contexts. Therefore it appears to be an implementation specific limitation - either poorly documented or simply a bug.

Related