I have the following requirement:
I implemented as follows:
This doesn't work. After fragment 0 shifts focus to fragment 1, its channel reader keeps spinning and doesn't "pass the baton" to the channel reader of fragment 1. Both for-loops seem to read the bus arbitrarily.
I tried introducing a "semaphore", some variable that knows "who should read from the bus", but that doesn't work: If I do the test inside the for-loop, it's too late (the message was already taken from the bus and I lost it); and if I test outside the for-loop it has no effect obviously.
Any tip on how to accomplish this? Many thanks
Adding fragment code:
class CoachingStep0Fragment : Fragment() {
private var _binding: FragmentCoachingStep0Binding? = null
private val binding get() = _binding!!
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
if (taskData != null) {
binding.Step0TaskName.text = taskData.taskName
}
binding.textViewPsmPrompt.visibility = View.VISIBLE
// setUpBusReceiver()
}
override fun onResume() {
super.onResume()
setUpBusReceiver()
}
private fun setUpBusReceiver() {
lifecycleScope.launch {
for (msg in EventBusProvider.coaching) {
when (msg) {
is EventBusAction.PlayPlex -> {
binding.textViewPsmPrompt.text = msg.param
}
is EventBusAction.DisplayScreen -> {
requireActivity().findViewById<ViewPager2>(R.id.coaching_pager).currentItem = 1
}
}
}
}
}
The second fragment is identical except its number is 1.
As explained in the Restartable Lifecycle-aware coroutines docs:
Even though the
lifecycleScope
provides a proper way to cancel long-running operations automatically when theLifecycle
isDESTROYED
, you might have other cases where you want to start the execution of a code block when the Lifecycle is in a certain state, and cancel when it is in another state. For example, you might want to collect a flow when the Lifecycle isSTARTED
and cancel the collection when it'sSTOPPED
. This approach processes the flow emissions only when the UI is visible on the screen, saving resources and potentially avoiding app crashes.For these cases,
Lifecycle
andLifecycleOwner
provide the suspendrepeatOnLifecycle
API that does exactly that.
In this case, ViewPager2
only moves exactly one fragment to the RESUMED
state at a time - your current page. Your code isn't failing because you started listening too soon (you didn't need to use onResume
at all), but because you failed to stop listening when your fragment was PAUSED
(e.g., when it stopped being the current page).
That means your code should actually look like:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Bundle.getSerializable is actually broken on API 33,
// so you should always use the BundleCompat method
val taskData = BundleCompat.getSerializable(
requireArguments(),
"task"
CoachingUiState::class.java
)
if (taskData != null) {
binding.Step0TaskName.text = taskData.taskName
}
binding.textViewPsmPrompt.visibility = View.VISIBLE
// Re-add this line back in
setUpBusReceiver()
}
private fun setUpBusReceiver() {
viewLifecycleOwner.lifecycleScope.launch {
// This is the line that causing the containing code
// to only run when you move up to RESUMED and cancel
// as soon as you get PAUSED, thus ensuring that only
// the current page is listening
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
for (msg in EventBusProvider.coaching) {
when (msg) {
is EventBusAction.PlayPlex -> {
binding.textViewPsmPrompt.text = msg.param
}
is EventBusAction.DisplayScreen -> {
requireActivity()
.findViewById<ViewPager2>(R.id.coaching_pager)
.currentItem = 1
}
}
}
}
}
}