Boolean flows sync - android

I have few StateFlow fields in the ViewModel class. It's add/edit form screen where each StateFlow is validation property for each editable field on the screen.
I would like to write some class FormValidation with StateFlow property for validation state of whole form. Value of this field based on the values of validation state of all fields and emit true when all field is valid and false when any field is invalid.
Something like this:
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
private val _isValid = MutableStateFlow(initValue)
val isValid: StateFlow<Boolean> = _isValid
init {
// todo: how to combine, subscribe and sync values of all fieldIsValid flows?
}
}
I know how to do it with LiveData<Boolean> and MediatorLiveData but i can't understand how to make it with flows.
Solution based on the answer of #tenfour04
class BooleanFlowMediator(scope: CoroutineScope, initValue: Boolean, vararg flows: Flow<Boolean>) {
val sync: StateFlow<Boolean> = combine(*flows) { values ->
values.all { it }
}.stateIn(scope, SharingStarted.Eagerly, initValue)
}
Demo code with StateFlow and ViewModel
class SyncViewModel : ViewModel() {
companion object {
private const val DEFAULT_VALUE: Boolean = false
}
private val values: List<List<Boolean>> = listOf(
listOf(false, false, false),
listOf(true, false, false),
listOf(false, true, true),
listOf(true, true, true)
)
private var index: Int = 0
private val _flow1 = MutableStateFlow(DEFAULT_VALUE)
val flow1: StateFlow<Boolean> = _flow1
private val _flow2 = MutableStateFlow(DEFAULT_VALUE)
val flow2: StateFlow<Boolean> = _flow2
private val _flow3 = MutableStateFlow(DEFAULT_VALUE)
val flow3: StateFlow<Boolean> = _flow3
val mediator = BooleanFlowMediator(viewModelScope, DEFAULT_VALUE,
flow1, flow2, flow3)
fun generateValues() {
val idx = (index + 1).mod(values.size).also { index = it }
val row = values[idx]
_flow1.value = row[0]
_flow2.value = row[1]
_flow3.value = row[2]
}
}

I think you can do this using combine. It returns a new Flow that emits each time any of the source Flows emits, using the latest values of each in a lambda to determine its emitted value.
There are also overloads of combine for up to five input Flows of different types, and one for an arbitrary number of Flows of the same type, which is what we want here.
Since Flow operators return basic cold Flows, but if you want to have a StateFlow so you can determine the initial value, you need to use stateIn to convert it back to a StateFlow with an initial value. And for that you'll need a CoroutineScope for it to run the flow in. I'll leave it to you to determine the best scope to use. Maybe it should be passed in from an owning class (like passing viewModelScope to it if the class instance is "owned" by the ViewModel). If you're not using a passed in scope, you will have to manually cancel the scope when this class instance is done with, or else the flow will leak.
I didn't test this code, but I think this should do it.
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
private val scope = MainScope()
val isValid: StateFlow<Boolean> =
combine(*fieldIsValid) { values -> values.all { it } }
.stateIn(scope, SharingStarted.Eagerly, initValue)
}
However, if you don't need to synchronously inspect the most recent value of the Flow (StateFlow.value), then you don't need a StateFlow at all, and you can just expose a cold Flow. The instant the cold Flow is collected, it will start collecting its source StateFlows, so it will immediately emit its first value based on the current values of all the sources.
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
val isValid: Flow<Boolean> = when {
fieldIsValid.isEmpty() -> flowOf(initValue) // ensure at least one value emitted
else -> combine(*fieldIsValid) { values -> values.all { it } }
.distinctUntilChanged()
}
}

Related

How to update properties of data class show compose ui can observe the changes

I have a CounterScreenUiState data class with a single property called counterVal (integer). If I am updating the value of my counter from viewModel which of the following is the correct approach?
Approach A:
data class CounterUiState(
val counterVal: Int = 0,
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState = uiState.copy(counterVal = uiState.counterVal + 1)
}
fun dec() {
uiState = uiState.copy(counterVal = uiState.counterVal - 1)
}
}
or
Approach B:
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState.counterVal.value = uiState.counterVal.value + 1
}
fun dec() {
uiState.counterVal.value = uiState.counterVal.value - 1
}
}
For the record, I tried both approach and both works well without unnecessary re-compositions.
Thanks in Advance!!!
So to summarize, "implementation" and "performance" wise, your'e only
choice is A.
This is not true. It's a common pattern that is used other Google's sample apps, JetSnack for instance, and default functions like rememberScrollable or Animatable are the ones that come to my mind. And in that article it's also shared as
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
or
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
Animatable class
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
Omitted some code from Animatable for simplicity but as can be seen it's a common pattern to use a class that hold one or multiple MutableStates. Even type AnimationState hold its own MutableState.
You can create state holder classes and since these are not e not variables but states without them changing you won't have recompositions unless these states change. The thing needs to be changed with option B is instead of using
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
You should change it to
class CounterUiState(
var counterVal by mutableStateOf(0)
)
since you don't need to set new instance of State itself but only the value.
And since you already wrap your states inside your uiState there is no need to use
var uiState by mutableStateOf(CounterUiState())
private set
you can have this inside your ViewModel as
val uiState = CounterUiState()
or inside your Composable after wrapping with remember
#Composable
fun rememberCounterUiState(): CounterUiState = remember {
CounterUiState()
}
With this pattern you can store States in one class and hold variables that should not trigger recomposition as part of internal calculations and it's up to developer expose these non-state variables based on the design.
https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
categories: List<SearchCategoryCollection>,
suggestions: List<SearchSuggestionGroup>,
filters: List<Filter>,
searchResults: List<Snack>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var categories by mutableStateOf(categories)
var suggestions by mutableStateOf(suggestions)
var filters by mutableStateOf(filters)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.Categories
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
Also for skippibility
Compose will treat your CounterUiState as unstable and down the road
it will definitely cause you headaches because what ever you do,
This is misleading. Most of the time optimizing for skippability is premature optimization as mentioned in that article and the one shared by originally Chris Banes.
Should every Composable be skippable? No.
Chasing complete skippability for every composable in your app is a
premature optimization. Being skippable actually adds a small overhead
of its own which may not be worth it, you can even annotate your
composable to be non-restartable in cases where you determine that
being restartable is more overhead than it’s worth. There are many
other situations where being skippable won’t have any real benefit and
will just lead to hard to maintain code. For example:
A composable that is not recomposed often, or at all.

Flow provides null from room database but it should have data

I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}

Why Android Datastore always returns same value with runblocking

I've been using Datastore for a long time. Today i had to read the values in the main thread. After reviewing the documentation, I decided to use runblocking. I created a long value which name is lastInsertedId.
I reading lastInsertedId in Fragment A then navigated to Fragment B and I'm changing the value of lastInsertedId. When i pop back to Fragment A i read lastInsertedId again. But lastInsertedId's value was still same. Actually it's value is changing but i can't read its last value.
I think it was because Fragment A was not destroyed. Only onDestroyView called and created from onCreateView. What i want is i need to access lastInsertedID's current value whenever i want in main thread.
When i create it as a variable, it always returns the same value. But when i convert it to function it works well. But i don't think this is the best practices. What's the best way to access this value?
Thanks.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "main")
#Singleton
class DataStoreManager #Inject constructor(#ApplicationContext appContext: Context) {
private val mainDataStore = appContext.dataStore
suspend fun setLastInsertedId(lastId: Long) {
mainDataStore.edit { main ->
main[LAST_INSERTED_ID] = lastId
}
}
// Returns always the same value
val lastInsertedId: Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// Returns as expected
fun lastInsertedId(): Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// This is also work perfectly but i need to access in main thread.
val lastInsertedId : Flow<Long> = mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: Constants.DEFAULT_FOOD_ID
}
companion object {
private val LAST_INSERTED_ID = longPreferencesKey("last_inserted_id")
}
}
You must add get() to your val definition like this.
val lastInsertedId: Long get() = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
You seems don't understand the differece between variable and function, take a look at this:
fun main() {
val randomNum1 = (1..10).random()
repeat(5) { println(randomNum1) }
repeat(5) { println(getRandomNum()) }
}
fun getRandomNum() = (1..10).random()
Output:
2
2
2
2
2
8
7
8
10
1
Variable holds a value, and it doesn't change until you assign it a new value.

How Does Android LiveData get() syntax work?

