GithubHelp home page GithubHelp logo

freeletics / flowredux Goto Github PK

View Code? Open in Web Editor NEW
691.0 27.0 26.0 6.49 MB

Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.

Home Page: https://freeletics.github.io/FlowRedux/

License: Apache License 2.0

Kotlin 97.28% Shell 1.86% CSS 0.86%
android architecture coroutines kotlin mvi mvi-android

flowredux's Introduction

FlowRedux

Building async. running Kotlin Multiplatform state machine made easy with a DSL and coroutines.

Usage

Full documentation and best practices can be found here: https://freeletics.github.io/FlowRedux/

sealed interface State

object Loading : State
data class ContentState(val items : List<Item>) : State
data class Error(val error : Throwable) : State


sealed interface Action
object RetryLoadingAction : Action


class MyStateMachine : FlowReduxStateMachine<State, Action>(initialState = Loading){
    init {
        spec {
            inState<Loading> {
                onEnter { state : State<Loading> ->
                    // executes this block whenever we enter Loading state
                    try {
                        val items = loadItems() // suspending function / coroutine to load items
                        state.override { ContentState(items) } // Transition to ContentState
                    } catch (t : Throwable) {
                        state.override { Error(t) } // Transition to Error state
                    }
                }
            }

            inState<Error> {
                on<RetryLoadingAction> { action : RetryLoadingAction, state : State<Error> ->
                    // executes this block whenever Error state is current state and RetryLoadingAction is emitted
                    state.override { Loading } // Transition to Loading state which loads list again
                 }
            }

            inState<ContentState> {
                collectWhileInState( flowOf(1,2,3) ) { value : Int, state : State<ContentState> ->
                    // observes the given flow as long as state is ContentState.
                    // Once state is changed to another state the flow will automatically
                    // stop emitting.
                    state.mutate {
                        copy( items = this.items + Item("New item $value"))
                    }
                }
            }
        }
    }
}
val statemachine = MyStateMachine()

launch {  // Launch a coroutine
    statemachine.state.collect { state ->
      // do something with new state like update UI
      renderUI(state)
    }
}

// emit an Action
launch { // Launch a coroutine
    statemachine.dispatch(action)
}

In an Android Application you could use it with AndroidX ViewModel like that:

class MyViewModel @Inject constructor(private val stateMachine : MyStateMachine) : ViewModel() {
    val state = MutableLiveData<State>()

    init {
        viewModelScope.launch { // automatically canceled once ViewModel lifecycle reached destroyed.
            stateMachine.state.collect { newState ->
                state.value = newState
            }
        }
    }

    fun dispatch(action : Action) {
        viewModelScope.launch {
            stateMachine.dispatch(action)
        }
    }
}

Dependencies

There are two artifacts that you can include as dependencis:

  1. flowredux: this is the core library and includes the DSL.
  2. compose: contains some convenient extensions to work with FlowReduxStateMachine in Jetpack Compose.

GitHub release (latest SemVer)

JVM / Android only

implementation 'com.freeletics.flowredux:flowredux-jvm:<latest-version>'
implementation 'com.freeletics.flowredux:compose:<latest-version>'

Multiplatform

implementation 'com.freeletics.flowredux:flowredux:<latest-version>'

FlowRedux is supported on:

  • JVM / Android
  • iOS
  • watchOS
  • tvOS
  • macOS
  • Linux
  • Windows

We do plan to add support for JavaScript but it’s not available yet.

Snapshot

Latest snapshot (directly published from main branch from CI on each change):

allprojects {
    repositories {
        // Your repositories.
        // ...
        // Add url to snapshot repository
        maven {
            url "https://oss.sonatype.org/content/repositories/snapshots/"
        }
    }
}

Then just use -SNAPSHOTsuffix as version name like

implementation 'com.freeletics.flowredux:flowredux:1.2.1-SNAPSHOT'

flowredux's People

Contributors

befrvnk avatar benjaminlifetime avatar cortinico avatar dependabot-preview[bot] avatar dependabot[bot] avatar dzinek avatar gabrielittner avatar hoc081098 avatar igorwojda avatar jalalawqati avatar josirichter avatar mcatta avatar rasfarrf5 avatar renovate[bot] avatar snappdevelopment avatar sockeqwe avatar ychescale9 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  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

flowredux's Issues

Idea: Rewrite FlowRedux internals with Jetpack Compose

Disclaimer: This is not about changing anything in the DSL per se, this is about rewriting some internals of FlowRedux.

At the moment FlowRedux internally uses Flows as some sort of broadcast channel to publish state changes to the outside but also to the inside (like DSL specific blocks that then trigger) and dispatch actions.

Especially blocks of the DSL (i.e. inState<State> {...}) where cancellation of ongoing background work is required and need to be managed is complex with pure Flows.

@gabrielittner came up with un interesting idea to basically use Jetpack Compose (compiler + runtime) because it is build around State and changes over time on State and has concepts of starting and canceling background work when state changes. That seems to be what we need in FlowRedux and with Compose and @Composable we could probably achieve the same functionality as we have today with our Flow based implementation but with lower complexity (mental overhead) for maintainers of the FlowRedux library itself.

In my head, this is something that we can tackle after FlowRedux 1.0 has been released as the goal is to refacor internals (so private APIs) and not the public API of the DSL etc.

What makes action SelfReducableAction to be called

