Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce StateFlow #1973

Closed
elizarov opened this issue Apr 29, 2020 · 60 comments
Closed

Introduce StateFlow #1973

elizarov opened this issue Apr 29, 2020 · 60 comments

Comments

@elizarov
Copy link
Contributor

elizarov commented Apr 29, 2020

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.

elizarov added a commit that referenced this issue Apr 29, 2020
StateFlow is a Flow analogue to ConflatedBroadcastChannel. Since Flow API
is simpler than channels APIs, the implementation of StateFlow is
simpler. It consumes and allocates less memory, while still providing
full deadlock-freedom (even though it is not lock-free internally).

Fixes #1973
@Dico200
Copy link

Dico200 commented Apr 29, 2020

I like it. You mentioned users can use convenient syntax like state.value++ - does this mean the implementation will not be thread safe?

@Dico200
Copy link

Dico200 commented Apr 29, 2020

Perhaps a confusing question - atomicity is something that can be added as a layer around the api if (compareAndSet(old, new)) state.value = new. But I would just like to ask what you're planning for the state machine and whether it will be threadsafe like Job, or more light and less thread safe.

@elizarov
Copy link
Contributor Author

The internal state machine is fully thread-state, but its public API is primarily designed for cases when there is a single "owner" that sequentially updates the state's value, so we don't provide operations like compareAndSet. They would only complicate API. We don't have compelling use-cases on the table to support them and, if need, you can always use other mechanisms to coordinate updaters if you have many of them.

@Dico200
Copy link

Dico200 commented Apr 29, 2020

Sounds good! 🎉

@zach-klippenstein
Copy link
Contributor

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.

@erikc5000
Copy link

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!

@igorwojda
Copy link

I also think that we should have separate MutableStateFlow() constructor to avoid confusion and keep things consistent (List, Map, Android LiveData all of them have Mutable equivalents)

It would be good to add extension to simplify conversion from MutableStateFlow to StateFlow

fun <T> MutableStateFlow<T>.toStateFlow() = this as StateFlow<T>

class CounterModel {
	private val _counter = MutableStateFlow(0)
	val counter = _counter.toStateFlow()
}

@JakeWharton
Copy link
Contributor

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.

@ZakTaccardi
Copy link

ZakTaccardi commented Apr 29, 2020

Unlike ConflatedBroadcastChannel, there is no way to create a state flow without a value.

I definitely feel weird about this - what if the initial state is built asynchronously? I currently use an actor coroutine to build state.

private val stateChannel = ConflatedBroadcastChannel<ProfileId?>()

private val actor = actor<Intention> {
  stateChannel.send(getActiveProfileId())
  
  for (intention in channel) {
    // update state channel based on incoming intentions
  }
}

I guess the new pattern would be:

val intentionsChannel = Channel<Intention>()
val states = flow<ProfileId?> {
    emit(getActiveProfileId())
  
    for (intention in intentionsChannel) {
        // emit new state based on incoming intentions
    }
}
  .stateIn(scope = this)

But this leaves the Channel<Intention> used to communicate into the StateFlow<T> disconnected, even though they are related.

EDIT: the new pattern doesn't work because stateIn(..) suspends. What is the value in that?

@ZakTaccardi
Copy link

ZakTaccardi commented Apr 29, 2020

You can consider it to have an embedded distinctUntilChanged out-of-the-box.

Will this be configurable? Could I inject my own predicate to evaluate if two items are different?

Obviously equality is an excellent default

@elizarov
Copy link
Contributor Author

elizarov commented Apr 30, 2020

@ZakTaccardi There is no configurability for comparisons that are used to avoid sending extra state-changes in the initial release. We are trying to keep it as simple as basic Set operations are. Create a separate issue with use-cases for configurable behaviour, please, if you have some use-cases for it in mind.

@igorwojda
Copy link

Right asStateFlow() would be better name indeed

Regarding the value well - main one is ease of use - this gives us much faster code completion, more convenient usage with IDE.

