1

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

SNM
  • 5,625
  • 9
  • 28
  • 77
  • If you don’t want it modified in the original location you’d need to pass a copy, no? I don’t remember enough Android to recall what get/putSerializable do. – Dave Newton Jun 13 '20 at 17:23
  • but if I pass the original object to Fragment B, then in Fragment b I'm creating a new instance of that object in which I'm making the changes, is weird that this changes also affects the original object – SNM Jun 13 '20 at 17:34
  • if you changed the passed object in other class it will affect the original one , unless you make a deep copy of object , so it will be another different object – Mohammed Alaa Jun 13 '20 at 17:40
  • What makes you think you’re getting a new, unrelated instance? – Dave Newton Jun 13 '20 at 17:41
  • But if I pass the object from Fragment A as a bundle as I'm doing right now, and I create a new variable of type Shop to hold it, why the changes I made there affects the original object – SNM Jun 13 '20 at 17:47
  • https://stackoverflow.com/questions/49053432/how-to-clone-object-in-kotlin – Mohammed Alaa Jun 13 '20 at 18:12
  • But I want to know why this object is modified in Fragment A , it shouldnt @MohammedAlaa – SNM Jun 13 '20 at 18:16
  • https://softwareengineering.stackexchange.com/questions/286008/parameters-are-passed-by-value-but-editing-them-will-edit-the-actual-object-li – Mohammed Alaa Jun 13 '20 at 18:43
  • This question is not as simple as "it's passed by reference so it's the same object and therefore mutation changes the class in both cases" because on Android this is Not always the case due to the way Android lifecycle works. – EpicPandaForce Jun 13 '20 at 19:16

2 Answers2

1

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() }
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • What you should remember about Java (if this explanation makes sense) is that a copy of a reference to the class is passed for non-primitives, and not a copy of the instance. To make a copy of the instance, you have to do that yourself. – EpicPandaForce Jun 13 '20 at 19:17
  • you know @EpicPandaForce, I did a copy before sending it into the bundle like val shop = shopAdapter.getItem(position) and then passed that shop copy to the bundle and I'm having the exact same behaviour, this is really weird – SNM Jun 13 '20 at 19:24
  • I also did a copy with .copy() on the data class, and also a val shop = Shop(...) and also does the same – SNM Jun 13 '20 at 19:25
  • That's not a copy, that's the exact same object instance from the shopAdapter. If this is Kotlin, and your shop is a `data class`, then to get a copy, all you need to do is call `shop.copy()`. If you never call copy or create a new instance, it'll be the same instance. Call `copy` when you're giving the shop to `putSerializable`. – EpicPandaForce Jun 13 '20 at 19:25
  • If you prefer `data class(val` instead of `var`, and use `List` instead of `MutableList`/`ArrayList` in your classes' public properties, you can reduce these sort of bugs. – EpicPandaForce Jun 13 '20 at 19:28
  • Check my edits , thanks a lot for your time, I'm gonna upvote – SNM Jun 13 '20 at 19:28
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215904/discussion-between-coffeebreak-and-epicpandaforce). – SNM Jun 13 '20 at 19:30
  • Your **third** option should work. If that doesn't work, then there is something mysterious going on, but I'd need more code to tell you what exactly is going wrong. Assuming name is a String (immutable) and quantity is an Int (primitive), this shouldn't be an issue at that point. – EpicPandaForce Jun 13 '20 at 19:31
0

Object references are passed by value

All object references in Kotlin are passed by value. This means that a copy of the value will be passed to a method. But the trick is that passing a copy of the value also changes the real value of the object. To understand why, try this example:

object  ObjectReferenceExample {
 fun transform(p:Person) {
        p.name = "Person2";
 }
}

class Person(var name:String) 
   fun main() {
     val  person =  Person("Person1");
     ObjectReferenceExample.transform(person);
     System.out.println(person.name);// output are Person2
}

The reason is that Kotlin object variables are simply references that point to real objects in the memory heap. Therefore, even though Kotlin passes parameters to methods by value, if the variable points to an object reference, the real object will also be changed. the same are applied to java

Mohammed Alaa
  • 3,140
  • 2
  • 19
  • 21