D/TaskRequestDetailsViewModel$dispatch: FlowRedux -> action : ApproveTask(taskId=TaskReferenceId(id=45019))
D/FlowRedux: Upstream: action ApproveTask(taskId=TaskReferenceId(id=45019)) received
D/FlowRedux: Upstream: reducing Success(taskDetails=TaskDetailsUi(id=TaskReferenceId(id=45019), title=TaskTitle(value=test forward 10), progress=TaskProgress(progress=0.0),
D/FlowRedux: SideEffect3: action SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> received
D/FlowRedux: SideEffect3: reducing Success(taskDetails=TaskDetailsUi(id=TaskReferenceId(id=45019), title=TaskTitle(value=test forward 10), progress=TaskProgress(progress=0.0
D/TaskRequestDetailsViewModel: FlowRedux -> state : Loading(type=ACTION)
D/FlowRedux: SideEffect3: action SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> received
D/FlowRedux: SideEffect3: reducing Loading(type=ACTION) with SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> -> Loading(type=ACTION)

Add overloads for even nicer DSL with function reference

The problem is that parameter with default args doesnt play nice with function references as it requires named arguments.

i.e.

inState<S1> {
   onAction<A1> (block = ::handlerMethod) //  this explicitly needs to named argument
}

Suggested changes:

  • Add methods overloads that only take the block as one parameter so that you could write something like that:
inState<S1> {
   onAction<A1> (::handlerMethod) //  no explicit named argument required
}

DSL for additional conditions

Just an idea that I have mixed feelings about, so dropping my idea here to hear opinions.

enum class MyEnum { A, B, C }
data class MyState(val e : MyEnum)


inState<MyState>(additionalCondition = { it.e == MyEnum.B }) { // means state is MyState AND state.e == B
    ... 
}

I am wondering if this could be expressed somehow with DSL constructs.

For example something like adding a new condition DSL block like this:

inState<MyState> {

    condition( {it.e == MyEnum.B} ) {
        onEnter {...}
        on<Action> {...}
        collectWhileInstate { ... }

        condition (...){
              // even subcondition could be supported
         }
     }
   
    
    onEnter {...}
    on<Action> {...}
    collectWhileInstate { ... }

    ... 
}

That reads for me a bit nicer compared to the current inState<S>(additionalCondition={...})

but obviously has some disadvantages like:

  • exhausting when functionality alike (could probably be solved with Lint checks or so)
  • DSL contains more and more logic that cannot be unit tested anymore individually and reminds me a bit of too much pollated kotest DSL
  • ...

Any thoughts?

Lint check for accessing stateSnapshot in MutateState

Accessing the stateSnapshot parameter inside a MutateState block should be forbidden by a lint Rule.

fun onActionXDoFoo(action: ActionX, stateSnapshot: State) {
   ....
   return MutateState {
       stateSnapshot.copy( ... ) // Accessing must be forbidden because lambda parameter state must be used instead
   }
}

Fix iOS sample

The iOS sample is broken and somehow doesn't sync with the shared multiplatform code anymore.

Some issue with coccoa pods setup I guess.

Maybe someone with more kotlin multi platform experience can help? Maybe @joreilly?

CoroutineScope

Hi, is it possible to call suspend function in on ?

ihave this state machine

inStateWithCondition(isInState = { state -> state.loading }) {
                onEnterEffect {
                    val questionsResult = complianceQuestionsUseCase("co")
                    Log.e("onEnterEffect", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }

                onEnter {
                    val questionsResult = complianceQuestionsUseCase("co")

                    Log.e("onEnter", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }

                on<GetQuestionsAction> { action, _ ->
                    val questionsResult = complianceQuestionsUseCase(action.country)

                    Log.e("GetQuestionsAction", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }
            }

in the onEnter and onEnterEffect methods the coroutine is canceled a first time then it enters the methods again and the normal call is made but in the on method the coroutines are always canceled

kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b8540dc
on<GetQuestionsAction> { action, _ ->
                    val questionsResult = complianceQuestionsUseCase(action.country)

                    Log.e("GetQuestionsAction", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    NoStateChange
                }

is cancelable but add GlobalScope.Launch is coroutine sucess

GlobalScope.launch {
                        val questionsResult = complianceQuestionsUseCase(action.country)

                        Log.e("GetQuestionsAction", "$questionsResult")

                        if (questionsResult is UIState.Success) {
                            OverrideState(
                                ComplianceState(
                                    showContent = true,
                                    loading = false,
                                )
                            )
                        }
                    }

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/build.yml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v4
.github/workflows/ios-build.yml
  • actions/checkout v4
  • actions/setup-java v4
  • maxim-lobanov/setup-xcode v1.6.0
  • actions/cache v4
  • actions/cache v4
  • sersoft-gmbh/xcodebuild-action v3.2.0
  • macos 14
.github/workflows/publish-docs.yml
  • actions/checkout v4
  • actions/setup-java v4
  • actions/setup-python v5
  • actions/upload-pages-artifact v3
  • actions/deploy-pages v4
.github/workflows/publish-release.yml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v4
  • softprops/action-gh-release v2
.github/workflows/publish-snapshot.yml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v4
gradle
gradle.properties
settings.gradle.kts
  • com.freeletics.gradle.settings 0.15.0
build.gradle.kts
compose/gradle.properties
compose/compose.gradle.kts
flowredux/gradle.properties
flowredux/flowredux.gradle.kts
gradle/libs.versions.toml
  • org.jetbrains.kotlin:kotlin-test 2.0.10
  • org.jetbrains.kotlin:kotlin-test-annotations-common 2.0.10
  • org.jetbrains.kotlin:kotlin-test-junit 2.0.10
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.8.1
  • org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.1
  • androidx.compose.runtime:runtime 1.6.8
  • androidx.compose.ui:ui 1.6.8
  • androidx.compose.foundation:foundation 1.6.8
  • androidx.compose.material:material 1.6.8
  • androidx.compose.material3:material3 1.2.1
  • org.jetbrains.compose.runtime:runtime 1.6.11
  • androidx.activity:activity 1.9.1
  • androidx.annotation:annotation 1.8.2
  • androidx.activity:activity-compose 1.9.1
  • androidx.appcompat:appcompat 1.7.0
  • androidx.constraintlayout:constraintlayout 2.1.4
  • androidx.core:core-ktx 1.13.1
  • androidx.lifecycle:lifecycle-common 2.8.4
  • androidx.lifecycle:lifecycle-livedata-core 2.8.4
  • androidx.lifecycle:lifecycle-viewmodel 2.8.4
  • androidx.recyclerview:recyclerview 1.3.2
  • com.google.android.material:material 1.12.0
  • com.hannesdorfmann:adapterdelegates4 4.3.2
  • com.hannesdorfmann:adapterdelegates4-kotlin-dsl 4.3.2
  • com.jakewharton.timber:timber 5.0.1
  • com.freeletics.mad:state-machine 0.24.0
  • app.cash.turbine:turbine 1.1.0
  • org.jetbrains.kotlinx:kotlinx-collections-immutable 0.3.7
  • com.freeletics.gradle.android 0.15.0
  • com.freeletics.gradle.android.app 0.15.0
  • com.freeletics.gradle.jvm 0.15.0
  • com.freeletics.gradle.multiplatform 0.15.0
  • com.freeletics.gradle.publish.oss 0.15.0
  • com.freeletics.gradle.root 0.15.0
  • com.freeletics.gradle.settings 0.15.0
  • com.android.application 8.5.2
  • com.android.library 8.5.2
  • org.jetbrains.kotlin.multiplatform 2.0.10
  • org.jetbrains.kotlin.android 2.0.10
  • org.jetbrains.kotlin.plugin.compose 2.0.10
  • com.vanniktech.maven.publish 0.29.0
  • org.jetbrains.dokka 1.9.20
  • org.jetbrains.kotlinx.binary-compatibility-validator 0.16.3
  • com.autonomousapps.dependency-analysis 1.33.0
sample/android/sample-android.gradle.kts
sample/shared_code/sample-shared_code.gradle.kts
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.9
kotlin-script
.kts/ktlint.main.kts
  • com.freeletics.gradle:scripts-formatting 0.15.0

  • Check this box to trigger a request for Renovate to run again on this repository

Artifact for Testing

The Problem:
To unit test a function that returns an instance of ChangeState<S> we need to "unwrap" it to get to the next state value by calling changeState.reduce(currentState). Not a big deal, but some repetitive boilerplate if you have to do that all the time for all of your tests. Also ChangeState is not meant to be compared directly with via equal with a real State like Assert.assertEquals(expectedState, changeState) (expectedState is of type State and changeState of type ChangeState).

Maybe we should provide an additional artifact to provide a more convenient way to unit test functions that involve ChangeState. Could be just a simple extension function like ChangeState.test(currentState) { newState -> ... } where you can do some assertions inside the lambda block on the newState object.

Or we could go wilder and define some more convenient API like:

 @Test
    fun `in loading state then load items and transition to content state`() = runBlocking {
        val items: List<Item> =  ...

        givenState<LoadingState, State>(LoadingState)
            .onEnter(::loadFirstPage) // loadFirstPage() is the function that we want to unit test
            .then(ShowContentState(notifications = items, currentPage = 1)) // Expected state

    }

UnitTestDSL.kt.txt

@gabrielittner @dmo60 do you thing that would be useful?

StateFlow and CoroutineScope as constructor Parameter

For FlowReduxStateMachine we should use StateFlow instead of Flow so that the StateMachine can work independent from it's state subscribers (like UI can subscribe and unsubscribe according to UI's lifecycle.

Furthemore, the statemachine should keep alive independent from subscribers lifecycle. Therefore a CoroutineScope should be passed as constructor parameter. That CoroutineScope will then be used to keep the whole statemachine alive until that scope gets canceled. On canceling this statemachine firther calls of dispatch() or state.collect() must throw an IllegalStateException.

State machine suspend invoke function in state

Hi, I made the changes as you advised me, but I have a problem when I make an http call, I'm getting a courutine is canceled error, I share my code with you

state machine

spec {
            inState<LoadingState> {
                on<GetQuestionsAction> { action, _ ->
                    Log.e(
                        "ComplianceMachineState",
                        "LoadingState GetQuestionsAction: ${action.country}"
                    )
                    OverrideState(GetQuestionsAnswerState(action.country))
                }
            }

            inState<GetQuestionsAnswerState> {
                onEnter { state ->
                    Log.e(
                        "ComplianceMachineState",
                        "GetQuestionsAnswerState"
                    )
                    try {
                        val result = complianceQuestionsUseCase(state.country) //suspend function call return error kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b74dc27
                        if (result is UIState.UnExpectedError) {
                            OverrideState(GetQuestionsAnswerErrorState(state.country))
                        } else {
                            OverrideState(AnswerShowContentState())
                        }
                    } catch (e: Exception) {
                        OverrideState(GetQuestionsAnswerErrorState(state.country))
                    }
                }
            }

            inState<AnswerShowContentState> {
                onEnter {
                    Log.e("ComplianceMachineState", "AnswerShowContentState")
                    NoStateChange
                }
            }

            inState<GetQuestionsAnswerErrorState> {
                onEnter { state ->
                    Log.e(
                        "ComplianceMachineState",
                        "GetQuestionsAnswerErrorState onEnter: ${state.country}"
                    )
                    OverrideState(GetQuestionsAnswerState(state.country))
                }
            }
        }

prints logs, no enter in GetQuestionsAnswerErrorState

2022-05-02 13:36:09.593 3080 E LoadingState GetQuestionsAction: co
2022-05-02 13:36:13.471 3080 E GetQuestionsAnswerState

my composable

@Composable
fun TestFlowReduxStateMachine(
    stateMachine: ComplianceMachineState,
) {
    val (state, dispatchAction) = stateMachine.rememberStateAndDispatch()


    LaunchedEffect(Unit) {
        dispatchAction(GetQuestionsAction("CO"))
    }
}

onEnter coroutine call complianceQuestionsUseCase is cancelled

Originally posted by @Jparrgam in #294 (comment)

Rename SelfReducableAction to SetStateAction

SelfReducableAction should be renamed to SetStateAction. SetStateAction should not be exposed publicly though, at the moment it is exposed via FlowReduxLogger as

reducing SelfReducableAction

This should be cleaned up as well a be only visible as setState { ... } in the logs.

Relates to #87

Artifact for Jetpack Compose

We could provide a convinience artefact for jetpack compose that

  • picks right coroutinescope to subscribe on, i.e.
      @Composable 
      fun MyComponent() {
           val (state : State<MyState>, dispatch : (MyAction) -> Unit)  by stateMachine(MyFlowReduxStateMachine) // Will do remember { mutableStateOf( ... ) } under the hood and start obsering the StateMachine.state property on the right coroutine scope
      }
  • would also allow to use FlowRedux "inline" i.e. to build state for a combination of UI widgets only
     @Composable 
     fun MyComponent() {
          val (state : State<MyState>, dispatch : (MyAction) -> Unit)  by stateMachine { 
             // statemachine spec DSL
                inState<S1> {
                   onEnter { ... }
                   onAction { ... }
                }  
                ...
           }
     }

Additional condition for `on<Action>` construct

We already have inState<S>(additionalCondition = { ... }) and maybe it would be useful to add additionalCondition also to on<Action>.

Use case:

private fun resetErrorState(action: RetryToggleFavoriteAction, state: State<GithubRepository>): ChangeState<GithubRepository> {
return if (action.id != state.snapshot.id) {
// Since all active MarkAsFavoriteStateMachine receive this action
// we need to ignore those who are not meant for this state machine
state.noChange()
} else {
state.mutate { copy(favoriteStatus = FavoriteStatus.OPERATION_IN_PROGRESS) }
}
}

Where the on<Action> block should actually only be triggered when action.id == someId

Just leaving it here for a potential future addition / improvement.

Rethink Logging

At the moment a FlowReduxLogger is passed in as a constructor parameter and it logs things.

The Api are awful, nor is it clear what the logger is good for and what it o is suppose to log (i.e. internal ro debug state machine? For FlowRedux maintainers to create bug reports? For consumer of FlowRedux library)?

Pass parameters for loading data via action?

Hi @sockeqwe,

first of all: great work!
I love FlowRedux and already played a little bit with this library.

But I am facing and problem:
I am dispatching a loading action to the statemachine and this loading actions contains the data for loading feeds (e.g. id for a specific article). i looked at your best practices but i don't know how to get the data out of my action.

my current implementation:

init {
  spec {
    inState<Loading> {
      onEnter(block = ::loadScreen)
    }
    inState<Content<Any>> {
      on<ActivityReloadAction<Ref>> { _, _, setState ->
        setState { Loading(refresh = true) }
      }
    }
    inState<Error> {
      on<ActivityReloadAction<Ref>> { _, _, setState ->
        setState { Loading(refresh = false) }
      }
    }
  }
}

suspend fun loadScreen(getState: GetState<ActivityViewState>, setState: SetState<ActivityViewState>) {
  // here i miss the action which contains the data to do the http request
}

Maybe you can tell me how i can get the action which triggered the state or how i can pass in paramters to load the data?

Thx
Bodo

Build iOS sample on CI

To ensure that iOS example doesn't break (again) we should build iOS sample app on CI for each PR etc. Too

Restructure Docs / website

We should restructure our docs to emphasize even more the DSL so that user of this library have a better guide to learn about it. I think a step by step guide (similar to Redux docs) could be useful. For that I would propose to break the big dsl.md file into multiple smaller .md files so that mkdocs also generates "section" entries in the main menu...

MutateState needs annoyingly Type Arguments

As a user of FlowRedux I need to specify explicitly the Type argiments of MutateState. Example:

sealed interface State

data class S1 (val i : Int) : State
object S2 : State
inState<S1> {
   onEnter { stateSnapshot ->
      ...
      MutateState<S1, State> { this.copy(i = this.i++) }  // MutateState needs S1 as type parameter; compiler cannot infer it.
   }
}

This is annoying and counterproductive to our goal of increase developer experience but I'm not sure how to solve this problem best. Some random ideas:

  1. Since all blocks inside the inState block already know of which type the State we want to mutate is, we could add a extension function to the onEnterBlock, onAction and collectWhileInstate that would allow us to write something like this:
inState<S1> {
   onEnter { stateSnapshot ->
      ...
      mutateStateExtensionFunction { this.copy(i = this.i++) }  // this == S1 can be inferred by the compiler via extensionFuntion
   }
}

(Please note that mutateStateExtensionFunction is just a random name to illustrate the point I try to make; not the final name in case we go for this solution)
While this approach works for blocks directly defined in the DSL it would not work for functional references like this:

inState<S1> {
   onEnter(::doSomethingOnEnterS1)
}


fun doSomethingOnEnterS1(stateSnapshot: S1) : ChangeState<State> {
   ...
   return  stateSnapshot ->
      ...
      mutateStateExtensionFunction { this.copy(i = this.i++) }  // wont work because no such extension function defined for an aribtarry function
   }
}

So I was looking if it is possible to define extension functions on "Functions" but didn't figure it out yet how to do it (nor am I sure if the kotlin IDE plugin would work properly). The following did not work (but I didn't dive deeper):

fun <R> kotlin.Function<R>.mutateStateExtensionFunction() {...}

fun <InputState, S> ((stateSnapshot : InputState ) -> S).mutateStateExtensionFunction() {...}

Maybe #189 could introduce a new supertype we could define a mutateStateExtensionFunction extension function on, but it doesn't look to solve the problem of function reference.

  1. Another option is to go away from defining MutateState with receiver type. Receiver type is handy especially with data classes used for State definition because then you can just call copy() to do a mutation as this is introduced by the compiler via reciever type. However, this introduces the problem that we need to tell in MutateState what the type of the receiver is causing developers to Explicitly specify the Generic Type Arguments.
    If we would remove the receiver type, the compiler's Type inference should be smart enough to infer types (although we still need to give the compiler a hint somewhere, i.e. define the parameter type of the state snapshot):
inState<S1> {
   onEnter { stateSnapshot : S1 ->
      ...
      MutateState { currentState : S1 -> currentState .copy(i = currentState.i++) }  //  Explicity have defined input parameter currentState : S1  is enough for compiler to be satisfied
   }
}

this however doesn't fully solve the problem of developer's productivity but could be some compromise.

Do you see any other options? Do I miss something?

Cleanup sample code

The sample is old and has a lot of unused dependencies like ktor or screenshot testing. We don't need that. Let's remove it as it just causes noise and confusion.

Make FlowReduxStatemachine "cold" again

At the beginning FlowRedux the FlowReduxStatemachine was a "cold flow" (meaning on every new state.collect() the statemachine started with the initial state again).

Starting in FlowRedux 0.6.0 we then have made FlowReduxStatemachine a "hot flow" and have started using StateFlow (hot means the same state machine state will be collected / shared by all collectors; so it doesn't start with the initial state for every new state.collect {} ; plus statemachine can run without any active Collector as long as the CoroutineScope where the state machine launched in is active).

We thought this could give us some advantages with Freeletic's DI setup and Jetpack Compose, however it turns out the drawback's are bigger than expected. Just a few pain points:

  • Cancelation of a shared flow (like FlowReduxStateMachine) is harder
  • Tests became more flaky because FlowReduxStateMachine starts working without active Collectors that have called state.collect {...}. That leads to some state transitions being missed in unit test because the collector used in the test was not subscribed yet.

Therefore we are going to revert this. We will move away from StateFlow and make FlowReduxStateMachine "cold" again .

collectWhileInState based on the current state

While my state machine is in a specific state I want to collect a query in my database. However the query has a filter that can change based on actions. I'd like to make that filter part of my state so that actions just change the state and then the query can update automatically. However for that I would need a Flow of the current state.

I'm currently working around this by having this in my state machine:

private val openingTime = MutableStateFlow(Instant.now())

init {
    spec {
        inState<MyState> {
            collectWhileInState(loadData(), block = ::updateStateWithData)
            on(block = ::onRefresh)
        }
    }
}

private fun loadData(): Flow<Feeds> {
    // here is where I'd like to have a `Flow<MyState>` instead
    return openingTime.flatMapLatest { repository.load(it) }
}

private fun updateStateWithData(...): ChangeState<MyState> { ... }

private suspend fun onRefresh(
    action: Refresh,
    state: MyState
): ChangeState<MyState> {
    // this should mutate MyState instead of updating an external Flow
    openingTime.emit(Instant.now())
    return NoStateChange
}

All of that feels very wrong with how the DSL based state machines work. Additionally if I need that Instant in my state for other purposes, like showing it in the UI, there would be 2 sources of truth for it and a chance of inconsistencies. For example if there'd be a second action that updates it I could forget to call openingTime.emit and then the UI would something different than what is being queried.

Maybe collectWhileInState should take a lambda that returns the Flow instead of directly receiving that Flow and we pass a Flow of the state to that lambda. So something like

fun <T> collectWhileInState(
    flow: (Flow<InputState>) -> Flow<T>,
    flatMapPolicy: FlatMapPolicy = FlatMapPolicy.CONCAT,
    block: InStateObserverBlock<T, InputState, S>
)

The lambda would also fit the style of using method references inside spec and maybe discourage a bit to build the Flow directly when calling collectWhileInState instead of in an extra function.

Note: With the change from #150 it would technically be possible to observe the main state of the FlowReduxStateMachine since it is visible to us and always collectable, but that feels also wrong. Especially in an inState block because state has all states and not just the filtered one.

DSL to make FlowReduxStateMachines composable from smaller "sub-statemachines"s

api could looks something like this:

class Myflowreduxstatemachine<MyState MyAction>) {
... 
spec {
   subStateMachine(subStateMachine: FlowReduxStateMachine sm1, actionsMapper : (MyAction) -> SubStateMachineAction,  stateMapper : (MySubState, RootState) -> RootState
}
  • Mapper functions are not suspending
  • do we need to check if statemachine + substatemachines are using same CoroutineScope

If #130 is implemented, should wr check that all sub statemachines run on the same CoroutineScope as the Parent StateMachine? @gabrielittner do you have any thoughts on that?

Overload for collectWhileInState with Builder

Currently we support collectWhileInState with flowBuilder as shown bellow:

 fun <T> collectWhileInState(
        flowBuilder: (Flow<InputState>) -> Flow<T>,  
        handler: InStateObserverHandler<T, InputState, S>
 )

I would like to add an overload with

 fun <T> collectWhileInState(
        flowBuilder: (InputState) -> Flow<T>, // just InputState , not Flow<InputState> as above
        handler: InStateObserverHandler<T, InputState, S>
 )

because I personally have more need for this variation of flowBuilder ( (InputState) -> Flow<T>) compare to the current one (Flow<InputState>) -> Flow<T>.

Obviously, that causes a clash on the JVM because both methods have same signature.

Thus we would need to rename one.

I would propose the following:

  • My assumption is that most people have use case for the version with (InputState) -> Flow<T> . Thus I would keep that one under the collectWhileInState name.
 fun <T> collectWhileInState(
        flowBuilder: (InputState) -> Flow<T>, // just InputState , not Flow<InputState> as above
        handler: InStateObserverHandler<T, InputState, S>
 )
  • I would rename the one with Flow<InputState>) -> Flow<T> to collectWhileInStateWithGenericBuilder():
fun <T> collectWhileInStateWithGenericBuilder(
       flowBuilder: (Flow<InputState>) -> Flow<T>,  
       handler: InStateObserverHandler<T, InputState, S>
)

Any thoughts?

cc @gabrielittner

How does this compare to CoRedux?

I switched from RxRedux to CoRedux and I think another switch seems unnecessary when you can integrate Flow into CoRedux.

I wrote these SideEffects a while ago supporting Flows, and I haven't found any issue yet, that's why I'm wondering whether yet another project is really necessary.

Thoughts? Sorry if I missed something...

onEnter doesn't need FlatMapPolicy

Since onEnter(FlatMapPolicy) is exactly triggered once (and doesnt emit later on again) it doesnt need a FlatMapPolicy as a parameter

Test dependencies packaged into jvmMain

It appears that com.freeletics.flow.test:test:0.0.2-SNAPSHOT (and a few other test libraries) are included in com.freeletics.flowredux:dsl:0.2.1 as a runtime dependency.

Are there any reasons the following dependencies are not defined in the jvmTest configuration?

implementation testLibraries.junit
implementation testLibraries.kotlintest
implementation testLibraries.coroutinesTest
implementation testLibraries.flowRecorder
implementation 'com.freeletics.flow.test:test:0.0.2-SNAPSHOT'

Remove println()

There are in some places still println() that has been introduced while developing FlowRedux and have been forgotten to remove

ABI Versions

I'm trying to use 0.0.3 of flowredux in a multiplatform project, and getting the following error while trying to target iOS:

The abi versions don't match. Expected '[22]', found '17'. The library produced by 1.3.61-release-13550 compiler

I assume this is an issue with the version of xcode under which things were compiled.

Not really sure how to proceed

Rename FlatMapPolicy to something else

Since Flow and underlying flatMaps are mainly hidden from the consumer of this library it makes most likely sense to rename FlatMapPolicy to something more specific the consumer can understand better.

My Proposal would be to rename it to ExecutionPolicy:

  • FlatMapPolicy.LATEST --> ExecutionPolicy.CANCEL_PREVIOUS_EXECUTION
  • FlatMapPolicy.MERGE --> ExecutionPolicy.UNORDERED
  • FlatMapPolicy.CONCAT--> ExceutionPolicy.ORDERED

What do you think?

Set up ktlint

To help to lint and format our codebase we would like to add ktlint to this project.

Crash on setState

I'm seeing a crash when doing the following, but couldn't wrap my head around what's causing it:

collectWhileInAnyState(UserStore.stream()) { userState, getState, setState ->
    setState {
        when (userState) {
            UserState.LoggedOut -> LoadModel.LoggedOut
            is UserState.LoggedIn -> LoadModel.LoggedIn(userState.user.name)
        }
    }
}

Crash:

java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once
        at kotlinx.coroutines.flow.ConsumeAsFlow.markConsumed(Channels.kt:100)
        at kotlinx.coroutines.flow.ConsumeAsFlow.collect(Channels.kt:123)
        at com.freeletics.flowredux.dsl.DslKt$reduxStore$$inlined$map$1.collect(SafeCollector.kt:127)
        at kotlinx.coroutines.flow.FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1.collect(SafeCollector.kt:127)
        at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:135)
        at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:102)
        at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
        at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:62)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
        at kotlinx.coroutines.DispatchedContinuation.resumeWith(DispatchedContinuation.kt:298)
        at kotlin.coroutines.ContinuationKt.startCoroutine(Continuation.kt:128)
        at kotlinx.coroutines.channels.AbstractChannel$ReceiveSelect.completeResumeReceive(AbstractChannel.kt:942)
        at kotlinx.coroutines.channels.ArrayChannel.offerInternal(ArrayChannel.kt:79)
        at kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:147)
        at kotlinx.coroutines.channels.ChannelCoroutine.send$suspendImpl(Unknown Source:2)
        at kotlinx.coroutines.channels.ChannelCoroutine.send(Unknown Source:0)
        at kotlinx.coroutines.flow.internal.SendingCollector.emit(SendingCollector.kt:19)
        at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:33)
        at com.freeletics.flowredux.dsl.StoreWideCollectBuilderBlock$setStateFlow$2$setState$2.invokeSuspend(StoreWideCollectBuilderBlock.kt:43)
        at com.freeletics.flowredux.dsl.StoreWideCollectBuilderBlock$setStateFlow$2$setState$2.invoke(Unknown Source:14)
        at com.freeletics.flowredux.dsl.SetStateImpl.invoke(SetState.kt:51)
        at com.freeletics.flowredux.dsl.SetState.invoke$default(SetState.kt:34)

refer to this issue that discussed the introduction of consumeAsFlow

Turn handler typealiases into functional interfaces

When you build a state machine we ideally want something like this

class MyStateMachine(...) : FlowReduxStateMachine(...) {
  init {
    spec {
       inState<MyState> {
         on(::onAction1TransisitionToA)
         on(::onAction2DoSomethingElse)
       }
    }
  }
}

suspend fun onAction1TransisitionToA(action: Action1, stateSnapshot: MyState): ChangeState<MyState> {
  // do stuff
}

suspend fun onAction2DoSomethingElse(action: Action2, stateSnapshot: MyState): ChangeState<MyState> {
  // do stuff
}

Having the handlers as standalone methods lets you easily test them without having to build the full state machine. Method references in the DSL help a lot with keeping the definition itself readable when the state machine gets bigger.

The issue arises when the handler needs some dependency. You either need to

  1. move the function into the state machine which hurts testability
  2. add the parameter to the function, which would mean to stop using method references
  3. put the function into it's own class that injects the required parameters

The latter would look like this

class MyStateMachine(
  action1TransisitionToA: Action1TransisitionToA,
  action2DoSomethingElse: Action2DoSomethingElse,
   ...
) : FlowReduxStateMachine(...) {
  init {
    spec {
       inState<MyState> {
         on(action1TransisitionToA::handle)
         on(action2DoSomethingElse::handle)
       }
    }
  }
}

class Action1TransisitionToA(...) {
  suspend fun handle(action: Action1, stateSnapshot: MyState): ChangeState<MyState> {
    // do stuff
  }
}

class Action2DoSomethingElse(...) {
  suspend fun handle(action: Action2, stateSnapshot: MyState): ChangeState<MyState> {
    // do stuff
  }
}

This works with everything that we have however we could improve the experience by turning

typealias OnActionHandler<InputState, S, A> = suspend (action: A, state: InputState) -> ChangeState<S>

into

fun interface OnActionHandler<InputState, S, A> {
  suspend fun handle(action: A, state: InputState): ChangeState<S>
}

That way those classes can implement the interface which makes implementing it easier and also prevents any unused_parameter warning on the function. Any existing usage and usecase would still work like before because fun interface support SAM conversion.

Inconsistency between collectWhileIn(Any)State and on

As an example let's say we have this state

sealed class MyState
class StateA : MyState()
class StateB : MyState()

Now in a state machine for MyState you can do

init {
    spec {
        collectWhileInAnyState(...) { ... }
        
        inState<MyState> {
            collectWhileInState(...) { ... }
            on<...>() { ... }
        }
    }
}

The 2 collectWhile... calls have the same lifetime because we are always in MyState. The inconsistency in my opinion is that there are effectively 2 ways to do collectWhileInAnyState but it's only possible to define an on that should happen in any state within inState. Initially I was searching for an onInAnyState because I knew collectWhileInAnyState exists.

Instead of having collectWhileInAnyState could we just remove it and recommend using collectWhileInState with the base class? Then it's consistent with on and there is just one way to do things.

Add support for Effects that dont change state

At the moment you cannot do a piece of work without the need of returning a ChangeState object.

This proposal is about adding "effects". A typical use case for an Effect is triggering navigation without changing the state of the statemachine itself or doing some analytics tracking calls.

Proposed Syntax

inState<MyState> {
  onEnterEffect { stateSnapshot : MyState ->
       // Do something.
       Unit // return unit, no state change
   }

  onActionEffect<MyAction> action : MyAction, stateSnapshot : MyState ->
       // Do something.
       Unit // return unit, no state change
   }

  collectWhileInStateEffect(flowOf(1,2)) { value : Int, 
stateSnapshot : MyState ->
       // Do something.
       Unit // return unit, no state change
   }
}

Specs:

  • Effects are suspending: i.e. signature lambda for onEnterEffect is suspend (State) -> Unit
  • Effects dont get canceled when state transition happening.
  • Effects are per default canceled when StateMachine CoroutineScope gets canceled. If you want to escape you can always use withContext(NonCancelable)

Brainstorming: enhance DSL to cover fine granular sub statemachine

The following use case is not really nicely handable by FlowRedux at the moment:

Let's say we have a list of Items that we want to display on screen. So the overall screen state looks something like this:

sealed interface ScreenState
data class ShowListScreenState(val items : List<Item>) : ScreenState
object LoadingState : ScreenState

What if in the UI each Item that is displayed in the list of Items also has a button. An by pressing that button a http request is done to update something to the backend for this particular item (i.e. mark a item as "Liked" on a facebook alike newsfeed post)?

In other words, each Item has their own state.

data class Item (val id : Int, val state : ItemState)

enum class ItemState {
  IDLE,
  LOADING
  ERROR,
  SUCCESSFUL
}
data class ItemButtonClickedAction (val itemId : Int) : Action

What I would like to propose and Brainstorm here is to take usage of the substate machines concept we already have.

spec {
  inState<LoadingState> {
   onEnter {
      val items = loadItemsFromServer()
      OverrideState(ShowListScreenState (items))
   }

  inState<ShowListScreenState> {
            
  
  // NEW DSL primitive
 
   onActionStartSubstateMachine<ItemButtonClickedAction>(
        
    stateMachineFactory = { action: ItemButtonClickedAction, state : ShowListScreenState -> 
        ItemButtonStateMachine( action.itemId)
     },

     actionMapper =  { it }, // Forward future incoming  actions to the substate machine (not needed in this example)


      additionalStopCondition= { state : ShowListScreenState,  stateFromItemButtonStateMachine : ItemState ->
         // Invoked after stateMapper ran
         stateFromItemButtonStateMachine != ItemState.LOADING 
      },
     

     stateMapper = { state : ShowListScreenState ,  stateFromItemButtonStateMachine : ItemState,

    MutateState<ShowListScreenState, ScreenState> {
        copy(items = items.replaceItem(Item(originatedAction.id, stateFromItemButtonStateMachine)))
    }
  }
}
class ItemButtonStateMachine (val itemId : Int) : FlowReduxStateMachine<Action, ItemState> {
   init {
     spec { 
         inState<LOADING> 
              onEnter {
                 val succesful : Boolean = makeHttpRequest(itemId)
                 OverrideState (
                      if (successful) SUCCESSFUL
                      else ERROR)
              }
         }
   }
  }
}

So basically you can start a "substatemachine" from an action. We have already known concepts (from already existing statemachine() DSL construct) like stateMachineFactory but alsostateMapper() and actionMapper()

The remaining question for me is when does a sub statemachine stops. As always it should stop when the surounding inState<...> condition (of parent statemachine) doesn't hold anymore but we may additionally need some more fine grained control. This is why I would propose to introduce additionalStopConidtion: (ParentStateMachineState, SubStateMachineState) -> Boolean. additionalStopConidtionis invoked after a state change of either parentStateMachine or subStateMachine (so it is called after reducer or stateMapper did run).

If additionalStopCondition returns true the child statemachine will be stopped (collection of state flow of child statemachine is cancelled; it is not possible to resume child state machine).

as usually ExecutionPolicy can be applied (thus ExecutionPolicy.CANCEL_PREVIOUS can cancel substatemachine as well).

the same concept of starting substatemachine can also be applied to collectWhileInState and onEnter. in fact current stateMachine() call is actually already onEnterStartSubStatemachine().


Alternative solutions

instead of onActionStartSubstateMachine() and the like we could also go a different route of introducing additional primitives for inState to make define state changes even mor fine granular detectable and triggerable.

maybe something like (pseudocode / non functional DSL)

inState<ShowListScreenState> { state –>
    // collectWhileInState () + on<Action> are available of course


      for (item in state.items){

          inState(condition = { item –> item.state == ItemState.LOADING }) {
            onEnter { item –> 
                 val succesful : Boolean = makeHttpRequest(item.id)
                 OverrideState (
                      if (successful) SUCCESSFUL
                      else ERROR
              }
      }      
} 

but that is an entirely shift of DSL as we use it right now. right now DSL is basically a one-time builder that executes one time at init but what I am proposing above is more like dynamic execution of the DSL over and over again on state changes like what Jetpack Compose does, so relates to #209.

I think for a FlowRedux 1.0 release going with first option (onActionStartSubstateMachine()) should be sufficient but I totally can see how jetpack compose (greetings to square molecule but also would be great to have kmp support, native jetbrains compose is at least to me cumbersome to work with at the moment) would allow us more flexibility and a sharp powerful tool to build even better state machines. I'm just not sure right now how much of this flexibility and dynamic-ism we need to guide users of FlowRedux in the dirction we want, powerful enough, but also specific and expressive enough to not have options to do things in multiple ways and shoot yourself in the foot. Also current stateamachine() (to start substatemachine) seems to work good for us, so if we only need one mor way to start sub statemachines on Actions, maybe that is actually all we need at least for now / FlowRedux 1.0.

What do you think?

cc @gabrielittner

Add more overloads for the InStateBlockBuilder methods

Currently when using method references you that the Kotlin compiler will flag your parameters as unused when you just add them to be to use method references. The most common case is probably an unused stateSnapshot, but for the action handlers it could even be the action itself if it's just an object. If you don't use method references this issue still exists. While you can just use _ to get rid of the warning you still of the verbosity of having to write out the lambda parameters (on<SomeAction> { action, _ ->).

Side note that influences potential solutions: I'm getting more and more convinced that we should not use method references anymore and rely on lambdas. The general aspiration of having the spec block as small as possible is still valid, however I think method references are not the right solution. There are multiple issues and while some of them are not our fault and could be fixed by Kotlin they still matter:

  • the initially mentioned unused parameter issue which either means you get tons of compiler warnings or need to put suppress annotations everywhere
  • in some circumstances (not sure when it happens) the IDE can't really auto complete the referenced function when you're writing ::...
  • the function name should describe what it does but in practice I'm usually seeing on(::buttonClicked), my assumption is that if you have on<ButtonClicked> { ... } you are less inclined to call the method the same
  • An addition to the above is that if you do on(::transitionToSomeState) you loose the information of when it happens while on<ButtonClicked> { transitionToSomeState(it) } would make getting an overview easier. Of course you can combine both in your function name but that leads to long/weird names. For example onButtonClickedTransitionToSomeState is long and would then result in on(::on...) while leaving out the on from the method name reads weirdly when looking at just the function
  • Method references don't work well when the method has a dependency. In practice the function is just put into the state machine class which means it's not testable on it's own anymore. While we introduced functional interfaces in #189 to make this easier, they are also a lot more verbose. If you just call a method from the lambda you can pass additional parameters to it and it keeps the syntax more aligned.

Going back to the original problem depending on whether we want to encourage method references or not all handlers would need overloads. If we are saying method references are the way to go then we'd need additional interfaces + overloads with 0 and 1 parameters for everything that has 1+ parameters. If we decide for lambdas then it would be interfaces + overloads for everything with 2+ parameters because a single parameter is just an implicit it either way.

Rename observeWhileInState to collectWhileInState

Renaming observeWhileInState() to collectWhileInState to be more aligned with flow.collect { ... }

Another interesting question would be though if we should do the same with
storewide observe(flow). Should it be renamed to collect(flow) which is could be misleading since flow.collect {...} is the way to call it. An alternative option is to rename observe(flow) to collectWhileStateMachineRuns(flow) or something like that.

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.