I'm switching over to Room for my database logic, but I'm having a hard time finding the best solution for handling initialization.
Previously, my app launches into MainActivity, checks if the database is null, and if it is, opens SplashActivity to show a loading screen while it setups the database.
With Room, I'm trying to do something similar, or possibly just removing the SplashActivity and having empty views for the contents while it's loading. Although I would need to be able to tell if it's loading, or just has no contents.
Here is my current attempt at a solution, I have a flag initialized that defaults to true, if the callback hits onCreate, I set it to false and init the database. Once it has been setup, I set it true, and fire an event to notify the SplashActivity.
abstract class MyRoomDatabase : RoomDatabase() {
fun init() {
val gson = App.application.gson
val content = gson.fromJsonFile(MY_FILE, Content::class.java)
content.let {
contentDao().insertAll(it.values)
}
// load the other content
}
companion object {
#Volatile
private var INSTANCE: MyRoomDatabase? = null
fun getInstance(context: Context): MyRoomDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
fun buildDatabase(context: Context): MyRoomDatabase {
val database = Room.databaseBuilder(context, MyRoomDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries()
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Logger.d("Database onCreate!")
getInstance(context).initialized = false
Single.fromCallable {
getInstance(context).init()
Logger.e("Database now initialized -- firing event.")
getInstance(context).initialized = true
App.application.postBusEvent(SetupDatabaseEvent())
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
Logger.e("Database already initialized.")
}
}).build()
INSTANCE = database
return database
}
}
}
There are a lot of issues with this solution, such as the storage usage seems to sometimes spike. After init it could be 500KB, then restarting the app may make it jump to 6MB. Other than that, I also don't think it's very safe.
What would be a better way to initialize this database? I want to know when it's ready, and when I should block the user.
I also need an Object from my database right away to setup my MainActivity's view. A user can select an Object, I mark that as isSelected, and next time they enter the app, I want to be able to show that Object as the current selection.
With Room, I need to fetch the current Object in the background, which makes it harder for me to display it correctly right away.
Other than caching in SharedPreferences, I'd like to know a way to pre-fetch this.
Any suggestions would be greatly appreicated, thanks!
Related
I have an repository that contains an in-memory cache list inside a StateFlow. The problem is that whenever the user logs out and logs into another account, the old data from the previous user is still there.
object Repository {
private lateinit var remoteDataSource: RemoteDataSource
operator fun invoke(remoteDataSource: remoteDataSource) {
this.remoteDataSource = remoteDataSource
return this
}
private val myList = MutableStateFlow(listOf<myData>())
suspend fun getData(): Flow<List<myData>> =
withContext(Dispatchers.IO) {
if (myList.value.isEmpty()) {
val response = remoteDataSource.getData()
if (response != null) {
myList.value = response.map { it.toMyData() }
}
}
myList
}
suspend fun addData(newData: MyData) =
withContext(Dispatchers.IO) {
myList.value = myList.value.plus(newData)
remoteDataSource.addData(myData.toMyDataRequest())
}
}
This repository is used by multiple ViewModels. The list itself is only observed by one screen (let's call it myFragment), but other screens can add new elements to it. I've tried to clear the repository on myFragment's onDestroyView, but it clears the list whenever the user navigates away from myFragment (even when it's not a logout).
We could observe whenever the user logs out in an userRepository, but i don't know how to observe data in one repository from another repository (there's nothing like viewModelScope.launch to collect flows or something like that).
What approach can be used to solve this? And how would it clear the list?
i don't know how to observe data in one repository from another repository
I'd argue you shouldn't in this case.
You have a use-case: Logout.
When you invoke this use-case, you should perform al the necessary operations that your app requires. In this case, you should call your repository to let it know.
suspend fun clearData() =
withContext(Dispatchers.IO) {
// clear your data
}
I'd argue that you shouldn't hardcode the Dispatcher, since you'll likely test this at some point; in your tests you're going to use TestDispatcher or similar, and if you hardcode it, it will be harder to test. You write tests, right?
So now your use case..
class LogoutUseCase(repo: YourRepo) {
operator fun invoke() {
repo.clearData()
//do the logout
}
}
That's how I would think about this.
Your scope for all this is the UI that initiated the logout...
I hope to get the total of all records with Room database at once. But, normally Room use background thread to query record asynchronously.
If I use getTotalOfVoiceAsLiveData() in Code A, it will return LiveData<Long>, you know that LiveData variable is lazy, maybe the result is null.
If I use getTotalOfVoice() in Code A, I will get error because I can't use return in viewModelScope.launch{ }.
How can I get the total of all records at once with Room database?
Code A
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch {
return mDBVoiceRepository.getTotalOfVoice() //It will cause error
}
}
fun getTotalOfVoiceAsLiveData(): LiveData<Long>{
return mDBVoiceRepository.getTotalOfVoiceAsLiveData() //It's lazy, maybe the result is null.
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
fun getTotalOfVoiceAsLiveData() = mDBVoiceDao.getTotalOfVoiceAsLiveData()
}
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
suspend fun getTotalOfVoice(): Long
//When Room queries return LiveData, the queries are automatically run asynchronously on a background thread.
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoiceAsLiveData(): LiveData<Long>
}
Add content
To Tobi: Thanks!
Why it is important to you to get the data directly?
I need to generate a filename based the total of the records, such as "untitled0", "untitled1", "untitled2"...
If I can get the query result at once, I can use the following code easyly.
Added again
I hope to record a voice by filename based the total of query records when I click Start button. You know the total of records will change when a reocrd is added or deleted!
Code B
fun getTotalOfVoice():Long {
//Get the query result at once
...
}
fun createdFileanme(){
return "untitled"+getTotalOfVoice().toString()
}
btnStart.setOnClickListener{
recordVoice(createdFileanme()) //I will record voice by filename
}
fun addRecord(){
...
}
fun deleteRecord(){
...
}
New added content
Thanks!
I think 'You should also move all of that into the viewmodel class, without LiveData ' is good way, you can see Image A and How can I get the value of a LivaData<String> at once in Android Studio? .
Do you agree with it?
Image A
Question: at once meaning synchronous or what ? if yes, what happens if the function to get the result has to take a longer time? like network call? well you can decide to do that on another thread.
What I think is for you to use a mutable Object and use the postValue function to dispatch the result to the observers. It should look something like below:
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
private val voices = MutableLiveData<Long>()
fun getTotalOfVoiceAsLiveData(): LiveData<Long> {
voices.postValue(mDBVoiceRepository.getTotalOfVoiceAsLiveData().value)
return voices;
}
}
Making use of it in your Fragment will look like below:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(requireActivity())
viewModel.get(HomeViewModel::class.java).getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { voices: Long ? ->
voices // Sound of music ? be very free to use ...
})
}
}
Happy Coding.
I hope to get the result at once, but LiveData is lazy
Sorry to tell, but this is how the Room interface is designed.
You are right with the lazyness of the returned LiveData object. But this allows you to handle it on a different thread without having to manually handle different threads.
Based on your new information!
You basically have two options:
A) you could do the following:
load data from Room via LivaData
add observer that stores the current total amount
when the button is clicked you just read the local copy
In your View: (only one observer and one clickListener)
val totalVoiceCount: long
val viewModel = ViewModelProvider(requireActivity()).get(HomeViewModel::class.java)
viewModel.getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { totalOfVoice : Long ? ->
if (totalOfVoice != null)
totalVoiceCount = totalOfVoice
})
btnStart.setOnClickListener{
viewModel.recordVoice(totalVoiceCount)
}
In your ViewModel: (the logic and everything else)
fun recordVoice(totalVoiceCount : long){
val fileName = createdFileanme(totalVoiceCount)
// create your recording // depending on how you do this, it probably runs on a background thread anyways
}
fun createdFileName(totalVoiceCount : long){
return "untitled"+ String.valueOf(totalVoiceCount)
}
This works reliably because the LiveData has enough time to update the local copy of totalVoiceCount before the user has the chance to click the button.
B) Based on the answer in your parallel question you can of course outsource even more to a background thread. Then you also have the option to call the DAO query with a non-livedata return (as room returns non-livedata queries only on background threads). Is it worth to implement the threading suggestion of Ridcully? Not possible to answer without knowing what else is going on simultaneously... To me it seems like an overkill, but he is right that the more you do on background threads the better for your refresh rate..
You can return Deferred<Long> from viewModelScope.async. I recommend you to use:
val deferred = viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}
val value = deferred.await()
await() is suspend
Edit:
If you want to get a getter which will use in your activity or fragment
you need to write a suspend function like this:
suspend fun getTotalOfVoice(): Long {
return viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}.await()
}
But mvvm pattern allows you to create LiveData inside your ViewModel, which gives your fragment an observer.
In view model:
private val _totalOfVoiceLD: MutableLiveData<Long> = MutableLiveData()
val totalOfVoiceLD: LiveData<Long>
get() = _totalOfVoiceLD
fun updateTotalOfVoice() {
viewModelScope.launch(Dispatchers.IO) {
val totalOfVoice = mDBVoiceRepository.getTotalOfVoice()
_totalOfVoiceLD.postValue(totalOfVoice)
}
}
and in your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.totalOfVoiceLD.observe(viewLifecycleOwner, Observer { totalOfVoice ->
totalOfVoiceTextView.text = totalOfVoice.toString()
})
}
You can use coroutineContext.async to get data from DB and wait for getting it's response with data by using .await function for a async dispatch.
suspend fun getAllVoices() : Long{
val awatingResults = viewModelScope.async(Dispatchers.IO) {
mDBVoiceRepository.getTotalOfVoice()
}
val records = awatingResults.await()
return records
}
It is necessary to call a Suspend function from a coroutine and
async.await() is always called in a suspended function so,
val voiceLiveData: MutableLiveData<Long> = MutableLiveData()
fun getAllVoicesFromDB() {
viewModelScope.launch(Dispatchers.IO) {
voiceLiveData.postValue(mDBVoiceRepository.getTotalOfVoice())
}
}
Now call it where ever you want to get your voice data from database and also remember do your further work inside your voiceLiveData observer where you get your response of voices :)
Live data is designed to be lazy, when the value of the live data changes internally it emits and wherever you are observing it, the onChange function will be invoked. It is designed to fire and forget.
Because room uses background thread to run the query.
You can't expect live data to behave like sharedpreference where you store key value pair.
If you want to achieve something like that.
I would suggest you to use
Paper Db or Realm.
If you need your Room result synchronously, your code should be execute in IO thread. In case of coroutines, you can use Dispatchers.IO. Your code can be changed to this to pass the error.
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch(Dispatchers.IO) { // here
return mDBVoiceRepository.getTotalOfVoice()
}
}
}
If you must run the queries in the main thread, then:
Allow android room to execute queries in main thread.
val dbInstance = Room
.databaseBuilder(ctx, YourDBClass::class.java, "YourDBName")
.allowMainThreadQueries()
.build()
Define the dao method as follows
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoice(): Long
}
Access the method in the repository
fun getTotalOfVoice():Long {
return dao.getTotalOfVoice()
}
I am currently trying to get data out of my Room Database without using a ViewModel. This is because I am working on a NotificationHandler which can be triggered at any point by an Alarm Manager.
Below is my code so far. This code below starts with a call to sortNotification from another class. sortNotification then calls launchAsyncTask which inturn goes off to the database by calling getQuotesFromDatabase. I then wait for the results (I believe), assign the data from the database to listOfQuotes variable, then call displayNotification to use it. My issue is, listOfQuotes is always null when I am trying to use it displayNotification.
Now I know the database has content as when I open my application and go to an Activity which has a ViewModel, the data is retrieved successfully. I think my issue is likely to be with the async task not completing properly or with my coroutineScope. I just need listOfQuotes to have data when the code gets into displayNotification. Any help would be greatly appreciated. Thanks in advance.
private var job = Job()
private val ioScope = CoroutineScope(Dispatchers.IO + job)
private lateinit var listOfQuotes: LiveData<List<DefaultQuote>>
fun sortNotification() {
launchAsyncTask()
}
private fun launchAsyncTask() = CoroutineScope(Dispatchers.IO).launch {
val asyncTask = ioScope.async {
getQuotesFromDatabase()
}
listOfQuotes = asyncTask.await()
displayNotification()
}
private suspend fun getQuotesFromDatabase(): LiveData<List<DefaultQuote>> {
return withContext(Dispatchers.IO) {
val defaultQuoteDao = QuoteDatabase.getDatabase(context, this).defaultQuoteDao()
val defaultQuoteRepository = DefaultQuoteRepository(defaultQuoteDao)
defaultQuoteRepository.allQuotes
}
}
private fun displayNotification() {
val quote = listOfQuotes.value?.let {
val size = it.size
val randomIndex = (0..size).random()
it[randomIndex]
} ?: throw NullPointerException("Quotes not found")
// ... then do notification stuff
I have also added in the code from my DAO:
#Dao
interface DefaultQuoteDao {
#Query("SELECT * FROM $DEFAULT_TABLE_NAME")
fun getAllQuotes(): LiveData<List<DefaultQuote>>
}
And the code from my repository:
class DefaultQuoteRepository(private val defaultQuoteDao: DefaultQuoteDao) {
val allQuotes: LiveData<List<DefaultQuote>> = defaultQuoteDao.getAllQuotes()
}
And the code for QuoteDatabase.getDatabase(Context, CoroutineScope):
fun getDatabase(context: Context, scope: CoroutineScope): QuoteDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
QuoteDatabase::class.java,
DATABASE_NAME
)
.fallbackToDestructiveMigration()
.addCallback(QuoteDatabaseCallback(scope))
.build()
INSTANCE = instance
return instance
}
}
The specific problem is you are never observing the value of the listOfQuotes LiveData. This is required to initiate the fetch from the database.
Overall you're doing this in a strange way. You should use either coroutines or LiveData. Both of them allow you to observe data in the database, but you don't need both. That would be like wrapping an async call inside and async call then having to unwrap them both. You should either:
Remove coroutines and synchronously return the LiveData and observe it.
Use Flow to return Flow<List<DefaultQuote>> from your dao function getAllQuotes
I recommend 2. if you expect your application to become medium large or complex. Flow allows you to map or combine data in a more succinct and flexible manner.
Then, your function sortNotification would become:
// Ideally this should be injected into the class, but for a Service that's a little difficult.
// At a minimum you should initialize it once for the class
val defaultQuoteRepository: DefaultQuoteRepository by lazy {
DefaultQuoteRepository(QuoteDatabase.getDatabase(context, this).defaultQuoteDao())
}
fun sortNotification() {
defaultQuoteRepository.allQuotes
.map { listOfQuotes ->
listOfQuotes.random()
}
.flowOn(Dispatchers.IO)
.onEach { randomQuote ->
displayNotification(randomQuote)
}
// This makes the above onEach lambda run on the main thread so you're safe to show notifications.
// Ideally you should have a specific scope defined but tbh if you're not testing it's not that important
.launchIn(GlobalScope)
}
In my app I am trying to use MVVM with repositories databases and all that. I like to keep all my external dependencies and such separate and compartmentalized into their own files/modules so that they can easily be replaced or swapped out.
With Realm I could make this work really well by using unmanaged objects. I can have a RealmHelper class for example which just opens a realm instance, queries or performs some transaction and then closes the realm and returns an object.
So how can I accomplish something similar with managed objects? The problem is in this case that you have to know when to close the realm. The obvious solution here I think is to let the database know when you are done with it, but this seems like a tedious and unoptimized solution. Is there another better way?
So I have attempted to come up with a solution to this myself. I haven't tested it very well yet but my idea is basically to modify the LiveRealmResults file from the official example to let the caller (RealmHelper for example) know when it changes states between inactive and active. When it is active the caller will open the realm and pass in the results. When it changes to inactive the caller will close the realm. This is what my LiveRealmResults looks like:
#MainThread
class LiveRealmResults<T : RealmModel>(
private val getResults: () -> RealmResults<T>,
private val closeRealm: () -> Unit
) : LiveData<List<T>>() {
private var results: RealmResults<T>? = null
private val listener = OrderedRealmCollectionChangeListener<RealmResults<T>> {
results, _ ->
this#LiveRealmResults.value = results
}
override fun onActive() {
super.onActive()
results = getResults()
if (results?.isValid == true) {
results?.addChangeListener(listener)
}
if (results?.isLoaded == true) {
value = results
}
}
override fun onInactive() {
super.onInactive()
if (results?.isValid == true) {
results?.removeChangeListener(listener)
}
removeObserver()
}
}
It will be used like so:
class RealmHelper() {
fun getObjects(): LiveData<List<Objects>> {
var realm: Realm? = null
return LiveRealmResults<Objects>(getResults = {
realm = Realm.getDefaultInstance()
realm!!.where<Objects>().findAll()
}, removeObserver = {
realm?.close()
})
}
}
This method at least allows me to keep all realm logic in the RealmHelper, only exposing LiveData and not RealmResults. Whenever the LiveData is inactive the Realm is closed. In my example I'm returning RealmObject but I'm fine converting from RealmObject to normal object so I'm am not concerned with that part for this example.
I have a service that runs in background. It starts on device's boot and totally separated from activity.
If I want to use room, how should I use databaseBuilder ? Is it ok, to build it twice - for service and an app ? Will it build the same instance of database ?
You should provide a single instance of your database for the entire application. You can use a singleton to archive this, such as this:
#Database(entities = [YourEntity::class], version = 1)
abstract class YourRoomDatabase: RoomDatabase() {
abstract fun yourDao(): YourDao
companion object {
private var INSTANCE: YourRoomDatabase? = null
fun getInstance(context: Context): YourRoomDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context, YourRoomDatabase::class.java, "yourdb.db").build()
}
return INSTANCE!!
}
fun destroyInstance() {
INSTANCE = null
}
}
}
Then you can call it from wherever you like like this:
YourRoomDatabase.getInstance(context)
Room database builder will create database on its first run. After that, its job is to open existing (created) database. For example we can think of callback function on database open. That might be different from Activity to Activity. So, you can use builder as much as you need in the application. But you should maintain good practice of closing connections, statements and resultsets, etc... properly.
In my case, i need save my location to Rooom database from service, this service use call of type Coroutine, and inside coroutine one thread, my code:
call type coroutine inside oncreate in my service
GlobalScope.launch (Dispatchers.Main) {
onNewLocation() //call to metodo of type suspend
}
you should created method type suspend, becasuse is asyncronus call
suspend fun onNewLocation() {
//call Room database inside thread
val thread = Thread {
val db = myDatabase.getDataBase(this#LocationUpdatesService)
db.locationDao().addLocation(locationentity)
}
thread.start()
}