Jose Gonzalez
Jose Gonzalez

Reputation: 1478

Navigation controller gets called twice inside livedata observer

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

Answers (2)

Luis Talavera
Luis Talavera

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

Anatolii
Anatolii

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

Related Questions