I'm kind of new to Android development, but I'm taking over some project somebody more experimented than me did.
I'm making an application that retrieves data from the internet at startup and creates (or updates) a room database to store the information.
So far, it works. However, I noticed that when I push an update of the application, for some users, the database doesn't update anymore. They can reinstall the app, it doesn't change anything. The only thing that works is to clear the cache and restart the application. Then everything goes back to normal, when data are retrieved from the internet, they are properly inserted in the database. But the problem comes back with the next update.
I added the 'fallbackToDestructiveMigration()' option but it doesn't help, because it's not a migration of the database per se, as the structure doesn't change here.
Ideally, I'd like to preserve the data already present in the database.
I'm using Room 2.2.5 and API 28.
I'm not sure why updating the app results in the database not updating anymore. Maybe the new version of the app creates another database and populates this one, but the app is still using the old one (not updated anymore).
The Storage Module:
val storageModule = module {
single {
Room.databaseBuilder(androidContext(), LessonDatabase::class.java, "MyDB")
.fallbackToDestructiveMigration().build()
}
single {
get<LessonDatabase>().lessonDao()
}
}
The LessonDatase:
#Database(entities = [Lesson::class], version = BuildConfig.DATABASE_VERSION, exportSchema = false)
abstract class LessonDatabase : RoomDatabase() {
abstract fun lessonDao(): LessonDao
}
The LessonDao:
#Dao
interface LessonDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertLesson(lesson: Lesson)
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertLessons(lessons: List<Lesson>)
#Update
fun updateLesson(lesson: Lesson)
#Delete
fun deleteLesson(lesson: Lesson)
#Query("DELETE FROM Lesson")
fun clearLessons()
#Query("SELECT * FROM Lesson WHERE id == :id")
fun getLessonById(id: String): Lesson
#Query("SELECT * FROM Lesson ORDER BY creation_date DESC")
fun getLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE favorite = 1")
fun getFavoriteLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE difficulty LIKE :level")
fun getLessonsByDifficulty(level: Int): List<Lesson>
}
And the code for the application startup:
class SplashscreenViewModel(
private val repository: LessonRepository,
private val lessonDao: LessonDao,
val context: Context
) : BaseViewModel() {
val nextScreenLiveData = MutableLiveData<Boolean>()
override fun start() {
ioScope.launch {
val lessons = repository.getLessons().filter {
it.site.contains("website")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
nextScreenLiveData.postValue(true)
}
}
override fun stop() {
}
}
A question I have is, if I update the application, I guess Room.databaseBuilder will be called again. But what happens if the name of the database is the same as the previous one? Will it retrieve the old one, or create a new one? Overwrite the old one?
Another question I have, in the Insert query, it says onConflictStrategy.IGNORE. But as I pass a list as parameters, what happens if some of the entries are already in the database and some not? Will it ignore all of them? Just the already existing ones?
Thank you.
Edit: I installed Android-Debug-Database (https://github.com/amitshekhariitbhu/Android-Debug-Database) and it seems the database is fine actually. The only problem is that when I update the app, the new entries I insert are returned at the end of the SELECT * FROM table query. So I tried to sort them by Id, and it seems to work.
Related
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.
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.
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" })
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.
I'm using room in my new Android app, and I'm trying to update an a model object's property at runtime, but it doesn't seem to get saved.
Entity
#Entity(tableName = "sessions")
data class Session(#ColumnInfo(name="id") #PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name="date") var date: Date,
#ColumnInfo(name="repetitions_completed") var repetitionsCompleted: Int,
#ColumnInfo(name="squeeze_time_per_rep") var squeezeTimePerRep: Int,
#ColumnInfo(name="finishing_repetitions_completed") var finishingRepetitionsCompleted: Int,
#ColumnInfo(name="finishing_squeeze_time_per_rep") var finishingSqueezeTimePerRep: Int)
In my app I'm using it like this
session = Session(0,date,0,slowSeconds,0,quickSeconds)
sessionDao.insertAll(sessionDB)
...
session.repetitionsCompleted = totalSlowReps
session.finishingRepetitionsCompleted = totalQuickReps
sessionDao.updateSessions(session)
The problem is that session's property values like repetitionsCompleted and finishingRepetitionsCompleted doesn't seem to be saved, and it always remain 0 when I restart the app. updateSessions also returns 0
Here is the code for the Dao
#Dao
interface SessionDao {
// https://developer.android.com/training/data-storage/room/accessing-data#query-params
#Query("SELECT * FROM sessions")
fun getAll(): List<Session>
#Query("SELECT * FROM sessions WHERE date BETWEEN :from AND :to")
fun getSessionsBetween(from: Date, to: Date): List<Session>
#Query("SELECT * FROM sessions ORDER BY date LIMIT 1")
fun getOldestSession(): List<Session>
#Update
fun updateSessions(vararg sessions: Session) : Int
#Insert
fun insertAll(vararg sessions: Session)
}
What's weirder is that update seems to work in my unit test, but not in the actual app code.
I don't know if this affects things, but I'm running this in a service.
I figured it out, the problem was that when I created the session object, object is returned with an id set to 0 rather than its autoGenerate primary key, so what I did was alter the insert to return it's inserted id
#Insert
fun insertAll(vararg sessions: Session) : List<Long>
When I create my session, I manually reassign its ID
sessionDB = Session(0,date,0,slowSeconds,0,quickSeconds)
val insertID = sessionDao.insertAll(sessionDB).first().toInt()
sessionDB.id = insertID
maybe it sounds a little stupid but it fixed my issue. I guess all of you are familiar with the OnConflictStrategy. it's making sense to set strategy for insert. but google need this strategy for #Update as well as the default strategy is ABORT.
#return How to handle conflicts. Defaults to {#link OnConflictStrategy#ABORT}.
so the solution would be.
#Update(onConflict = OnConflictStrategy.REPLACE)
Maybe you're not sending the class id.
the value #PrimaryKey of the #Entity Class