Solvedkotlinx.coroutines Introduce StateFlow

We need to be able to conveniently use Flow to represent an updateable state in applications. This change introduces StateFlow -- an updateable value that represents a state and is a Flow. The design is flexible to fit a variety of needs:

  • StateFlow<T> interface is a read-only view that gives access to the current value and implements a Flow<T> to collect updates to the values.
  • MutabaleStateFlow<T> interface adds value-modification operation.

A MutableStateFlow(x) constructor function is provided. It returns an implementation of MutableStateFlow with the given initial value. It can be exposed to the outside world as either StateFlow<T> if fast non-reactive access to the value is needed, or as Flow<T> if only reactive view of updates to the value is needed.

Core state flow API can be summarized like this:

package kotlinx.coroutines.flow

interface StateFlow<T> : Flow<T> {
    val value: T // always availabe, reading it never fails
}

interface MutableStateFlow<T> : StateFlow<T> {
    override var value: T // can read & write value
}

fun <T> MutableStateFlow(value: T): MutableStateFlow<T> // constructor fun

Implementation is available in PR #1974.

StateFlow vs ConflatedBroadcastChannel

Conceptually state flow is similar to ConflatedBroadcastChannel and is designed to completely replace ConflatedBroadcastChannel in the future. It has the following important improvements:

  • StateFlow is simpler because it does not have to implement all the Channel APIs, which allows for faster, garbage-free implementation, unlike ConflatedBroadcastChannel implementation that allocates objects on each emitted value.
  • StateFlow always has a value that can be safely read at any time via value property. Unlike ConflatedBroadcastChannel, there is no way to create a state flow without a value.
  • StateFlow has a clear separation into a read-only StateFlow interface and a MutableStateFlow.
  • StateFlow conflation is based on equality, unlike conflation in ConflatedBroadcastChannel that is based on reference identity. It is a stronger, more practical conflation policy, that avoids extra updates when data classes are emitted. You can consider it to have an embedded distinctUntilChanged out-of-the-box.
  • StateFlow cannot be currently closed like ConflatedBroadcastChannel and can never represent a failure. This feature might be added in the future if enough compelling use-cases are found.

StateFlow is designed to better cover typical use-cases of keeping track of state changes in time, taking more pragmatic design choices for the sake of convenience.

Example

For example, the following class encapsulates an integer state and increments its value on each call to inc:

class CounterModel {
    private val _counter = MutableStateFlow(0) // private mutable state flow
    val counter: StateFlow<Int> get() = _counter // publicly exposed as read-only state flow

    fun inc() {
        _counter.value++ // convenient state update
    }
}

Experimental status

The initial version of this design is going to be introduced under @ExperimentalCoroutinesApi, but it is highly unlikely that the core of the design as described above, is going to change. It is expected to be stabilized quite fast.

There are also some future possible enhancements (see below) that are not provided at this moment.

Possible future enhancement: Closing state flow

A state flow could be optionally closed in a similar way to channels. When state flow is closed all its collectors complete normally or with the specified exception. Closing a state flow transitions it to the terminal state. Once the state flow is closed its value cannot be changed. The most recent value of the closed state flow is still available via value property. Closing a state is appropriate in situations where the source of the state updates is permanently destroyed.

To support closing there would be a MutableStateFlow.close(cause: Throwable? = null): Boolean method for the owner of the state flow to close it via its writeable reference and StateFlow.isClosed: Boolean property on read-only interface.

UPDATE: Turning Flow into a StateFlow

