LiveData update does not cause Recompostion of my Composables - android

In my ViewModel I have a few LiveData objects like this one:
val invitations = MutableLiveData(listOf<Session>())
I have set up multiple Firestore Snapshot Listeners that should update my LiveData. Therefore I give the function that instantiates the listener a lambda like this:
repository.getInvitations {
invitations.value = it
Log.d("ONLINE", "Receiving Invitations.")
}
Here is the repository function:
override fun getInvitations(onDataUpdate: (List<Session>) -> Unit) {
val invitationsQuery = db.collection("invitations").whereEqualTo("player_id", auth.currentUser?.uid)
val listener = invitationsQuery.addSnapshotListener{ querySnapshot, exception ->
if (exception != null){
Log.w("ONLINE", "Error handling Snapshot",exception)
return#addSnapshotListener
}
val sessions = mutableListOf<Session>()
querySnapshot?.documents?.forEach{
sessions.add(it.toSession())
}
onDataUpdate(sessions)
}
listeners.add(listener)
}
In my Composable I observe the LiveData with observeAsState() from the viewModel
val invitations = viewModel.invitations.observeAsState()
In my UI I use the received state as usual with invitation.value.
That worked quite fine for a while. Before I had another normal firestore query running in the init block of my viewModel that updated some other LiveData. But since I did not needed that function anymore and deleted it, my UI is not updating anymore.
My instant suggestion is that the lambda callback in the snapshot listener is not updating my liveData correctly. Which I more or less proved wrong by adding a Text element under the rest of my UI where I also use the same state. That text somehow can detect the change in the LiveData.
My other UI code is quite complex but here is quick overview:
#Composable
fun Screen(viewModel: MyviewModel = koinViewModel()){
val invitations = viewModel.invitations.observeAsState()
MyUIElement(invitations.value)
Text("${invitations.value}")
}
If more of the UI code is needed I can give it.
I also tried asking ChatGPT about this. It mentioned that snapshot listeners are running in background threads and the LiveData can only detect data changes in the Main thread. But it did not seem to be helpful to just add a runBlocking(Dispatchers.Main) {} to update it in the Main thread.
Also this cant really be the problem for my UI to not update, because my Text is showing all my changes to the firestore data if I update it in the online console.
This was bothering me for a while now. I really dont want to refactor all my Code now. A quick fix would be appreciatd. :)

Related

Managing Data With Coroutines

In my android project I have tried to implement a shared View Model which does all the reading and writing data. I do this by using Mutable Live Data and my activity calls an update function within the View Model to update the Live Data. However I can't figure out how to get the data after it has been accessed. It seems that I am trying to update my UI before the data gets accessed. I have looked up this problem and it seems the solution has something to do with coroutines. I have not been successful implementing coroutines and I always get a null value for my data.
ViewModel :
private val firebaseDatabase: DatabaseReference = FirebaseDatabase.getInstance().reference
private val fAuth = FirebaseAuth.getInstance()
private val user: FirebaseUser = fAuth.currentUser!!
private var _saveLocation: MutableLiveData<LocationEvent> = MutableLiveData<LocationEvent>()
val saveLocation: LiveData<LocationEvent> get() = _saveLocation
fun loadData() {
firebaseDatabase.child("User").child(user.uid).child("SaveLocation").get()
.addOnSuccessListener {
_saveLocation.value = LocationEvent(
it.child("title").getValue<String>()!!,
it.child("organizer").getValue<String>()!!,
LatLng(
it.child("locationLatLng").child("latitude").value as Double,
it.child("locationLatLng").child("longitude").value as Double
),
it.child("address").getValue<String>()!!,
it.child("description").value as String,
it.child("allDay").value as Boolean,
it.child("sdate").getValue<Calendar>()!!,
it.child("edate").getValue<Calendar>()!!,
it.child("notifications").getValue<MutableList<Int>>()!!,
user.uid
)
}.addOnFailureListener {}
}
Activity function :
private fun loadSaveData() {
dataViewModel.loadData()
//using log statement just to see if any value
//Always get null
Log.d("MainFragment", "${dataViewModel.saveLocation.value}")
}
I did not include any attempt at coroutines above.
Question
How can I use coroutines to fix this problem?
If not coroutines than what?
(Side Question) : Why does casting to type Calendar cause a crash?
Any help whether its a solution or pointing me to a solution would be much appreciated.
Whenever you use code with names like "add listener" or "set listener" or ones with words like "fetch" or "async" in the name and take lambda parameters, you are calling an asynchronous function. This means the function returns before it finishes (and usually before it even starts) doing what you requested it to.
The purpose of the listener/callback/lambda function you pass to it is to do something sometime in the future, whenever the work eventually is completed. It could only be a few milliseconds in the future, but it absolutely will not happen until after your other code under the function call is complete.
In this case, your get() call to Firebase is synchronous, and you are adding a listener to it to tell it what to do with the results, when they eventually arrive. Then your flow of code continues on synchronously. Back in your loadSaveData() function, you are checking for the results, but the request and your listener have not been completed yet.
You don't need coroutines to get around this. Coroutines are a convenient syntax for dealing with code that normally uses callbacks, but regardless of whether you use coroutines, you need to understand what is going on. IO operations like what you're using cannot be done on the main thread, which is why they are done synchronously.
There's a lot more info about this in this StackOverflow question.

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

