3

The title itself is my problem, whenever I open MainActivity then navigate to another fragment available in the hamburger/drawer menu then press/swipe back to return in main screen (first fragment) it recreates. Is there away for Nav Component to make it not recreate the first fragment? I am using the Jetpack Navigation template generated by Android Studio and it seems that is the default behavior.

This is the MainActivity

class MainActivity : AppCompatActivity() {

    private lateinit var appBarConfiguration: AppBarConfiguration
    private var _binding: ActivityMainBinding? = null

    // This property is only valid between onCreate and
    // onDestroyView.
    private val binding get() = _binding!!

    private lateinit var drawerLayout: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setSupportActionBar(binding.appBarMain.toolbar)

        drawerLayout = binding.drawerLayout
        val navView: NavigationView = binding.navView
        val navController = findNavController(R.id.nav_host_fragment_content_main)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        appBarConfiguration = AppBarConfiguration(setOf(
                R.id.nav_home, R.id.nav_marketcap, R.id.nav_about), drawerLayout)
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)

    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.main, menu)
        menu.findItem(R.id.action_settings).isChecked = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
        return true
    }


    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment_content_main)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

    override fun onBackPressed() {
        if (drawerLayout.isDrawerOpen(GravityCompat.START))
            drawerLayout.closeDrawer(GravityCompat.START)
        else
            super.onBackPressed()
    }

}

This is the Home Fragment (The first fragment in MainActivity) that holds child fragment AssetFragment

class HomeFragment : Fragment() {

    private val homeViewModel: HomeViewModel by activityViewModels()
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    private lateinit var viewPager : ViewPager2

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        viewPager = binding.viewPagerContainer
        val bottomNav = binding.bottomNav
//        val tabLayout = binding.tabLayout

        val fragmentList : MutableList<Pair<String, Fragment>> = mutableListOf()
        fragmentList.add(Pair(getString(R.string.assets), AssetFragment.newInstance()))
        fragmentList.add(Pair(getString(R.string.news), NewsFragment.newInstance()))
        fragmentList.add(Pair(getString(R.string.videos), VideosFragment.newInstance()))

        val adapter = AppFragmentAdapter(fragmentList, this)

        viewPager.adapter = adapter
        viewPager.offscreenPageLimit = 2

        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                bottomNav.menu.getItem(position).isChecked = true
                homeViewModel.setTitle(adapter.getFragmentTabName(position))
            }

        })

        val bottomNavListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
            when(item.itemId) {
                R.id.page_1 -> {
                    // Respond to navigation item 1 click
                    viewPager.setCurrentItem(0, true)
                    true
                }
                R.id.page_2 -> {
                    // Respond to navigation item 2 click
                    viewPager.setCurrentItem(1, true)
                    true
                }
                R.id.page_3 -> {
                    // Respond to navigation item 3 click
                    viewPager.setCurrentItem(2, true)
                    true
                }
                else -> false
            }
        }

        bottomNav.setOnNavigationItemSelectedListener(bottomNavListener)

//        val layoutInflater : LayoutInflater = LayoutInflater.from(context)
        //Connect TabLayout with ViewPager2
//        TabLayoutMediator(tabLayout, viewPager){ tab, position ->
//            tab.customView = prepareTabView(layoutInflater, tabLayout, adapter.getFragmentTabName(position), tabIcons[position])
//        }.attach()

        return root
    }

//    private fun prepareTabView(
//        layoutInflater: LayoutInflater,
//        tabLayout: TabLayout,
//        fragmentName: String,
//        drawableId: Int
//    ): View {
//
//        val rootView : View = layoutInflater.inflate(R.layout.main_custom_tab_text, tabLayout, false)
//
//        val tabName : AppCompatTextView = rootView.findViewById(R.id.tabName)
//
//        tabName.text = fragmentName
//        tabName.setCompoundDrawablesWithIntrinsicBounds(null, AppCompatResources.getDrawable(requireContext(), drawableId), null, null)
//
//        return tabName
//
//    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onResume() {
        super.onResume()

        requireView().isFocusableInTouchMode = true
        requireView().requestFocus()
        requireView().setOnKeyListener(object : View.OnKeyListener {

            override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
                if (event!!.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
                    onBackPress()
                    return true
                }
                return false
            }

        })
    }

    fun onBackPress() {

        if (viewPager.currentItem != 0)
            viewPager.setCurrentItem(0, true)
        else
            requireActivity().onBackPressed()

    }
}

This is one of the child fragment displayed in ViewPager hosted by parent fragment HomeFragment

class AssetFragment : Fragment() {

    companion object {
        fun newInstance() = AssetFragment()
    }

    private lateinit var viewModel: AssetViewModel

    private var _binding: FragmentAssetsBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    private lateinit var logTxt: AppCompatTextView
    private lateinit var recyclerView: RecyclerView
    private lateinit var swipeRefreshLayout: SwipeRefreshLayout

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {

        _binding = FragmentAssetsBinding.inflate(inflater, container, false)
        val root: View = binding.root

        recyclerView = binding.recyclerView
        swipeRefreshLayout = binding.refreshLayout
        logTxt = binding.errorLog

        recyclerView.layoutManager = LinearLayoutManager(context)
        adapter = AssetAdapter(requireContext(), this)
        recyclerView.adapter = adapter

        swipeRefreshLayout.isRefreshing = true
        fetchAssets("30")

        swipeRefreshLayout.setOnRefreshListener {
            swipeRefreshLayout.isRefreshing = true
            fetchAssets("30")
        }

        return root

    }

