I was following one of the UDACITYs Android Tutorial on LiveData/Room/Persistence and Repository Architecture.
After gluing the codes all together, I came across (what I believe, a very common issue) Type Mismatch exception.
On the course example, a VideosRepository was created with a member videos which is a LiveData:
class VideosRepository(private val database: VideosDatabase) {
/**
* A playlist of videos that can be shown on the screen.
*/
val videos: LiveData<List<Video>> =
Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
and in the Model, I have a introduce a MutableLiveData of _video
val playlist = videosRepository.videos //works fine
// added by me
private val _video = MutableLiveData<List<Video>>()
val video: LiveData<List<Video>> = _video
When I tried to access the LiveData, this is where I am getting the Type mismatch.
fun sample(){
_video.value = videosRepository.videos //does not work and throws a Type mismatch.
//Required: List<Video> Found: LiveData<List<Video>>
}
And if I try to just stuff all LiveData in the ViewModel (meaning, only the ViewModel will have the LiveData object declarations) and converting all LiveData to just plain List and a function such as
fun getVideos(): List<Video>{
return database.videoDao.getVideo()
}
I would then get Cannot access database on the main thread since it may potentially lock the UI for a long period of time. which I understand clearly. So if that is the case, then LiveData is the only way to do it.
But how can I get away from the Type mismatch.
PS. I understand concepts of OOP as well as Java, but never had the in-depth hands-on experience, so please bear with me.
Input of _video.value is a List<Video> but you assigned videosRepository.videos that is a LiveData<List<Video>>
You have to get List<Video> from LiveData :
_video.value = videosRepository.videos.value
videosRepository.videos's data type is LiveData<List<Video>> but _video.value's data type is List<Video>, so you can't assign like that.
Try:
val video: LiveData<List<Video>> = videosRepository.videos
Then in the view, observe the livedata to do what you want with the data, an example in Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewModel.video.observe(viewLifecycleOwner, Observer {
val data: List<Video> = it
// Do something with the data such as showing it...
})
}
If you really want to have a MutableLiveData in case of modifying it later, use MediatorLiveData:
private val _video = MediatorLiveData<List<Video>>().apply {
addSource(videosRepository.videos) {
value = it
}
}
val video: LiveData<List<Video>> = _video
Related
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)
}
Am learning android kotlin follow this:
https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers(it)
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Dont know how to write the fun loadUsers()
Here is my User:
class User {
constructor(name: String?) {
this.name = name
}
var name:String? = null
}
If dont use the keyword 'also' , i know how to do it.
But if use 'also' , it seems not work.
Here is how i try to write the fun loadUsers:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it = MutableLiveData<List<User>>(users)
}
Error tips near it : Val cant be ressigned
Part 1: According to the Kotlin documentation, also provides the object in question to the function block as a this parameter. So, every function call and property object you access is implied to refer to your MutableLiveData<List<User>>() object. also returns this from the function block when you are done.
Thus, another way of writing your MutableLiveData<> would be like this:
val users = MutableLiveData<List<User>>()
users.loadUsers()
Part 2: As far as how to implement loadUsers(), that is a separate issue (your question is not clear). You can use Retrofit + RxJava to load the data asynchronously, and that operation is totally outside of the realm of ViewModel or also.
Part 3: With your approach, you have conflicting things going on. Instead of doing a loadUsers() from your lazy {} operation, I would remove your lazy {} operation and create a MutableLiveData<> directly. Then, you can load users later on and update the users property any time new data is loaded. Here is a similar example I worked on a while ago. It uses state flows, but the idea is similar. Also use a data class to model the User instead of a regular class. Another example.
It is solved change to code:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it.value = users
}
it can't be reassigned , but it.value could .
I am learning android development and I decided to build a weather app using api that comes from service named open water map. Unfortunately I’ve got the following problem:
In order to get the weather data for wanted city, I first need to perform request to get the geographical coordinates. So what I need to do is to create one request, wait until it is finished, and after that do another request with data that has been received from the first one.
This is how my view model for location looks like:
class LocationViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
fun refresh() {
CoroutineScope(Dispatchers.IO).launch {
// call fetch location here in coroutine
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
}
And this is how my view model for weather looks like”
class WeatherSharedViewModel constructor(private val repository: WeatherRepository): ViewModel() {
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<List<SharedWeatherModel>>()
val weatherLoadError = MutableLiveData<Boolean>()
val loading = MutableLiveData<Boolean>()
fun refresh(lat: String, lon: String) {
loading.value = true
CoroutineScope(Dispatchers.IO).launch {
// call fetchWeather here in coroutine
}
loading.value = false
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
I am using both view models in a fragment in such way:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val weatherService = WeatherApi.getInstance()
val repository = WeatherRepository(weatherService)
locationViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(LocationViewModel::class.java)
weatherViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(WeatherSharedViewModel::class.java)
locationViewModel.refresh()
Log.d(TAG, "lat: ${locationViewModel.location.value?.get(0)?.get(0)?.lat.toString()}, lon: ${locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()}")
weatherViewModel.refresh(
locationViewModel.location.value?.get(0)?.get(0)?.lat.toString(),
locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()
)
val weatherList = view?.findViewById<RecyclerView>(R.id.currentWeatherList)
weatherList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentWeatherAdapter
}
val cityList = view?.findViewById<RecyclerView>(R.id.currentCityList)
cityList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentLocationAdapter
}
observerLocationViewModel()
observeWeatherViewModel()
}
So on a startup both models are refreshed, which means that requests are made. I was trying to somehow synchronize those calls but my last attempt ended that data passed to the refresh method of weather view model was null. So problem is that both coroutine are launched one after another, first one is not waiting for second.
The main question: is there any synchronisation mechanism in coroutines? That I can launch one coroutine and wait with launching second one as long as first is not finished?
You are violating the "Single Responsibilty Principle" you need to learn how to write CLEAN code. that is why you are running into such problems. A member of stackoverflow has explained it in depth: single responsibility
A few tips:
Your general design is somewhat convoluted because you are trying to update LiveData with coroutines, but one LiveData's exposed data is something determined by the other LiveData. This is theoretically OK if you need to be able to access the city even after you already have the weather for that city, but since you've split this behavior between two ViewModels, you end up having to manage that interaction externally with your Fragment, which is very messy. You cannot control it from a single coroutine unless you use the fragment's lifecycle scope, but then the fetch tasks restart if the screen rotates before they're done. So I would use a single ViewModel for this.
In a ViewModel, you should use viewModelScope for your coroutines instead of creating an ad hoc CoroutineScope that you never cancel. viewModelScope will automatically cancel your coroutines when the ViewModel goes out of scope.
Coroutines make it extremely easy to sequentially do background work. You just need to call suspend functions in sequence within a single coroutine. But to do that, once again, you really need a single ViewModel.
It's convoluted to have separate LiveDatas for the loading and error states. If you use a sealed class wrapper, it will be much simpler for the Fragment to treat the three possible states (loading, error, have data).
Putting this together gives the following. I don't really know what your repo is doing and how you convert Response<GeocodingModel> to List<GeocodingModel> (or why), so I am just using a placeholder function for that. Same for the weather.
sealed class WeatherState {
object Loading: WeatherState()
object Error: WetaherState()
data class LoadedData(val data: List<SharedWeatherModel>)
}
class WeatherViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<LoadedData>().apply {
value = WeatherState.Loading
}
fun refreshLocation() = viewModelScope.launch {
weather.value = WeatherState.Loading
val locationResponse = fetchLocation() //Response<GeocodingModel>
val locationList = unwrapLocation(location) //List<GeocodingModel>
location.value = locationList
val latitude = locationList.get(0).get(0).lat.toString()
val longitude = locationList.get(0).get(0).lon.toString()
try {
val weatherResponse = fetchWeather(latitude, longitude) //Response<SharedWeatherModel>
val weatherList = unwrapWeather(weatherResponse) //List<SharedWeatherModel>
weather.value = WeatherState.LoadedData(weatherList)
} catch (e: Exception) {
weather.value = WeatherState.Error
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
And in your Fragment you can observe either LiveData. The weather live data will always have one of the three states, so you have only one place where you can use a when statement to handle the three possible ways your UI should look.
Without referring to your actual code only to the question itself:
By default code inside coroutines is sequential.
scope.launch(Dispatcher.IO) {
val coordinates = repository.getCoordinates(place)
val forecast = repository.getForecast(coordinates)
}
Both getCoordinates(place) and getForecast(coordinates) are suspend functions since they're making network requests and waiting for the result.
getForecast(coordinates) won't execute until getCoordinates(place) is done and returned the coordinates.
I saw all of the following scenarios in different example projects from Google's Codelabs and other sources and do not fully understand where the values from the LiveData object are retrieved from.
Scenario 1 - Current Understanding:
According to https://developer.android.com/.../viewmodel one reason to use a ViewModel is to store/cache UI related data that I want to re-use after the corresponding UI has been rebuild after a configuration change.
Given the following simplified ViewModel and Repository: After updateName() is called the first time, the LiveData object of _currentName contains a String. If the UI is then rebuild after a screen rotation, the view that needs to display the current name requests it by observing currentName which in turn returns the value of the LiveData object that is contained in the field of the _currentName property. Am I correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this._currentName.value = this.repository.updateName()
}
}
Repository
class NamesRepository() {
fun updateName(): String {
val nextName: String
...
return nextName
}
}
Scenario 2:
What happens if the UI is rebuild after a screen rotation in the following case? _currentName in the ViewModel 'observes' currentName in the repository, but it still is a property and therefore stores its own LiveData object in its field. When the view then requests currentName from the ViewModel, the value is retrieved from the LiveData object that is contained in the field of the _currentName property in the ViewModel. Is this correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: LiveData<String?> = this.repository.currentName
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
Scenario 3:
In the following scenario, if the UI is rebuild and a view requests currentNam from the ViewModel, where is the requested value stored? My current understanding is, that currentName falls back to the field of the property _currentName in the repository. Isn't that against the idea of the ViewModel to store relevant UI data to be re-used after a configuration change? In the case below, it might be no problem to retrieve the value from the repository instead of the viewModel, but what if the repository itself retrieves the value directly from a LiveData object that comes from a Room database? Wouldn't a database access take place every time a view requests _currentName from the viewModel?
I hope somebody can clarify the situation more, in order to understand how to cache UI related data in the viewModel the correct way (or at least to understand what are the incorrect ways).
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
val currentName: LiveData<String?> get() = this.repository.currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
To answer your question scenario#1 is correct usage of LiveData.
Firstly, LiveData is not responsible for caching, it is just LifeCycleAware Observable, given that caching is done at ViewModel, when your activity recreates due to any configuration changes, android will try to retrieve the existing instance of ViewModel, if found then it's state and data are retained as is else it will create a new instance of ViewModel.
Second, using LiveData in repository is a bad idea at many levels, repository instances are held by ViewModel and LiveData are part of Android Framework which makes repositories rely on Android Framework thus creating problems in Unit Testing. Always use LiveData only in ViewModels.
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)
})
...
}