Search code examples
androidkotlinmvvmnavigationviewmodel

Android Navigation not working properly with shared view model mvvm


I have two fragments that share same viewModel

ShoeListFragment that displays list of sheos with fab and ShoeDetailsFragment that let user add new shoe on fab click

In ShoeDetailsFragment user can click add or cancel. on add the the shoe will be added and displayed in ShoeListFragment

if user clicks cancel we will navigate user to ShoeListFragment.

Now in first run I tried to add new shoe in fab click it got added and displayed. In second run I tried cancel button and I was navigated to ShoeListFragment.

However if in the same run I tried to add and then I'm navigated to ShoeListFragment I click on fab it got clicked and ShoeDetailsFragment is getting called but the layout is not visible!

I mean the ShoeListFragment layout is visible only.

ShoeListFragment

class ShoeListFragment : Fragment() {

lateinit var viewModel : ShoeListViewModel
lateinit var binding : FragmentShoeListBinding
lateinit var parentLayout: LinearLayout

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

    binding = DataBindingUtil.inflate(inflater , R.layout.fragment_shoe_list , container , false)

    binding.fabAddShoe.setOnClickListener({
        Timber.i("fab clicked")
        findNavController().navigate(ShoeListFragmentDirections.actionShoesFragmentToShoeDetailFragment())
    })

    viewModel = ViewModelProvider(requireActivity()).get(ShoeListViewModel::class.java)

    Timber.i(viewModel.mShoeList.size.toString())

    viewModel.shoeList.observe(viewLifecycleOwner , Observer { shoeList ->
        Timber.i("List shoe observer")
        displayList(shoeList)
    })

    parentLayout = binding.llParent
    setHasOptionsMenu(true)

    // Inflate the layout for this fragment
    return binding.root
}


override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    Timber.i("onCreateOptionsMenu")
    inflater.inflate(R.menu.main_menu, menu)
    super.onCreateOptionsMenu(menu, inflater)
}

// Menu item id must match the destination id to navigate there
override fun onOptionsItemSelected(item: MenuItem): Boolean {
    Timber.i("onOptionsItemSelected")
    return NavigationUI.onNavDestinationSelected(item, requireView().findNavController())
            || super.onOptionsItemSelected(item)
}
}

ShoeDetailFragment

class ShoeDetailFragment : Fragment() {

lateinit var binding : FragmentShoeDetailBinding
lateinit var viewModel : ShoeListViewModel

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

    // View binding
    binding = DataBindingUtil.inflate(inflater,R.layout.fragment_shoe_detail , container , false)

    // Data binding - shared view model
    viewModel = ViewModelProvider(requireActivity()).get(ShoeListViewModel::class.java)
    binding.shoeListViewModel = viewModel

    binding.shoeModel = Shoe("" , 0.0 , "" , "")

    //Observers
    viewModel.isCancelAdd.observe(viewLifecycleOwner , Observer { isCancel ->
        Timber.i("Fragment Cancel shoe")
        findNavController().navigate(ShoeDetailFragmentDirections.actionShoeDetailFragmentToShoesFragment())
    })

    viewModel.isAddShoe.observe(viewLifecycleOwner , Observer { isAdd ->
       findNavController().navigate(ShoeDetailFragmentDirections.actionShoeDetailFragmentToShoesFragment())
    })

    // Inflate the layout for this fragment
    return binding.root
}

shared viewModel

class ShoeListViewModel : ViewModel() {

/* Should we make shoeList observing mShoeList instead of doing
shoeList.value = mShoeList in once we add new shoe?
*/

private var _shoeList = MutableLiveData<MutableList<Shoe>>()
val shoeList : LiveData<MutableList<Shoe>>
    get() = _shoeList

private var _isCancelAdd = MutableLiveData<Boolean>()
val isCancelAdd : LiveData<Boolean>
    get() = _isCancelAdd

private var _isAddShoe = MutableLiveData<Boolean> ()
val isAddShoe : LiveData<Boolean>
    get() = _isAddShoe

var mShoeList : MutableList<Shoe>



init {
    mShoeList = mutableListOf(
        Shoe("Filla" , 40.0 , "Filla" , "Filla shoes") ,
        Shoe("Addidad" , 30.0 , "Addidas" , "Addidas shoes")
    )
    _shoeList.value = mShoeList
}

fun addShoe (shoe : Shoe) {
    _isAddShoe.value = true
    mShoeList.add(shoe)
    _shoeList.value = mShoeList
    Timber.i("In Add shoe")
}

fun cancelAddShoe () {
    _isCancelAdd.value = true
    Timber.i("In Cancel shoe")
}

I tried debuggin it , in second time this line execute in shoeDetailsFragment

 binding = DataBindingUtil.inflate(inflater,R.layout.fragment_shoe_detail , container , false)

until it reaches

 return binding.root

Then this got called

binding = DataBindingUtil.inflate(inflater , R.layout.fragment_shoe_list , container , false)

That's why I think the shoe detail fragment is not visible. But why this behavior happenned?


Solution

  • The issue is that, your isCancelAdd value will be cached, so in the 2nd time immediately you'll get the event after navigation and you'll navigate back from the detail.

    Usually for events you'll have two options:

    1. Use a SingleLiveEvent pattern or a Consumable, to make sure events are not consumed multiple times. See this blog for more info: https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70

    2. Use SharedFlows. I'd go with this, since this is now the recommended way and LiveDatas are on their way to getting deprecated