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?
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:
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
Use SharedFlows. I'd go with this, since this is now the recommended way and LiveDatas
are on their way to getting deprecated