返回

Android导航图Fragment复用: 多Tab高效刷新

Android

导航图中复用Fragment

问题:相同Fragment的多种导航选择

应用开发中,常会遇到一个情景:需要使用底部导航栏(BottomNavigationView)或其他形式的导航,不同的导航项都指向同一个Fragment。Fragment的内容会根据所选导航项ID动态变化,例如展示不同类型的数据列表。一个常见的错误认知是直接把不同导航项都指向同一个Fragment的固定实例,但这样并不能成功触发Fragment的刷新逻辑。本文探讨这种问题的起因及有效解决方法。

通常,当用户选择BottomNavigationView上的一个标签时,如果导航图中存在多个相同的fragment的定义,应用不会刷新已经存在的fragment。系统默认的导航逻辑倾向于复用当前已经显示的fragment实例。它认为我们已经位于要到达的位置了,因此不会重新创建和配置它。我们需要考虑如何让应用明白我们需要每次选择导航时都触发更新。

解决方法一:动态设置Fragment参数

核心思路在于,不要让导航图定义直接关联到Fragment的“固定”实例,而是让每个导航动作都产生一个新的Fragment实例,并通过参数(Bundle)区分。每个导航选择会重新创建Fragment, 并用选择的navigationID给Fragment进行初始化,通过它加载内容。

操作步骤:

  1. 导航图中,配置统一的目标 Fragment。 所有底部导航项,它们的destination都是同一个fragment。
  2. 在Java或Kotlin代码中,使用findNavController()设置 Fragment 启动的 bundle参数,传递导航项 ID 。每当点击BottomNavigationView的不同项时,构建一个携带特定参数的Bundle,例如当前选中的导航项 ID。使用findNavController()找到合适的导航控制器,利用其 navigate 功能跳转,并在跳转时携带bundle参数。
  3. 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每次onViewCreatedonCreateView 时观察 ViewModel 中的 navigation ID, 根据当前 ID 去请求相关数据即可。 使用ViewModel 方式优势是可以利用数据持有能力,Fragment横竖屏切换或者销毁重建不用重新请求数据,提高性能。

操作步骤:

  1. 创建一个 ViewModel 并在 Activity 级别绑定.
  2. 在Activity 的底部导航栏监听中,更新 ViewModel 中当前选择的 Id 值,通过viewModel.setSelection(menuItem.itemId)
  3. Fragment 的 onCreateView()onViewCreated() 里,观察 ViewModel 中当前的选择的导航id, 根据不同的Id值去加载不同的内容;
  4. 使用lifecycleScoperepeatOnLifecycle可以确保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 用于存放MenuItemitemId, asStateFlow() 可以暴露不可变的 StateFlow 数据,避免直接更改。StateFlow 使用collectLatest()只保留最新的value,取消了中间态的所有计算。
  • repeatOnLifecycle 可以保障Fragment 是start状态才会collect最新的值,这样当Activity在后台状态的时候会暂停数据流动,当Activity在前台时候恢复流动。这个模式的开销较少,性能表现比 launchWhenStarted() 等方式要好,而且写法上更简便。

安全建议:

  • 初始值设置合理, 对于新App,默认显示的Item, 给currentNavigationItemId设置一个默认值。

这两种方法都能有效地解决在导航图中复用Fragment的问题。在实践中应根据实际项目需求权衡选择。