Android LiveData Transformation: Changing LiveData object value - android

I have one LiveData object that holds a list of Users and I am trying to transfer over the data to another LiveData object to be used elsewhere.
I am using MVVM with Room so I get LiveData from the database and on the ViewModel, I am trying to convert the User object in the LiveData to a Person object to show in the UI.
So I have one variable that is LiveData<List<User>>
class User(var firstName: String, var lastName: String, var age: Integer)
and I am trying to convert it to LiveData<List<Person>> (as an example)
class Person() {
lateinit var firstName: String
lateinit var age: Integer
}
and the way I am trying to change them is by using LiveData Transformations.map
ViewModel:
val list2: LiveData<List<User>> = repo.getAll()
var liveList: LiveData<ArrayList<Person>> = MutableLiveData()
liveList = Transformations.map(list2) { list ->
val newList: ArrayList<Person> = ArrayList()
list?.forEach {
val temp = Person()
temp.firstName = it.firstName
temp.age = it.age
newList.add(temp)
}
return#map newList
}
but when I run it, it crashes or doesn't update the UI.
Thanks!

The main problem with your code is that it uses var in var liveList: LiveData instead of using val.
You should declare the liveList variable like this:
val liveList = Transformations.map(list2) { list ->
...
}
Why?
Generally, a LiveData variable should always be declared with val. The reason is that the purpose of LiveData is to allow us to observe the up-to-date value held by the LiveData. We do it by code like this:
liveList.observe(this) { list ->
showList(list)
}
With this code, we ensure that the updated list is always shown. Whenever the list value which is held by liveList changes, the UI is updated as a result. But if the liveList itself also changes, the code will only observe the first LiveData of the liveList variable, and the UI will not be updated correctly.

val liveList = MutableLiveData(repo.getAll().value.orEmpty().map { user ->
Person(user.firstName, user.age)
})
This would be a more compact way, you could pull out the repo.getAll() call into its own variable if you like

Related

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)
}

Android, how to observe LiveData from within the same ViewModel

I'm a complete novice when it comes to LiveData and MVVM architecture. I'm trying to figure out how to observe a LiveData<List> in the ViewModel to update another variable depending on if it is empty or not.
I'm getting the LiveData from my Room database with this:
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
I would like for another variable in the ViewModel, private val empty: Boolean, to update anytime the list is returned empty (null). This will be used in updating an ImageView in the Fragment from Visible.GONE to Visible.VISIBLE.
How do I check if val meals is empty synchronously?
I've read around and saw some people said to useobserveForever, but the architecture guide explicitly advises against any observers in ViewModels.
I could probably observe the LiveData in the Fragment, but that would require business logic in the Fragment, ie:
viewModel.meals.observe(viewLifecycleOwner) {
if meals.value.isEmpty() imageView.visibility = View.VISIBLE else imageView.visibility = View.GONE
}
And I'd like to keep the Fragment as 'dumb' as possible, so I'd prefer to have that logic in the ViewModel. Is that possible?
You can check live data meal to see if it's empty or null and then trigger with your live data empty like this:
In the viewmodel, you create a livedata isEmptyMeals. This live data variable will always trigger when meals value change and will check if your meals value are empty or null.
MealsViewModel.kt
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
val isEmptyMeals = meals.map {
it.isNullOrEmpty()
}
}
And in the fragment, you will listen to observe the livedata isEmptyMeals and perform the logic to hide or show the image view you want.
Fragment.kt
viewModel.isEmptyMeals.observe(viewLifecycleOwner) {
imageView.visibility = if (it) View.VISIBLE else View.GONE
}
I don't know exactly how your code is set up but you can do something like below
Add variable to ViewModel
val empty = MutableLiveData<Boolean>()
In meals observer
viewModel.meals.observe(viewLifecycleOwner) {
viewModel.empty,postValue(meals.value.isEmpty())
Then observe from empty
Using MediatorLiveData
In your ViewModel class, create
val empty = MediatorLiveData<Boolean>()
Then
empty.addSource(meals) {
empty.value = it.isEmpty()
}

How does ViewModel cache LiveData?

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.

Kotlin: How to change values of MutableLiveData in ViewModel without using getters and setters

Here is my viewmodel:
class MyProfileEditSharedViewModel : ViewModel() {
val question = MutableLiveData<String>()
val answer = MutableLiveData<String>()
fun setQuestion (q: String) {
question.value = q
}
fun setAnswer (a: String) {
answer.value = a
}
}
I set the data using setQuestion and setAnswer like this:
viewModel.setQuestion(currentUserInList.question)
viewModel.setAnswer(currentUserInList.answer)
I try to get question and answer from the ViewModel like this:
val qnaQuestionData = communicationViewModel.question as String
val qnaAnswerData = communicationViewModel.answer as String
Compiler says I cannot cast MutableLiveData to string.
Should I make a separate getter like my setter? I heard that you don't need to use getters and setters in kotlin, is there anyway to edit val question and val answer in my viewmodel without using getters and setters?
Thank you!!
You can't cast it to String because the type of object is MutableLiveData, but you can access the value with .value property
val qnaQuestionData = communicationViewModel.question.value
val qnaAnswerData = communicationViewModel.answer.value
in this case, may facing errors about MutableLiveData initialization.
another way is observing the LiveData for changes:
communicationViewModel.question.observe(this, Observer{ data->
...
})
Or if you have not accessed to any lifecycle owner
communicationViewModel.question.observeForever(Observer{ data->
...
})
but please remember to remove the observer through removeObserver method
for setting the values it's better to use properties directly or binding way
communicationViewModel.question.postValue("some new value")
Or
communicationViewModel.question.value = "some new value"
Suggestion for MutableLiveData properties:
val question: MutableLiveData<String> by lazy { MutableLiveData<String>() }
val answer: MutableLiveData<String> by lazy { MutableLiveData<String>() }
https://developer.android.com/reference/android/arch/lifecycle/LiveData
Create some sort of getter method in your ViewModel
fun getQuestion(): LiveData<String> {
return question //this works because MutableLiveData is a subclass of LiveData
}
Then, you can observe the value in whatever class you care about the value. ie:
communicationsViewModel.getQuestion().observe(this, Observer {
//do something with the value which is 'it'. Maybe qnaQuestionData = it
}
Note if you're trying to observe the value from a fragment or something, you will have to change the parameter this, to viewLifecycleOwner

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