Reputation: 635
I'm having a problem where when executing
findNavController(R.id.main_nav_host).navigateUp()
or
findNavController(R.id.main_nav_host).popBackStack()
Instead of going back to the last fragment in the backstack, it reopens/navigates to the same/current fragment.
Can somebody point me in the right direction why this is happening?
Navigation graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation_root"
app:startDestination="@+id/dest_main">
<fragment
android:id="@+id/dest_main"
android:name="com.example.popularmovies.ui.main.views.MainMoviesFragment"
android:label="@string/home"
tools:layout="@layout/fragment_main_movies">
<action
android:id="@+id/action_dest_main_to_dest_movie_details"
app:destination="@+id/dest_movie_details"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/dest_movie_details"
android:name="com.example.popularmovies.ui.details.movie.view.MovieDetailsFragment"
android:label="@string/movie_details"
tools:layout="@layout/fragment_movie_details"/>
</navigation>
MainActivity layout:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/main_navigation"/>
</FrameLayout>
MainActivity:
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
initNavUi()
}
override fun onBackPressed() {
findNavController(R.id.main_nav_host).popBackStack()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.main_nav_host).navigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
private fun initNavUi() {
val navController = Navigation.findNavController(this, R.id.main_nav_host)
appBarConfiguration = AppBarConfiguration(
setOf(R.id.dest_main)
)
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
}
}
Destination home fragment:
class MainMoviesFragment : Fragment(), Injectable, MovieViewHolder.MovieClickListener {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var fragmentViewModel: MainMoviesFragmentViewModel
private lateinit var moviesRv: RecyclerView
private lateinit var moviesAdapter: MainMoviesAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews(view)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
fragmentViewModel = ViewModelProviders.of(this,viewModelFactory).get(MainMoviesFragmentViewModel::class.java)
fragmentViewModel.start()
observe()
}
override fun onMovieClicked(position: Int) {
fragmentViewModel.onMovieClicked(position)
}
private fun initViews(view: View) {
moviesRv = view.findViewById<RecyclerView>(R.id.fragment_main_movies_rv).apply{
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
moviesAdapter = MainMoviesAdapter(this@MainMoviesFragment)
adapter = moviesAdapter
}
}
private fun observe() {
fragmentViewModel.moviesLiveData.observe(this, Observer { moviesAdapter.submitList(it) })
fragmentViewModel.onMovieClickedLiveEvent.observe(this, Observer { handleMovieClickedEvent(it) })
}
private fun handleMovieClickedEvent(movieModel: MovieModel?){
val action = MainMoviesFragmentDirections.actionDestMainToDestMovieDetails()
findNavController().navigate(action)
}
}
Destination target fragment:
class MovieDetailsFragment : Fragment() {
private lateinit var viewModel: MovieDetailsFragmentViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_movie_details, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MovieDetailsFragmentViewModel::class.java)
}
}
The project code on GitHub can be found here
Upvotes: 12
Views: 5513
Reputation: 199825
Your onMovieClickedLiveEvent
, used in your MainMoviesFragmentViewModel
, is firing every time you go back to your MainMoviesFragment
since MutableLiveData
saves the current value. This means that popBackStack()
works just fine, but then you instantly get navigated back to the detail page (note: you'll still want to remove your code in onBackPressed()
since right now you can't exit the app by hitting the back button).
It seems like, particularly with the name of the variable, that you should be using the SingleLiveEvent class, instead of MutableLiveData
directly, as per this blog post.
Of course, there's no particular reason to use a LiveData or go through the ViewModel at all in this case. Your MovieViewHolder
could pass the MovieModel
directly to onMovieClicked
, which could call handleMovieClickedEvent
directly. That would avoid the use of LiveData (which is designed to store state, not events) and better model what you actually want to achieve: an event listener.
Upvotes: 10