When it comes to library mode we know that majority of devs are writing apps, not libraries, but still these two solution are not mutually exclusive.

@fvasco
Copy link
Contributor

fvasco commented Apr 30, 2020

A valuable implementation should provide -at least- the defensive programming, otherwise a plain cast can be enough.

fun <T> MutableStateFlow<T>.asStateFlow() = object : StateFlow<T> by this {}

@elizarov
Copy link
Contributor Author

elizarov commented Apr 30, 2020

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

@LouisCAD
Copy link
Contributor

LouisCAD commented Apr 30, 2020

It is possible to use the MutableStableFlow(value) constructor, and have a deprecated symbol (error level) for StateFlow(value) with a ReplaceWith clause to use the proper and explicit one.

@fvasco
Copy link
Contributor

fvasco commented Apr 30, 2020

StateFlow(value) and MutableStableFlow(value) look like listOf(value) and mutableListOf(value).

@RobertKeazor
Copy link

Very Nice Work! The API actually looks very similar to LiveData AND MutableLiveData. But def like the very simple design .

@erikc5000
Copy link

class CounterModel {
   private val _counter = MutableStateFlow(0)
   val counter: StateFlow<Int> get() = _counter
}

...is basically the pattern that's used with Android's LiveData type, so it should look familiar to a lot of people.

@zach-klippenstein
Copy link
Contributor

Not all kotlin devs are Android devs 😉

I like @LouisCAD's suggestion about adding an error-deprecated alias function for discoverability. I don't really think the extra length/explicitness of the name MutableStateFlow is a problem - it's much more clear, and code is read a lot more than it's written.

That said, as another alternative, how important is it to make this function look like a constructor? If it were called stateFlow(), it would still be fairly discoverable, but since it doesn't look like a constructor it wouldn't imply quite as strongly that the return type is exactly StateFlow.

@elizarov
Copy link
Contributor Author

elizarov commented Apr 30, 2020

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.

@ZakTaccardi
Copy link

ZakTaccardi commented Apr 30, 2020

As StateFlow<T> must always have a value, I'm still curious how we are supposed to leverage use of a StateFlow<T> that does not have a value already initialized

See #1973 (comment)

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.

Do you expect that my use case will be addressed in this family of stateIn(..) operators?

@nschwermann
Copy link

@ZakTaccardi make a sealed class that has a representation for not being initialized

@ZakTaccardi
Copy link

make a sealed class that has a representation for not being initialized
@nschwermann

this is a non-starter. I want to cleanly support the scenario where my Flow<T> behaves like a StateFlow<T> but doesn't emit a value until it's been lazily initialized. And I would prefer if this use case is supported by the Coroutines framework rather than a custom wrapper .

This sounds like something a .stateIn(..) operator could potentially address, which is why I'm looking forward to understanding if this is something the Coroutines framework will officially support in some way

@ZakTaccardi
Copy link

...wait, can't you just make it a StateFlow<T?> when you want the value to be nullable? After all, while it is not initialized, the value is absent.

null can have semantic meaning. for example:

val currentUserId: Flow<UserId?>
  1. lack of value: we don’t know yet. Info could still be loading from disk
  2. null: no user found. We loaded from disk, and did not find an active user. Likely means that user is not logged in
  3. non-null: logged in user

I don’t really want to start a discussion about how a sealed class should be used in this scenario to promote clarity, because yes I agree. But supporting this use case is still useful.

@ZakTaccardi
Copy link

I created a rudimentary NoInitialStateFlow. I'm certainly curious if my flow { .. }.conflate() is safe

@RobertKeazor
Copy link

hmm interesting. But it seems abit much, just to represent an empty state? When you can just emit a State.Empty/State.Loading/or some kind of flag that represents that represents the state not being initialized? What would be an example use case that would make one use this over just having a state representation of not being initialized state?

@ZakTaccardi
Copy link

When you can just emit a State.Empty/State.Loading/or some kind of flag that represents that represents the state not being initialized?

This increases verbosity. However, I do agree it’s a good practice.

