Jetpack Navigation Drawer 始终重新创建第一个片段,即使在 onBackPress 中也是如此

Jetpack Navigation Drawer always recreates the first fragment even in onBackPress

标题本身就是我的问题,每当我打开 MainActivity 然后导航到 hamburger/drawer 菜单中可用的另一个片段,然后 press/swipe 回到主屏幕中的 return(第一个片段)它重新创建。 Nav Component 是否有办法让它不重新创建第一个片段?我正在使用 Android Studio 生成的 Jetpack Navigation 模板,这似乎是默认行为。

这是 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()
    }

}

这是包含 child 片段 AssetFragment

的主页片段(MainActivity 中的第一个片段)
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()

    }
}

这是显示在由 parent 片段 HomeFragment

托管的 ViewPager 中的 child 片段之一
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
    }

}

导航xml

这是将显示在抽屉菜单中的片段

    <?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>

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>

流量:

打开应用程序

启动 MainActivity

显示 HomeFragment (AssetFragment)

打开抽屉菜单

Select 项,例如关于 (AboutFragment)

Press/Swipe 返回

这里有问题 HomeFragment onCreateView 再次被触发

预期行为 HomeFragment 将不再需要膨胀视图,因为我们只是让用户回到最初的目的地。除非用户自己按下抽屉菜单中的 Home 项,否则 HomeFragment 将在此时重新创建。

根据 Saving state with fragments guide,您的 Fragment 视图(而不是 Fragment 本身)应该在返回堆栈上时被销毁并重新创建。

根据该指南,一种状态类型是非配置状态:

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.

因此您的片段应该永远不会onCreateView()中调用fetchAssets("30")。相反,此逻辑应该发生在 ViewModel 内部,以便当片段 returns 来自返回堆栈时它 立即 可用。根据 ViewModel guide,您的 fetchAssets 应该在 ViewModel 中完成,您的 Fragment 会观察到该数据。