'My observer is always firing when I come back from one fragment to another

I'm using Navigation Components, I have Fragment A and Fragment B, from Fragment A I send an object to Fragment B with safe args and navigate to it.

override fun onSelectableItemClick(position:Int,product:Product) {
        val action = StoreFragmentDirections.actionNavigationStoreToOptionsSelectFragment(product,position)
        findNavController().navigate(action)
    }

Now, after some logic in my Fragment B , I want to deliver that data to Fragment A again, which I use

  btn_add_to_cart.setOnClickListener {button ->     
 findNavController().previousBackStackEntry?.savedStateHandle?.set("optionList",Pair(result,product_position))
                findNavController().popBackStack()
            }

Then in Fragment A, I catch up this data with

findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Pair<MutableList<ProductOptions>,Int>>("optionList")
            ?.observe(viewLifecycleOwner, Observer {
                storeAdapter.updateProductOptions(it.second,it.first)
            })

Now, this is working fine, but if I go from Fragment A to Fragment B and press the back button, the observer above fires again duplicating my current data, is there a way to just fire this observer when I only press the btn_add_to_cart button from Fragment B ?



Solution 1:[1]

You use this extenstion:

fun <T> Fragment.getResult(key: String = "key") =
    findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)


fun <T> Fragment.getResultLiveData(key: String = "key"): MutableLiveData<T>? {

    viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            findNavController().previousBackStackEntry?.savedStateHandle?.remove<T>(key)
        }
    })

    return findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
}

fun <T> Fragment.setResult(key: String = "key", result: T) {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

Example:

FragmentA -> FragmentB

Fragment B need to set the result of the TestModel.class

ResultTestModel.class

data class ResultTestModel(val id:String?, val name:String?)

Fragment A:

 override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
    // ...

    getNavigationResultLiveData<PassengerFragmentResultNavigationModel>(
           "UNIQUE_KEY")?.observe(viewLifecycleOwner) { result ->
           Log.i("-->","${result.id} and ${result.name}")
    }

    //...
}

Fragment B: set data and call popBackStack.

ResultTestModel(id = "xyz", name = "Rasoul")
setNavigationResult(key = "UNIQUE_KEY", result = resultNavigation)
findNavController().popBackStack()

Solution 2:[2]

It is not clear from your code where your last piece of code is called - where you add an Observer to LiveData. I am guessing it is inside one of the methods onResume() or onViewStateRestored() or any other lifecycle callback which is called again whenever you return to Fragment A from Fragment B. If that is the case, then you are adding a new observer to the LiveData and any observer of a LiveData receives an instant update for the current value.

Move that piece of code to one of the callbacks methods which is called only once during the lifecycle of a fragment.

Solution 3:[3]

Facing same issue

Resolve this by removing old data from savedStateHandle live data

Inside Fragment B :

      button?.setOnClickListener {

        findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
        findNavController().popBackStack()

     }

Inside Fragment A:

Here is key to remove the old data by using live data remove method and it should be after view created like in onViewCreated method of fragment

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
        findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)?.observe(viewLifecycleOwner) {
            result(it)
            findNavController().currentBackStackEntry?.savedStateHandle?.remove<String>(key)
        }
        
    }

Update :

I have created Extension for this for better usage

   fun <T> Fragment.setBackStackData(key: String, data: T, doBack: Boolean = false) {
        findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
        if (doBack)
            findNavController().popBackStack()
    }
    
    fun <T> Fragment.getBackStackData(key: String, singleCall : Boolean= true , result: (T) -> (Unit)) {
        findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
            ?.observe(viewLifecycleOwner) {
                result(it)
                //if not removed then when click back without set data it will return previous data
                if(singleCall) findNavController().currentBackStackEntry?.savedStateHandle?.remove<T>(key)
            }
    }

Calling inside fragment be like

While setting data in fragment B

   var user : User = User(data) // Make sure this is parcelable or serializable   
   setBackStackData("key",user,true)

While getting data inside fragment A

getBackStackData<User>("key",true) { it ->

}

Thanks to This Guy

Solution 4:[4]

https://stackoverflow.com/a/66111168/8354145

This answer should help in this case. Use SingleLiveEvent.

Otherwise in these cases, maybe using a shared view model (might be scoped to the nav graph) however you won't need to use savedStateHandle.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Rasoul Miri
Solution 2 alperozge
Solution 3 sushant gosavi
Solution 4 Tinashe Makuti