Hi! I am currently evaluating orbit-mvi and uniflo...
# orbit-mvi
s
Hi! I am currently evaluating orbit-mvi and uniflow-kt (still on theoretical level). Since I am quite new to these topics, I already asked a lot of questions in the #uniflow channel. Not all of them have been answered so far. Feel free to provide your point of view on those and additional aspects here. I am especially interested in the following (see comments):
• Flows: Orbit uses and exposes Flows while Uniflow does not. What are the benefits and/or drawbacks (in the long run)? I am a newbie and cannot judge this at the moment.
• Threading-1: By default, Uniflow runs all actions on IO. If I understand your "Threading" chapter correctly, Orbit intents do not block the UI, because they do not run on the main thread. Instead, they are always offloaded to an event-loop in a background coroutine. How is this different from Uniflow's IO? Like in Uniflow, this entails 2 extra thread switches when modifying the UI, right? Blocking code in intents, block this event-loop. So by default, intents are sequenced like in Uniflow with its Actor, right? If I want to run transformations (of different intents) in parallel, I should switch coroutine context here myself as needed. Is the latter easier with Orbit than in Uniflow? What are the best practices?
• Threading-2: Given that my understanding in "Threading-1" is somehow correct, the code snippet in your MVI framework comparison would complete in 5000 + 2500 = 7500 ms in Uniflow and Orbit, right? I am asking because I initially assumed, that Orbit would run transformations in parallel by default and therefore complete in 5000 ms (first action2, then action1).
• Types/Generics: Orbit always requires Generics for its container. Uniflow relies on UIState and UIEvent and provides actionOn<>. If I want to support several different states like in Uniflow (which I think offers a lot of flexibility), I should do this in Orbit with a sealed class as shown in one of the previous posts, right? After that, state and effect handling with when() is the same in both frameworks. No advantages/disadvantages (type casting) on either side. Or do I oversee something? Also things as shown in this

