Giter VIP home page Giter VIP logo

respawn-app / flowmvi Goto Github PK

View Code? Open in Web Editor NEW
252.0 252.0 7.0 4.81 MB

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

Kotlin 100.00%
android android-architecture android-library architecture coroutines ios jvm kmp kotlin kotlin-coroutines kotlin-multiplatform multiplatform mvi udf wasm

flowmvi's Introduction

Respawn Team is a startup dedicated to helping people become better.

Our Projects

  • Respawn - a modern, gorgeous mobile self-improvement app.

Our open-source projects:

  • FlowMVI - a multiplatform coroutine-based MVI library.
  • KMMUtils - a collection of things that are missing from Kotlin STL and other popular libraries.

Get in touch:

flowmvi's People

Contributors

codacy-badger avatar code-factor avatar dependabot[bot] avatar nek-12 avatar sweep-ai[bot] avatar wiryadev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

flowmvi's Issues

[๐Ÿš€] Add Koin Integration

Description (required)

  • Koin scopes tied to the store's lifecycle
  • Automatically closing scopes
  • Koin Containers
  • Koin Scopes for stores (take from the sample app?)
  • StoreViewModel injection from the sample app - can also make it multiplatform?

[๐Ÿš€] [Help wanted] Design a universal subscription lifecycle API

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:

  1. 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.

  2. 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

LocalKoinScope crash in ComposeScreen

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?

[๐Ÿš€] Debugger Enhancements & Fixes

Description:

  • Crash: Cannot coerce value in range when list of stores contains 3+ values
  • Stores can be duplicated when recreated because a unique ID is assigned upon creation - we don't want that. Start requiring a valid name and use the name as an identifier of the store.
  • Write error messages for form validation
  • Update color scheme
  • When removing store filters, they are sometimes not updated immediately
  • Remove workaround of essenty lifecycle and use the official function
  • Implement full-on decompose architecture
  • Set text style of the clipboard content as monospaced
  • Move timeline screen top bar into scaffold's top bar
  • Try using jetbrains's dynamic two pane scaffold variation they made
  • Add an icon for the divider on timeline that can be dragged
  • Use localized date-time formatting
  • Migrate to string resources
  • Exclude the server module from dokka javadoc publishing. Use per-project application of dokka.
  • Ability to pause and unpause the store

Source sets are not published

Please configure the publish code to include the source sets, because right now when trying to debug the code it looks like this:
Screenshot 2023-03-28 at 14 33 43

[๐Ÿš€] Create a new multiplatform sample app

  • Parallel tasks
  • Side effect styles
  • MVVM+ style
  • Subscription management
  • Custom plugins
  • Android XML
  • Logging - print on screen
  • Undo/redo
  • Decompose
  • DI Setup
  • LCE
  • Wasm publishing
  • Lifecycle setup
  • Desktop app publishing (to releases)
  • Android app publishing (to releases)
  • Refactor saved state logic to support wasm and localstorage

`derivedStateOf` doesn't work for `MVIComposable`

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.

State doesn't update from Store

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

[๐Ÿš€] Implement Time Travel for Debugger

Description

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?

[๐Ÿš€] DSL For Lazy plugins with access to the store.

Description (required)

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.

`updateState` & `withState` with state inside receiver potentially leads to overlooking the problems

Problem

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
            }
        }
    }
}

Possible solution

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?

  • Deprecate actual function with, at least, 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).
  • Introduce new functions that will provide state as a parameter, but not as receiver.

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.

Typo in the README.md file

androidMainImplementation("pro.respawn.flowmvi:compose:$flowmvi")

Should be:

androidMainImplementation("pro.respawn.flowmvi:android-compose:$flowmvi")

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.