Stephan Schuster
08/23/2021, 8:53 AMMikolaj Leszczynski
08/23/2021, 9:57 AMFlows
- I think this is down to our personal decision. The framework is heavily coroutine based anyway so we didn’t think it would make a difference if we hid away the Flow
types on the container. Coroutines have been recommended by google to use for Android async operations. Since we expose Flow
we can make good recommendations as to how to consume the API safely in a view in terms of its lifecycle.
• Threading-1
- I can’t really comment on what the problem with Uniflow’s threading is, but yes, it could result in unresponsive UI’s based just on its observed behaviour. The worst part of it is it’s not just blocking work, it’s even suspending work that can cause problems. In Orbit we launch a coroutine for every action which means we can handle new incoming events while doing suspending work in the background. Blocking work done in intents without context switch will still block the event loop though so be careful with that. Nothing we can do about it.
• Threading-2
- Orbit would complete that in 5000ms, as suspending functions do not block the event loop.
• Types/Generics
- The first version of orbit was using streams for actions in fact. We’ve used the library heavily at babylon health and we’ve noticed that losing strong typing comes with a host of problems and is not really necessary. We struggled to come up with benefits of piping all actions down a stream. E.g. we’ve noticed that some of our view models have flows that reference actions that were never being sent any more - because we couldn’t be alerted to it by the IDE or static analysis tools. With Orbit you can see that a ContainerHost function is never being called. Another nice thing is not having to wrap all your params into objects. To answer the second part of your question - yeah handling a sealed class state should be the same in both frameworks. I think @appmattus might be able to comment more on navigation using compose. Generally we leave navigation implementations to the user as it’s not the library’s responsibility so it’s up to you to figure out what works for you.
• Jetpack Compose
- @appmattus could shine a light here as he wrote the latest sampleappmattus
08/23/2021, 12:52 PMval state = viewModel.container.stateFlow.collectAsState() // returns State<T>
LaunchedEffect(viewModel) {
launch {
viewModel.container.sideEffectFlow.collect { handleSideEffect(navController, it) }
}
}
I guess the neatest one liner we could come up with would be something like val state = viewModel.observe { handleSideEffect(navController, it) }
Using the following implementation:
@Composable
fun <STATE : Any, SIDE_EFFECT : Any> ContainerHost<STATE, SIDE_EFFECT>.observe(block: suspend (sideEffect: SIDE_EFFECT) -> Unit): State<STATE> {
LaunchedEffect(this) {
launch {
container.sideEffectFlow.collect { block(it) }
}
}
return container.stateFlow.collectAsState()
}
@Mikolaj Leszczynski, we'll need to create a specific compose module at some point for this code though, although if we can wait till after the multiplatform code i'm working on is in that would be good as i'm already upgrading AGP etc for that (and using compose too).. was also going to suggest now Compose is v1.0 we can merge the separate sample app into the main project tooIn general, I feel that uniflow-kt has a simpler, cleaner and more consistent API with better naming. By intention it abstracts away more stuff.@Stephan Schuster It would be great to get your feedback of what you think we can do better; this was partly why I wrote that article in the first place too, and indeed lead to us introducing the
observe
shorthand - the boilerplate became very obvious when reviewing other libraries.In fact, uniflow-kt relies solely on LiveData for state and events while I basically read everywhere that StateFlow and SharedFlow/Channel is the way to go for MVI. Is this true? Should this bother me?Of course it's hard to know exactly why Uniflow uses
LiveData
under the hood.
As far as I am concerned, with the advent of Flow
and the inherent complexities of combining `LiveData`'s together I would imagine Google deprecating it in the not so distant future. Flow
is here to stay.
StateFlow
works well for MVI given the visibility of the current state value
.
From days past, LiveData
was never great for working with one-off events, that is you want a Toast to appear once and so when you rotate the screen you want that event to be consumed and not triggered again. The main hack around this was to use SingleLiveEvent
but my understanding is this is buggy, @Mikolaj Leszczynski may be able to give some more concrete examples. Without digging into the uniflow code, I'm not sure how they handle the consumption of events, there's nothing obvious in https://github.com/uniflow-kt/uniflow-kt/blob/master/uniflow-android/src/main/java/io/uniflow/android/livedata/LiveDataPublisher.kt as its just a MutableLiveData
. Additionally, SharedFlow
isn't setup for single consumption either and actually I believe using a Channel
is the best approach today.
Should it bother you that uniflow-kt uses LiveData under the hood? As long as there are sufficient tests in place and you are happy with how it works as a consumer of the library then I wouldn't worry about it.Flow
internally but it actually added a lot of complexity to the code base especially when considering the structured concurrency of coroutines - we basically felt we were re-writing the framework, so in the end we bit the bullet and exposed it.Inheritance instead of composition is okay for me. It's surely less flexible but sufficient and a bit simpler in most cases.Of course, nothing stops your own apps creating a base view model to inherit from, lets call it OrbitViewModel. We chose not to because there's little complexity and it highlights that you can use anything to host a container. It's worth noting that for Kotlin Multiplatform support we will need to introduce a base class, so watch this space there may very well be an OrbitViewModel coming soon anyway.
Uniflow uses an Actor to always run entire actions in sequence which ensures that they modify state in a reproducible way. I assume this is (sometimes) slower than the Orbit approach which runs transformations still in parallel and only sequences the reduce blocks working on volatile state but therefore probably cannot guarantee reproducability until all actions/intents are done.The reproducibility in Orbit depends on what you are doing in the Orbit thread (i.e. directly in the
intent
block). As @Mikolaj Leszczynski highlights above the work you perform on the Orbit thread is blocking and so as long as you don't have any suspending calls in place before a reduce
block then the reduce
block will run in the sequence the actions are received.Mikolaj Leszczynski
08/23/2021, 3:00 PMundefined
dispatcher, which means they execute on the same thread until the first suspension point. So as long as you don’t have any suspending calls you can modify state in a reproducible sequential way.appmattus
08/23/2021, 3:02 PMViewModel
(much easier for inserting analytics) in which case the user cannot navigate back until completion of a network call they no longer care aboutFlow
but the way Flow
is exposed on the iOS side is, frankly, broken so you end up wrapping your code around this.
Secondly, there is no support for Swift only frameworks such as SwiftUI and Combine.
What we've been looking at is to generate Swift code, in a similar way to https://github.com/icerockdev/moko-kswift, requires your project uses cocoapods for ease of integration.
Ultimately we can do it but it will remain an alpha feature for some time.Stephan Schuster
08/23/2021, 6:27 PMMikolaj Leszczynski
08/23/2021, 6:37 PM