Undefined function
Undefined function

Reputation: 868

Jetpack Navigation Drawer always recreates the first fragment even in onBackPress

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.

Upvotes: 3

Views: 2471

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 199880

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.

Upvotes: 1

Related Questions