A regular Flow is cold. It does not have the concept of the last value and it only becomes active when collected. We introduce a stateIn operator to turn any Flow into a hot StateFlow in the as a part of the family of flow-sharing operators (see #2047 ). It is designed to become a replacement for broadcastIn operator. It would subsume the need of having to have a separate "state flow builder" as you can simply write flow { .... }.stateIn(scope) to launch a coroutine that emits the values according to the code in curly braces.

48 Answers

βœ”οΈAccepted Answer

@erikc5000 @igorwojda Indeed, the naming for constructor functions is quite a controversial issue here. We have two basic naming options:

  • MutableStateFlow() is totally explicit but somewhat extraneous. Unlike collections, where it does make sense to construct both a MutableList and a read-only List, here we have a case when creating a read-only version itself does not make any use.

  • StateFlow() (as proposed in this issue) is shorter, but has a downside that the actual return type is MutableStableFlow. We followed a precedent of Job() constructor here, that is in a similar situation and returns a CompletableJob that the owner of the Job needs while exposing a Job to the outside world. There were no complaints so far on it, but it is worth noting that it is not that widely use as StateFlow is expected to become. Also, note that CompletableDeferred() constructor naming does not follow this precedent.

As for "accidentally exposing a mutable type" it does not seem to be a problem if you are writing small, self-contained code. On the contrary, it is consistent with Kotlin's "public default" policy that is aimed at reducing verbosity for non-library end-user application code:

// never state the type explicitly
val myState = StateFlow(initialValue) 
// now I can update my state
myState.value = another
// and I can operate on it as flow
myState.collect { .... }
// the fact that mystate: MutableStateFlow is quite secondary here

However, when you do write a library or a larger application, then you have to be careful at the boundaries with respect to the types you expose. The problem I see here is that a proposed declaration pattern here reads somewhat weird:

This proposal with StateFlow

private val _counter = StateFlow(0)
val counter: StateFlow<Int> get() = _counter 

It reads as if you created a StateFlow and then cast it to StateFlow, which looks weird unless you know the details of the API or IDE is helping you with inferred types. Adding asStateFlow() looks useful, for cases where you are not in the library (so you don't want to be explicit about), but when you want to control types between different layers of your application and don't want to explicitly spell the full type:

Adding asStateFlow():

private val _counter = StateFlow(0)
val counter get() = _counter.asStateFlow()

This somewhat weird look of StateFlow as StateFlow remains here, though, just as before.

Changing name of the constructor to MutableStateFlow

private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter.asStateFlow() // A
val counter get() = _counter.asStateFlow() // or B

Either way, with MutableStateFlow(value) it becomes more explicit at the expense of longer and harder-to-discover name. See, the feature will be known as StateFlow, so people will be looking for StateFlow to use it. That's also the reason as to why most documentation is concentrated in KDoc for StateFlow interface.

All in all, I don't have a strong opinion on my own here and cannot strongly convince myself in either direction. What does this community think? Let's do a quick poll:

πŸ‘ For StateFlow(value) constructor.
πŸš€ For MutableStableFlow(value) constructor.

Other Answers:

Having the StateFlow interface represent a read-only state flow while the constructor function with the same name is actually mutable seems like it could lead to a lot of state flows getting unintentionally exposed as mutable. Is there a reason to not use MutableStateFlow as the name of the constructor function?

Otherwise, it's looking pretty good to me!

That would be an "as", not a "to", but I'm opposed to that. I don't see it adding meaningful value compared to just specifying the type explicitly. It's actually more characters, not that character count provides any meaningful metric. Plus when you enable library mode (explicit types always required) it means that function wouldn't even be needed.

UPDATE: Based on your feedback we've decided to rename StateFlow() constructor to MutableStableFlow() and to fast-track all the core StateFlow APIs to stabilization. However, we'll pull out and postpone all the questionable parts of state flow design, including cosing of the flow and stateIn operator.

The main reason here is that stateIn is only one operator of a big family of sharing operators (see #1261 for discussion). Releasing just stateIn, even as a preview feature, carries a high risk that people would start cramming all of their flow-sharing use-cases into this single narrowly-scoped operator, while there will a whole family of them, covering a variety of use-cases. I'll keep you in the loop on the updates to the design process for sharing. It is the next big priority for the team. I'll update PR and introductory text to this issue soon.

This API looks great, really exciting!

It would be useful to have a version of stateIn that takes an initial value and does not suspend. This would make it easy to define a "loading" or default state while waiting for a resource to spin up.

fun <T> Flow<T>.stateIn(initialValue: T, scope: CoroutineScope): StateFlow<T>

Some example use cases:

  • A streaming GRPC call that won't emit until it gets a value from the network, but you can provide a reasonable initial state locally.
  • A settings store that will emit whenever a setting is changed, but allows you to provide a default value that will be used if the setting has never been set or until it's initially loaded from disk.

This function could be implemented on top of the existing stateIn using onStart and runBlocking (on the JVM target) but it would be more efficient to just initialize the internal MutableStateFlow eagerly.

Related Issues:

82
kotlinx.coroutines Introduce StateFlow
@erikc5000 @igorwojda Indeed the naming for constructor functions is quite a controversial issue her...
65
kotlinx.coroutines Compilation error on the androidTest configuration after updating to 1.3.6
I found 2 workarounds: Exclude the duplicated files: Exclude the kotlinx-coroutines-debug dependency...
61
kotlinx.coroutines Flow.shareIn and stateIn operators
πŸ“£ There is an important question on the design of sharing operators we need community help with ...
37
kotlinx.coroutines Default dispatcher and UI dispatcher support for iOS
@kamerok below is the implementation I'm currently using Updated for coroutines 1.0 and implemented ...
24
kotlinx.coroutines Replacing Java Timer with Kotlin Coroutine Timer
You are welcome to use your startCoroutineTimer solution but we don't plan to a function like startC...
24
kotlinx.coroutines Help newbies to handle exceptions in coroutines
You can switch to async instead of launch and use await instead of join This way exceptions will per...
18
kotlinx.coroutines Flow.collects receives items after it was cancelled on a single thread
I have replaced every single collect with a safeCollect function in my project: It would be great if...
17
kotlinx.coroutines Provide abstraction for cold streams
I question the very need of Single/Solo The typealias digression was just to demonstrate what this c...
14
kotlinx.coroutines Support runBlocking for UI Tests
It is already reverted in develop branch Will be part of the next build (tentatively 0.24.1). ...
13
kotlinx.coroutines [question] Is this "IllegalStateException: This job has not completed yet" while using runBlockingTest normal?
I replaced runBlockingTest on runBlocking and it helped. Why when executing the following test: The ...
7
kotlinx.coroutines Lifecycle handling
Is there any problem to allow the code to be excecuted on the main thread after onDestroy per se? Ac...
6
kotlinx.coroutines BroadcastChannel.asFlow().onStart(...) is invoked before the subscription is opened
UNDISPATCHED is predictable: Prints: 132 In the docs we have this example of onStart(...): Not just ...
4
kotlinx.coroutines java.lang.NoSuchMethodError: kotlinx.coroutines.SupervisorKt.SupervisorJob
Does your version of kotlinx-coroutines-test matches with the version of kotlinx-coroutines-core? ...
96
fastapi WARNING: Unsupported upgrade request.
This error is not part of the FastAPI codebase When attempting to run this (using UviCorn) it starts...
84
actix web actix-web 1.0
1.0.0-rc is released next release is 1.0 I am planing to move 0.7 to separate branch and move 1.0 to...
64
fastapi [QUESTION] How to bridge Pydantic models with SQLAlchemy?
I just finished integrating Pydantic ORM mode into FastAPI it is released as version 0.30.0 πŸŽ‰ The n...
52
fastapi [QUESTION] How to send 204 response?
Instead of returning None and instead of injecting the response just return a newly created response...
51
swoole src Compile Error on Mac Big Sur with PHP 8
I can get it work by manually symlink the required file. Please answer these questions before submit...
51
swoole src Swoole's admin interface hot-loads code from a third-party server ?
At this stage all the member's releases have to use matyhtf's PECL account which is ridiculous for a...
42
fastapi OpenAPI UI not working properly when using automatic swagger-ui CDN (swagger-ui-3.30.1)
Thanks for reporting it and for all the discussion here everyone! πŸš€ β˜• Indeed it's a bug in Swagger ...
38
aiohttp "RuntimeError: Event loop is closed" when ProactorEventLoop is used
I found another solution for this problem if some still having issues with it This involves directly...
34
RxGo RxGo v2
Hey My opinion about what should be part of RxGo v2 General Iterable should be moved to an interface...
34
fastapi [QUESTION] Is this the correct way to save an uploaded file ?
@classywhetten FastAPI has almost no custom logic related to UploadFile -- most of it is coming from...
34
fastapi [QUESTION] Storing object instances in the app context
@ebarlas you're 100% right Description In Flask ...
34
tortoise orm Migrations
Hey guys I'm excited to announce that now we have a migrate tool written by pure python and just for...
32
You Dont Know JS let hoisting?
Hoisting is not a real thing It's a made up concept Hi thanks for taking the time to write such a gr...
30
fastapi [QUESTION] aiohttp integration best practice
That is one way if you want create a new session for every request You can also use a singleton appr...
26
fastapi logs with FastAPI and Uvicorn
Doing : is exactly what I was looking for ! Thank you dbanty. Hello Thanks for FastAPI easy to use i...
22
fastapi [QUESTION] Client Credentials Flow openAPI UI
I think I found the solution for others looking to implement the code - tiangolo has already enabled...
21
react query How to use useInfiniteQuery with custom props
The issue here is that you are not mapping up your query key to your query function's arguments prop...
21
fastapi FastAPI 0.65.2 POST request fails with "value is not a valid dict" when using the Requests library; 0.65.1 works (with a caveat)
Can confirm this still happens! We solved it by adding a -H Content-Type: application/json to the cu...
19
fastapi [QUESTION] Using pydantic models for GET request query params? Currently not possible, have to use dataclasses or normal classes.
@LasseGravesen You would do it like this: Check the docs here: https://fastapi.tiangolo.com/tutorial...
18
aiohttp ssl.SSLError: [SSL: KRB5_S_INIT] application data after close notify (_ssl.c:2605)
For those looking for a work-around to at least silence these exceptions: the traceback seen is outp...
18
fastapi [QUESTION] about threads issue with fastapi.
Hello Hi I have a question about the threads issue with fastapi ...
17
ava Allow tests files to have any extension (i.e. .jsx, .ts)
I think what @sindresorhus is getting at is the following With the latest release(0.13) and the abil...
17
ava Only exclude helpers directory when inside a test directory
I feel like we're talking past each other Any chance the exclude rule for helpers can be relaxed so ...
17
ava Flow type definition
Okay so I was planning on just playing around with it but you nerd sniped me and I ended up doing th...
16
react query Array of queries hook
@tannerlinsley yeah Problem: I have a use case were a component would ideally consume a dynamic numb...
15
fastapi [FEATURE] support for rate-limit
I've taken a stab at adapting flask-limiter to starlette and FastAPI Is your feature request related...
14
react query Unable to type useQueries options or results without casting
Hey @matthewdavidrodgers! As @TkDodo has already indicated there's a PR open which looks to improve ...
14
fastapi [QUESTION] How can I serve static files (html, js) easily?
Hi in case a solution is still needed though the issue is closed Description How can I serve static ...
14
fastapi Using UploadFile and Pydantic model in one request
Oups sorry I forgot I made custom validator to transform str to json for Model: ...
13
fastapi How can I pass the configuration in the app?
Is the example I posted above not clear enough? Without going into all the nuances of everything my ...
13
vibe.d undefined reference to methods in ssl
Also try to install libssl1.0-dev - this solves same problem for me Good day I'm trying to generate ...
12
You Dont Know JS Does const declaration is subject to the Variable Hoisting?
This example isn't actually about hoisting at all You don't call foo() until after you've declared a...
12
react query Thoughts on mutate function not handling rejected promises
I came to this issue because I was following the docs and my try/catch wasn't catching even though m...
12
fastapi [BUG] openapi.json fails to be generated for nested models
oups sorry I think your mistake is putting response_model=SimilarProducts in the wrong spot it's in ...
11
opencv4nodejs fatal error: 'tesseract/baseapi.h' file not found
@GiulioPettenuzzo Let's say you receive output like this from your shell: The non-ideal ...
11
pyrogram Pyrogram v1.2.9 auth not working. [406 Not Acceptable]: [406 UPDATE_APP_TO_LOGIN]
This issue has been fixed You can upgrade with the usual command pip install -U pyrogram. ...
11
fastapi Debug Logging (Maybe just a n00b issue)
ok so basically I'm using this in a applog package you use it wherever your entrypint is this way sh...