1

I am getting null pointers (sometimes) on views within fragments using synthetic. I do not know what is wrong because I am declaring the fragments in XML and I think that when I call the populate method from the Activity, the root view is already created

Anyway, I do not believe is correct check this each time I call the populate...

I write an example code of what happens (the null pointer would be on tv):

The fragment:

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_foo.*

class FooFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_foo, container, false)
    }

    fun populate(fooName: String) {
        tv.text = fooName
    }
}

And the XML related within the XML of the Activity related

<fragment
android:id="@+id/fFoo"
android:name="com.foo.FooFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="@layout/fragment_foo"/>

This would the Activity related:

class FooActivity : AppCompatActivity() {
    private lateinit var fooFragment: FooFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_foo)
        fooFragment = fFoo as FooFragment
    }

    private fun loadDataProductionInfo(productionId: Long) {
        FooAPIParser().getFoo {
            initFoo(it.fooName)
        }
    }

    private fun initFoo(fooName: String) {
        fooFragment.populate(fooName)
    }
}

And finally the XML of the specific fragment:

 <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:layout_width="match_parent"
    android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tvFoo"
            style="@style/Tv"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Gabi Moreno
  • 871
  • 1
  • 8
  • 21

3 Answers3

2

Kotlin synthetic properties are not magic and work in a very simple way. When you access btn_K, it calls for getView().findViewById(R.id.tv)

The problem is that you are accessing it too soon getView() returns null in onCreateView. Try doing it in the onViewCreated method:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    //Here
    }
}

Using views outside onViewCreated.

tv?.text = "kotlin safe call"

If you are going to use Fragments you need to extend FragmentActivity

Sharan
  • 1,055
  • 3
  • 21
  • 38
0

Thinking in your answers I have implemented a patch. I do not like very much, but I think it should work better than now. What do you think?

I would extend each Fragment I want to check this from this class:

import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.fragment.app.Fragment

open class BaseFooFragment : Fragment() {

    private var viewCreated = false
    private var attempt = 0

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewCreated = true
    }

    fun checkViewCreated(success: () -> Unit) {
        if (viewCreated) {
            success()
        } else {
            initLoop(success)
        }
    }

    private fun initLoop(success: () -> Unit) {
        attempt++
        Handler().postDelayed({
            if (viewCreated) {
                success()
            } else {
                if (attempt > 3) {
                    return@postDelayed
                }
                checkViewCreated(success)
            }
        }, 500)
    }
}

The call within the Fragment would be more or less clean:

fun populate(fooName: String) {
    checkViewCreated {
        tv.text = fooName
    }
}
Gabi Moreno
  • 871
  • 1
  • 8
  • 21
0

Finally, I have received help to find out a better answer.

open class BaseFooFragment : Fragment() {

    private var listener: VoidListener? = null
    private var viewCreated = false

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewCreated = true
        listener?.let {
            it.invoke()
            listener = null
        }
    }

    override fun onDestroyView() {
        viewCreated = false
        super.onDestroyView()
    }

    fun doWhenViewCreated(success: VoidListener) {
        if (viewCreated) {
            success()
        } else {
            listener = success
        }
    }
}

The VoidListener is simply this:

typealias VoidListener = () -> Unit

One way to do this more generic (for example, when you want to use more than one listener) could be like this:

open class BaseFooFragment : Fragment() {

    private val listeners: MutableList<VoidListener> = mutableListOf()
    private var viewCreated = false

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewCreated = true
        listeners.forEach { it() }
        listeners.clear()
    }

    override fun onDestroyView() {
        viewCreated = false
        super.onDestroyView()
    }

    fun doWhenViewCreated(success: VoidListener) {
        if (viewCreated) {
            success()
        } else {
            listeners.add(success)
        }
    }
}
Gabi Moreno
  • 871
  • 1
  • 8
  • 21