How to chain transformations in Android when using live data? - android

Given the following setup:
I have 2 repositories: Repository A and Repository B both of them return live data.
I have a ViewModel that uses both of these repositories.
I want to extract something from Repository A and depending on the result I want to grab something from Repository B and then transform the result before returning to UI.
For this I have been looking at the LiveData Transformation classes. The examples show a single transformation of the result however I want something along the lines of chaining two transformations. How can I accomplish this?
I have tried setting something up like this but get a type mismatch on the second transformation block:
internal val launchStatus: LiveData<String> = Transformations
.map(respositoryA.getData(), { data ->
if (data.isValid){
"stringA"
} else {
//This gives a type mismatch for the entire block
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
(Also please let me know if there is an alternate/recommended approach for grabbing something for chaining these call i.e. grab something from A and then grab something from B depending on result of A and so on)

Your lambda sometimes returns the String "stringA", and sometimes returns the LiveData<String> given by:
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
This means that your lambda doesn't make sense - it returns different things in different branches.
As others have mentioned, you could write your own MediatorLiveData instead of using the one given by Transformations. However, I think it's easier to do the following:
internal val launchStatus: LiveData<String> = Transformations
.switchMap(respositoryA.getData(), { data ->
if (data.isValid) {
MutableLiveData().apply { setValue("stringA") }
} else {
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
All I've done is made the first code branch also return a LiveData<String>, so now your lambda makes sense - it's a (String) -> LiveData<String>. I had to make one other change: use switchMap instead of map. This is because map takes a lambda (X) -> Y, but switchMap takes a lambda (X) -> LiveData<Y>.

I used MediatorLiveData to solve this problem.
MediatorLiveData can observer other LiveData objects and react to them.
Instead of observing either of the repositories. I created myData (instance of MediatorLiveData) in my ViewModel class and have my view observe this object. Then I add Repository A as the initial source and observe that and only add Repository B if the result of A requires it. This allows me to keep the transformations associated with the live data of each of the repo and still process each result in the correct order. See below for implementation:
internal val myData: MediatorLiveData<String> = MediatorLiveData()
private val repoA: LiveData<String> = Transformations.map(
respositoryA.getData(), { data ->
if (data.isValid) "stringA" else ""
})
private val repoB: LiveData<String> = Transformations.map(
repositoryB.getData(), { data -> "stringB"
})
fun start() {
myData.addSource(repoA, {
if (it == "stringA") {
myData.value = it
} else {
myData.addSource(repoB, {
myData.value = it
})
}
})
}
Note: The solution does not cover the case where repoB might be added multiple times but it should be simple enough to handle.

I would try using switchMap instead of map:
Similar to map(), applies a function to the value stored in the LiveData object and unwraps and dispatches the result downstream. The function passed to switchMap() must return a LiveData object.

You can nest transformations.
val finalLiveData = Transformations.switchMap(liveData1){
val search = it
Transformations.switchMap(liveData2) {
db(context).dao().all(search, it)
}
}

You can transform the data by using switchmap. Here's an example documentation.

Related

Kotlin - combining flows

In my viewModel I have:
private val setEntitiesList = mutableStateListOf<Exercise>()
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesList.asFlow()) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
In my fragment I have:
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED){
addTrainingViewModel.exercises.collectLatest {
exercisesAdapter.submitList(it)
}
}
}
It doesnt work. Nothing is collected in fragment
If I change in fragment to collect "addTrainingViewModel.exercisesFromDB" it works - values are emited and collected.
What I would like to achieve: new list of exercises is emitted when setEntitiesList or exercisesFromDB are changed and I am able to do sth with a list of exercises before it is emitted
I assume that setEntitiesList variable is of type Iterable. In that case when you convert it to Flow like setEntitiesList.asFlow() you create a cold Flow, it means that when you add new elements to the setEntitiesList iterable they won't be emitted to the combined Flow.
On the other hand, by calling addTrainingViewModel.exercisesFromDB you get a hot Flow, so when you tested it separately you got the right result.
So you can't use a converted iterable object setEntitiesList as hot Flow. You need somehow to rewrite your logic and use a hot Flow instead of cold Flow. You can use MutableSharedFlow for that. The code will be something like the following:
private val setEntitiesListFlow = MutableSharedFlow<Exercise>(extraBufferCapacity = 64)
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesListFlow) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
// somewhere else in your code emitting Exercise:
setEntitiesListFlow.tryEmit(Exercise(...))

Can a method parameter contain a reference to other variable instead of containing a value?

In the code below, i'd like to generalize it so I instead of viewBinding.editText.text and viewModel.property.price can use the same method for e.g viewBinding.secondEditText.text and viewModel.property.income.
I'm thinking exchanging viewBinding.editText.text for a variable defined in the primary constructor, but then I'd need the variable to contain a reference to viewBinding.editText.text/viewBinding.secondEditText.text etc. instead of containing a value.
Is this possible? I've looked at lengths for this but can't find anything useful.
fun updateProperty() {
//... other irrelevant code
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
//... other irrelevant code
}
You can pass parameters into a function, yeah!
This is the easy one:
fun updateProperty(editText: EditText) {
val contents = editText.text.toString()
}
simple enough, you just pass in whatever instance of an EditText and the function does something with it.
If you're just using objects with setters and getters, you can just define the type you're going to be using and pass them in. Depending on what viewmodel.property is, you might be able to pass that in as well, and access price and income on it. Maybe use an interface or a sealed class if there are other types you want to use - they need some commonality if you're going to be using a generalised function that works with them all.
Properties are a bit tricker - assuming viewmodel.property contains a var price: Double, and you didn't want to pass in property itself, just a Double that exists somewhere, you can do it like this:
import kotlin.reflect.KMutableProperty0
var wow: Double = 1.2
fun main() {
println(wow)
setVar(::wow, 6.9)
println(wow)
}
fun setVar(variable: KMutableProperty0<Double>, value: Double) {
variable.set(value)
}
>> 1.2
>> 6.9
(see Property references if you're not familiar with the :: syntax)
KMutableProperty0 represents a reference to a mutable property (a var) which doesn't have any receivers - just a basic var. And don't worry about the reflect import, this is basic reflection stuff like function references, it's part of the base Kotlin install
Yes, method parameters can also be references to classes or interfaces. And method parameters can also be references to other methods/functions/lambdas.
If you are dealing with cases that are hard to generalize, consider using some kind of inversion of control (function as parameter or lambda).
You add a lambda parameter to your updateProperty function
fun updateProperty(onUpdate: (viewBinding: YourViewBindingType, viewModel: YourViewModelType) -> Unit) {
//... other irrelevant code
// here you just call the lambda, with any parameters that might be useful 'on the other side'
onUpdate(viewBinding, viewModel)
//... other irrelevant code
}
Elsewhere in code - case 1:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
Elsewhere in code - case 2:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Elsewhere in code - case 3:
updateProperty() { viewBinding, viewModel ->
// I am a totally different case, because I have to update two properties at once!
viewModel.property.somethingElse1 = viewBinding.thirdEditText.text.toString().toBoolean()
viewModel.property.somethingElse2 = viewBinding.fourthEditText.text
.toString().replaceAll("[- ]*", "").toInt()
}
You could then go even further and define a function for the first 2 cases, since those 2 can be generalized, and then call it inside the lambda (or even pass it as the lambda), which would save you some amount of code, if you call updateProperty() in many places in your code or simply define a simple function for each of them, and call that instead, like this
fun updatePrice() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
fun updateIncome() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Then elsewhere in code you just call it in a really simple way
updatePrice()
updateIncome()

Why is Livedata setValue ignored when called twice?

I have the following ViewModel with MutableLiveData data and another LiveData ones that is derived from data in a way that it updates its value only if the data.number is equal to 1.
class DummyViewModel : ViewModel() {
private val data = MutableLiveData<Dummy>()
val ones = data.mapNotNull { it.takeIf { it.number == 1 } }
init {
data.value = Dummy(1, "Init")
doSomething()
}
fun doSomething() {
data.value = Dummy(2, "Do something")
}
}
data class Dummy(val number: Int, val text: String)
fun <T, Y> LiveData<T>.mapNotNull(mapper: (T) -> Y?): LiveData<Y> {
val mediator = MediatorLiveData<Y>()
mediator.addSource(this) { item ->
val mapped = mapper(item)
if (mapped != null) {
mediator.value = mapped
}
}
return mediator
}
I observe ones in my fragment. However, If I execute doSomething, I don't receive any updates in my fragment. If I don't execute doSomething, the dummy Init is correctly present in ones and I receive an update.
What is happening here? Why is ones empty and how can I overcome this issue?
Maybe I'm missing something, but the behavior seems like expected to me...
Lets' try to reproduce both cases sequentially.
Without doSomething() :
Create Livedata
Add Dummy(1, "Init")
Start listening in the fragment: Because number is 1, it passes your filter and the fragment receives it
With doSomething():
Create Livedata
Add Dummy(1, "Init")
Add Dummy(2, "Do something") (LiveData keeps only the last value, so if nobody observes, the first value is getting lost)
Start listening in the fragment: Because number is 2, the value gets filtered and the fragment receives nothing
A little offtopic: it's always good to write tests for ViewModel cases like this, because you'll be able to isolate the problem and find the real reason quickly.
EDIT: also be aware that your filter is only working on observing, it isn't applied when putting the value into LiveData.

Why Transformation.switchMap(anyLiveData) isn't fire when i change the value of "anyLiveData"

I will hope that when i call to "addPlantToGarden()" passing respect "plantId" parameter then fire the "observers" "Transformations.switchMap(plantName)" but that doesn't happen, what is the error?
private val plantName: MutableLiveData<String> = MutableLiveData()
val plant: LiveData<Plant> = Transformations.switchMap(plantName){plantId ->
plantRepository.getPlant(plantId)
}
val isPlanted: LiveData<Boolean> = Transformations.switchMap(plantName){plantId ->
gardenPlantingRepository.isPlanted(plantId)
}
fun addPlantToGarden(plantId: String) {
plantName.value = plantId
}
These are a few things to consider:
1. Check your Repository
Make sure your plantRepository.getPlant(plantId) returns LiveData. Since methods from Repository are executed in background, I prefer encapsulate the function using this:
liveData {
// some async process (e.g. HTTP Request)
emit(/*your value*/)
}
Reference: https://developer.android.com/topic/libraries/architecture/coroutines#livedata
2. Check your Observer
Are you observing on a correct view lifecycle owner? If your ViewModel is inside a Fragment, make sure to do this:
viewModel.plant.observe(viewLifecycleOwner, Observer{
// action
})
instead of:
viewModel.plant.observe(this, Observer{
// action
})
And make sure to observe first before trying to change your plantName value.
3. Start with a simple case
I have no idea how you changed your plantName value. But try from a simple hardcoded/mock value first, for example:
plantName.value = "1"
then trace it through your Repository, then down to your Observer. Hopefully this will help you.

What is the difference between emit and emitSource with LiveData ? ( as in REAL time USE-CASE )

emit accepts the data class whereas emitSource accepts LiveData<T> ( T -> data ). Considering the following example :- I have two type of calls :-
suspend fun getData(): Data // returns directly data
and the other one ;
suspend fun getData(): LiveData<Data> // returns live data instead
For the first case i can use:-
liveData {
emit(LOADING)
emit(getData())
}
My question : Using the above method would solve my problem , WHY do we need emitSource(liveData) anyway ?
Any good use-case for using the emitSource method would make it clear !
As you mentioned, I don't think it solves anything in your stated problem, but I usually use it like this:
If I want to show cached data to the user from the db while I get fresh data from remote, with only emit it would look something like this:
liveData{
emit(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
emit(db.getData())
}
But with emitSource it looks like this:
liveData{
emitSource(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
}
Don't need to call emit again since the liveData already have a source.
From what I understand emit(someValue) is similar to myData.value = someValue whereas emitSource(someLiveValue) is similar to myData = someLiveValue. This means that you can use emit whenever you want to set a value once, but if you want to connect your live data to another live data value you use emit source. An example would be emitting live data from a call to room (using emitSource(someLiveData)) then performing a network query and emitting an error (using emit(someError)).
I found a real use-case which depicts the use of emitSource over emit which I have used many times in production now. :D The use-case:
Suppose u have some user data (User which has some fields like userId, userName ) returned by some ApiService.
The User Model:
data class User(var userId: String, var userName: String)
The userName is required by the view/activity to paint the UI. And the userId is used to make another API call which returns the UserData like profileImage , emailId.
The UserData Model:
data class UserData(var profileImage: String, var emailId: String)
This can be achieved internally using emitSource by wiring the two liveData in the ViewModel like:
User liveData -
val userLiveData: LiveData<User> = liveData {
emit(service.getUser())
}
UserData liveData -
val userDataLiveData: LiveData<UserData> = liveData {
emitSource(userLiveData.switchMap {
liveData {
emit(service.getUserData(it.userId))
}
})
}
So, in the activity / view one can ONLY call getUser() and the getUserData(userId) will be automatically triggered internally via switchMap.
You need not manually call the getUserData(id) by passing the id.
This is a simple example, imagine there is a chain of dependent-tasks which needs to be executed one after the other, each of which is observed in the activity. emitSource comes in handy
With emitSource() you can not only emit a single value, but attach your LiveData to another LiveData and start emitting from it. Anyway, each emit() or emitSource() call will remove the previously added source.
var someData = liveData {
val cachedData = dataRepository.getCachedData()
emit(cachedData)
val actualData = dataRepository.getData()
emitSource(actualData)
}
The activity that’s observing the someData object, will quickly receive the cached data on the device and update the UI. Then, the LiveData itself will take care of making the network request and replace the cached data with a new live stream of data, that will eventually trigger the Activity observer and update the UI with the updated info.
Source: Exploring new Coroutines and Lifecycle Architectural Components integration on Android
I will like share a example where we use "emit" and "emitsource" both to communicate from UI -> View Model -> Repository
Repository layer we use emit to send the values downstream :
suspend fun fetchNews(): Flow<Result<List<Article>>> {
val queryPath = QueryPath("tata", apikey = AppConstant.API_KEY)
return flow {
emit(
Result.success(
openNewsAPI.getResponse(
"everything",
queryPath.searchTitle,
queryPath.page,
queryPath.apikey
).articles
)
)
}.catch { exception ->
emit(Result.failure(RuntimeException(exception.message)));
}
}
ViewModel layer we use emitsource to pass the live data object to UI for subscriptions
val loader = MutableLiveData<Boolean>()
val newsListLiveData = liveData<Result<List<Article>>> {
loader.postValue(true)
emitSource(newRepo.fetchNews()
.onEach {
loader.postValue(false)
}
.asLiveData())
}
UI Layer - we observe the live data emitted by emitsource
viewModel.newsListLiveData.observe(viewLifecycleOwner, { result ->
val listArticle = result.getOrNull()
if (result.isSuccess && listArticle != null) {
setupList(binding.list, listArticle)
} else {
Toast.makeText(
appContext,
result.exceptionOrNull()?.message + "Error",
Toast.LENGTH_LONG
).show()
}
})
We convert Flow observable to LiveData in viewModel

Categories

Resources