What would be an example use case that would make one use this over just having a state representation of not being initialized state?

When you don’t want to define a sealed class for the following:

  1. lack of value: we don’t know yet. Info could still be loading from disk
  2. null: no user found. We loaded from disk, and did not find an active user. Likely means that user is not logged in
  3. non-null: logged in user

It’s the same argument that allows nulls to be emitted in a Flow<T?> but not a Publisher<T>.

@elizarov
Copy link
Contributor Author

There is a followup design on SharedFlow that closes most of the issues that were raised in the comments here. See #2034

@adam-hurwitz
Copy link

adam-hurwitz commented May 18, 2020

Please let me know if this is the best place for feedback on StateFlow since this issue has been closed.

It would desirable for the ability to create a MutableStateFlow with no value assigned to it similar to MutableLiveData.

I'm refactoring the Android ViewModel's view state and effects in the CryptoTweets sample app from LiveData/MutableLiveData to StateFlow/MutableStateFlow in order to update the view UI, in this case, a fragment, accordingly.

LiveData

The MutableLiveData is created with no value assigned to it.

FeedViewState.kt

data class _FeedViewState(
    val _feed: MutableLiveData<PagedList<Tweet>> = MutableLiveData()
)

data class FeedViewState(private val _viewState: _FeedViewState) {
    val feed: LiveData<PagedList<Tweet>> = _viewState._feed
}

StateFlow

The MutableStateFlow is created with a null value, thus requiring the type contained by the MutableStateFlow to allow nulls. Ideally, it would be easier to create an empty MutableStateFlow because for this use case it would be difficult to create an empty PagedList to initialize the view state with.

FeedViewState.kt

@ExperimentalCoroutinesApi
data class _FeedViewState (
    val _feed: MutableStateFlow<PagedList<Tweet>?> = MutableStateFlow(null),
)

@ExperimentalCoroutinesApi
data class FeedViewState(private val _viewState: _FeedViewState) {
    val feed: StateFlow<PagedList<Tweet>?> = _viewState._feed
}

In the ViewModel the view states are initialized with default empty values before being populated in the init{ ... } of the ViewModel.

FeedViewModel.kt

@ExperimentalCoroutinesApi
class FeedViewModel(private val feedRepository: FeedRepository) : ViewModel(), FeedViewEvent {
    val LOG_TAG = FeedViewModel::class.java.simpleName

    private val _viewState = _FeedViewState()
    val viewState = FeedViewState(_viewState)
   ...
}

FeedFragment.kt

@ExperimentalCoroutinesApi
private fun initViewStates() {
    viewModel.viewState.feed.onEach { pagedList ->
        adapter.submitList(pagedList)
    }.launchIn(lifecycleScope)
}

@Zhuinden
Copy link

Zhuinden commented May 18, 2020

 val _feed: MutableLiveData<PagedList<Tweet>> = MutableLiveData()

This doesn't do what you think it does, and you don't actually want this

@adam-hurwitz
Copy link

adam-hurwitz commented May 19, 2020

Thanks @Zhuinden. Is there a portion of the MutableLiveData code you'd recommend examining further in order to understand why this is a bad idea?

@RobertKeazor
Copy link

RobertKeazor commented May 19, 2020

I don't know, Im still in the belief that its better to have a state that represents missing item, rather than refactoring it handle a missing value. This api, seems to be built with the intention that
value : T is always available , trying to handle a missing a case to where a item sounds more like a anti-pattern. MutableLiveData is different because it was nullable from the beginning . Which is something most people didn't like

sealed class TweetState {
    object NoTweet  : TweetState  
    data class Tweet(val tweet : TweetState) 
}

@ExperimentalCoroutinesApi
class FeedViewModel(private val feedRepository: FeedRepository) : ViewModel(), FeedViewEvent {
    val LOG_TAG = FeedViewModel::class.java.simpleName

    private val _viewState: MutableStateFlow<TweetState> = _MutableStateFlow(TweetState.NoTweet)
    val viewState: StateFlow<TweetState> = FeedViewState(_viewState)
}

