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

StateFlow implementation #1974

Merged
merged 10 commits into from May 7, 2020
Merged

StateFlow implementation #1974

merged 10 commits into from May 7, 2020

Conversation

elizarov
Copy link
Contributor

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 (see the issue for design and discussion)

This was referenced Apr 29, 2020
@elizarov
Copy link
Contributor Author

elizarov commented May 7, 2020

Note, that it also fixes #395 and it removes the need for ConflatedBroadcastChannel to be allocation-free. StateFlow implementation is allocation-free and covers the use-cases presented there.

elizarov and others added 9 commits May 7, 2020 16:46
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
Fixes #395
Fixes #1816
* Move StateInTest
* Add `out T` projection to StateFlow type
Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
@elizarov
Copy link
Contributor Author

elizarov commented May 7, 2020

Also fixes #1816

Copy link
Member

@qwwdfsad qwwdfsad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@elizarov elizarov merged commit 1541886 into develop May 7, 2020
@elizarov elizarov deleted the state-flow branch May 7, 2020 16:52
@proto-mvp
Copy link

proto-mvp commented May 26, 2020

@elizarov
How can we use StateFlow, in case we need to emit data classes, that have same values ?
In other words with distinctUntilChanged disabled.

It makes sense when a sequence of actions needs to emit the same result through the StateFlow, or simply when retrying an action from a user interface has the same result.

@LouisCAD
Copy link
Contributor

@proto-mvp Adding a timestamp or index to your data class can do. Otherwise, StateFlow is not designed for what you're looking for.

@proto-mvp
Copy link

@LouisCAD I did something to make my data classes unique, but wanted to know if we could override it.

@dave08
Copy link

dave08 commented May 27, 2020

@proto-mvp

If you have something like val MutableStateFlow<T?>.valueNotDistinct set(newValue: T) { value = null; value = newValue }, I think you could simulate that behavior without extra allocations. And then use filterNotNull() to listen to it.

@elizarov
Copy link
Contributor Author

@proto-mvp What's your actual use-case? What kind of data are you sending?

@proto-mvp
Copy link

emitting states of UI on an MVI - reactive architecture.
Was using previously a MutableLiveData with the asFlow operator, and got into this after switching to StateFlow, when had to emit the same state again.
e.g. There is an error, user presses retry , emitting second time a Retry data class wasn't captured.

Ended up having an extra Id field to distinguish and get the behaviour I wanted with StateFlow.

I know it is by design distinctUntilChanged enabled, but would be handy to have a StateFlow without this behaviour, in cases we need to drop MutableLiveData, or PublishSubject from Rx.Java, and not having to use channels.

@proto-mvp
Copy link

proto-mvp commented May 27, 2020

@dave08

If you have something like val MutableStateFlow<T?>.valueNotDistinct set(newValue: T) { value = null; value = newValue }, I think you could simulate that behavior without extra allocations. And then use filterNotNull() to listen to it.

that is a good tip. Changing the sequence of the values should work. Will give it a go. Cheers!

@LouisCAD
Copy link
Contributor

would be handy to have a StateFlow without this behaviour

This is what SharedFlow will have.

@adam-hurwitz
Copy link

adam-hurwitz commented Jun 6, 2020

How can we use StateFlow, in case we need to emit data classes, that have same values?
In other words with distinctUntilChanged disabled.

It makes sense when a sequence of actions needs to emit the same result through the StateFlow, or simply when retrying an action from a user interface has the same result.

Great question @proto-mvp. I experienced the same use case this week when building a Model-View-Intent (MVI) pattern proof-of-concept with StateFlow.

The proposed solution of 'having an extra ID field to distinguish unique data' will work. Another solution, if no extra data is required for the intent, is using a new class or data class instance, when data is needed. Either can be set to the MutableStateFlow value which will have a unique hash code and thus be considered as a unique value by distinctUntilChanged.

In the sample below every time loadRequestIntent.value = Emit() is implemented, SomeViewModel will observe the emission of the unique intent/event.

Emit.kt

class Emit

SomeFragment.kt

@ExperimentalCoroutinesApi
class FeedFragment : Fragment(), SomeView {
    private var loadRequestIntent = MutableStateFlow<Emit?>(null)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if (savedInstanceState == null)
            loadRequestIntent.value = Emit()
    }
    override fun loadNetworkIntent() = loadRequestIntent.filterNotNull()
}

SomeView.kt

interface SomeView {
    /**
     * Load feed from the network
     *
     * @return A flow emitting the feed state returned from the network
     */
    @ExperimentalCoroutinesApi
    fun loadNetworkIntent(): Flow<Emit>
}

SomeViewModel.kt

@ExperimentalCoroutinesApi
class FeedViewModel(private val repository: FeedRepository) : ViewModel() {
    fun bindIntents(view: FeedView) {
        view.loadNetworkIntent().onEach {
            loadNetwork(it.toRetry)
        }.launchIn(viewModelScope)
    }

@Zhuinden
Copy link

@AdamSHurwitz

   if (savedInstanceState == null)
       loadRequestIntent.value = Emit()

Who's going to trigger a data reload after process death then? 🤔

@adam-hurwitz
Copy link

This is a great point @Zhuinden. In the sample above, when a process death occurs savedInstanceState != null, and the ViewModel data will be cleared according to the documentation, Options for preserving UI state. In summary, the ViewModel does not survive a process death, and the saved instance state does survive.

The result in the sample code is there will not be a new network request for data, however existing data persisted in Room database will populate the view state. In the full ViewModel implementation the above sample is derived from, if a new network data request is not made from loadRequestIntent, the existing view state will be initialized from Room's database and a user can swipe-to-refresh in order to initiate a new network request initiating a new loadRequestIntent emission.

I need to think more in-depth about a clean way to refresh the network request on process death vs. relying on a manual swipe-to-refresh to this point. I'm open to suggestions as well! 🙏🏻 Also, I'm updating the open-source Coinverse app with a full sample of the above pattern I will share with you @Zhuinden once drafted.

@adam-hurwitz
Copy link

adam-hurwitz commented Jul 21, 2020

@Zhuinden, here is the full Coinverse sample using StateFlow with a Model-View-Intent pattern.

Re: "Who's going to trigger a data reload after process death then?"

Thinking more about your WorkManager (WM) suggestion over DMs. An implementation could look like WM checking a SharedPref value to see when the network data was last updated, and emit loadRequestIntent.value = true when a given threshold of time has passed and when WM conditions have been met. Upon process death, if the data has not been updated in a given amount of time, instead of loading the existing data from local storage, it will initiate a request from WorkManager for new network data.

recheej pushed a commit to recheej/kotlinx.coroutines that referenced this pull request 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants