Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Navigation Component - Navigate up opens the same fragment

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

like image 395
Roy Hen Engel Avatar asked Apr 05 '19 16:04

Roy Hen Engel


People also ask

How do you complete a fragment navigation component?

How do I get rid of current fragment navigation component? You add to the back state from the FragmentTransaction and remove from the backstack using FragmentManager pop methods: FragmentManager manager = getActivity(). getSupportFragmentManager();

Can we navigate from activity to fragment?

Move activity logic into a fragment With the fragment definition in place, the next step is to move the UI logic for this screen from the activity into this new fragment. If you are coming from an activity-based architecture, you likely have a lot of view creation logic happening in your activity's onCreate() function.

What is navigateUp?

navigateUp() Attempts to navigate up in the navigation hierarchy. boolean.


1 Answers

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.

like image 125
ianhanniballake Avatar answered Oct 12 '22 01:10

ianhanniballake