Reputation: 1478
What I'm trying to do is to use the Navigation controller inside a LiveData observer, so when the user clicks an item from the list it notifies the ViewModel, then the ViewModel updates the data and when this happens the fragment observes this and navigates to the next.
My problem is that for some reason the observer gets called twice and the second time I get an exception saying that the destination is unknown to this NavController.
My Fragment onCLick:
override fun onClick(view: View?) {
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
}
And in my ViewModel:
fun onProductSelected(productId: String) {
productSelected.value = getProductById(productId)
}
Upvotes: 2
Views: 1911
Reputation: 121
Catching the exception may work, but it can also make you miss several other issues. It might be better to check the current layout with the destination to validate if the user is already there. Another alternative that I prefer is to check with the previous destination, something like:
fun Fragment.currentDestination() = findNavController().currentDestination
fun Fragment.previousDestination() = findNavController().previousBackStackEntry?.destination
fun NavDestination.getDestinationIdFromAction(@IdRes actionId: Int) = getAction(actionId)?.destinationId
private fun Fragment.isAlreadyAtDestination(@IdRes actionId: Int): Boolean {
val previousDestinationId = previousDestination()?.getDestinationIdFromAction(actionId)
val currentDestinationId = currentDestination()?.id
return previousDestinationId == currentDestinationId
}
fun Fragment.navigate(directions: NavDirections) {
if (!isAlreadyAtDestination(directions.actionId)) {
findNavController().navigate(directions)
}
}
Basically, here we validate that we are not already at the destination. This can be done by comparing the previous action destination with the current destination. Let me know if the code helps!
Upvotes: 2
Reputation: 14660
It's called twice because first you subscribe and so you get a default value back, then you change a value in your productSelected LiveData
and so your observer gets notified again.
Thereof, start observing after onProductSelected
is called as below:
override fun onClick(view: View?) {
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
}
Once again, beware that once you start observing your LiveData
it will get notified each time productSelected
is changed. Is it what you want? If not, then you should remove the observer once it's used once.
Upvotes: 2