Android导航图Fragment复用: 多Tab高效刷新
2025-01-08 12:37:48
导航图中复用Fragment
问题:相同Fragment的多种导航选择
应用开发中,常会遇到一个情景:需要使用底部导航栏(BottomNavigationView)或其他形式的导航,不同的导航项都指向同一个Fragment。Fragment的内容会根据所选导航项ID动态变化,例如展示不同类型的数据列表。一个常见的错误认知是直接把不同导航项都指向同一个Fragment的固定实例,但这样并不能成功触发Fragment的刷新逻辑。本文探讨这种问题的起因及有效解决方法。
通常,当用户选择BottomNavigationView上的一个标签时,如果导航图中存在多个相同的fragment的定义,应用不会刷新已经存在的fragment。系统默认的导航逻辑倾向于复用当前已经显示的fragment实例。它认为我们已经位于要到达的位置了,因此不会重新创建和配置它。我们需要考虑如何让应用明白我们需要每次选择导航时都触发更新。
解决方法一:动态设置Fragment参数
核心思路在于,不要让导航图定义直接关联到Fragment的“固定”实例,而是让每个导航动作都产生一个新的Fragment实例,并通过参数(Bundle)区分。每个导航选择会重新创建Fragment, 并用选择的navigationID给Fragment进行初始化,通过它加载内容。
操作步骤:
- 导航图中,配置统一的目标 Fragment。 所有底部导航项,它们的destination都是同一个fragment。
- 在Java或Kotlin代码中,使用
findNavController()
设置 Fragment 启动的 bundle参数,传递导航项 ID 。每当点击BottomNavigationView的不同项时,构建一个携带特定参数的Bundle
,例如当前选中的导航项 ID。使用findNavController()
找到合适的导航控制器,利用其 navigate 功能跳转,并在跳转时携带bundle参数。 - Fragment中,在
onCreateView()
或者onViewCreated()
中,接收Bundle,并据此初始化数据。 使用arguments?.getInt("your_key", defaultValue)
安全地从Bundle
中取出所需参数(导航项 ID)。根据不同的 ID 值加载对应的业务数据,然后更新fragment视图。
代码示例 (Kotlin):
// Bottom Navigation 的监听代码:
bottomNavigationView.setOnItemSelectedListener { menuItem ->
val navOptions = NavOptions.Builder()
navOptions.setLaunchSingleTop(true);
val bundle = Bundle()
bundle.putInt("selectedItemId", menuItem.itemId)
findNavController(fragment_container).navigate(R.id.destinationFragment, bundle, navOptions.build())
true
}
//Fragment代码:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_destination, container, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getInt("selectedItemId", 0)?.let{selectedId ->
//根据 selectedId 进行数据加载并刷新UI
updateViewWithData(selectedId)
}
}
说明:
setLaunchSingleTop(true)
:为了避免频繁创建新的实例,在相同跳转目的地的时候尝试复用之前Fragment,保证只有一个实例,这个是非常推荐的方式。- Fragment 利用
arguments
安全地提取导航传递过来的bundle
数据,并且做了空判断防止空指针异常,并赋予一个默认值0,避免初始没有点击的时候可能出现的情况。
安全建议:
- 对
bundle
数据进行有效性验证 :确保传递的数据符合预期,比如selectedItemId
的值是不是合法范围内。
解决方法二:使用共享 ViewModel
可以使用ViewModel
跨Fragment管理数据。在ViewModel
中保存当前选中的 navigation ID, Fragment每次onViewCreated
或onCreateView
时观察 ViewModel
中的 navigation ID, 根据当前 ID 去请求相关数据即可。 使用ViewModel
方式优势是可以利用数据持有能力,Fragment横竖屏切换或者销毁重建不用重新请求数据,提高性能。
操作步骤:
- 创建一个 ViewModel 并在 Activity 级别绑定.
- 在Activity 的底部导航栏监听中,更新 ViewModel 中当前选择的 Id 值,通过
viewModel.setSelection(menuItem.itemId)
; - Fragment 的
onCreateView()
或onViewCreated()
里,观察ViewModel
中当前的选择的导航id, 根据不同的Id值去加载不同的内容; - 使用
lifecycleScope
和repeatOnLifecycle
可以确保Fragment是active状态的时候去触发ViewModel数据变更引起的观察响应,避免Activity 处于stop状态带来的不必要计算开销。
代码示例 (Kotlin):
//Activity
private val viewModel by viewModels<MyViewModel>();
// 底部导航监听:
bottomNavigationView.setOnItemSelectedListener { menuItem ->
viewModel.setSelection(menuItem.itemId)
true
}
//MyViewModel 代码
class MyViewModel: ViewModel() {
private val _currentNavigationItemId = MutableStateFlow<Int>(R.id.initial_nav_item)
val currentNavigationItemId = _currentNavigationItemId.asStateFlow()
fun setSelection(itemId : Int){
_currentNavigationItemId.value = itemId;
}
}
//Fragment代码:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.currentNavigationItemId.collectLatest {
updateViewWithData(it);
}
}
}
}
说明:
- 在Activity 中定义了共享ViewModel
private val viewModel by viewModels<MyViewModel>()
,viewModels()
是Koltin的扩展方法。 MutableStateFlow
用于存放MenuItem
的itemId
,asStateFlow()
可以暴露不可变的StateFlow
数据,避免直接更改。StateFlow
使用collectLatest()
只保留最新的value,取消了中间态的所有计算。repeatOnLifecycle
可以保障Fragment 是start状态才会collect最新的值,这样当Activity在后台状态的时候会暂停数据流动,当Activity在前台时候恢复流动。这个模式的开销较少,性能表现比launchWhenStarted()
等方式要好,而且写法上更简便。
安全建议:
- 初始值设置合理, 对于新App,默认显示的Item, 给
currentNavigationItemId
设置一个默认值。
这两种方法都能有效地解决在导航图中复用Fragment的问题。在实践中应根据实际项目需求权衡选择。