    private fun fetchAssets(limit: String) {

        //Network stuff
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProvider(this).get(AssetViewModel::class.java)
        // TODO: Use the ViewModel
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}

Navigation xml

This are the fragments that will be shown in the drawer menu

    <?xml version="1.0" encoding="utf-8"?>
<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/mobile_navigation"
    app:startDestination="@+id/nav_home">

<fragment
    android:id="@+id/nav_home"
    android:name="com.myapp.ui.home.HomeFragment"
    android:label="@string/home"
    tools:layout="@layout/fragment_home" />

<fragment
    android:id="@+id/nav_marketcap"
    android:name="com.myapp.ui.marketcap.MarketCapFragment"
    android:label="@string/marketCap"
    tools:layout="@layout/fragment_marketcap" />

<fragment
    android:id="@+id/nav_about"
    android:name="com.myapp.ui.about.AboutFragment"
    android:label="@string/about"
    tools:layout="@layout/fragment_about" />

</navigation>

The menu.xml

 <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">

    <item android:title="@string/menu">
        <menu>

            <item
                android:id="@+id/nav_home"
                android:icon="@drawable/ic_assets"
                android:title="@string/home" />

            <item
                android:id="@+id/nav_marketcap"
                android:icon="@drawable/ic_marketcap"
                android:title="@string/marketCap" />

            <item
                android:id="@+id/nav_about"
                android:icon="@drawable/ic_about"
                android:title="@string/about" />

        </menu>
    </item>

</group>



     <item android:title="@string/connect">
            <menu>
                <item
                    android:id="@+id/email_connect"
                    android:icon="@drawable/ic_email"
                    android:title="@string/fui_email_hint" />
            </menu>
        </item>

</menu>

Flow:

Open the app

Launching the MainActivity

Show HomeFragment (AssetFragment)

Open drawer menu

Select item e.g. About (AboutFragment)

Press/Swipe back

Problem here The HomeFragment onCreateView is being triggered once again

Expected behavior HomeFragment will no longer need to inflate view since we just literally make the user back to the very first destination. Unless a user itself press Home item in our drawer menu, that is the time HomeFragment will be recreated.

8
  • No, your Fragment is not being recreated (onDestroy() is not called when you navigate to another screen nor is onCreate() on a new instance of your first Fragment being called when you go back to the Fragment). Why do you think your whole Fragment is being recreated? What steps have you taken to save and restore your state. Please include your Fragment code that you're having a problem with. Commented Apr 23, 2021 at 3:23
  • Thanks @ianhanniballake it calls onCreateView again when back to the first fragment, will add some code in the question Commented Apr 23, 2021 at 3:25
  • @ianhanniballake I added the codes Commented Apr 23, 2021 at 3:34
  • @ianhanniballake sorry I added another codes to give you a better insight Commented Apr 23, 2021 at 3:53
  • So what's the actual problem you're having? What state have you lost? Commented Apr 23, 2021 at 3:57

1 Answer 1

1

As per the Saving state with fragments guide, it is expected that your Fragment's view (but not the fragment itself) is destroyed and recreated when it is on the back stack.

As per that guide, one of the types of state is non config state:

NonConfig: data pulled from an external source, such as a server or local repository, or user-created data that is sent to a server once committed.

NonConfig data should be placed outside of your fragment, such as in a ViewModel. The ViewModel class inherently allows data to survive configuration changes, such as screen rotations, and remains in memory when the fragment is placed on the back stack.

So your fragment should never be calling fetchAssets("30") in onCreateView(). Instead, this logic should happen inside a ViewModel such that it is instantly available when the fragment returns from the back stack. As per the ViewModel guide, your fetchAssets should be done inside the ViewModel and your Fragment would observe that data.

Sign up to request clarification or add additional context in comments.

13 Comments

So it is expected here in this sample that HomeFragment onCreateView will be called once again even by just pressing back when a user is from another drawer's menu fragment?
That's how the fragment back stack works. You have all the tools you need to avoid re-doing your data fetching and keeping your state - the exact same tools you need to use to also support configuration changes.
Does this implementation also uses FragmentTransaction behind the scene? Before Jetpack Nav Component or when handling your own back stack, a way of preventing it is to use beginTransaction.replace(content, HomeFragmet(), tag) . Then use .add(R.id.content, ANOTHER_FRAGMENT, tag) so any fragment from drawer menu will be just added on the top of the base fragment. You can also use popBackStack and addToBackStack(null) to exclude, remove or replace any previous added fragment on the top of base/home fragment when adding a new one at the top as well thus base fragment is not being touch
You should just handle your state correctly, as you must to handle configuration changes correctly. You mentioned that you expected only onResume() to be called - that's also not the case with add() since that would not affect the Lifecycle of the previous fragments at all (they'd all be stuck in resumed).
Thanks for the help, I watched your talk yesterday about Single Activity which is very informative.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.