Skip to content

Instantly share code, notes, and snippets.

@gmk57
Last active February 13, 2024 15:17
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gmk57/330a7d214f5d710811c6b5ce27ceedaa to your computer and use it in GitHub Desktop.
Save gmk57/330a7d214f5d710811c6b5ce27ceedaa to your computer and use it in GitHub Desktop.
Sending events to UI with Channel/Flow + custom collector (see my first comment for reasons behind it)
/**
* Starts collecting a flow when the lifecycle is started, and **cancels** the collection on stop.
* This is different from `lifecycleScope.launchWhenStarted { flow.collect{...} }`, in which case
* the coroutine is just suspended on stop.
*/
inline fun <reified T> Flow<T>.collectWhileStarted(
lifecycleOwner: LifecycleOwner,
noinline action: suspend (T) -> Unit
) {
object : DefaultLifecycleObserver {
private var job: Job? = null
init {
lifecycleOwner.lifecycle.addObserver(this)
}
override fun onStart(owner: LifecycleOwner) {
job = owner.lifecycleScope.launch {
collect { action(it) }
}
}
override fun onStop(owner: LifecycleOwner) {
job?.cancel()
job = null
}
}
}
class MyViewModel : ViewModel() {
// You can specify exact buffer size and onBufferOverflow strategy here, or go full blast with Channel.UNLIMITED
private val eventChannel = Channel<String>(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val eventFlow = eventChannel.receiveAsFlow()
fun sendEvent(element: String) = eventChannel.trySend(element) // `trySend` replaces `offer` since Coroutines 1.5
}
class MyFragment : Fragment(R.layout.fragment_my) {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// You can collect flow in `onCreate` using `this` as lifecycleOwner
// This is a bit more efficient: `LifecycleObserver` is registered only once
viewModel.eventFlow.collectWhileStarted(this) { Log.i(TAG, "event: $it") }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Or you can collect flow in `onViewCreated` using `viewLifecycleOwner`
// This more closely resembles a typical LiveData observer
viewModel.eventFlow.collectWhileStarted(viewLifecycleOwner) { Log.i(TAG, "event: $it") }
}
}
@gmk57
Copy link
Author

gmk57 commented Dec 23, 2021

@fergusonm Thanks for the update, I haven't seen this article before.

It's good to see that Google is aware of the issue. I like UDF & try to use it as much as possible. From my point of view, the main problem is an "impedance mismatch" between state-down-events-up principle and event-driven classic Android UI.

Event wrappers and userMessageShown() are both workarounds for this mismatch, with the latter being much more verbose. Wrapper just encapsulates basically the same logic to avoid repetition.

Compose mostly alleviates this concern, e.g. AlertDialog is now just a composable and can be clearly modeled as part of state.

On the other side, AFAIK, Navigation Compose is still event-based. Is this code guaranteed to be called only once?

LaunchedEffect(viewModel.uiState)  {
    if (viewModel.uiState.isUserLoggedIn) {
        currentOnUserLogIn()
    }
}

Another point that bothers me is the suggestion to handle "UI behavior logic" directly in UI, bypassing ViewModel. This goes against the "single source of truth" principle. It also complicates handling configuration changes, which becomes a larger issue on Android 12.

BTW, one phrase caught my eye: "business logic remains the same for the same app on different mobile platforms". Is Google starting to push us towards KMP? ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment