How Does Android LiveData get() syntax work? - android

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)

Related

which is recommended use getter or just equals in live data in view model android kotlin

I have view model and I use live data in encapsulation, which one is recommended to use and why?
private val _licenseStatusFromWebService = MutableLiveData<String?>()
val licenseStatusFromWebService: LiveData<String?> = _licenseStatusFromWebService
private val _licenseStatusFromWebService = MutableLiveData<String?>()
val licenseStatusFromWebService: LiveData<String?>
get() = _licenseStatusFromWebService
It Does not matter which way you use it as long as the MutableLiveData you are referring to is a val and not a var, but if you are going to modify or reassign the MutableLiveData to something else the getter approach get() = will return the latest instance and equals approach = will return the initial instance.
Also, Kotlin internally builds a getter for every property you have so if you are choosing the equals approach = for the sole purpose of reducing code on production, it will amount to nothing.
I think using an object directly is recommended way in ViewModel
private val _licenseStatusFromWebService = MutableLiveData<String?>()
val licenseStatusFromWebService: LiveData<String?> = _licenseStatusFromWebService
because, I am using this approach in some of my projects
It`s just to encapsulate mutable LiveData from immutable. As in UI you should use already prepared data from ViewModel to avoid modifying it from the UI directly.
private val _licenseStatusFromWebService = MutableLiveData<String?>()
val licenseStatusFromWebService: LiveData<String?> = _licenseStatusFromWebService

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

Boolean flows sync

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

Why do i get "useless cast" when casting MutableLiveData to LiveData?

I have an array of feedback channels because (outside of question scope) in my ViewModel.
Now, I don't want to expose my MutableLiveData to outside my Viewmodel.
So, i make a private list of LiveData objects, but compiler complains of "Useless Cast"
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels
get() = _feedbackChannels.map{
#Suppress("USELESS_CAST") // it is not useless as it no longer exposes the mutableLiveData
it as LiveData<*>
}
Why do I get USELESS_CAST warning?
Compiler doesn't realize you're doing it only to force implication of property type.
Just specify type explicitly and you'll be able to drop the cast entirely. You won't even have to use map, a simple toList() will do:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels : List<LiveData<FeedbackEvent>>
get() = _feedbackChannels.toList()
Clearly the compiler doesn't understand the point of the cast. In order to do this in a more explicit way and remove the costly map function, you can just upcast it like this:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: Array<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
Edit
If you wanted to expose a List specifically (avoid exposing a mutable array) then you should probably just create one in the first place:
private val _feedbackChannels = List(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: List<out LiveData<FeedbackEvent>>
get() = _feedbackChannels

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

Categories

Resources