androidbroadcast / ViewBindingPropertyDelegate

Make work with Android View Binding simpler
https://proandroiddev.com/make-android-view-binding-great-with-kotlin-b71dd9c87719
Apache License 2.0
1.42k stars 102 forks source link

Clear happens after onViewCreated #116

Open Miartg opened 1 year ago

Miartg commented 1 year ago

Description:

If you quickly add and remove a fragment from the back stack, then there is a situation where onViewDestroyed is called after onViewCreated. This causes onViewCreated to use the old view. The problem is floating and reproduces over time.

I think the problem is that postClear of onDestroy from viewLifecycle doesn't guarantee that clear will be called before onViewCreated.

Source code:

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    fun onFragmentAClick() {
        supportFragmentManager.executePendingTransactions()
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, FragmentB())
            .setTransition(TRANSIT_FRAGMENT_FADE)
            .addToBackStack(null)
            .commit()
    }

    fun onFragmentBClick() {
        supportFragmentManager.executePendingTransactions()
        supportFragmentManager.popBackStack()
    }
}

class FragmentA : Fragment(R.layout.fragment_a) {
    private val viewBinding by viewBinding(
        vbFactory = FragmentABinding::bind,
        onViewDestroyed = { Log.d(LOG_TAG, "FragmentA onViewDestroyed binding = $it") }
    )

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d(LOG_TAG, "FragmentA onViewCreated binding = $viewBinding")
        viewBinding.root.text = "Fragment A onViewCreated"
        viewBinding.root.setOnClickListener { (context as? MainActivity)?.onFragmentAClick() }
    }
}

class FragmentB : Fragment(R.layout.fragment_b) {
    private val viewBinding by viewBinding(
        vbFactory = FragmentBBinding::bind,
        onViewDestroyed = { Log.d(LOG_TAG, "FragmentB onViewDestroyed binding = $it") }
    )

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d(LOG_TAG, "FragmentB onViewCreated binding = $viewBinding")
        viewBinding.root.text = "Fragment B onViewCreated"
        viewBinding.root.setOnClickListener { (context as? MainActivity)?.onFragmentBClick() }
    }
}

private const val LOG_TAG = "ViewBindingDelegate"

<!--activity_main.xml-->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:name=".FragmentA"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

<!--fragment_a.xml-->
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?colorPrimary"
    android:gravity="center"
    android:text="Fragment A"
    android:textColor="?colorOnPrimary"
    />

<!--fragment_b.xml-->
<TextView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?colorSecondary"
    android:gravity="center"
    android:text="Fragment B"
    android:textColor="?colorOnSecondary"
    />

Logs:

FragmentB onViewCreated binding = FragmentBBinding@d1b9dfd
FragmentA onViewDestroyed binding = FragmentABinding@a96db92
FragmentA onViewCreated binding = FragmentABinding@10bb7ab
FragmentB onViewDestroyed binding = FragmentBBinding@d1b9dfd
FragmentB onViewCreated binding = FragmentBBinding@51e195a
FragmentA onViewCreated binding = FragmentABinding@10bb7ab
FragmentA onViewDestroyed binding = FragmentABinding@10bb7ab
FragmentB onViewDestroyed binding = FragmentBBinding@51e195a

It can be seen that the FragmentA onViewCreated binding = FragmentABinding@10bb7ab was called twice

LeafyLappa commented 1 year ago

I'm having the same problem. Took me hours already and I haven't even been able to solve it.

upd: in case you want to know how I fixed it.

The presentation logic was very poorly written (we had to publish the application ASAP) which is why in some cases there was a fragment opening and instantly closing without displaying anything. I changed the logic so that the fragment wouldn't be needlessly created and the whole problem was gone.

This sort of implies that this issue won't really need a fix.

kirich1409 commented 11 months ago

Hi. It's interesting case. Need to add additional check of Fragment View state. Will be investigated

xloger commented 10 months ago

I had the same problem when I used Navigation and fast forward and back. Let me first describe my situation: My general logic in MainFragment is as follows, and there are no other ViewBinding operations. Returned in BFragment via findNavController().popBackStack().

//MainFragment
private val binding by viewBinding(FragmentMainBinding::bind, onViewDestroyed = {
        printer.debug("onViewDestroyed;binding:${it.idPrint()}")
})

fun onViewCreated() {
    printer.info("onViewCreated")
    // set background is red,then background is white when happen bug。
    binding.root.setBackgroundColor(Color.RED)
    binding.root.setOnClickListener {
            printer.debug("navigate to BFragment")
            findNavController().navigate(R.id.bFragment, null, animOptions)
        }
}

fun onDestroyView() {
    printer.debug("onDestroyView")
}

