-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Comments
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
I like it. You mentioned users can use convenient syntax like |
Perhaps a confusing question - atomicity is something that can be added as a layer around the api |
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 |
Sounds good! 🎉 |
This API looks great, really exciting! It would be useful to have a version of fun <T> Flow<T>.stateIn(initialValue: T, scope: CoroutineScope): StateFlow<T> Some example use cases:
This function could be implemented on top of the existing |
Having the Otherwise, it's looking pretty good to me! |
I also think that we should have separate It would be good to add extension to simplify conversion from MutableStateFlow to StateFlow
|
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. |
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 EDIT: the new pattern doesn't work because |
Will this be configurable? Could I inject my own predicate to evaluate if two items are different? Obviously equality is an excellent default |
@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 |
Right 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. |
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 {} |
@erikc5000 @igorwojda Indeed, the naming for constructor functions is quite a controversial issue here. We have two basic naming options:
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:
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
It reads as if you created a Adding
This somewhat weird look of Changing name of the constructor to MutableStateFlow
Either way, with 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 |
It is possible to use the |
|
Very Nice Work! The API actually looks very similar to LiveData AND MutableLiveData. But def like the very simple design . |
class CounterModel {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter
} ...is basically the pattern that's used with Android's |
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 That said, as another alternative, how important is it to make this function look like a constructor? If it were called |
UPDATE: Based on your feedback we've decided to rename The main reason here is that |
As See #1973 (comment)
Do you expect that my use case will be addressed in this family of |
@ZakTaccardi make a sealed class that has a representation for not being initialized |
this is a non-starter. I want to cleanly support the scenario where my This sounds like something a |
I don’t really want to start a discussion about how a |
I created a rudimentary |
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? |
This increases verbosity. However, I do agree it’s a good practice.
When you don’t want to define a sealed class for the following:
It’s the same argument that allows nulls to be emitted in a |
There is a followup design on |
Please let me know if this is the best place for feedback on It would desirable for the ability to create a I'm refactoring the Android ViewModel's view state and effects in the CryptoTweets sample app from LiveDataThe 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
} StateFlowThe 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 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)
} |
This doesn't do what you think it does, and you don't actually want this |
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? |
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 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)
} |
@AdamSHurwitz It is better to create separate issues for new proposals.
This particular desire is taken into account by the design of |
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 |
@Zhuinden We have quite lengthy experience with a similar design for |
Okay then, that should be great 🙂 |
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 |
This idea was huge good. I'm wondering if is still handled? |
|
Is it available for implementation already? |
It's not stable for implementing yourself, but it is stable for use in the latest coroutines release (see the kdoc). |
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 <[email protected]>
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 |
@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 |
@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. |
@deinlandel I think there's a bug in GitHub search/indexing. Here's the permalink to the implementation of
|
We need to be able to conveniently use
Flow
to represent an updateable state in applications. This change introducesStateFlow
-- an updateable value that represents a state and is aFlow
. The design is flexible to fit a variety of needs:StateFlow<T>
interface is a read-only view that gives access to the currentvalue
and implements aFlow<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 ofMutableStateFlow
with the given initial value. It can be exposed to the outside world as eitherStateFlow<T>
if fast non-reactive access to thevalue
is needed, or asFlow<T>
if only reactive view of updates to the value is needed.Core state flow API can be summarized like this:
Implementation is available in PR #1974.
StateFlow vs ConflatedBroadcastChannel
Conceptually state flow is similar to
ConflatedBroadcastChannel
and is designed to completely replaceConflatedBroadcastChannel
in the future. It has the following important improvements:StateFlow
is simpler because it does not have to implement all theChannel
APIs, which allows for faster, garbage-free implementation, unlikeConflatedBroadcastChannel
implementation that allocates objects on each emitted value.StateFlow
always has a value that can be safely read at any time viavalue
property. UnlikeConflatedBroadcastChannel
, there is no way to create a state flow without a value.StateFlow
has a clear separation into a read-onlyStateFlow
interface and aMutableStateFlow
.StateFlow
conflation is based on equality, unlike conflation inConflatedBroadcastChannel
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 embeddeddistinctUntilChanged
out-of-the-box.StateFlow
cannot be currently closed likeConflatedBroadcastChannel
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
: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 viavalue
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 andStateFlow.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 astateIn
operator to turn anyFlow
into a hotStateFlow
in the as a part of the family of flow-sharing operators (see #2047 ). It is designed to become a replacement forbroadcastIn
operator. It would subsume the need of having to have a separate "state flow builder" as you can simply writeflow { .... }.stateIn(scope)
to launch a coroutine that emits the values according to the code in curly braces.The text was updated successfully, but these errors were encountered: