Respawn Team is a startup dedicated to helping people become better.
- Respawn - a modern, gorgeous mobile self-improvement app.
A Kotlin Multiplatform MVI library based on coroutines with a rich DSL and a powerful plugin system.
Home Page: https://opensource.respawn.pro/FlowMVI/
License: Apache License 2.0
server
module from dokka javadoc publishing. Use per-project application of dokka.We have migrated to the new central publishing console. Need to update the publishing configuration to use a community plugin that supports the new endpoints.
Since we are only using store plugins within the store itself, it makes sense to assume that the plugins could have access to some properties of the store they were installed into. However right now this is not the case and the plugins can't access StoreConfiguration
in their builders. Lazy plugins right now are just plugins wrapped in lazy init, but we can make a plugin which invokes its build
function when the store is built, not immediately upon installation, which would let it have access to the store configuration and maybe even other plugins.
The task is to implement a dsl for lazy plugins and let them use StoreConfiguration
inside.
We should use either synchronized collections or immutable collections to mutate the values. A simple "atomic" does nothing in our case.
There is an issue with the lifecycle in version 2.5. The lifecycle is now cross-platform and is propagated through CompositionLocal
by default by Compose itself. However, libraries (such as Essenty) define their own lifecycle.
This results in the following:
If FlowMVI uses this CompositionLocal
, it will not be aware that the lifecycle should be different (as provided by the navigation library). This will introduce resource leaks when subscribing to stores. The biggest downside is that this local lifecycle is ALREADY being used on Android.
If FlowMVI does NOT use this lifecycle, then it will be necessary to create yet another duplicate of half of the lifecycle code and force the user to convert their lifecycle into FlowMVI's lifecycle and manually pass it as an argument on each screen. This will lead to the deprecation of the subscription function, which is used on every screen, boilerplate during subscriptions, and also the exposure of part of the library's internals to users, who will now have to implement this (yet another) lifecycle.
I encourage everyone to provide their feedback on how they would like to see this resolved, which option to choose and how to gracefully migrate users to the new API
---thx
Probably I'm not fully understanding how this works and missing something. So, here it goes:
With my previous MVI implementation I would usually have a ViewModel with a state as such:
data class MyState(
val data: SomeData? = null,
val isLoading: Boolean = false,
val error: String? = null
)
sealed class MyEvent {
data object ClickEvent : MyEvent()
}
class MyViewModel(
): ViewModel() {
var state by mutableStateOf(MyState())
private set
fun onEvent(event: MyEvent) {
when(event) {
is MyEvent.ClickEvent->
// do stuff and get some new data
state = state.copy(
data = SomeData(),
isLoading = false,
error = null
)
}
}
}
}
Then on my ViewModel because the state is a MutableState my views will update automatically
@Composable
fun MyComposable(vm: ViewModel) {
if (vm.state.isLoading) {
CircularProgressIndicator()
} else {
Button(onClick = { vm.onEvent(MyEvent.OnClick) }) {
Text(text = "click")
}
}
viewModel.state.error?.let { error ->
Text(text = error)
}
}
*** Using FlowMVI
Here kinda my code:
class MyContainer(
) {
val store = store<MyState, MyIntent, MyAction>(initial = MyState.Loading) {
reduce {
when(intent) {
MyIntent.ClickIntent -> {
// do stuff
updateState<MyState.Success, _)> {
copy(
data = SomeData()
)
}
}
}
}
}
}
class MainActivity : ComponentActivity() {
@Inject
lateinit var store: StoreViewModel<MyState, MyIntent, MyAction>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyTheme {
MVIComposable(store = store) { state ->
when(state) {
is MyState.Loading -> CircularProgressIndicator()
is MyState.Success -> {
Button(onClick = {
store.intent(MyIntent.ClickIntent)
}) {
Text(text = "click")
}
is MyState.Error -> Text(text = error)
}
}
}
}
}
My issue is that after Button click the reduce in the MyContainer gets called with the intent, but after I update the state using updateState{ copy() } the event does not get propagated down stream to my Composables.
If MVIComposable gave me MutableStateOf instead of a MyState this implementation would work.
Like I said at the beginning perhaps I'm missing something I didn't fully understand
androidMainImplementation("pro.respawn.flowmvi:compose:$flowmvi")
Should be:
androidMainImplementation("pro.respawn.flowmvi:android-compose:$flowmvi")
We should explain how to test their stores and plugins to users, and especially how to update states correctly and what "atomic state updates" mean.
When I want to use the derivedStateOf
for my state, which comes from the MVIComposable, is not working. The reason is that the entire content
composable is recomposed every time the state change.
derivedStateOf
works only with the androidx.compose.runtime.State<S>
, but in MVIComposable
the state is already unwrapped.
Example of the issue:
MVIComposable(provider = viewModel) { state ->
Column(modifier = modifier) {
val shouldShowButton by remember {
derivedStateOf {
//This will be invoked only once, but should be every time the state changes
state.shouldShowButton()
}
}
}
To fix it I created a slightly changed function that returns androidx.compose.runtime.State<S>
and allows the developer to unwrap the state manually:
@Composable
fun <S : MVIState, I : MVIIntent, A : MVIAction, VM : MVIProvider<S, I, A>> MVIComposableWithState(
provider: VM,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
content: @Composable ConsumerScope<I, A>.(state: State<S>) -> Unit,
) {
val scope = rememberConsumerScope(provider, lifecycleState)
// see [LifecycleOwner.subscribe] in :android for reasoning behind the dispatcher
val state = provider.states.collectAsStateOnLifecycle(Dispatchers.Main.immediate, lifecycleState)
content(scope, state)
}
And this allows me to modify my example:
MVIComposable(provider = viewModel) { wrappedState ->
val state by wrappedState // Unwrap the state manually and don't recompose the entire content
Column(modifier = modifier) {
val shouldShowButton by remember {
derivedStateOf {
//This will be invoked every time the state changes -> as expected
state.shouldShowButton()
}
}
}
I propose adding the method overload which will return the wrapped State<S>
instead of just S
.
When running the samples and opening Compose screen, there will be crash with this error.
2023-09-25 14:21:17.632 14587-14587 AndroidRuntime pro.respawn.flowmvi E FATAL EXCEPTION: main
Process: pro.respawn.flowmvi, PID: 14587
org.koin.compose.error.UnknownKoinContext: No Koin context has been provided
at org.koin.compose.KoinApplicationKt$LocalKoinScope$1.invoke(KoinApplication.kt:49)
at org.koin.compose.KoinApplicationKt$LocalKoinScope$1.invoke(KoinApplication.kt:48)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29)
at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31)
at androidx.compose.runtime.CompositionLocalMapKt.read(CompositionLocalMap.kt:88)
at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:2049)
at pro.respawn.flowmvi.sample.compose.ComposableSingletons$ComposeActivityKt$lambda-1$1.invoke(ComposeActivity.kt:30)
at pro.respawn.flowmvi.sample.compose.ComposableSingletons$ComposeActivityKt$lambda-1$1.invoke(ComposeActivity.kt:20)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:428)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:252)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:251)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:195)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:119)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:118)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:110)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:158)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:157)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:157)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:142)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:78)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3340)
at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3273)
at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:588)
at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:1013)
at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:520)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:142)
2023-09-25 14:21:17.632 14587-14587 AndroidRuntime pro.respawn.flowmvi E at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:133)
at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1191)
at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:133)
at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:183)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.kt:314)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.kt:192)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:133)
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1266)
at android.view.View.dispatchAttachedToWindow(View.java:20479)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3489)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2417)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I found two way to solve this, we can wrap it inside KoinApplication composable, or use the parent scope to be passed down to composable. Either way it involves passing our own scope so LocalKoinScope can detect something. Is it bug from Koin or the FlowMVI itself?
Something like
FlowMVI provides useful functions to prevent state racing through updateState
and withState
functions, but sometimes the way it's declared can lead to potential problem overlooking. The problem is that the state is handled in the receiver of the lambda, not as a parameter.
For example:
reduce { intent ->
when (intent) {
is EmailChange -> updateState { copy(email = EmailAddress(intent.value)) }
is ButtonClicked -> updateState {
copy(email = email.validate()).let {
if (email.isValid) { // we used previous state that isn't validated, what means it'll always be false
it.copy(isLoading = true)
authorizeAsync(..)
} else it
}
}
}
}
To avoid such problems, but within parameters in the chain of inner lambdas, Intellij Idea has dedicated inspection that warns when you have the same parameters names. For the receivers, there's no such validation or check (except of DSLs contexts) and it makes updateState
and withState
potentially problematic even in simple cases.
Introducing the separate functions without such problem can help, but does not solve problem fully as we can't be sure that someone else will use the right function. In addition, it adds a new layer of complexity, as there's already useState
, withState
and updateState
that you should understand.
What would I do?
DeprecationLevel.WARNING
level or with RequiresOptIn
(it will make able people to opt-in the error / warning on the project level and will not break existing code).Overall, I think everything except of DSLs builders that requires particular contexts to run, should be handled using parameters as it's more obvious in case I described and, for example, when looking on code on platforms like GitHub, but not in IDE.
https://github.com/JetBrains/compose-multiplatform/releases/tag/v1.6.10-dev1584
requireLifecycle
and DefaultLifecycle
.Find the typo in ./docs/quickstart.md in the section about store properties. Correct the name of function updateStateImediate
Look for other typos as well.
Okio on Wasm does not support saving files (obviously), so the workaround is probably to save them to localstorage.
We already have everything we need for the time travel support. What we need right now is to retrieve the information about store states, intents, actions that the client keeps track of and allow the user to rollback to that state.
The idea is to send the time travel information from the client to the server. The client could PUT into the server's storage directly using a request?
Investigate how well the app handles the migration of formats. Maybe change the extension to .bin
?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.