3

I've switched my code to View Binding but now I've got a problem with updating UI in a thread. The code worked OK in synthetic syntax. I'm getting error:

java.lang.NullPointerException
    at HomeFragment.getBind(HomeFragment.kt:25)
    at HomeFragment.updateHomeUI$lambda-6(HomeFragment.kt:190)
    at HomeFragment.$r8$lambda$7K03ZbIZrY_5ngvcMBPsw15TPbw(Unknown Source:0)
    at HomeFragment$$ExternalSyntheticLambda10.run(Unknown Source:2)
    at java.lang.Thread.run(Thread.java:919)

My code:

class HomeFragment : Fragment(R.layout.fragment_home) {
    private var _binding: FragmentHomeBinding? = null
    private val bind get() = _binding!!  // <-- line 25

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return bind.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

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

    private fun updateHomeUI() {
        Thread {
            while (bind.tvName != null) {  // Stop the loop after changing the fragment
                ...
                // Lots of UI update like this:
                if (activity != null) (activity as MainActivity).runOnUiThread { bind.tvName?.text = str }
                ...
                Thread.sleep(1000)
            }
        }.start()
    }

The error shows up when I switch to a different fragment. If I am right, the thread is running after onDestroyView () and the binding goes empty. I came up with idea of pausing onDestroyView until Thread is finished but I think It's not best kind of solution because it may stop whole application.

override fun onDestroyView() {
    super.onDestroyView()
    threadStop = true
    while (threadRunning) {
        Thread.sleep(1)
    }
    _binding = null
}
private fun updateHomeUI() {
    Thread {
        threadRunning = true
        threadStop = false
        while (!threadStop) {
            ...
        }
        threadRunning = false
    }
}

How to properly avoid this problem?

Best regards!

Artyum
  • 169
  • 2
  • 13
  • What if you just use the nullable type (`_binding`) in the thread instead with appropriate null safety (so something like `_binding?.tvName?.text = str`)? – Tyler V Dec 30 '21 at 02:43
  • Also might want `while(_binding != null)` in there... – Tyler V Dec 30 '21 at 02:50
  • while(binding != null) won't work because the error is from inside the loop. I would have to check binding at every UI element update and that would be ugly solution as I've got many elements in UI – Artyum Dec 30 '21 at 02:57
  • why need _binding = null in the onDestroyView? why not remove it? – Matrix Dec 30 '21 at 03:13
  • @Anna holding references to views after `onDestroyView` may cause a memory leak. Have a look [here](https://stackoverflow.com/questions/65295104/android-view-binding-clear-binding-in-fragment-lifecycle) – Tyler V Dec 30 '21 at 03:15

1 Answers1

1

You may find this easier if you ditch the !! getter and just use nullable types with local non-null variables as-needed. If you have a lot of places to use the binding inside the loop you could grab a non-null value of it at the start of the loop.

private fun updateHomeUI() {
    Thread {
        // this could also be while(true) since the calls just
        // inside will break out when it goes null
        while (_binding != null) {  // Stop the loop after onDestroyView sets this to null
            val binding = _binding ?: break
            val a = activity as? MainActivity ?: break
            
            
            // Lots of UI update like this:
            a.runOnUiThread { binding.tvName.text = str }
            
            ...
            Thread.sleep(1000)
        }
    }.start()
}

I'm not sure how your existing bind.tvName != null loop condition would ever return false without first failing the !! check. The views in the binding class are non-null by default (unless they don't exist in all layout permutations), so that would either be true, or would fail in the bind getter when _binding is null.

Tyler V
  • 9,694
  • 3
  • 26
  • 52
  • That seems to work but still... won't it crash if _binding goes null just after the ?: break ? – Artyum Dec 30 '21 at 03:28
  • No, because at that point you have saved off a reference to the binding that is locally non-null. Even if the class member gets set to null the local one will remain non-null for that cycle of the loop and the object will remain valid (although not visible to the user any more) since you still have a reference to it. You just have to make sure to not use `_binding` anywhere in there after that, only use the local `binding` variable. – Tyler V Dec 30 '21 at 03:33