@elizarov
Copy link
Contributor Author

elizarov commented May 19, 2020

Please let me know if this is the best place for feedback on StateFlow since this issue has been closed.

@AdamSHurwitz It is better to create separate issues for new proposals.

It would desirable for the ability to create a MutableStateFlow with no value assigned to it similar to [MutableLiveData]

This particular desire is taken into account by the design of SharedFlow. See #2034. Using SharedFlow you can have a StateFlow-like behavior without initial value and a number of other possible tweaks. Of course, with SharedFlow you loose access to current .value, since it may be missing, but you can do sharedFlow.replayCache.lastOrNull() if you really need it and know it is there. If something is absent in SharedFlow design for your use-case, please comment on the open issue #2034.

@Zhuinden
Copy link

 interface MutableStateFlow<T> : StateFlow<T> {

Are you sure it's not an issue that you can potentially smart-cast an "immutable" state flow into a mutable one?

Of course, that's technically true of List and I haven't really seen people do it, just pass MutableLists around if they really want to by-pass "read-only"-ness.

@elizarov
Copy link
Contributor Author

elizarov commented May 25, 2020

Are you sure it's not an issue that you can potentially smart-cast an "immutable" state flow into a mutable one?

@Zhuinden We have quite lengthy experience with a similar design for List/MutableList in Kotlin and it is not causing any problems to be worried about it, as far as I know.

@Zhuinden
Copy link

Okay then, that should be great 🙂

@muthuraj57
Copy link

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.

Is this enhancement tracked elsewhere where I can follow? I'm trying to publish download progress and need to close the flow once the download completes so that the collector is finished.

@elizarov
Copy link
Contributor Author

Is this enhancement tracked elsewhere where I can follow? I'm trying to publish download progress and need to close the flow once the download completes so that the collector is finished.

@muthuraj57 No. We've abandoned it. On the collector side you can use takeWhile { ... } to stop collector when download completes. See longer discussion on this decision in #2034

@JonathanSampayoSolis
Copy link

This idea was huge good. I'm wondering if is still handled?
I'm facing an use case to send my repository states by reactive like ->(ConflatedBroadcastChannel.send(Loading), (ConflatedBroadcastChannel.send(Sucess), etc. But Conflated... is already obsolete. Any solution into channels lib?

@zach-klippenstein
Copy link
Contributor

StateFlow is the replacement for ConflatedBroadcastChannel.

@JonathanSampayoSolis
Copy link

StateFlow is the replacement for ConflatedBroadcastChannel.

Is it available for implementation already?

@zach-klippenstein
Copy link
Contributor

It's not stable for implementing yourself, but it is stable for use in the latest coroutines release (see the kdoc).

recheej pushed a commit to recheej/kotlinx.coroutines that referenced this issue Dec 28, 2020
StateFlow is a Flow analogue to ConflatedBroadcastChannel. Since Flow API
is simpler than channels APIs, the implementation of StateFlow is
simpler. It consumes and allocates less memory, while still providing
full deadlock-freedom (even though it is not lock-free internally).

Fixes Kotlin#1973
Fixes Kotlin#395
Fixes Kotlin#1816

Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
@deinlandel
Copy link

Cannot find any actual implementation of asStateFlow() in source code, despite the fact that it is mentioned in the examples. It's also not recognized by the IDE

@gildor
Copy link
Contributor

gildor commented Jan 15, 2021

@deinlandel It's part of MutableStateFlow API - https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/as-state-flow.html

If you want to convert any other Flow there is an extension stateIn

@deinlandel
Copy link

@gildor I indeed found this javadoc, but the problem is, I cannot locate this function in this repository's source code, and also my IDE fails to compile the code when I try to use it even with the latest kotlinx.coroutines libraries.

@LouisCAD
Copy link
Contributor

@deinlandel I think there's a bug in GitHub search/indexing. Here's the permalink to the implementation of asStateFlow():

public fun <T> MutableStateFlow<T>.asStateFlow(): StateFlow<T> =

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests