You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 01:41:00