Kotlin Flow - Nullable value - android

Is it possible to check if Flow sends back a value and to act on it if it doesn't?
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD: Flow<UserResponse>? = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.collect {
userResponseDAO.updateUser(userResponse)
} ?: {
userResponseDAO.insertUser(userResponse)
}
}

Several remarks:
There are two types of queries - one-shot and streams. For your use-case you need one-shot query (to ge response or null once with searchUserByToken and once - to insert or update value). For one-shot queries in Room you can use suspend function with no Flow:
#Query("SELECT * FROM userResponse where id = :id")
suspend fun searchUserByToken(id: Int):UserResponse?
And your code with checking value would be:
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.let { userResponseDAO.updateUser(userResponse)}
?: userResponseDAO.insertUser(userResponse)
}
With Flow you get stream, that issues values whenever data is updated in DB. That's why in your use-case you can get values in loop: you get value value from searchUserByToken -> you update value -> you get new value since Room uses Flow and invokes searchUserByToken again -> you update value -> ...
If you use Room's #Insert(onConflict = OnConflictStrategy.REPLACE) you can not to check if userResponse is in DB and use just insertUser method (since if user is in db insert would cause it's update)

Related

What's the recommended way to update Jetpack Compose UI on Room database update?

Right now, my method of updating my jetpack compose UI on database update is like this:
My Room database holds Player instances (or whatever they're called). This is my PlayerDao:
#Dao
interface PlayerDao {
#Query("SELECT * FROM player")
fun getAll(): Flow<List<Player>>
#Insert
fun insert(player: Player)
#Insert
fun insertAll(vararg players: Player)
#Delete
fun delete(player: Player)
#Query("DELETE FROM player WHERE uid = :uid")
fun delete(uid: Int)
#Query("UPDATE player SET name=:newName where uid=:uid")
fun editName(uid: Int, newName: String)
}
And this is my Player Entity:
#Entity
data class Player(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "name") val name: String,
)
Lastly, this is my ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getDatabase(application)
val playerNames = mutableStateListOf<MutableState<String>>()
val playerIds = mutableStateListOf<MutableState<Int>>()
init {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().getAll().collect {
playerNames.clear()
playerIds.clear()
it.forEach { player ->
playerNames.add(mutableStateOf(player.name))
playerIds.add(mutableStateOf(player.uid))
}
}
}
}
fun addPlayer(name: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().insert(Player(name = name))
}
}
fun editPlayer(uid: Int, newName: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().editName(uid, newName)
}
}
}
As you can see, in my ViewHolder init block, I 'attach' a 'collector' (sorry for my lack of proper terminology) and basically whenever the database emits a new List<Player> from the Flow, I re-populate this playerNames list with new MutableStates of Strings and the playerIds list with MutableStates of Ints. I do this because then Jetpack Compose gets notified immediately when something changes. Is this really the only good way to go? What I'm trying to achieve is that whenever a change in the player table occurs, the list of players in the UI of the app gets updated immediately. And also, I would like to access the data about the players without always making new requests to the database. I would like to have a list of Players at my disposal at all times that I know is updated as soon as the database gets updated. How is this achieved in Android app production?
you can instead use live data. for eg -
val playerNames:Livedata<ListOf<Player>> = db.playerDao.getAll().asliveData
then you can set an observer like -
viewModel.playerNames.observe(this.viewLifecycleOwner){
//do stuff when value changes. the 'it' will be the changed list.
}
and if you have to have seperate lists, you could add a dao method for that and have two observers too. That might be way more efficient than having a single function and then seperating them into two different lists.
First of all, place a LiveData inside your data layer (usually ViewModel) like this
val playerNamesLiveData: LiveData<List<Player>>
get() = playerNamesMutableLiveData
private val playerNamesMutableLiveData = MutableLiveData<List<Player>>
So, now you can put your list of players to an observable place by using playerNamesLiveData.postValue(...).
The next step is to create an observer in your UI layer(fragment). The observer determines whether the information is posted to LiveData object and reacts the way you describe it.
private fun observeData() {
viewModel.playerNamesLiveData.observe(
viewLifecycleOwner,
{ // action you want your UI to perform }
)
}
And the last step is to call the observeData function before the actual data posting happens. I prefer doing this inside onViewCreated() callback.

Unlike Livedata, using Flow in Room query doesn't trigger a refresh when updating a table entry but works if I delete or insert an entry

When using Livedata as a return type for a select* query on a table in Room, then I observe on it, I get triggers if I update/insert/delete an entry in that table. However, when I tried using Kotlin Flow, I only get 2 triggers.
The first trigger gives a null value as the initial value of the stateflow is a null. The second trigger is the list of entries in the Room table.
If I perform an insert/delete action on the DB, I receive a trigger from the StateFlow.
However, If I update an entry, the Stateflow doesn't trigger.
N.B: The update operation works correctly on the DB. I checked using DB inspector.
Data class & DAO
#Entity
data class CartItem (
#PrimaryKey
val itemId: Int,
var itemQuantity: Int=1
)
#Dao
interface CartDao {
#Query("SELECT * FROM CartItem")
fun getAllItems(): Flow<List<CartItem>>
#Update
suspend fun changeQuantityInCart(cartItem:CartItem)
#Insert
suspend fun insert(item: CartItem)
#Delete
suspend fun delete(cartItem:CartItem)
}
ViewModel
val cartItems: StateFlow<List<CartItem>?> =
repo.fetchCartItems().stateIn(viewModelScope, SharingStarted.Lazily, null)
Fragment
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.cartItems.collect {
Log.e("Update","Update")
}
My pitfall was that I was updating the object like this:
currentItem.itemQuantity = currentItem.itemQuantity + 1
changeQuantity(currentItem)
(currentItem is an object of class CartItem which is received initially from the getAllItems Flow in the DAO.)
(changeQuantity fun calls the changeQuantityInCart fun in the DAO.
This caused the reference of the CartItem object in the StateFlow to hold the updated value of the object with the new itemQuantity value before calling the update on the DB.
After that, when calling the Update fun in the DAO, the DB entry is updated and the Flow value changes, but when putting it in the Stateflow no changes are detected. Thus, the stateflow doesn't trigger as it is how stateflows differ from livedata.
In the case of livedata, it will trigger regardless if the new value is the same or not.
Thus, to solve this bug do not change the value of the object in the stateFlow before calling a DB update operation like this:
val updatedCartItem = cartItem.copy(itemQuantity = cartItem.itemQuantity + 1)
changeQuantity(updatedCartItem)

Android Room - best way to retrieve a record to be updated

I have an app which has a session end routine. I want to update the session with the end date/time and potentially other information when the End Session button is clicked. I have a dao, a repository, and a ViewModel.
I thought the best way to do this would be to get the record, update the fields in the object, and save the object back to Room.
I don't exactly know the best way to go about this. Here is the code I am working with:
In the Dao:
#Query("SELECT * FROM Session WHERE id=:id")
Single<Session> getSessionById(int id);
In the repository:
public Single<Session> getSessionById(int sessionId) {
return sessionDao.getSessionById(sessionId);
}
In the ViewModel:
public void endSession () {
Single<Session> session = sessionRepository.getSessionById(sessionId);
//????? session.doOnSuccess()
//get current date/time
Date date = Calendar.getInstance().getTime();
//set the end date
session.setEndTime(date);
//update the session
sessionRepository.update(session);
}
I would love any advice on the range of options. I had started using a plain object, but got errors related to running the query on the main thread and wanted to avoid that. I don't need an observable/flowable object. I understand Async is to be deprecated in Android 11. So I thought Single would work.
Any advice on my choice to use Single or code would be really helpful.
UPDATE:
I just need a simple example in Java of pulling a single record from and the main part is the functionality in the ViewModel (what does the doOnSuccess look like and optionally on error as well).
If you just want to update without retrieving the whole record, writing a custom query is the option that I go with:
#Query("UPDATE table_name SET column_name = :value WHERE id = :id")
fun update(id: Int, value: String): Completable
In repository:
fun update(id: Int, value: String): Completable
In ViewModel or UseCase:
update().subscribeOn(...).observeOn(...).subscribe()
If you want to avoid using RxJava:
#Query("UPDATE table_name SET column_name = value WHERE id = :id")
fun update(id: Int, value: String): Boolean
And use Executors to run transactions on a background thread.
Executors.newSingleThreadExecutor().execute {
repository.update()
}
If you want to perform both retrieving and updating you can use #Update and #Query to retrieve the recorded inside your ViewModel or UseCase (interactor) and push the update toward Room.
RxJava Single Example:
#Query("SELECT * FROM table_name")
fun getAll(): Single<List<Model>>
Repository:
fun getAll(): Single<List<Model>> = modelDao.getAll()
ViewModel or UseCase:
val statusLiveData = MutableLive<Strig>()
modelRepository.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ statusLiveData.value = "Success" }, { statusLiveData.value = "Failed" })