I understand the need for creating getter and setter points for LiveData in the ViewModel, but I'm looking to understand how the get() syntax works in Android.
ie:
val isRealtime: LiveData<Boolean>
get() = _isRealtime
private val _isRealtime = MutableLiveData<Boolean>()
get() is not related to Android.
val isRealtime: LiveData<Boolean>
get() = _isRealtime
Here, get() is overriding the automatically-generated Kotlin getter function for the isRealtime property. So, instead of returning its own value, it returns the value of _isRealtime.
Personally, I recommend simpler syntax:
private val _isRealtime = MutableLiveData<Boolean>()
val isRealtime: LiveData<Boolean> = _isRealtime
The objective of either of these is to keep the mutability private, so consumers of this class do not accidentally update the MutableLiveData themselves.
In Kotlin we have multiple ways of exposing live data from ViewModel to the view.
class MyViewModel: ViewModel() {
// Solution 1 - make MutableLiveData public
// This approach works, but this is a bad idea because
// view can modify the LiveData values
val liveDataA1 = MutableLiveData<State>()
// Solution 2 - let's make LiveData public (expose it instead of MutableLiveData)
// Now from view perspective this solution looks fine, bu we have a problem,
// because we need MutableLiveData within ViewModel to put/post new values to
// the stream (we can't post values to LiveData).
val liveDataA2 = MutableLiveData<State>() as LiveData<State>
// Let's capture our requirements:
// 1. We need to expose (immutable) LiveData to the view,
// so it cannot edit the data itself.
// 2. We need to access MutableLiveData from ViewModel to put/post new values.
// Now, let's consider few appropriate solutions
// Solution 3
// Let's name mutable live data using underscore prefix
private val _liveData3 = MutableLiveData<State>()
val liveData3 = _liveData3 as LiveData<State>
// Solution 4
// We can also perform casting by specifying type for a variable
// (we can do it because MutableLiveData extends LiveData)
private val _liveData4 = MutableLiveData<State>()
val liveData4: LiveData<State> = _liveData4
// Solution 5
// Starting from Kotlin 1.4-M.2 we can delegate call to another property
private val _liveData5 = MutableLiveData<State>()
val liveData5 by this::_liveData5
// Solution 6
// These above solutions work quite well, but we could do even better by
// defining custom asLiveData extension function.
private val _liveData6 = MutableLiveData<State>()
val liveData6 = _liveData6.asLiveData()
fun <T> MutableLiveData<T>.asLiveData() = this as LiveData<T>
// Amount of code is similar, but notice that this approach works much better
// with code completion.
// Solution 7 (IMO Best)
// We can also use alternative naming convention - use "mutableLiveData"
// as variable for mutable live data instead of using underscore prefix
private val mutableLiveData7 = MutableLiveData<State>()
val liveData7 = mutableLiveData7.asLiveData()
// BTW
// We could also expose getLiveData8() method, but liveData is a state not an action.
// Solution 9
// This does not create backing field for the property
// (more optimised but still Solution 7 is easier to use)
private val _liveData9 = MutableLiveData<State>()
val liveData9 get() = _liveData9 as LiveData<State>
}
I wrote a util function for this logic:
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import kotlin.reflect.KProperty
fun <T> immutable(data: MutableLiveData<T>): Immutable<T> {
return Immutable(data)
}
class Immutable<T>(private val data: MutableLiveData<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): LiveData<T> {
return data
}
}
Then you can use in any of your ViewModel as:
private val _counter: MutableLiveData<Int> = MutableLiveData()
val counter: LiveData<Int> by immutable(_counter)
or in short:
private val _counter = MutableLiveData<Int>()
val counter by immutable(_counter)

Android - Best Practices for ViewModel State in MVVM?

