androidandroid-fragmentsandroid-lifecyclefragmenttransaction

Fragment popped call OnViewCreated after PopBackStack


i have 1 Activity with 3 Fragments. (A, B and C). So,

Activity -> FragmentContainerView with fragment A

    <androidx.fragment.app.FragmentContainerView
    android:id="@+id/host_fragment"
    android:name="cl.gersard.shoppingtracking.ui.product.list.ListProductsFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="ListProductsFragment" />

Fragment A has a button to go to Fragment B

Fragment A -> Fragment B (with addToBackStack)

Then, i go to from Fragment B to Fragment C

Fragment B -> Fragment C (without addToBackStack)

i need when i save a item in Fragment C, come back to Fragment A, so i dont use addToBackStack.

The problem is when in Fragment C i use

requireActivity().supportFragmentManager.popBackStack()

or

requireActivity().onBackPressed()

the Fragment A appears but the method OnViewCreated in Fragment C is called so execute a validations that i have in that Fragment C.

I need from Fragment C come back to Fragment A without calling OnViewCreated of Fragment C

Code of interest

MainActivity

fun changeFragment(fragment: Fragment, addToBackStack: Boolean) {
    val transaction = supportFragmentManager.beginTransaction()
        .replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
    if (addToBackStack) transaction.addToBackStack(null)
    transaction.commit()
}

Fragment A (ListProductsFragment)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    setupRecyclerView()
    observeLoading()
    observeProducts()
    viewModel.fetchProducts()
    viewBinding.btnEmptyProducts.setOnClickListener { viewModel.fetchProducts() }
    viewBinding.fabAddPurchase.setOnClickListener { addPurchase() }
}

    private fun addPurchase() {
    (requireActivity() as MainActivity).changeFragment(ScanFragment.newInstance(),true) 
}

Fragment B (ScanFragment)

    override fun barcodeDetected(barcode: String) {
    if (processingBarcode.compareAndSet(false, true)) {
        (requireActivity() as MainActivity).changeFragment(PurchaseFragment.newInstance(barcode), false)
    }
}

Fragment C (PurchaseFragment)

    private fun observePurchaseState() {
    viewModel.purchasesSaveState.observe(viewLifecycleOwner, { purchaseState ->
        when (purchaseState) {
            is PurchaseSaveState.Error -> TODO()
            is PurchaseSaveState.Loading -> manageProgress(purchaseState.isLoading)
            PurchaseSaveState.Success -> {
                Toast.makeText(requireActivity(), getString(R.string.purchase_saved_successfully), Toast.LENGTH_SHORT).show()
                requireActivity().supportFragmentManager.popBackStack()
            }
        }
    })
}

The full code is here https://github.com/gersard/PurchaseTracking


Solution

  • OK, I think I see your issue. You are conditionally adding things to the backstack which put the fragment manager in a weird state.

    Issue

    You start on Main, add the Scanner to the back stack, but not the Product. So when you press back, you're popping the Scanner off the stack but the Product stays around in the FragmentManager. This is why get a new instance each and every time you scan and go back. Why this is happening is not clear to me - seems like maybe an Android bug? You are replacing fragments so it's odd that extra instances are building up.

    One Solution

    Change your changeFragment implementation to conditionally pop the stack instead of conditionally adding things to it.

    fun changeFragment(fragment: Fragment, popStack: Boolean) {
        if (keepStack) supportFragmentManager.popBackStack()
    
        val transaction = supportFragmentManager.beginTransaction()
            .replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
        transaction.addToBackStack(null) // Always add the new fragment so "back" works
        transaction.commit()
    }
    

    Then invert your current logic that calls changeFragment:

    private fun addPurchase() {
        // Pass false to not pop the main activity
        (requireActivity() as MainActivity)
            .changeFragment(ScanFragment.newInstance(), false)
    }
    

    And ...

    override fun barcodeDetected(barcode: String) {
        if (processingBarcode.compareAndSet(false, true)) {
            // Pass true to pop the barcode that's currently there
            (requireActivity() as MainActivity)
            .changeFragment(PurchaseFragment.newInstance(barcode), true)
        }
    }