Android/Kotlin:如何让两个Fragment监听同一个Channel?
ViewPager2多Fragment共享Channel时的事件抢占与丢失问题解决方案
需求
- 事件源从各类来源获取事件
- 两个不同页面需根据传入事件展示对应信息
- 包含“跳转至另一页面”类型的事件
- 同一时间仅展示一个页面
- 事件不丢失,需按到来顺序展示
当前实现方案
- 事件生成器通过
send()方法向Channel写入事件 - ViewPager2中的两个Fragment负责展示逻辑
- 每个Fragment在
onResume()中通过lifecycleScope启动协程,以无限循环读取同一个Channel并处理事件 - 应用初始展示fragment 0,收到跳转指令时切换ViewPager2至position 1
遇到的问题
切换Fragment后,原Fragment的Channel读取协程仍在运行,两个协程会抢占读取事件,导致事件被错误处理或丢失。尝试用信号量标记读取权限但无效:循环内判断时事件已被取出导致丢失,循环外判断则无法动态控制读取权。
解决方法
核心思路是只保留一个Channel读取源,由统一的组件负责读取事件,并根据当前可见的Fragment分发事件,彻底避免多协程抢占。
方案1:使用ViewModel统一管理事件分发
这是最可靠的架构方案,将事件读取和分发逻辑集中到ViewModel,利用其与Activity绑定的生命周期保证事件不丢失,同时精准控制事件流向。
代码实现
首先创建ViewModel:
class CoachingViewModel : ViewModel() { val eventChannel = Channel<EventBusAction>(Channel.UNLIMITED) // 无限缓冲区确保事件不丢失 var currentFragmentPosition = 0 // 跟踪ViewPager当前选中位置 init { viewModelScope.launch { // 唯一的事件读取协程 for (event in eventChannel) { // 根据当前位置分发事件给对应Fragment when (currentFragmentPosition) { 0 -> getCurrentFragment()?.let { it as? CoachingStep0Fragment }?.handleEvent(event) 1 -> getCurrentFragment()?.let { it as? CoachingStep1Fragment }?.handleEvent(event) } } } } // 从宿主Activity获取当前显示的Fragment private fun getCurrentFragment(): Fragment? { val activity = getActivity() as? CoachingActivity ?: return null return activity.supportFragmentManager.findFragmentByTag("f$currentFragmentPosition") } }
修改Fragment,移除自行读取Channel的逻辑,改为实现事件处理方法:
class CoachingStep0Fragment : Fragment() { private var _binding: FragmentCoachingStep0Binding? = null private val binding get() = _binding!! private val viewModel by activityViewModels<CoachingViewModel>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentCoachingStep0Binding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 初始化视图逻辑 val taskData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { arguments?.getSerializable("task", CoachingUiState::class.java) } else { arguments?.getSerializable("task") as? CoachingUiState } taskData?.let { binding.Step0TaskName.text = it.taskName } binding.textViewPsmPrompt.visibility = View.VISIBLE } // 事件处理方法,由ViewModel调用 fun handleEvent(event: EventBusAction) { when (event) { is EventBusAction.PlayPlex -> binding.textViewPsmPrompt.text = event.param is EventBusAction.DisplayScreen -> requireActivity().findViewById<ViewPager2>(R.id.coaching_pager).currentItem = 1 } } override fun onDestroyView() { super.onDestroyView() _binding = null } }
在宿主Activity中更新ViewModel的当前Fragment位置:
class CoachingActivity : AppCompatActivity() { private val viewModel by viewModels<CoachingViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_coaching) val viewPager = findViewById<ViewPager2>(R.id.coaching_pager) // 设置ViewPager适配器... // 监听页面切换,更新ViewModel中的当前位置 viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { viewModel.currentFragmentPosition = position } }) } }
方案2:利用Lifecycle+Mutex控制读取权
如果不想大幅修改现有架构,可通过repeatOnLifecycle限制Fragment仅在前台可见时尝试读取,同时用Mutex确保同一时间只有一个Fragment进入读取循环。
代码示例(修改Fragment的setUpBusReceiver)
private fun setUpBusReceiver() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { // 抢占读取锁,同一时间仅一个Fragment能进入循环 EventBusProvider.readLock.withLock { for (msg in EventBusProvider.coaching) { // 每次读取前检查是否仍处于可见状态,避免切换后继续处理 if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) break when (msg) { is EventBusAction.PlayPlex -> binding.textViewPsmPrompt.text = msg.param is EventBusAction.DisplayScreen -> requireActivity().findViewById<ViewPager2>(R.id.coaching_pager).currentItem = 1 } } } } } } // 在EventBusProvider中添加读取锁 object EventBusProvider { val coaching = Channel<EventBusAction>(Channel.UNLIMITED) val readLock = Mutex() }
关键说明
- 方案1是最优解,单一读取源彻底避免抢占,ViewModel的生命周期也能保证事件不会因Fragment重建而丢失
- 方案2适合快速临时修复,但需要额外处理生命周期检查,潜在风险更高
内容的提问来源于stack exchange,提问作者Yanay Lehavi
相关产品推荐
相关产品推荐

