Android Room Pre-populated Data not visible first time - android

Freshly installing the app, the view model doesn't bind the data.
Closing the app and opening it again shows the data on the screen.
Is there any problem with the pre-population of data or is the use of coroutine is not correct?
If I use Flow in place of LiveData, it collects the data on the go and works completely fine, but its a bit slow as it is emitting data in the stream.
Also, for testing, The data didn't load either LiveData/Flow.
Tried adding the EspressoIdlingResource and IdlingResourcesForDataBinding as given here
Room creation
#Provides
#Singleton
fun provideAppDatabase(
#ApplicationContext context: Context,
callback: AppDatabaseCallback
): AppDatabase {
return Room
.databaseBuilder(context, AppDatabase::class.java, "database_name")
.addCallback(callback)
.build()
AppDatabaseCallback.kt
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
val data = computePrepopulateData(assets_file_name)
data.forEach { user ->
dao.get().insert(user)
}
}
}
Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
#Query("SELECT * FROM $table_name")
suspend fun getAllUser(): List<User>
ViewModel
CoroutineScope(Dispatchers.IO).launch {
repository.getData().let {
listUser.postValue(it)
}
}
Attaching the data using BindingAdapter
app:list="#{viewModel.listUser}"

Your DAO returns suspend fun getAllUser(): List<User>, meaning it's a one time thing. So when the app starts the first time, the DB initialization is not complete, and you get an empty list because the DB is empty. Running the app the second time, the initialization is complete so you get the data.
How to fix it:
Switch getAllUser() to return a Flow:
// annotations omitted
fun getAllUser(): Flow<List<User>>
Switch insertUser to use a List
// annotations omitted
suspend fun insertUser(users: List<User>)
The reason for this change is reducing the number of times the Flow will emit. Every time the DB changes, the Flow will emit a new list. By inserting a List<User> instead of inserting a single User many times the (on the first run) Flow will emit twice (an empty list + the full list) compared to number of user times with a single insert.
Another way to solve this issue is to use a transaction + insert a single user.
I recommend you use viewModelScope inside the ViewModel to launch coroutines so it's properly canceled when the ViewModel is destroyed.

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)

Room insert lost when closing fragment and using ViewModelScope

I have followed the Android Room with a View tutorial but have changed it to a single-activity app with multiple fragments. I have a fragment for inserting records. The save button calls the viewmodel save method and then pops the backstack to return to the previous (list) fragment. Sometimes this works but often the insert does not occur. I assume that this is because once the fragment is destroyed, the ViewModelScope cancels any pending operations so if the insert has not already occured, it is lost.
Fragment:
private val wordViewModel: WordViewModel by viewModel
...
private fun saveAndClose() {
wordViewModel.save(word)
getSupportFragmentManager().popBackStack()
}
Viewmodel:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
Repository:
suspend fun insert(word: Word) {
wordDao.insert(word)
}
How do I fix this? Should I be using GlobalScope instead of ViewModelScope as I never want the insert to fail? If so, should this go in the fragment or the viewmodel?
One option is to add NonCancellable to the context for the insert:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO + NonCancellable) {
repository.insert(word)
}
The approach recommended and outlined in this post is to create your own application-level scope and run your non-cancellable operations in it.

Android LiveData: How to do an ArrayList<LiveData<RetrofitObject>>?

I am creating an app with MVVM architecture and I ran into an issue of getting a list of LiveData to show in the View.
In my ViewModel I have a getAll() function that retrieves a list of strings from the database using Room. From there, I get the strings and call my Retrofit function to send each string individually to a web-server that returns an object. Here is where my issue occurs.
From the MVVM tutorials I see online, they usually have the LiveData> style but in this since I am getting each object individually, it becomes List> but I don't think this is the correct way of doing it because in my View I would need to do a ForEach loop to observe each LiveData object in the list.
I have tried other work arounds but it doesn't seem to work. Is there a better way of doing this?
DAO
#Query("SELECT * FROM table")
fun getAll(): LiveData<List<String>>
Repository
fun getAll(): LiveData<List<String>> {
return dao.getAll()
}
fun getRetrofitObject(s: String): LiveData<RetrofitObject> {
api = jsonApi.getRetrofitObjectInfo(s, API_KEY)
val retrofitObject: MutableLiveData<RetrofitObject> = MutableLiveData()
api.enqueue(object : Callback<RetrofitObject> {
override fun onFailure(call: Call<RetrofitObject>?, t: Throwable?) {
Log.d("TEST", "Code: " + t.toString())
}
override fun onResponse(call: Call<RetrofitObject>?, response: Response<RetrofitObject>?) {
if (response!!.isSuccessful) {
retrofitObject.value = response.body()
}
}
})
return retrofitObject
}
MainActivityViewModel (ViewModel)
var objectList ArrayList<LiveData<retrofitObject>> = ArrayList()
// This is getting objects using Retrofit
fun getRetrofitObject(s: String): LiveData<retrofitObject> {
return repo.getRetrofitObject(s)
}
// This is getting all the strings from the internal database
fun getAll(): ArrayList<LiveData<retroFitObject>> {
repo.getAll().value?.forEach {it ->
objectList.add(getRetrofitObject(it)) //How else would I be able to do this?
}
return objectList
}
MainActivity (View)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityViewModel.getAll().forEach {
it.observe(this, Observer {it ->
mainActivityViewModel.objectList.add(it) //Here is part of the issue since I don't want to use a forloop in the View
})
}
adapter.objectList = mainActivityViewModel.objectList
recyclerView.adapter = adapter
}
Thanks, let me know if there is anything else needed or confusion!
By looking at the above code you are trying to fetch a list of item for each table row from server and and trying to update the result to your recycler view. Your logic is little confusing..
So on your activity .. first init your adapter and recyclerview
Then call your viewmodel function to get all values inside table and make a loop to call your network thread fuction in background and store the values in a live data object.
Just observe this livedata in your activity/fragment and just pass the list to adapter and notify it.by doing this,whenever your livedata got a change your recyclerview also reflect the items
The problem with your code is, you are called a retrofit network function with enque option and its a background thread process.so, code wont wait for the network completion. And it will return the retrofitObject data.but it has not got the data yet.so this will make error.
There might be other methods exist I don't know about them.
But you can deal with situation using Transformations for more information please look at documentation page.
Transformations.switchMap(LiveData trigger, Function> func)
You don't have to put the live data observer inside for loop.

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