StateFlow dan SharedFlow

StateFlow dan SharedFlow adalah Flow API yang memungkinkan alur memunculkan pembaruan status dan nilai secara optimal ke beberapa konsumen.

StateFlow

StateFlow adalah alur yang dapat diamati pemegang status yang akan memunculkan pembaruan status saat ini dan yang baru kepada kolektornya. Nilai status saat ini juga dapat dibaca melalui properti value. . Untuk memperbarui status dan mengirimkannya ke alur, tetapkan nilai baru ke properti value dari class MutableStateFlow.

Di Android, StateFlow sangat cocok untuk class yang perlu mempertahankan status yang dapat diubah dan diamati.

Dengan mengikuti contoh dari alur Kotlin, StateFlow dapat ditampilkan dari LatestNewsViewModel sehingga View dapat mendeteksi pembaruan status UI dan secara inheren membuat status layar tidak terpengaruh perubahan konfigurasi.

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Update View with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(exception: Throwable): LatestNewsUiState()
}

Class yang bertanggung jawab untuk memperbarui MutableStateFlow adalah produser, dan semua class yang mengumpulkan dari StateFlow adalah konsumen. Tidak seperti alur dingin yang dibuat menggunakan builder flow, StateFlow bersifat panas: pengumpulan dari alur tidak memicu kode produser apa pun. StateFlow selalu aktif, berada dalam memori, dan valid untuk pembersihan sampah memori hanya jika tidak ada referensi lain terhadapnya dari root pembersihan sampah memori.

Saat konsumen baru mulai mengumpulkan dari alur, status terakhir dalam aliran data dan status berikutnya akan diterima. Anda dapat menemukan perilaku ini dalam class lain yang dapat diamati seperti LiveData.

View akan mendeteksi StateFlow seperti pada alur lainnya:

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // This coroutine will run the given block when the lifecycle
        // is at least in the Started state and will suspend when
        // the view moves to the Stopped state
        lifecycleScope.launchWhenStarted {
            // Triggers the flow and starts listening for values
            latestNewsViewModel.uiState.collect { uiState ->
                // New value received
                when (uiState) {
                    is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                    is LatestNewsUiState.Error -> showError(uiState.exception)
                }
            }
        }
    }
}

Untuk mengonversi alur apa pun menjadi StateFlow, gunakan operator perantara stateIn.

StateFlow, Alur, dan LiveData

StateFlow dan LiveData memiliki kemiripan. Keduanya merupakan class pemegang data yang dapat diamati, dan mengikuti pola serupa saat digunakan dalam arsitektur aplikasi.

Namun, perlu diperhatikan bahwa StateFlow dan LiveData berperilaku berbeda:

  • StateFlow memerlukan status awal untuk diteruskan ke konstruktor, sedangkan LiveData tidak.
  • LiveData.observe() akan otomatis membatalkan pendaftaran konsumen saat tampilan beralih ke status STOPPED, sedangkan pengumpulan dari StateFlow atau alur lainnya tidak.

Pada contoh sebelumnya yang menggunakan launchWhenStarted untuk mengumpulkan alur, bila coroutine yang memicu pengumpulan alur ditangguhkan saat View beralih ke latar belakang, produser yang mendasarinya akan tetap aktif.

Dengan implementasi panas, berhati-hatilah saat melakukan pengumpulan bila UI tidak ada di layar karena hal ini dapat menghapus resource. Sebagai gantinya, Anda dapat berhenti mengumpulkan alur secara manual, seperti ditunjukkan pada contoh berikut:

class LatestNewsActivity : AppCompatActivity() {
    ...
    // Coroutine listening for UI states
    private var uiStateJob: Job? = null

    override fun onStart() {
        super.onStart()
        // Start collecting when the View is visible
        uiStateJob = lifecycleScope.launch {
            latestNewsViewModel.uiState.collect { uiState -> ... }
        }
    }