I am working on an Android App using the MVVM pattern along LiveData (possibly Transformations) and DataBinding between View and ViewModel. Since the app is "growing", now ViewModels contain lots of data, and most of the latter are kept as LiveData to have Views subscribe to them (of course, this data is needed for the UI, be it a Two-Way Binding as per EditTexts or a One-Way Binding). I heard (and googled) about keeping data that represents the UI state in the ViewModel. However, the results I found were just simple and generic. I would like to know if anyone has hints or could share some knowledge on best practices for this case. In simple words, What could be the best way to store the state of an UI (View) in a ViewModel considering LiveData and DataBinding available? Thanks in advance for any answer!
I struggled with the same problem at work and can share what is working for us. We're developing 100% in Kotlin so the following code samples will be as well.
UI state
To prevent the ViewModel from getting bloated with lots of LiveData properties, expose a single ViewState for views (Activity or Fragment) to observe. It may contain the data previously exposed by the multiple LiveData and any other info the view might need to display correctly:
data class LoginViewState (
val user: String = "",
val password: String = "",
val checking: Boolean = false
)
Note, that I'm using a Data class with immutable properties for the state and deliberately don't use any Android resources. This is not something specific to MVVM, but an immutable view state prevents UI inconsistencies and threading problems.
Inside the ViewModel create a LiveData property to expose the state and initialize it:
class LoginViewModel : ViewModel() {
private val _state = MutableLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
init {
_state.value = LoginViewState()
}
}
To then emit a new state, use the copy function provided by Kotlin's Data class from anywhere inside the ViewModel:
_state.value = _state.value!!.copy(checking = true)
In the view, observe the state as you would any other LiveData and update the layout accordingly. In the View layer you can translate the state's properties to actual view visibilities and use resources with full access to the Context:
viewModel.state.observe(this, Observer {
it?.let {
userTextView.text = it.user
passwordTextView.text = it.password
checkingImageView.setImageResource(
if (it.checking) R.drawable.checking else R.drawable.waiting
)
}
})
Conflating multiple data sources
Since you probably previously exposed results and data from database or network calls in the ViewModel, you may use a MediatorLiveData to conflate these into the single state:
private val _state = MediatorLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
_state.addSource(databaseUserLiveData, { name ->
_state.value = _state.value!!.copy(user = name)
})
...
Data binding
Since a unified, immutable ViewState essentially breaks the notification mechanism of the Data binding library, we're using a mutable BindingState that extends BaseObservable to selectively notify the layout of changes. It provides a refresh function that receives the corresponding ViewState:
Update: Removed the if statements checking for changed values since the Data binding library already takes care of only rendering actually changed values. Thanks to #CarsonHolzheimer
class LoginBindingState : BaseObservable() {
#get:Bindable
var user = ""
private set(value) {
field = value
notifyPropertyChanged(BR.user)
}
#get:Bindable
var password = ""
private set(value) {
field = value
notifyPropertyChanged(BR.password)
}
#get:Bindable
var checkingResId = R.drawable.waiting
private set(value) {
field = value
notifyPropertyChanged(BR.checking)
}
fun refresh(state: AngryCatViewState) {
user = state.user
password = state.password
checking = if (it.checking) R.drawable.checking else R.drawable.waiting
}
}
Create a property in the observing view for the BindingState and call refresh from the Observer:
private val state = LoginBindingState()
...
viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = state
Then, use the state as any other variable in your layout:
<layout ...>
<data>
<variable name="state" type=".LoginBindingState"/>
</data>
...
<TextView
...
android:text="#{state.user}"/>
<TextView
...
android:text="#{state.password}"/>
<ImageView
...
app:imageResource="#{state.checkingResId}"/>
...
</layout>
Advanced info
Some of the boilerplate would definitely benefit from extension functions and Delegated properties like updating the ViewState and notifying changes in the BindingState.
If you want more info on state and status handling with Architecture Components using a "clean" architecture you may checkout Eiffel on GitHub.
It's a library I created specifically for handling immutable view states and data binding with ViewModel and LiveData as well as glueing it together with Android system operations and business use cases.
The documentation goes more in depth than what I'm able to provide here.
Android Unidirectional Data Flow (UDF) 2.0
Update 12/18/2019: Android Unidirectional Data Flow with LiveData — 2.0
I've designed a pattern based on the Unidirectional Data Flow using Kotlin with LiveData.
UDF 1.0
Check out the full Medium post or YouTube talk for an in-depth explanation.
Medium - Android Unidirectional Data Flow with LiveData
YouTube - Unidirectional Data Flow - Adam Hurwitz - Medellín Android Meetup
Code Overview
Step 1 of 6 — Define Models
ViewState.kt
// Immutable ViewState attributes.
data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)
// View sends to business logic.
sealed class ViewEvent {
data class ScreenLoad(...) : ViewEvent()
...
}
// Business logic sends to UI.
sealed class ViewEffect {
class UpdateAds : ViewEffect()
...
}
Step 2 of 6 — Pass events to ViewModel
Fragment.kt
private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEvent
private val _viewEvent = MutableLiveData<Event<ViewEvent>>()
override fun onCreate(savedInstanceState: Bundle?) {
...
if (savedInstanceState == null)
_viewEvent.value = Event(ScreenLoad(...))
}
override fun onResume() {
super.onResume()
viewEvent.observe(viewLifecycleOwner, EventObserver { event ->
contentViewModel.processEvent(event)
})
}
Step 3 of 6 — Process events
ViewModel.kt
val viewState: LiveData<ViewState> get() = _viewState
val viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffect
private val _viewState = MutableLiveData<ViewState>()
private val _viewEffect = MutableLiveData<Event<ViewEffect>>()
fun processEvent(event: ViewEvent) {
when (event) {
is ViewEvent.ScreenLoad -> {
// Populate view state based on network request response.
_viewState.value = ContentViewState(getMainFeed(...),...)
_viewEffect.value = Event(UpdateAds())
}
...
}
Step 4 of 6 — Manage Network Requests with LCE Pattern
LCE.kt
sealed class Lce<T> {
class Loading<T> : Lce<T>()
data class Content<T>(val packet: T) : Lce<T>()
data class Error<T>(val packet: T) : Lce<T>()
}
Result.kt
sealed class Result {
data class PagedListResult(
val pagedList: LiveData<PagedList<Content>>?,
val errorMessage: String): ContentResult()
...
}
Repository.kt
fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also { lce ->
lce.value = Lce.Loading()
/* Firestore request here. */.addOnCompleteListener {
// Save data.
lce.value = Lce.Content(ContentResult.PagedListResult(...))
}.addOnFailureListener {
lce.value = Lce.Error(ContentResult.PagedListResult(...))
}
}
Step 5 of 6 — Handle LCE States
ViewModel.kt
private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...)) {
lce -> when (lce) {
// SwitchMap must be observed for data to be emitted in ViewModel.
is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Error -> {
_viewEffect.value = Event(SnackBar(...))
Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
}
Step 6 of 6 — Observe State Change!
Fragment.kt
contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
adapter.submitList(contentList)
})
...
}

Categories

Resources