Kotlin: How to Observe Once a liveData in a viewModel ? Using ObserveForever() and removeObserver()

I'm learning Kotlin and I'm trying to use the same ViewModel for display a list of users and for edit of a user.
I'm using room so I have a "getPersonnelById() which needs to be Observed. The problem is that I would like to Observe only Once and I don't know how to do...
Here's my function
private fun retrievePersonnelData(id: Long){
if(id != -1L){
val observer = dataSource.getPersonnelById(id).observeForever{
newPersonnel.value = it
Timber.e("Valeur newPersonnel = ${newPersonnel.value}")
}
}
}
I've used as recommended a observeForever but I don't know how to use removeObserver in this case...
Thank you very much
If you need to get data once - consider using suspend functions in Room and get data by demand.
If you need to get a particular Personnel object and observe changes in DB of it, store value of getPersonnelById(id) in LiveData<Personnel> and observe it from Activity/Fragment
observeForever is mostly needed in testing purposes, you should better use observe function to not manually remove an observer every time.

Kotlin Coroutines Flow with Room and state handling

I'm trying out the new coroutine's flow, my goal is to make a simple repository that can fetch data from a web api and save it to db, also return a flow from the db.
I'm using room and firebase as the web api, now everything seems pretty straight forward until i try to pass errors coming from the api to the ui.
Since i get a flow from the database which only contains the data and no state, what is the correct approach to give it a state (like loading, content, error) by combining it with the web api result?
Some of the code i wrote:
The DAO:
#Query("SELECT * FROM users")
fun getUsers(): Flow<List<UserPojo>>
The Repository:
val users: Flow<List<UserPojo>> = userDao.getUsers()
The Api call:
override fun downloadUsers(filters: UserListFilters, onResult: (result: FailableWrapper<MutableList<UserApiPojo>>) -> Unit) {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
onResult.invoke(FailableWrapper(users.toMutableList(), null))
} catch (e: java.lang.Exception) {
onResult.invoke(FailableWrapper(null, "Error parsing data"))
}
}.addOnFailureListener {
onResult(FailableWrapper(null, it.localizedMessage))
}
}
I hope the question is clear enough
Thanks for the help
Edit: Since the question wasn't clear i'll try to clarify. My issue is that with the default flow emitted by room you only have the data, so if i were to subscribe to the flow i would only receive the data (eg. In this case i would only receive a list of users). What i need to achieve is some way to notify the state of the app, like loading or error. At the moment the only way i can think of is a "response" object that contains the state, but i can't seem to find a way to implement it.
Something like:
fun getUsers(): Flow<Lce<List<UserPojo>>>{
emit(Loading())
downloadFromApi()
if(downloadSuccessful)
return flowFromDatabase
else
emit(Error(throwable))
}
But the obvious issue i'm running into is that the flow from the database is of type Flow<List<UserPojo>>, i don't know how to "enrich it" with the state editing the flow, without losing the subscription from the database and without running a new network call every time the db is updated (by doing it in a map transformation).
Hope it's clearer
I believe this is more of an architecture question, but let me try to answer some of your questions first.
My issue is that with the default flow emitted by room you only have
the data, so if i were to subscribe to the flow i would only receive
the data
If there is an error with the Flow returned by Room, you can handle it via catch()
What i need to achieve is some way to notify the state of the app,
like loading or error.
I agree with you that having a State object is a good approach. In my mind, it is the ViewModel's responsibility to present the State object to the View. This State object should have a way to expose errors.
At the moment the only way i can think of is a "response" object that
contains the state, but i can't seem to find a way to implement it.
I have found that it is easier to have the State object that the ViewModel controls be responsible for errors instead of an object that bubbles up from the Service layer.
Now with these questions out of the way, let me try to propose one particular "solution" to your issue.
As you mention, it is common practice to have a Repository that handles retrieving data from multiple data sources. In this case, the Repository would take the DAO and an object that represents getting data from the network, let's call it Api. I am assuming that you are using FirebaseFirestore, so the class and method signature would look something like this:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo>
}
Now the question becomes how to turn a callback based API into a Flow. Luckily, we can use callbackFlow() for this. Then Api becomes:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo> = callbackFlow {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
offer(users.toMutableList())
} catch (e: java.lang.Exception) {
cancel(CancellationException("API Error", e))
}
}.addOnFailureListener {
cancel(CancellationException("Failure", e))
}
}
}
As you can see, callbackFlow allows us to cancel the flow when something goes wrong and have someone donwnstream handle the error.
Moving to the Repository we would now like to do something like:
val users: Flow<List<User>> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
There are a few caveats here. first() and concat() are operators you will have to come up with it seems. I did not see a version of first() that returns a Flow; it is a terminal operator (Rx used to have a version of first() that returned an Observable, Dan Lew uses it in this post). Flow.concat() does not seem to exist either. The goal of users is to return a Flow that emits the first value emitted by any of the source Flows. Also, note that I am mapping DAO users and Api users to a common User object.
We can now talk about the ViewModel. As I said before, the ViewModel should have something that holds State. This State should represent data, errors and loading states. One way that can be accomplished is with a data class.
data class State(val users: List<User>, val loading: Boolean, val serverError: Boolean)
Since we have access to the Repository the ViewModel can look like:
val state = repo.users.map {users -> State(users, false, false)}.catch {emit(State(emptyList(), false, true)}
Please keep in mind that this is a rough explanation to point you in a direction, there are many ways to accomplish state management and this is by no means a complete implementation. It may not even make sense to turn the API call into a Flow, for example.
The answer from Emmanuel is really close to answering what i need, i need some clarifications about some of it.
It may not even make sense to turn the API call into a Flow
You are totally right, in fact i only want to actually make it a coroutine, i don't really need it to be a flow.
If there is an error with the Flow returned by Room, you can handle it via catch()
Yes i discovered this after posting the question. But my problem is more something like:
I'd like to call a method, say "getData", this method should return the flow from db, start the network call to update the db (so that i'm going to be notified when it's done via the db flow) and somewhere in here, i would need to let the ui know if db or network errored, right?. Or should i maybe do a separate "getDbFlow" and "updateData" and get the errors separately for each one?
val users: Flow> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
This is a good idea, but i'd like to keep the db as the single source of truth, and never return to the ui any data directly from the network

Android - observing DB changes within ViewModel

I am using a lot of LiveData in my projects and it's great in those cases where I need to pass something to views since it's intention is to be observed by lifecycle owners (i.e. views).
But I wonder what should I use in those cases when I need to apply some logic in my view models every time when some data from DB changes?
I am familiar with Transformations (map and switch) but (if I am right) they are just a way to transform liveData objects, not a place where I can execute some viewmodel's logic.
If I understand correctly, observing LiveData in viewModels is bad practice.
What is an alternative? Some of the RxJava observable types? Or something else?
"they are just a way to transform liveData objects, not a place where I can execute some viewmodel's logic."
Yes you're right. It's because:
Functions in both map() and switchMap() run on the Main Thread,
so no long running operations should be done there!
But I don't think observing LiveData in ViewModel is bad practice, because in Android we have MediatorLiveData for this purpose. If you take a look at source code of map and switchMap function, you'll see they use MediatorLiveData in there.
So the only problem here is that if some logic you want to execute is a long running task, you must run it in background thread when observe changes from the source LiveData. You can use Rx or something to run it in background thread like below:
private val _downloadState = MutableLiveData<DownloadDataState>()
val downloadState: LiveData<DownloadDataState> = _downloadState
// version observe changes in downloadState
val version = MediatorLiveData<String>().apply {
addSource(downloadState) {
// Whenever value of downloadState changes, this block will run your logic
Single.just(dataRepository.fetchDataVersion())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
// Then set value to the observer
value = result
},
{ e: Throwable ->
e.printStackTrace()
}
)
}
}

Categories

Resources