    override fun onStop() {
        // Stop collecting when the View goes to the background
        uiStateJob?.cancel()
        super.onStop()
    }
}

Cara lain untuk berhenti mendeteksi perubahan uiState bila tampilan tidak terlihat adalah dengan mengonversi alur ke LiveData menggunakan fungsi asLiveData() dari library lifecycle-livedata-ktx:

class LatestNewsActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state ->
            // Handle UI state
        }
    }
}

Membuat alur dingin menjadi panas menggunakan shareIn

StateFlow adalah alur panas, yang akan tetap berada di memori selama alur tersebut dikumpulkan atau saat referensi lain terhadapnya tersedia dari root pembersihan sampah memori. Anda dapat mengubah alur dingin menjadi panas menggunakan operator shareIn.

Dengan menggunakan callbackFlow yang dibuat dalam alur Kotlin sebagai contoh, alih-alih meminta setiap kolektor membuat alur baru, Anda dapat membagikan data yang diambil dari Firestore di antara kolektor menggunakan shareIn. Anda harus meneruskan hal berikut:

  • CoroutineScope yang digunakan untuk membagikan alur. Cakupan ini harus lebih tahan lama dari konsumen mana pun agar alur bersama tetap aktif selama diperlukan.
  • Jumlah item yang akan diputar ulang ke setiap kolektor baru.
  • Kebijakan perilaku mulai.
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

Dalam contoh ini, alur latestNews akan memutar ulang item yang terakhir dimunculkan ke kolektor baru dan akan tetap aktif selama externalScope masih aktif dan terdapat kolektor yang aktif. Kebijakan mulai SharingStarted.WhileSubscribed() akan membuat produser upstream tetap aktif meskipun terdapat subscriber yang aktif. Kebijakan mulai lainnya tersedia, misalnya SharingStarted.Eagerly untuk segera memulai produser atau SharingStarted.Lazily untuk mulai berbagi setelah subscriber pertama muncul dan membuat alur tetap aktif selamanya.

SharedFlow

Fungsi shareIn akan menampilkan SharedFlow, yakni alur panas yang memunculkan nilai ke semua konsumen yang mengumpulkan darinya. A SharedFlow adalah generalisasi StateFlow yang sangat dapat dikonfigurasi.

Anda dapat membuat SharedFlow tanpa menggunakan shareIn. Misalnya, Anda dapat menggunakan SharedFlow untuk mengirim tick ke bagian lain aplikasi agar semua konten di-refresh secara berkala pada waktu yang sama. Selain mengambil berita terbaru, Anda juga dapat me-refresh bagian informasi pengguna dengan koleksi topik favoritnya. Dalam cuplikan kode berikut, TickHandler menampilkan SharedFlow sehingga class lainnya tahu kapan harus me-refresh kontennya. Seperti halnya StateFlow, gunakan properti pendukung jenis MutableSharedFlow dalam class untuk mengirim item ke alur:

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
    val tickFlow: SharedFlow<Event<String>> = _tickFlow

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit)
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler,
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect {
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

Anda dapat menyesuaikan perilaku SharedFlow dengan cara berikut:

  • replay memungkinkan Anda mengirim ulang sejumlah nilai yang sebelumnya dimunculkan untuk subscriber baru.
  • onBufferOverflow memungkinkan Anda menentukan kebijakan tentang kapan buffer penuh berisi item akan dikirim. Nilai defaultnya adalah BufferOverflow.SUSPEND, yang membuat pemanggil ditangguhkan. Opsi lainnya adalah DROP_LATEST atau DROP_OLDEST.

MutableSharedFlow juga memiliki properti subscriptionCount yang berisi jumlah kolektor aktif, sehingga Anda dapat mengoptimalkan logika bisnis yang sesuai. MutableSharedFlow juga berisi fungsi resetReplayCache jika Anda tidak ingin memutar ulang informasi terbaru yang dikirim ke alur.

Referensi alur lainnya