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 会观察到该数据。
标题本身就是我的问题,每当我打开 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 会观察到该数据。