Search code examples
androidandroid-fragmentskotlinandroid-architecture-componentsandroid-architecture-navigation

Object is changed after sending it to another Fragment


So, I have a really weird issue, I pass an object from Fragment A to Fragment B , I modify this object in a new instance in Fragment B, but after I change a value on this object it also changes that value when I pop Framgment B and that object keeps modified now also for Fragment A

Fragment A

...

   override fun onItemClick(v: View?, position: Int) {
        searchView.clearFocus()
        val bundle = Bundle()
        bundle.putSerializable("shop", landingAdapter.getItem(position))
        findNavController().navigate(R.id.action_navigation_landing_to_shopFragment, bundle)
    }

...

Now, from Fragment B I get this object

Fragment B

    private lateinit var shop: Shop
    private lateinit var shopAdapter:ShopAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        shopAdapter = ShopAdapter(sharedViewModel, requireContext(),this)
        arguments?.let {
             shop = it.getSerializable(ARG_SHOP) as Shop
            if (shop.products.isNotEmpty()) {
                shopAdapter.setItems(shop.products)
            }
        }
    }

Now, after I get this Shop object from Fragment A, I modify it in Fragment B only with

onViewCreated(){

    shop.quantity = 1

}

but when I go back to Fragment A, now that Shop object quantity value is 1 , but it should be nothing since I have only changed the object at Fragment B not Fragment A , and in Fragment B is a new instance of that object

I'm really confused

EDIT

What I have tried so far to send a fresh instance each time I go from Fragment A to Fragment b

   val bundle = Bundle()
                bundle.putSerializable("shop", landingAdapter.getItem(position).copy())  

findNavController().navigate(R.id.action_navigation_landing_to_shopFragment, bundle)



         val bundle = Bundle()
                val shop = landingAdapter.getItem(position)
                bundle.putSerializable("shop", shop)  findNavController().navigate(R.id.action_navigation_landing_to_shopFragment, bundle)


         val bundle = Bundle()
                val shop = Shop(landingAdapter.getItem(position).name,landingAdapter.getItem(position).quantity)
                bundle.putSerializable("shop", shop)  
    findNavController().navigate(R.id.action_navigation_landing_to_shopFragment, bundle)

None of them sends a fresh instance of shop to Fragment B, so whenever I change quantity at fragment B, fragment A gets the same quantity value which should not mutate


Solution

  • This is actually not an obvious question with an obvious answer. I got this question about 2 months ago and it confused me as well, as this is sometimes the behavior you get, and sometimes not.

    First thing to note is, when you give arguments to a Fragment, you put them in a Bundle. This bundle internally stores a Map for string keys.

    So when you call fragment.setArguments(bundle), you're basically "sending a map".

    Therefore, as no IPC happens (unlike in Activities, where you talk to Android through an Intent), the same object instance exists in the map.

    That is until the properties of the Bundle arguments are used by the system, and the instance is indeed recreated.

    After process death (you have the app in background, Android reclaims for memory, and on restart Android rebuilds your task stack and recreates the active Fragments based on existing history saved into onSaveInstanceState from the FragmentManager), the originally passed arguments are used to "recreate" the incoming properties.

    At this time, the Serializable was recreated by the system, and if you were to navigate back, it would be a different instance than the one you had originally sent.

    Therefore, you get the same instance because Bundle is internally a map.

    You can also get a different instance because Android can recreate it.

    Solution: use a copy before sending your instance for consistent behavior.

    EDIT:

    This also applies for nested objects inside mutable lists. Therefore, if you have classes like

    class A(
        private val list: List<B>
    )
    

    And

    class B(
        private var blah: String
    )
    

    Then if B is mutated after sending A over through the Bundle, then the List<B> has the Bs in it change, and this will reflect on both screen, UNLESS after process death.

    So that's something to keep in mind.

    To create a copy, you could do

    val copyList = a.list.map { it.copy() }