Giter Club home page Giter Club logo

mutator's Introduction

Mutator

JVM Tests Mutator Core Mutator Coroutines

badge badge badge badge badge badge badge badge

Android Weekly Feature

Please note, this is not an official Google repository. It is a Kotlin multiplatform experiment that makes no guarantees about API stability or long term support. None of the works presented here are production tested, and should not be taken as anything more than its face value.

Introduction

Mutator is a Kotlin multiplatform library that provides a suite of tools that help with producing state while following unidirectional data flow (UDF) principles. More specifically it provides implementations of the paradigm newState = oldState + Δstate.

Where StateMutators are defined as:

interface StateMutator<State : Any> {
    val state: State
}

and Δstate represents state changes over time and is expressed in Kotlin with the type:

typealias Mutation<State> = State.() -> State

At the moment, there are two implementations:

fun <State : Any> CoroutineScope.stateFlowMutator(
    initialState: State,
    started: SharingStarted,
    inputs: List<Flow<Mutation<State>>>
): StateMutator<StateFlow<State>>  

and

fun <Action : Any, State : Any> CoroutineScope.actionStateFlowMutator(
    initialState: State,
    started: SharingStarted,
    inputs: List<Flow<Mutation<State>>>,
    actionTransform: (Flow<Action>) -> Flow<Mutation<State>>
): StateMutator<StateFlow<State>>

stateFlowMutator is well suited for MVVM style applications and actionStateFlowMutator for MVI like approaches.

Foreground execution limits

Both implementations enforce that coroutines launched in them are only active as specified by the SharingStarted policies passed to them. For most UI StateMutators, this is typically SharingStarted.whileSubscribed(duration). Any work launched that does not fit into this policy (a photo upload for example) should be queued to be run with the appropriate API on the platform you're working on. On Android, this is WorkManager.

Download

implementation("com.tunjid.mutator:core:version")
implementation("com.tunjid.mutator:coroutines:version")

Where the latest version is indicated by the badge at the top of this file.

Examples and sample code

Please refer to the project website for an interactive walk through of the problem space this library operates in and visual examples.

CoroutineScope.stateFlowMutator

CoroutineScope.stateFlowMutator returns a class that allows for mutating an initial state over time, by providing a List of Flows that contribute to state changes. A simple example follows:

data class SnailState(
    val progress: Float = 0f,
    val speed: Speed = Speed.One,
    val color: Color = Color.Blue,
    val colors: List<Color> = MutedColors.colors(false).map(::Color)
)

class SnailStateHolder(
    scope: CoroutineScope
) {

    private val speed: Flow<Speed> = scope.speedFlow()

    private val speedChanges: Flow<Mutation<Snail7State>> = speed
        .map { mutationOf { copy(speed = it) } }

    private val progressChanges: Flow<Mutation<Snail7State>> = speed
        .toInterval()
        .map { mutationOf { copy(progress = (progress + 1) % 100) } }

    private val stateMutator = scope.stateFlowMutator(
        initialState = Snail7State(),
        started = SharingStarted.WhileSubscribed(),
        inputs = listOf(
            speedChanges,
            progressChanges,
        )
    )

    val state: StateFlow<Snail7State> = stateMutator.state

    fun setSnailColor(index: Int) = stateMutator.launch {
        emit { copy(color = colors[index]) }
    }

    fun setProgress(progress: Float) = stateMutator.launch {
        emit { copy(progress = progress) }
    }
}

CoroutineScope.actionStateFlowMutator

The actionStateFlowMutator function transforms a Flow of Action into a Flow of State by first mapping each Action into a Mutation of State, and then reducing the Mutations into an initial state within the provided CoroutineScope.

The above is typically achieved with the toMutationStream extension function which allows for the splitting of a source Action stream, into individual streams of each Action subtype. These subtypes may then be transformed independently, for example, given a sealed class representative of simple arithmetic actions:

sealed class Action {
    abstract val value: Int

    data class Add(override val value: Int) : Action()
    data class Subtract(override val value: Int) : Action()
}

and a State representative of the cumulative result of the application of those Actions:

data class State(
    val count: Int = 0
)

A StateFlow Mutator of the above can be created by:

        val mutator = scope.actionStateFlowMutator<Action, State>(
            initialState = State(),
            started = SharingStarted.WhileSubscribed(),
            transform = { actions ->
                actions.toMutationStream {
                    when (val action = type()) {
                        is Action.Add -> action.flow
                            .map {
                                mutationOf { copy(count = count + value) }
                            }
                        is Action.Subtract -> action.flow
                            .map {
                                mutationOf { copy(count = count - value) }
                            }
                    }
                }
            }
        )

Non trivially, given an application that fetches data for a query that can be sorted on demand. Its State and Action may be defined by:

data class State(
    val comparator: Comparator<Item>,
    val items: List<Item> = listOf()
)

sealed class Action {
    data class Fetch(val query: Query) : Action()
    data class Sort(val comparator: Comparator<Item>) : Action()
}

In the above, fetching may need to be done consecutively, whereas only the most recently received sorting request should be honored. A StateFlow Mutator for the above therefore may resemble:

val mutator = scope.actionStateFlowMutator<Action, State>(
    initialState = State(comparator = defaultComparator),
    started = SharingStarted.WhileSubscribed(),
    transform = { actions ->
        actions.toMutationStream {
            when (val action = type()) {
                is Action.Fetch -> action.flow
                    .map { fetch ->
                        val fetched = repository.get(fetch.query)
                        mutationOf {
                            copy(
                                items = (items + fetched).sortedWith(comparator),
                            )
                        }
                    }
                is Action.Sort -> action.flow
                    .mapLatest { sort ->
                        mutationOf {
                            copy(
                                comparator = sort.comparator,
                                items = items.sortedWith(comparator)
                            )
                        }
                    }
            }
        }
    }
)

In the above, by splitting the Action Flow into independent Flows of it's subtypes, Mutation instances are easily generated that can be reduced into the current State.

A more robust example can be seen in the Me project.

Nuanced use cases

Sometimes when splitting an Action into a Mutation stream, the Action type may need to be split by it's super class and not it's actual class. Take the following Action and State pairing:

data class State(
    val count: Double = 0.0
)

sealed class Action

sealed class IntAction: Action() {
    abstract val value: Int

    data class Add(override val value: Int) : IntAction()
    data class Subtract(override val value: Int) : IntAction()
}

sealed class DoubleAction: Action() {
    abstract val value: Double

    data class Divide(override val value: Double) : DoubleAction()
    data class Multiply(override val value: Double) : DoubleAction()
}

By default, all 4 Actions will need to have their resulting Flows defined. To help group them into Flows of their super types, a keySelector can be used:

val actions = MutableSharedFlow<Action>()

actions
    .toMutationStream(
        keySelector = { action ->
            when (action) {
                is IntAction -> "IntAction"
                is DoubleAction -> "DoubleAction"
            }
        }
    ) {
        when (val type = type()) {
            is IntAction -> type.flow
                .map { it.mutation }
            is DoubleAction -> type.flow
                .map { it.mutation }
        }
    }

In the above the two distinct keys map to the IntAction and DoubleAction super types allowing for granular control of the ensuing Mutation stream.

Ultimately a Mutator serves to produce a stream of State from a stream of Actions, the implementation of which is completely open ended.

License

Copyright 2021 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

mutator's People

Contributors

adammc331 avatar dturner avatar tunjid 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

Watchers

 avatar  avatar

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.