I know this question has been asked a couple of times, but I have a funny situation here that I can't figure out.
I have a database with "TASKS" and, apart from the writing/updating methods, I'm calling the get method twice: one for getting the last created task, and another to select an specific task by ID
#Query("SELECT * FROM tasks_history_table ORDER BY taskId DESC LIMIT 1")
suspend fun getCurrentTask(): Task2?
#Query("SELECT * from tasks_history_table WHERE taskId = :key ")
suspend fun get(key: Long): Task2?
Then in the viewModel I'm launching a coroutine for each time I call one of these methods.
This one for the last created Task:
private fun initializeCurrentTask(){
viewModelScope.launch {
_currentTask.value = getCurrentTaskFromDatabase()!!
}
}
suspend fun getCurrentTaskFromDatabase(): Task2? {
var currentTask = database.getCurrentTask()
return currentTask
}
And this one for the specific task
fun initializeSelectedTask(){
viewModelScope.launch {
_currentTask.value = getSelectedTaskFromDatabase(selectedTaskId.value!!)!!
}
}
suspend fun getSelectedTaskFromDatabase(taskId: Long): Task2? {
var currentTask = database.get(selectedTaskId.value!!)!!
return currentTask
}
So they are both pretty much the same, except for the parameter Id passed.
Then, I'm sending that data to the Fragment to update the UI, via LiveData
private val _currentTask = MutableLiveData<Task2>()
val currentTask : LiveData<Task2>
get() = _currentTask
And here the observer:
timerViewModel.currentTask.observe(viewLifecycleOwner) {
updateUIText()
updateCountdownUI()
updateAnimation()
}
Everytime I call the function to get the las saved task, the observers are called and everything works fine. But whenever I call the function to get a specific task by Id, the observers are not called.
I've set Logs all around and I've reached the conclusion that the LiveData is getting updated, but the observer is not triggered.
Here's the repo in case someone can have it a look. Thanks!!
https://github.com/arieldipietro/PomodoroTechnique
In FragmentTimer and FragmentHistory, you have created viewModel instances using their respective fragments as owners, which makes them to observe liveData which are triggered by their instances only. So, now when you trigger a task from FragmentHistory it isn't get observed in FragmentTimer.
You need to use SharedViewModel for passing data between fragments, you have to create object of TimerViewModel using activity as its owner, then you would be able to observe from FragmentTimer. Pass requireActivity() as the owner in ViewModelProvider's constructor.
timerViewModel = ViewModelProvider(requireActivity(), viewModelFactory)[TimerViewModel::class.java]
You can read more about it in this codelab tutorial.
Related
In my ViewModel I have a function that is called from UI. It can be called many times per second.
Data comes from barcode scanner ViewModel. I'm passing it from one ViewModel to another thru UI for simplicity and to reuse barcode scanner ViewModel.
For simplicity lets assume that I have something like this:
// called from the fragment (that observes some property on the another viewmodel)
public fun processScannedBarcode(barcode : String){
// process data after necessaryData is ready (not null?)
val item = findItemByBarcode(barcode)
}
private var dataFromApi: ArrayList<Item>?
private fun loadNecessaryDataFromTheApi(){
viewModelScope.launch {
canReload = false
dataFromApi = myapi.getDataFromApi() // suspend fun inside
canReload = true
}
}
// called from the fragment button click
public fun reloadNecessaryDataFromTheApi(){
loadNecessaryDataFromTheApi()
}
init {
loadNecessaryDataFromTheApi()
}
My data required to process may be not ready yet because it comes from the API. I have to deferr that processing and resume when data is ready.
I could simply solve this with some Queue and a Thread, but maybe it is possible to do that with Kotlin coroutines?
As your loadNecessaryDataFromTheApi() only sets properties, it is not that easy to observe it or wait for it. You need to use some synchronization utility like locks, channels, etc. In this case it will be probably the easiest to keep a Deferred of the necessary data:
public suspend fun processScannedBarcode(barcode : String){
val data = dataFromApi.await()
val item = findItemByBarcode(barcode)
}
private val dataFromApi = CompletableDeferred<ArrayList<Item>>()
private fun loadNecessaryDataFromTheApi(){
viewModelScope.launch {
canReload = false
dataFromApi.complete(myapi.getDataFromApi())
canReload = true
}
}
Some notes:
We have to make processScannedBarcode suspend if it is going to wait for something.
If you want reloadNecessaryDataFromTheApi() to defer processing barcodes again, simply replace dataFromApi with a new deferred.
I need to get some database objects from Room inside of a composable function. Currently when I try to call:
#Composable
fun loadDashboard() {
val db = DatabaseService.getDatabase(null)
val userDao = db.userDao()
val userModel = userDao.getOne()
}
I receive an error:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Is there a best practice solution for accessing the database in this situation so I can get data to make an API call?
Best practice would be to create a repository class and do database operations there.
You can return a flow from the get method of the repository and emit the result when the operation is done. This flow can then be collected by a viewmodel inside a coroutine that is dispatched on the IO thread. When the result is collected, store it inside a state that is observed by the composable.
You can change your userDao.getOne() function to suspending function and call it from ViewModel inside a scope, ViewModel has viewModelScope by default, or your custom scope, this makes testing easy.
If you don't want to use ViewModel or repo, you can simply call this function inside LaunchedEffect by calling your function in its lambda.
Or produceState functions of Compose
Data with loading, success or error states with produceState for instance is as
#Composable
private fun getOneFromDb(): State<Result> {
return produceState<Result>(initialValue = Result.Loading) {
// In a coroutine, can make suspend calls
val one = db.getOne()
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (one == null) {
Result.Error
} else {
Result.Success(one)
}
}
}
val oneState = getOneFromDb()
sealed class Result {
object Loading : Result()
object Error : Result()
class Success(val one: OneModel) : Result()
}
I'm trying to store value using DataStore.
class BasicDataStore(context: Context) :
PrefsDataStore(
context,
PREF_FILE_BASIC
),
BasicImpl {
override val serviceRunning: Flow<Boolean>
get() = dataStore.data.map { preferences ->
preferences[SERVICE_RUNNING_KEY] ?: false
}
override suspend fun setServiceRunningToStore(serviceRunning: Boolean) {
dataStore.edit { preferences ->
preferences[SERVICE_RUNNING_KEY] = serviceRunning
}
}
companion object {
private const val PREF_FILE_BASIC = "basic_preference"
private val SERVICE_RUNNING_KEY = booleanPreferencesKey("service_running")
}
}
#Singleton
interface BasicImpl {
val serviceRunning: Flow<Boolean>
suspend fun setServiceRunningToStore(serviceRunning: Boolean)
}
And in a Service, trying to monitor that value, here is the respective code :
private fun monitorNotificationService() {
Log.d("d--mua-entry-service","entry")
CoroutineScope(Dispatchers.IO).launch {
Log.d("d--mua-entry-service","entry scope")
basicDataStore.serviceRunning.collect{
Log.d("d--mua-entry-service","$it current status - collect")
}
basicDataStore.serviceRunning.onEach {
Log.d("d--mua-entry-service","$it current status - on each")
}
}
}
In EntryService :
init {
basicDataStore = BasicDataStore(this)
}
But it seems that onEach isn't working at all. And collect is working once, as it should. So how should I monitor/observe a flow?
Logcat :
2021-03-25 20:41:49.462 30761-30761/com.mua.roti D/d--mua-entry-service: entry
2021-03-25 20:41:49.465 30761-30900/com.mua.roti D/d--mua-entry-service: entry scope
2021-03-25 20:41:49.471 30761-30901/com.mua.roti D/d--mua-entry-service: false current status - collect
I haven't used DataStore, but I expect the Flow is infinite, meaning it never finishes collecting until you cancel the coroutine. So any code below the first call to collect() will never be reached. Also, when you call onEach, it simply returns another Flow. The code in onEach won't be called until you collect that returned Flow. onEach is for tacking on side effects to the collecting you will be doing later. Or alternatively for setting up what to do when using launchIn to collect it.
It is a code smell to be creating a CoroutineScope that you don't store in a property for cancellation. The point of the scope is that you can cancel it when the lifetime of the associated component is over. If you create it and throw away your reference like this, you can never cancel its children, so calls to collect, for example, will leak the surrounding class. If you are working on Android, you rarely if ever need to create any CoroutineScope because the framework provides scopes for various lifecycle components like Activity, Fragment, ViewModel, and LifecycleService.
School project and I'm pretty new to Android development.
The problem
I have a button with a onClick listener in a save person fragment which will save person data to the database. Everything works fine except for some reason with the first click it wont return me the inserted row ID but it will do so with 2nd click onwards.
I really need this ID in before proceeding to the next fragment.
Not sure if this is important but whenever I return (reload) to this save person fragment, the behaviour is always the same that the first click allways fails to capture the inserted row ID.
input data:
first name = John
last name = Smith
Just for demo purpose, if I will try to use this button 3x to insert the person data (returned insert ID is in the log), I will get all 3 rows in database with name John Smith, but the very first inserted row ID is not captured (default initialised value is 0), please see the log below:
Log
2020-10-19 12:49:20.320 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 0
2020-10-19 12:49:40.153 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 5
2020-10-19 12:49:40.928 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 6
EDITED ORIGINAL post
As suggested in the comments, I'm trying to go about the way of using LiveData and observer, but I'm still little bit stuck.
The setup
The below is the current setup.
Entity
#Entity(tableName = "person")
data class Person(
#PrimaryKey(autoGenerate = true)
val id: Int,
DAO
#Dao
interface PersonDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addPerson(person: Person): Long
Repository
class PersonRepository(private val personDao: PersonDao) {
val readAllPersonData: LiveData<List<Person>> = personDao.readAllPersonData()
suspend fun addPerson(person: Person): Long {
return personDao.addPerson(person)
}
ViewModel
I'm not sure if I'm doing things right at all here. I broke it down here in steps and created separate variables insertedPersonILiveData and insertedPersonId.
How could pass the returned row id to insertedPersonILiveData?
class PersonViewModel(application: Application) : AndroidViewModel(application) {
var insertedPersonILiveData: LiveData<Long> = MutableLiveData<Long>()
var insertedPersonId: Long = 0L
val readAllPersonData: LiveData<List<Person>>
private val repository: PersonRepository
init {
val personDao = ContactDatabase.getDatabase(application).personDao()
repository = PersonRepository(personDao)
readAllPersonData = repository.readAllPersonData
}
suspend fun addPerson(person: Person) = viewModelScope.launch {
insertedPersonId = repository.addPerson(person)
// ****************************************************************
// insertedPersonILiveData = insertedPersonId (what to do here) ???
// ****************************************************************
}
Save person fragment
This is the way I'm calling out the addPerson via modelView.
val person = Person(0, firstName, lastName)
lifecycleScope.launch {
personViewModel.addPerson(person)
}
Log.d("TEST_ADD_PERSON_ID","insertedPersonId: ${personViewModel.insertedPersonId}")
And this is the way I have done the observer (not sure if it's even correct).
val returnedIdListener: LiveData<Long> = personViewModel.insertedPersonILiveData
returnedIdListener.observe(viewLifecycleOwner, Observer<Long> { id: Long ->
goToAddContactFragment(id)
})
private fun goToAddContactFragment(id: Long) {
Log.d("TEST_ADD_PERSON_ID", "id: " + id)
}
Create database
#Database(
entities = [Person::class, Contact::class, ContactType::class],
views = [ContactDetails::class],
version = 1,
exportSchema = false
)
abstract class ContactDatabase : RoomDatabase() {
abstract fun personDao(): PersonDao
abstract fun contactTypeDao(): ContactTypeDao
abstract fun contactDao(): ContactDao
abstract fun contactDetailsDao(): ContactDetailsDao
companion object {
// For Singleton instantiation
#Volatile
private var instance: ContactDatabase? = null
fun getDatabase(context: Context): ContactDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): ContactDatabase {
return Room.databaseBuilder(context, ContactDatabase::class.java, "contacts_database")
.addCallback(
object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance(context).enqueue(request)
}
}
)
.build()
}
}
}
You're starting a coroutine to run addPerson, and then immediately calling Log with the current value of insertedPersonId in the viewmodel. The coroutine will run, insert the person, and update the VM with the ID of the inserted row, but that will happen long after your Log has run. Probably all of your results are actually the ID of the last record that was inserted.
I'm new to a lot of this too, but just based on what you have now, I think you just need to add
insertedPersonILiveData.value = insertedPersonId
in your addPerson function. That way you're updating that LiveData with a new value, which will be pushed to any valid observers. You've written some code that's observing that LiveData instance, so it should get the update when you set it.
edit your problem is that insertedPersonILiveData is the immutable LiveData type, so you can't set the value on it - it's read-only. You're creating a MutableLiveData object but you're exposing it as a LiveData type.
The recommended pattern for this is to create the mutable one as an internal object, expose a reference to it as an immutable type, and create a setter method that changes the value through the mutable reference (which it can access internally)
class myViewModel : ViewModel() {
// mutable version is private, all updates go through the setter function
// (the _ prefix is a convention for "private versions" of data fields)
private val _lastInsertedPersonId = MutableLiveData<Long>()
// we're making the instance accessible (for observing etc), but as
// the immutable LiveData supertype that doesn't allow setting values
val lastInsertedPersonId: LiveData<Long> = _lastInsertedPersonId
// setting the value on the MutableLiveData instance
// is done through this public function
fun setLastInsertedPersonId(id: Long) {
_lastInsertedPersonId.value = id
}
}
and then your observer would just call lastInsertedPersonId.observe, you don't need to copy the LiveData and observe that (like you're doing with returnedIdListener.
That's the basic pattern right there - internal MutableLiveData, exposed publicly as an immutable LiveData val, with a setter method to update the value. Everything outside the view model either observes the LiveData that's visible, or calls the setter method to update. Hope that makes sense! It's not that complicated once you get your head around what's basically going on
Room executes queries that return LiveData on a background thread automatically. But I want to return a single value that is not wrapped into LiveData (because I don't want live updates).
How do I implement this using coroutines?
How do I return the Task object from this function?
fun getTask(id: Int): Task {
viewModelScope.launch {
repository.getTask(id)
}
}
This function is inside the ViewModel. It forwards the call down to the DAO:
#Query("SELECT * FROM task_table WHERE id = :id")
fun getTask(id: Int): Task
If you don't return a LiveData from Room you won't get updates from the DB. You can however return a LiveData from your viewModel.
val data = liveData {
emit(repository.getTask(id))
}
The liveData extension function runs in a coroutine and then you can use the suspend version of your DAO to handle backgrounding properly.
#Query("SELECT * FROM task_table WHERE id = :id")
suspend fun getTask(id: Int): Task?
A big thing you need to do is make sure it is nullable if you aren't using an aggregate function in your query.
If you are really wanting to call the method in your viewModel to return the task you should run the launch from your activity/fragment (not recommended)
ViewModel
suspend fun getTask(id: Int): Task {
repository.getTask(id)
}
Activity/Fragment
lifecycleScope.launch {
val task = viewModel.getTask(id)
// Do What you want with the task
}
Flow is the new LiveData!
I had a similar problem in two project before, each solved differently. But recently I learnt to use Flow and it appears that it is the cleanest way yet.
Alternative to LiveData
If you don't need to use LiveData you have two option:
Retrieving a Cursor by a #query, suitable for refactoring old projects.
Using Flow, suitable for new projects.
Retrieving only one object/value
LiveDate: You can unsubscribe from the LiveData, remove the observer after the first fetch. Not clean way in my opinion.
Flow: You can retrive just a single object/value if you want and then stop the flow collecting.
Dao:
getTask(): this method return a Flow<Task>:
#Query("SELECT * FROM task_table WHERE id = :id")
fun getTask(id: Int): Flow<Task>
ViewModel:
getTask(): return a Task object (not Flow<Task>), also it is suspend function.
first() The terminal operator that returns the first element emitted by the
flow and then cancels flow’s collection. Throws NoSuchElementException
if the flow was empty.
suspend fun getTask(id: Int): Task {
return dao.getTask(id).first()
}
Fragment/Activity:
Properties:
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
don't forget to cancel viewModelJob when fragment/activity not needed aka onClear/onDestory/... so that all coroutines tight to this is canceled.
Usage
Now whenever we want to retrieve our object Task from that suspended function we need to be inside a suspend or coroutine. Therefore using launch builder to create a coroutine is suitable here (since we don't want any return object from that builder we only want to run a suspend function, otherwise async to return a deferred).
onCreate() / onCreateView()
.
..
...
uiScope.launch() {
// Here are the Task & Main-UI
val task = viewModel.getTask(1)
binding.taskTitleTextView.text = task.title
}
If we don't use first() then we need to collect the flow viewModel.getTasks().collect{ it } there are many useful function at kotlinx.coroutines.flow. Flow is the best thing that happen in Coroutine Package, and oh sorry that I pass the repository layer, it is just a duplicated for viewModel in most cases 😅.
Suspend functions in Room are main-safe and run on a custom
dispatcher. Same as LiveData as you mentioned in your question. Below is the example to achieve the same
Inside some function in viewmModel class
viewModelScope.launch {
// suspend and resume make this database request main-safe
// so our ViewModel doesn't need to worry about threading
someLiveData.value =
repository.getSomething()
}
In repository class
suspend fun getSomething(): List<Something> {
return dao.getSomething()
}
In Dao class
#Query("select * from tableName")
suspend fun getSomething(): List<Something>
One of the workaround would be to return Deferred object immediately and then call .await() on the return deferred
fun getTaskAsync(id: Int): Deferred<Task> = viewModelScope.async {
repository.getTask(id)
}
//call-site
getTaskAsync(id).await() // <- this is suspension point