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()
}
Related
I am new to mvvm. So I want to ask how to retrieve the result of the method executed in viewModelScope.
In my example I want to save a book in database and retrieve the saved book. Is there a better way to do it?
fun addBook (book:Book): BookEntity {
var bookEntity = BookEntity()
viewModelScope.launch {
bookEntity = repository.addBook(book)
}
return bookEntity
}
The coroutine, launched in addBook method using launch builder, will be executed after the function returns, so bookEntity will no be reassigned with a new value from DB. You should think about how you want to use the data. If you want it to be used just as an input data for some another calculations then it make sense to make the addBook() function suspend:
suspend fun addBook(): BookEntity {
val bookEntity = repository.addBook(book) // I assume function repository.addBook(book) is suspend
return bookEntity
}
If you want it to be displayed in UI you can make it suspend like above and call it in a coroutine using lifecycleScope:
in Activity/Fragment
lifecycleScope.launch {
val book = viewModel.addBook()
// update UI
}
Another alternative is to apply reactive approach using LiveData or Kotlin Flow
I have a repository where a chain of network requests is calling. The repository is accessed from the interactor. And interactor is accessed from viewModel. The view model is attached to activity A. If I go to activity B, which has its own viewModel, then the request chain in the repository of activity A does not complete its execution.
Is it possible to make a repository whose life cycle will be equal to the life cycle of the application. I need all requests to complete even if I go to a new activity.
Please, help me.
This is covered in the Coroutines guide on developer.android.com
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope: CoroutineScope,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
// As we want to complete bookmarking the article even if the user moves
// away from the screen, the work is done creating a new coroutine
// from an external scope
suspend fun bookmarkArticle(article: Article) {
externalScope.launch(defaultDispatcher) {
articlesDataSource.bookmarkArticle(article)
}
.join() // Wait for the coroutine to complete
}
}
Here externalScope is defined like this (for a scope with application lifetime):
class MyApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
You should create a singleton Repository class, then access its instance anywhere you want.
object Repository {
val instance: Repository
get() {
return this
}
}
You can create create its object in ViewModel for A and create object in ViewModel for B.
Both will have same instance for Repository class, so in this way you can achieve what you need.
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)
}
I have a shared piece of code in a function, that should be accessed one at a time . I used mutex.lock/unlock to achieve this while working with coroutines.
public class TestAbc {
val mutex = Mutex()
suspend fun testfunction() {
mutex.lock()
arrayList.add("abc")
hashmap.put("abc", "efg")
mutex.unlock()
}
}
public class InitiatorClass{
val testAbc: TestAbc = TestAbc()
public fun startJob() {
GlobalScope.launch {
testAbc.testfunction()
}
}
}
I tested this by calling Start Job Function twice from a java class ,from different threads.
wanted only one coroutine to access critical-section at once, with help of mutex but its not working.I see multiple coroutines entering the lock.
My Bad, I was creating Multiple Instance of the Class InitiatorClass.
When i corrected it , everything works Fine .
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!