video ("orchestrating states &amp; views)

are possible with Orbit then, right? BTW: What do you think of the "multiple screens" scenario? In your Jetpack Compose sample app you are using navigation-compose. Is this preferable?
• Jetpack Compose: Is there a chance to beautify/simplify the wiring of Jetpack Compose with a one-liner like Orbit's "viewModel.observe(...)" or Uniflow's "onState/Event(...)"?
m
Wow thanks for so many great q’s
I’ll try to answer them to the best of my ability
Flows
- 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 sample
a
It's worth noting some MVI frameworks use their own reactive framework, our own view is why re-invent the wheel and give users of your library yet another thing to learn - we wanted Orbit to be as simple to use as possible and to be able to rely on what is built-in to the language, such as suspend and Flow, or the platform, such as SavedStateHandle, idle resources, etc, where possible. As @Mikolaj Leszczynski mentions, since July 2020 Google officially recommends coroutines and Flow for asynchronous work. On the Android side this gives us great compatibility with Google's libraries and the wider community with little effort on our part.
With uniflow, it seems you're already aware of this issue around using sealed classes, https://github.com/uniflow-kt/uniflow-kt/issues/71. I would hope it'll be fixed soon given the issue was opened over 3 months ago... Of course, other than the necessary type casting in uniflow, then there's not much difference in the syntax afterwards when dealing with your state object. The only other to say is whether a sealed class is necessary; of course very much depends on your use case and what the UI looks like, but often you can get away with not having specific Loading state and representing it as nullable data instead - sealed class is more explicit of course. Error handling is always an interesting area and doesn't always result in the main ui state changing but rather on side effects to show dialogs or toasts.
As @Mikolaj Leszczynski points out with the Compose sample I've stuck to the platform patterns where I can, more people will be familiar with using nav component so its easier to understand the interactions with the library (hopefully). That doesn't mean you have to use it though, really its whatever works best for you.
Regarding tidying up the compose interface, yes, perhaps, I didn't at the time as I wanted to get something working and Compose wasn't released when I started writing the sample app. The essence of the current code is you have to do this:
Copy code
val 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:
Copy code
@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 too
👍 2
In 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.
Incidentally, our early drafts of Orbit 2 did hide the fact it used
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.
m
Yeah, we use a trick - scheduling intent coroutines on the
undefined
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.
a
The reason you don't want to block transformations is what does this mean when you're performing a slow network call? As far as i could tell no events from your UI can be processed until that initial call completes; of course it depends how you have your UI and ViewModels setup of whether this matters or not, I'm typically putting things like back presses through the
ViewModel
(much easier for inserting analytics) in which case the user cannot navigate back until completion of a network call they no longer care about
Before I forget, multiplatform is another interesting topic. I wouldn't let uniflows lack of support necessarily put you off it. KMM is still in its infancy on the Swift side. I've been working these last couple of weeks to see if we can come up with something usable from the Orbit side, but it is early days. Firstly, we expose
Flow
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.
s
Wow. I am overwhelmed. Thank you guys for this immediate and detailed answers/feedback/insights. And by the way, also for all the obvious work, passion and experience you put into your framework. I really appreciate this and know how much time this consumes. I am sure it will pay off and you will soon reach 1k+ GitHub stars. You definitely deserve it. You meanwhile clarified all my open points. Even my remaining questions related to Flow/Channel, threading, performance and reproducability were answered during the last hour on its own. Thanks so much! The only thing I didn't really understand was this comment related to types/generics: "_Another nice thing is not having to wrap all your params into objects_." Could you elaborate a bit here? I would really appreciate such a one-liner for Jetpack Compose. Your suggestion is close to what I had in mind. For the sake of consistency, I would probably allow state handling also via lambda (maybe in addition to returning it with collectAsState().value). And of course we need default params for e.g. the no-side-effect use case. My feedback on what you could do better? This is hard to tell for me with my limited knowledge regarding these new topics. I guess not much. From the very beginning I had the gut feeling that Orbit might be the better choice in the long run. It's more technical, influenced by several approaches/learnings from the past and backed by at least two core developers who can exchange their point of view and share work. I really like the strategy of not reinventing the wheel and sticking closely to platform/language standards. I also believe you when you're saying that you're trying to keep things as simple as possible. However, for a newbie like me, Uniflow still seems/seemed easier on the surface. This might sound silly and is surely a matter of taste, but I feel/felt more comfortable (and consistent) with "action/setState/sendEvent/onState/onEvent" instead of "intent/reduce/postSideEffect/...stateFlow.collectAsState().../LaunchedEffect(viewModel)...". But the API is just the first contact point. It goes on with exposing flows directly, using composition over inheritance, supporting KMM, and more the like. All of this is (probably) the correct way. But again, in sum for someone who has no experience in Kotlin, Coroutines, Flow, MVI and Compose it looked like a bit too much in the beginning. I hope it will turn into an advantage once I get used to the new world. Next, documentation. As you already wrote yourself, Uniflow docs are well written and support the impression of being simpler to use with even more features. You already did a first step forward with your new doc site. But also here things felt a bit more technical and sometimes incomplete (not even sure if so). Maybe you could also highlight your advantages and your strategy as you did above. Very valuable was your comparison article and your MVI video. Such things help to spread the word. Continue/update those. Maybe a video which focuses more on the framework features instead of MVI and internals would be a good addition, like Uniflow did. And one thing I missed a lot when I started my research on several MVI frameworks was a proper sample and docs dedicated to Jetpack Compose. In my opinion this is what currently drives lots of the interest behind MVI frameworks since many devs now want/need to change their architecture. I am sure you can still attract many more users in this area. So to summarize: keep your strategy and technical focus but try to lower the entry hurdle as far as possible by simple APIs ("one-liner" etc.), better/more docs and spot on Jetpack Compose. In any case, keep your dedication and passion. I'll keep you updated once I leave the theoretical level and start to write code and understand the new topics. I am not insisting on an "OrbitViewModel". Quite the contrary. I am well aware of the benefits of composition. It's just one of the little pieces that felt more like the stuff we have at the moment. KMM is certainly nice to have but not critical at all for me. If it complicates things, I would rather omit it. I work in the automotive industry. Our apps need to run on AAOS. iOS is currently not needed. But maybe KMM becomes interesting internally with Compose Desktop/Web. Once again: THANK YOU.
m
My comment regarding params was regarding traditional redux style MVI frameworks rather than uniflow - traditionally all actions are piped down a stream to the framework so you need to create a sealed class for your actions and every action becomes an object you need to create to wrap your action parameters in.
Thanks a lot for your feedback. We're humbled by your comments!