Then I quickly switch MainFragment and BFragment, which is easier to reproduce . When an exception is encountered, its log is like this:

2023-11-08 14:56:42.769 I  onViewCreated
2023-11-08 14:56:42.770 D  useBinding:FragmentMainBinding@32292761
2023-11-08 14:56:42.770 I  onViewCreated end
2023-11-08 14:56:42.770 V  MainFragment@166602538 onStart
2023-11-08 14:56:42.772 V  MainFragment@166602538 onResume
2023-11-08 14:56:42.918 D  navigate to BFragment
2023-11-08 14:56:42.933 V  MainFragment@166602538 onPause
2023-11-08 14:56:42.934 V  MainFragment@166602538 onStop
2023-11-08 14:56:42.936 V  BFragment@148110504 onCreate
2023-11-08 14:56:42.946 V  BFragment@148110504 onStart
2023-11-08 14:56:42.961 V  BFragment@227944255 onDestroy
2023-11-08 14:56:42.962 V  BFragment@148110504 onResume
2023-11-08 14:56:43.085 D  navigate popBackStack to AFragment
2023-11-08 14:56:43.096 V  BFragment@148110504 onPause
2023-11-08 14:56:43.098 V  BFragment@148110504 onStop
2023-11-08 14:56:43.098 V  MainFragment@166602538 onStart
2023-11-08 14:56:43.103 V  MainFragment@166602538 onResume
2023-11-08 14:56:43.273 V  BFragment@148110504 onDestroy
2023-11-08 14:56:43.281 D  navigate to BFragment
2023-11-08 14:56:43.298 V  MainFragment@166602538 onPause
2023-11-08 14:56:43.299 V  MainFragment@166602538 onStop
2023-11-08 14:56:43.301 V  BFragment@248523369 onCreate
2023-11-08 14:56:43.313 V  BFragment@248523369 onStart
2023-11-08 14:56:43.314 V  BFragment@248523369 onResume
2023-11-08 14:56:43.453 D  navigate popBackStack to AFragment
2023-11-08 14:56:43.457 V  BFragment@248523369 onPause
2023-11-08 14:56:43.458 V  BFragment@248523369 onStop
2023-11-08 14:56:43.459 V  MainFragment@166602538 onStart
2023-11-08 14:56:43.463 V  MainFragment@166602538 onResume
2023-11-08 14:56:43.632 V  BFragment@248523369 onDestroy
2023-11-08 14:56:43.642 D  navigate to BFragment
2023-11-08 14:56:43.657 V  MainFragment@166602538 onPause
2023-11-08 14:56:43.659 V  MainFragment@166602538 onStop
2023-11-08 14:56:43.661 V  BFragment@199799963 onCreate
2023-11-08 14:56:43.673 V  BFragment@199799963 onStart
2023-11-08 14:56:43.674 V  BFragment@199799963 onResume
2023-11-08 14:56:43.827 D  navigate popBackStack to AFragment
2023-11-08 14:56:43.829 D  onDestroyView
2023-11-08 14:56:43.831 V  BFragment@199799963 onPause
2023-11-08 14:56:43.832 V  BFragment@199799963 onStop
2023-11-08 14:56:43.849 I  onViewCreated
2023-11-08 14:56:43.850 D  useBinding:FragmentMainBinding@32292761
2023-11-08 14:56:43.850 I  onViewCreated end
2023-11-08 14:56:43.850 V  MainFragment@166602538 onStart
2023-11-08 14:56:43.852 V  MainFragment@166602538 onResume
2023-11-08 14:56:43.853 D  onViewDestroyed;binding:FragmentMainBinding@32292761
2023-11-08 14:56:44.028 V  BFragment@199799963 onDestroy

It can be found that after using FragmentMainBinding@32292761 last time, after frequent switching, onDestroyView was called correctly, but this time onViewCreated used the last FragmentMainBinding@32292761 before it happened onViewDestroyed of ViewBindingPropertyDelegate.

I read several previous issues and learned that you have fixed similar problems. My testing came to the following conclusions:

  1. If you do not use ViewBindingPropertyDelegate, but use _binding and onDesotoryView { _binding = null }, it is normal
  2. If animation is not used when switching Fragments (animOptions is set to null), it is also normal.
    val animOptions = navOptions {
            anim {
                enter = androidx.navigation.ui.R.anim.nav_default_enter_anim
                exit = androidx.navigation.ui.R.anim.nav_default_exit_anim
                popEnter = androidx.navigation.ui.R.anim.nav_default_pop_enter_anim
                popExit = androidx.navigation.ui.R.anim.nav_default_pop_exit_anim
            }
        }

I believe the problem lies with Fragment's transition animation, but I have no idea how to solve this problem on ViewBindingPropertyDelegate.