Почему `findViewById` возвращает ноль? (используя `by lazy`) - PullRequest
4 голосов
/ 10 октября 2019

У меня есть небольшой проект в качестве примера:

// 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>

Но когда я запускаю его, он падает, отображая следующееlog: log of the crash

И я не знаю, почему он падает. Идентификатор представления, которое я получаю, в точности равен тому, который можно найти в макете .xml.

Мне бы хотелось узнать причину, по которой ленивый возвращает null вместосмотреть сам, потому что это не имеет смысла: /


enter image description here

Через этот GIF, мы можем видеть, что:

  • textView (то есть lazy val) считается нулевым;
  • notNullTextView не равно нулю и выполняет тот же код, найденный внутри text view lazyБлок инициализации val;
  • , выполняющий тот же код инициализации textView в Watches IDE, также возвращает ноль.

Ответы [ 2 ]

0 голосов
/ 10 октября 2019

Код внутри ленивой лямбды не может быть выполнен. По-видимому, он не находится в пределах досягаемости, когда commonInit вызывается системой. Ваш код работает, если вы перемещаете код инициализации в функцию commonInit (без ключевого слова private).

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}"
        )
    }
}

Эта проблема не связана с фактическими значениями и функциями, которые вы используете, конструкция правильнаямудрый и преуспевающий в других контекстах. Поэтому, похоже, это ограничение для конкретной реализации - либо плохо документировано, либо просто является ошибкой.

0 голосов
/ 10 октября 2019

На самом деле, ваша проблема вообще не связана с findViewById. Вы можете заменить своего ленивого на

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

, и вы получите ту же ошибку! Я думаю, что это связано с реализацией lazy и тем, что вы передаете ему не полностью созданный объект, а затем пытаетесь вызвать. К сожалению, я не могу сказать, что именно не так.

Вы можете легко исправить ошибку с помощью lateinit, например:

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}"
        )
    }
}
...