Asynchronously iterate over all Room records only once

I would like to perform an asynchonous operation on each record in a large Room table.
I thought I could add a method returning Flow in my DAO like this:
#Query("SELECT * FROM events")
fun getEvents(): Flow<EventEntity>
But according to this blog post and this documentation page returning a Flow is making an "observable read" so the Flow never completes and it watches for database changes.
My goal is to iterate over all the entities only once. I don't want the "observability" behavior. Also, since the table is very large, I don't want to load all the records into a List at once in order to avoid consuming too much memory.
Could you recommend some solution, please?
Create a new method that does not use Flow.
#Query("SELECT id FROM events")
fun getAllIds(): List<Int> // If your primary key is Integer.
#Query("SELECT * FROM events WHERE id = :id")
fun getById(id: Int): EventEntity?
Use Kotlin coroutines to call this method on IO thread.
There could be several strategies to load one row at a time. This is the simplest - get all ids and load each item one at a time.
suspend fun getEvents() {
withContext(Dispatchers.IO) {
// Get entities from database on IO thread.
val ids = dao.getAllIds()
ids.forEach { id ->
val event = dao.getById(id)
}
}
}
Pagination based approach
This approach assumes that you have a column that stores timestamp (eg. created_at).
#Query("SELECT * from events WHERE created_at > :timestamp ORDER BY created_at LIMIT 10")
fun getAfter(timestamp: Long): List<EventEntity>
You can use this method to paginate.
suspend fun getEvents() {
withContext(Dispatchers.IO) {
var timestamp: Long = 0
while (true) {
// Get entities from database on IO thread.
val events = dao.getAfter(timestamp)
// Process this batch of events
// Get timestamp for pagination offset.
timestamp = events.maxBy { it.createAt }?.createAt ?: -1
if (timestamp == -1) {
// break the loop. This will be -1 only if the list of events are empty.
}
}
}
}

