I am working on refactoring an android application (I'm not the original author) which uses a pre-created sqlite database file received from the backend. It is done like this because my client's use case needs a local database in which one of the tables can have 1 million rows in some cases. It is a stock-taking app for a rugged device which needs to work offline which means that the device needs to store the entire database of all the various products that can be found for the given warehouse so that the workers can see a product's information after scanning it's barcode. Every day at the start of the work on a new project, the pre-created database gets acquired from the backend and is used for the remainder of the project for the rest of the day.
I use Room for the database and also use Hilt. Normally everything works fine. The problem arises when/if the client uses a functionality in which the app can re-download the pre-created database from the backend which means that the entire database file Room uses gets rewritten. To avoid having references to a database that no longer exists, I close the database by calling my closeDatabase() method which then later gets recreated. The database class is the following (I shortened it and changed names due to NDA reasons):
#Database(
entities = [
ItemTable::class
],
exportSchema = false,
version = 1
)
abstract class ProjectDatabase : RoomDatabase() {
abstract fun roomItemDao(): ItemDao
companion object {
#Volatile
private var INSTANCE: ProjectDatabase? = null
fun getDatabase(): ProjectDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
MyApplication.appContext,
ProjectDatabase::class.java,
getDbNameWithPath()
).createFromAsset(getDbNameWithPath())
.build()
INSTANCE = instance
instance
}
}
fun closeDatabase() {
INSTANCE?.close()
INSTANCE = null
}
private fun getDbNameWithPath(): String {
return MyApplication.appContext.filesDir.toString() +
File.separator + Constants.PROJECT_DATABASE_NAME
}
}
}
I also use a Hilt module for the database like this:
#Module
#InstallIn(SingletonComponent::class)
class ProjectDatabaseModule {
#Provides
fun provideProjectDatabase(): ProjectDatabase {
return ProjectDatabase.getDatabase()
}
#Provides
fun provideItemDao(
projectDatabase: ProjectDatabase
): ItemDao {
return projectDatabase.roomItemDao()
}
}
My problem is that when I set the INSTANCE to null, then the next call to getDatabase() creates a new instance, but all the references previously created by Hilt for the various classes still reference the old instance.
If I don't call INSTANCE = null then the database doesn't get reopened. If I don't close the database, then Room goes insane due to having its entire underlying database completely changed. Previously I always called getDatabase().xyz which worked but was kinda ugly, thus I started to use Hilt for it.
Is there a solution for this via Hilt? I'm afraid I'll have to go back to my old solution. Even if I change my scope to something else, the already existing classes will use the old reference.
Basically what I wish for is a call to ProjectDatabase.getDatabase.roomItemDao() every time I call a method of ItemDao.
I decided that the best solution in this case is to either ask the user to restart your app or do it programmatically according to your given use case.
Thanks to #MikeT for his reassuring comment.
Related
I made a screen like the current image.
Data such as A, B, C.. are currently being set by getting from the strings.xml resource file.
I am now going to use Room DB instead of strings.xml and I want to get these data from Room.
To do this, we need to pre-populate the Room with data.
In the sample code I found, the method called addCallback() was usually used.
like this :
#Database(entities = arrayOf(Data::class), version = 1)
abstract class DataDatabase : RoomDatabase() {
abstract fun dataDao(): DataDao
companion object {
#Volatile private var INSTANCE: DataDatabase? = null
fun getInstance(context: Context): DataDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Sample.db")
// prepopulate the database after onCreate was called
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// insert the data on the IO Thread
ioThread {
getInstance(context).dataDao().insertData(PREPOPULATE_DATA)
}
}
})
.build()
val PREPOPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
}
}
However, as you can see from the code, in the end, data (here, val PREPOPULATE_DATA) is being created again within the code. (In another code, db.execSQL() is used)
In this way, there is no difference from fetching data from resource file in the end.
Is there any good way?
Developer documentation uses assets and files.
However, it is said that it is not supported within In-memory Room databases.
In this case, I do not know what In-memory means, so I am not using it.
In this case, I do not know what In-memory means, so I am not using it.
In-Memory will be a database that is not persistent, that is the database is created using in memory rather than as a file, at some time it will be deleted. You probably do not want an in-memory database.
However, as you can see from the code, in the end, data (here, val PREPOPULATE_DATA) is being created again within the code. (In another code, db.execSQL() is used)
This is a common misconception when writing Apps as the onCreate method of an activity is often repeated when an App is running. With an SQLite database the database is created once in it's lifetime, which would be from the very first time the App is run until the database file is deleted. The database will otherwise remain (even between App version changes).
Is there any good way?
You basically have two options for a pre-populated database. They are
to add the data when/after the database is created, as in your example code (which is not a good example as explained below), or
to utilise a pre-packaged database, that is a database that is created outside of the App (typically using an SQlite tool such as DBeaver, Navicat for SQlite, SQLiteStudio, DB Browser for SQLite).
Option 1 -Adding data
If the data should only be added once then using the overridden onCreate method via the CallBack can be used. However, using functions/methods from the #Dao annotated class(es) should not be used. Instead only SupportSQLiteDatabase functions/methods should be used e.g. execSQL (hence why the SupportSQLiteDatabase is passed to onCreate).
This is because at that stage the database has just been created and all the underlying processing has not been completed.
You could protect against duplicating data quite easily by using INSERT OR IGNORE .... rather than INSERT ..... This will skip insertion if there is an applicable constraint violation (rule being broken). As such it relies upon such rules being in force.
The two most commonly used constraints are NOT NULL and UNIQUE, the latter implicitly for a primary key.
In your case if a Data object has just the 2 fields (columns in Database terminology) then, as Room requires a primary key, an implicit UNIQUE constraint applies (could be either column or a composite primary key across both). As such adding Data(1,"val") a second time would result in a constraint violation which would result in either
The row being deleted and another inserted (if INSERT OR REPLACE)
This further complicated by the value of autogenerate.
An exception due to the violation.
The insert being skipped if INSERT OR IGNORE were used.
This option could be suitable for a small amount of data but if over used can start to bloat the code and result in it's maintainability being compromised.
If INSERT or IGNORE were utilised (or alternative checks) then this could, at some additional overhead, even be undertaken in the Callback's onOpen method. This being called every time the database is opened.
Pre-packaged Database
If you have lots of initial data, then creating the database externally, including it as an asset (so it is part of the package that is deployed) and then using Room's .createFromAsset (or the rarer used .createFromFile) would be the way to go.
However, the downfall with this, is that Room expects such a database to comply with the schema that it determines and those expectations are very strict. As such just putting together a database without understanding the nuances of Room then it can be a nightmare.
e.g. SQLite's flexibility allows column types to be virtually anything (see How flexible/restricive are SQLite column types?). Room only allows column types of INTEGER, TEXT, REAL or BLOB. Anything else and the result is an exception with the Expected .... Found ... message.
However, the easy way around this is to let Room tell you what the schema it expects is. To do so you create the #Entity annotated classes (the tables), create the #Database annotated class, including the respective entities in the entities parameter and then compile. In Android Studio's Android View java(generated) will then be visible in the explorer. Within that there will be a class that is the same name as the #Database annotated class but suffixed with _Impl. Within this class there is a function/method createAllTables and it includes execSQL statements for all the tables (the room_master_table should be ignored as Room will always create that itself).
The database, once created and saved, should be copied into the assets folder and using .createFromAsset(????) will then result in the pre-packaged data being from the package to the appropriate local storage location.
I have an application with prefilled database (I use Room, Kotlin). Database class:
#Database(
entities = [FurnitureModel::class, ImageModel::class],
version = Database.VERSION,
exportSchema = true
)
abstract class Database : RoomDatabase() {
abstract val furnitureDao: FurnitureDao
abstract val imageDao: ImageDao
companion object {
const val NAME = "application-data"
const val VERSION = 3
#Volatile
private var instance: Database? = null
fun getInstance(context: Context): Database {
synchronized(this) {
var inst = instance
if (inst == null) {
inst = Room.databaseBuilder(
context.applicationContext,
Database::class.java,
"$NAME-local.db"
)
.fallbackToDestructiveMigration()
.createFromAsset("$NAME.db")
.build()
instance = inst
}
return inst
}
}
}
}
It worked fine. Once I decided to update some data inside database. No stucture changes, just adding and changing some data. For versions, I changed constant VERSION from 3 to 4. When I run the application, I've got empty database. As I understand, it is possible to get empty database with fallbackToDestructiveMigration in few situations. I could made some mistake somewhere in data. To get more details I decided to run application without preinstalled previous version. Clean install, if I can say like this. To do it, I wiped data for Virtual device in AVD Manager, removed build directories in application and app directories and run 'Invalidate cache/restart...' in Android Studio (I know that there is no need to do it). But the result is always the same: trying to install my application I always get error
Caused by: java.lang.IllegalStateException: A migration from 3 to 4 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
As I understand, the error must not be shown when you install the application for the first time. But it is shown. I tried to create new virtual device and run app there, but the error is still there.
What should I do to forse Android Studio to forget that the application was installed before?
Thank you.
UPD1: I don't need to keep any data, actually I want to delete all data from previous versions of application. But after wiping data it still works as if I didn't wipe it. This is the problem.
RESULT: I found that the problem was in prefilled database. Unfortunatedly I can't find that is exactly wrong with database. I disabled creating database from assets, run application so that Room created bew empty database, copied the database from emulator and compared each table with my database. No any difference. So to solve the problem I just moved all data in each table to the same table in new empty database (using DataGrid). After running the application new database the problem disapeared.
I'm new to android programming and want to try to learn best practices. My first app I'm building is a podcast app to display podcasts from an rss feed and play them. What I have so far is working, but I know I can make it work better.
I'm using a Room Database with a Repository pattern, which might be overkill because I probably don't need to persist the podcast list across app death if I'm just going to re-parse the feed on startup. In my repository class I'm calling my FetchRSS class to do the network call in the init{ } block which returns a List<Podcast>.
I know I'm not doing something right.
In my PodcastDao, I have to use #Insert(onConflict = OnConflictStrategy.REPLACE) because the database already exists and I get an SQL error 1555 regarding duplicate primary key ids. Logically, it'd be better to have a check to see if the entry to be added is already in the database, but I'm not sure how to go about doing that. Or, illogically, clear the database on app death, but then why bother with a database at all. Ideally, I'd like to have a swipe to update function(even if the RSS only updates at most twice a week), but I'm not sure how best to do that.
If anyone has any thoughts about improving this, or a good book for learning android, I'd be all ears.
Thank you so much to everyone who takes the time to look at this!
PodcastDao.kt
#Dao
interface PodcastDao {
#Query("SELECT * FROM podcast") // get everything from the database
fun getPodcasts(): LiveData<List<Podcast>>
#Query("SELECT * FROM podcast WHERE id=(:id)") // get the specific podcast
fun getPodcast(id: String): LiveData<Podcast?>
// #Insert(onConflict = OnConflictStrategy.REPLACE)
// fun addPodcasts(podcasts: LiveData<List<Podcast>>)
// this causes a build error with the generated PodcastDao.java file
// logcat error: Type of the parameter must be a class annotated with #Entity or a collection/array of it.
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addPodcast(podcast: Podcast)
}
PodcastRepository.kt
class PodcastRepository private constructor(context: Context) {
private lateinit var podcasts: List<Podcast>
init {
CoroutineScope(Dispatchers.Main).launch {
podcasts = FetchRSS().fetchRss() // executes on Dispatchers.IO and returns parsed rss List<Podcast>
// this seems silly to add them one at a time, especially since the list is rather large
for (pod in podcasts) {
addPodcast(pod)
}
//it seems a better choice to dump the full list into the database at once
//however I can't figure out how to put the List<Podcast> into a LiveData<List<Podcast>> object
//or maybe I'm misunderstanding something about LiveData<>
//addPodcasts(podcasts)
}
}
suspend fun addPodcast(podcast: Podcast){
withContext(Dispatchers.IO){
podcastDao.addPodcast(podcast)
}
// this needs to receive the LiveData<List<Podcast>>, or a List<Podcast> and cram it into LiveData<>?
// suspend fun addPodcasts(podcasts: LiveData<List<Podcast>>) {
// withContext(Dispatchers.IO){
// podcastDao.addPodcasts(podcasts)
// }
// }
}
fun addPodcasts(podcasts: LiveData<List<Podcast>>)
should be
fun addPodcasts(podcasts: <List<Podcast>>)
So, now you can call podcastDao.addPodcasts(podcasts) (where podcasts is of type List<Podcast>>) from inside your repository instead of inserting them one by one through a for loop.
You cannot insert a LiveData into Room, only objects marked with #Entity. You can, however, have a query return a LiveData with a List of those entities. You can also return just a List as well.
So currently I have a Dao with a function that emits a Flow<>
#Query("SELECT * FROM ${Constants.Redacted}")
fun loadAllContacts(): Flow<List<Redacted>>
I am calling this from a repository like so
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
I am injecting the repository into the viewModel's constructor, and then at the top of my viewModel I have a val like so
val contacts: LiveData<List<Redacted>> = contactRepository.loadAllContacts.asLiveData()
Which is being observed in my Activity like so
viewModel.contacts.observe(this) { contacts ->
viewModel.onContactsChange(contacts)
}
My thinking is that the Flow is converted to a LiveData, and then I can observe this LiveData from my activity and kick off this function to actually update the viewModel upon the data being updated.
For now onContactsChange just looks like
fun onContactsChange(list: List<Redacted>) {
Timber.i("VIEW UPDATE")
}
The problem is that I only see this Timber log upon opening the activity, and never again. I verified that data IS going into my database, and I verified that an insert occurred successfully while the activity & viewModel are open. But I never see the log from onContactsChange again. When I close the activity, and reopen it, I do see my new data, so that is another reason I know my insert is working correctly.
I would like to add that I am using a single instance (singleton) of my repository, and I think I can verify this by the fact that I can see my data at all, at least when the view is first made.
Figured it out:
Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process.
If your app runs in multiple processes, include enableMultiInstanceInvalidation() in your database builder invocation. That way, when you have an instance of AppDatabase in each process, you can invalidate the shared database file in one process, and this invalidation automatically propagates to the instances of AppDatabase within other processes.
It's a little bit hard to follow your question, but I think I see the overall problem with your Flow object not updating the way you want it too.
Following this quick tutorial, it seems that first you should declare your Flow object inside your Repository the same way you're already doing
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
and have your VM 'subscribe' to it by using the collect coroutine which would then allow you to dump all this data into a MutableLiveData State
data class YourState(..)
val state = MutableLiveData<YourState>()
init {
contactRepository.loadAllContacts().collect {
if (it.isNotEmpty()) {
state.postValue(YourState(
...
)
}
}
}
that your Activity/Fragment could then observe for changes
viewModel.state.observe(.. { state ->
// DO SOMETHING
})
P.S. The tutorial also mentions that because of how Dao's work, you might be getting updates for even the slightest of changes, but that you can use the distinctUntilChanged() Flow extension function to get more specific results.
I'am using dagger2 + retrofit + coroutines + firebaseRemoteConfig
I can't get totoName updated every time I change the remote configs in the Firebase console. The problem is my use of coroutines... can you help me? Thanks
#Provides
#Singleton
#Named("toto")
suspend fun provideToto(remoteConfig: FirebaseRemoteConfig): String {
var totoName = fetchToto(remoteConfig)
return totoName
}
suspend fun fetchToto(remoteConfig: FirebaseRemoteConfig): String {
var totoName = remoteConfig.getString("toto_name")
withContext(Dispatchers.IO) {
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
totoName = remoteConfig.getString("toto_name")
}
}
return totoName
}
I'm not sure that Dagger itself is configured to work with suspensions. Dagger works in pass. First, it asks for the object, if the object isn't ready it ask for object creation. There isn't any inbuilt functionality for awaiting for the suspension to complete. Moreover, dagger objects are lazy initialized Singletons hence you only get what you create. So you already saving memory. Plus firebase is initialized at the application level (preloaded). So you always get ready to use firebase instances. So no need to suspend for creation.
Of course that doesn't work. First of all if you use #Singleton there fill be only one fetch() for the entire app.
Next, I'm not really sure you may do that with dagger at all. Your methods should be located in the respective classes and not on some dependency graph.