Dorian Pavetić
Dorian Pavetić

Reputation: 115

(Android) Kotlin Lazy init

Problem:

Kotlin by lazy throws unexpected NullPointerException when reached. Through debugger, when reached, this is properly evaluated and findViewById<ShapeableImageView>(R.id.image_view_icon) returns valid ImageView view, so I can't figure out why this happens - why is NullPointerException thrown if this is not null and if view exists and is already inflated at the point of lazy initialization?

Use case example:

I have custom Android view component - simple label view SimpleLabelView. This components supports setting icon and it's implementation defaults to setting it as a compound drawable to label TextView.. Now, I want to create a new component CompactIconLabelView which extends existing SimpleLabelView. New CompactIconLabelView uses different layout, and now I want to change the behavior of setting the icon - instead of setting it as a drawable to label, I want to set that drawable to custom ImageView that is contained in that new drawable. In order to do that, I need to initialize given ImageView lazily

Error

Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object kotlin.Lazy.getValue()' on a null object reference
at hr.simplifystay.sharedmodelslibrary.customViews.label.CompactIconLabelView.getIconImageViewThatCrashes(CompactIconLabelView.kt:40)
at hr.simplifystay.sharedmodelslibrary.customViews.label.CompactIconLabelView.setIcon(CompactIconLabelView.kt:37)
at hr.simplifystay.sharedmodelslibrary.customViews.label.SimpleLabelView.<init>(SimpleLabelView.kt:157)
at hr.simplifystay.sharedmodelslibrary.customViews.label.CompactIconLabelView.<init>(CompactIconLabelView.kt:24)
at hr.simplifystay.sharedmodelslibrary.customViews.label.CompactIconLabelView.<init>(CompactIconLabelView.kt:18)
at hr.simplifystay.sharedmodelslibrary.customViews.label.CompactIconLabelView.<init>(Unknown Source:14)

Code

SimpleLabelView

open class SimpleLabelView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int = R.attr.labelViewStyle,
    defStyle: Int = R.style.Theme_Simplify,
    @LayoutRes layoutResId: Int = R.layout.layout_simple_label
) : ConstraintLayout(context, attrs, defStyleAttr, defStyle) {

    @StyleableRes
    private val iconAttr = R.styleable.SimpleLabelView_icon

    open var icon: Drawable? = null
        set(value) {
            field = value
            label.setCompoundDrawablesRelativeWithIntrinsicBounds(value, null, null, null)
        }

    val label: MaterialTextView

    init {
        val view = inflate(context, layoutResId, this)
        ... some initializing code
        icon = typedArray.getDrawable(iconAttr) // Line 157 from stacktrace
        ... more code
    }
}

CompactIconLabelView

open class CompactIconLabelView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int = R.attr.labelViewStyle,
    defStyle: Int = R.style.Theme_Simplify,
    @LayoutRes layoutResId: Int = R.layout.layout_compact_icon_label
) : SimpleLabelView(context, attrs, defStyleAttr, defStyle, layoutResId) {


    override var icon: Drawable? = null
        get() = super.icon
        set(value) {
            field = value
            // This works
            if (!(::iconImageView.isInitialized))
                iconImageView = findViewById(R.id.image_view_icon)
            iconImageView.setImageDrawable(value)
            
            // This crashes
            iconImageViewThatCrashes.setImageDrawable(value) // Line 37 from stacktrace
        }

    private val iconImageViewThatCrashes: ShapeableImageView by lazy { findViewById(R.id.image_view_icon) } // Line 40 from stacktrace

    private lateinit var iconImageView: ShapeableImageView

}

So I am wondering, am I doing anything wrong or is this some kind of a bug in Kotlin?

Upvotes: 2

Views: 64

Answers (2)

kevin liao
kevin liao

Reputation: 1

In Java, when creating a subclass object, the order of initialization is to first initialize the member variables of the parent class and call the constructor of the parent class, and then initialize the member variables of the subclass and call the constructor of the subclass.

Therefore, trying to access the member variables of the subclass within the constructor of the parent class will cause an error.

Upvotes: 0

Dhruv Varde
Dhruv Varde

Reputation: 17

The issue could arise from trying to access a lazily initialized property before its containing class is fully initialized.

What to Consider here : by lazy Lifecycle in Android Views: In Android, findViewById and view inflation rely on the View hierarchy being fully constructed and attached. But, by lazy runs its initializer the first time the property is accessed. If it's accessed too early, the Context or View hierarchy might not be ready.

Initialization Order in Kotlin: When a subclass is being initialized, the base class constructor is invoked first. If the setIcon method (or any other method accessing the lazy property) is called in the base class constructor, it can lead to the lazy property being accessed prematurely, which could result in NullPointerException.

About your question regarding that Why the Debugger Shows a Valid View so the answer may be, By the time you inspect the lazy property in the debugger, the View might have been fully initialized. But during actual runtime, the lazy initialization might be triggered too early. so to exactly check the same what you need to do is that you can check with the log. If the issue is that what i defined above, then your log will shows null value to you.

Upvotes: 0

Related Questions