Why does livedata return stale data from Room

I have come across a problem that I am not being able to solve without implementing fragile hacks.
I have a table Users.
And I am observing it via LiveData.
Everytime I launch an update on that table , my observer invokes twice. Once with the old value , and then with the newly updated one.
To illustrate the problem I have created a small example I would share below
UserDao.kt
#Dao
interface UserDao {
//region Inserts
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
#Update
fun update(user:User)
#Query("select * from users ")
fun users(): LiveData<List<User>>
}
I observe the live data in my MainActivity.
observe(
database.usersDao().users()
){
Log.d("Here",it.name) // i first get the previous val then the new one
}
And this is how i am registering an update also in the MainActivity
GlobalScope.launch {
database.usersDao().update(
User(
102,
"John",
"asdas",
roleCsv = "aaa",
accessToken = AccessToken("asd", "asd", 0),
loggedIn = false
)
)
}
What transpires here is catastrophic for my system .
I get a user object that has a previous name , and then I get the updated "John"
the observe is just an extension method to easily register observers
fun <T : Any, L : LiveData<T>> LifecycleOwner.observe(liveData: L, body: (T) -> Unit) =
liveData.observe(this, Observer(body))
My question was is this by design ?. Can I do something such that only the final picture from the database invokes my observer?
I recommend observing the following liveData in your case:
Transformations.distinctUntilChanged(database.usersDao().users())
Source:
https://developer.android.com/reference/androidx/lifecycle/Transformations.html#distinctUntilChanged(androidx.lifecycle.LiveData%3CX%3E)
On the other note, hold the liveData reference inside androidx's viewModel.

Categories

Resources