When I use Flow ( kotlinx.coroutines.flow.Flow) I get null as response. Dao code below :
#Query("SELECT * FROM connect")
fun getProfiles(): Flow<List<ConnectModel>>
But when I use List<ConnectModel> and remove flow I get expected result.
My connectImpl code:
override fun getProfilesFromDB(): LiveData<List<ConnectModel>>
{
val result = connectDAO.getProfiles()
return result.asLiveData()
}
In viewmodel
private val _users = MutableLiveData<List<ConnectModel>>()
val users: LiveData<List<ConnectModel>>
get() = _users
fun getConnectUsers() {
viewModelScope.launch {
val response = getConnectUsersFromDbUseCase.invoke()
_users.postValue(response.value)
}
}
If I make the _users lateinit then it works fine but not when it is MutableLiveData.
In Fragment
viewModel.users.observe(viewLifecycleOwner,{list ->
if (list != null) {
for(profile in list)
Timber.i("Observer Active ${profile.name}")
}
})
Database
Any kind of help is appreciated. Thanks in advance.
Use collect or first like the code below
connectViewModel.getProfilesFromDB().collect {
//do stuff
}
There is nothing wrong in your code, I've tried it and it's working perfectly fine as it should. It may be how you are observing this live data after getting it. I've this method in my view model and this is how I'm observing it inside fragment.
connectViewModel.getProfilesFromDB().observe(viewLifecycleOwner){
//do stuff
}
Related
I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}
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)
}
I am using MVVM, LiveData and trying and implement Repository pattern.
But, calling a method in my repository class - RegisterRepo which returns LiveData is not working. I have no idea why. Any suggestions would be greatly appreciated.
Boilerplate code is removed for breivity.
Activity' s onCreateMethod
mViewModel.status.observe(this, Observer {
when (it) {
true -> {
Log.d("----------", " true ") //These message is never being printed.
}
false -> {
Log.d("----------", "false ") //These message is never being printed.
}
}
})
button.setOnClickListener {
mViewModel.a()
}
ViewModel
class AuthViewModel (val repo: RegisterRepo): ParentViewModel() {
//...
var status = MutableLiveData<Boolean>()
fun a() {
status = repo.a()
}
}
RegisterRepo
class RegisterRepo () {
fun a(): MutableLiveData<Boolean> {
var result = MutableLiveData<Boolean>()
result.value = true
return result
}
}
However, if I change my code in ViewModel to this, everything is working fine.
ViewModel
class AuthViewModel (val repo: RegisterRepo): ParentViewModel() {
//...
var status = MutableLiveData<Boolean>()
fun a() {
status.value = true //Change here causing everything work as expected.
}
}
In the first ViewModel code, when method a is called, you assign another LiveData to status variable, this live data is different from the one observed by the Activity, so that the value won't be notify to your Activity
the 2nd way is correct to use and it will work fine the 1st is not working because you are creating new MutableLive data in your RegisterRepo, so basically at the time your create an observable to "status" is deferent where you assign a value into it is different. so the second one is the only way to do this
I'm using LiveData's version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05". Once my LiveData block executes successfully I want to explicitly trigger it to execute again, e.g.
I navigate to a fragment
User's data loads
I click delete btn while being in the same fragment
User's data should refresh
I have a fragment where I observe my LiveData, a ViewModel with LiveData and Repository:
ViewModel:
fun getUserLiveData() = liveData(Dispatchers.IO) {
val userData = usersRepo.getUser(userId)
emit(userData)
}
Fragment:
viewModel.getUserLiveData.observe(viewLifecycleOwner,
androidx.lifecycle.Observer {..
Then I'm trying to achieve desired behaviour like this:
viewModel.deleteUser()
viewModel.getUserLiveData()
According to the documentation below LiveData block won't execute if it has completed successfully and if I put a while(true) inside the LiveData block, then my data refreshes, however I don't want this to do since I need to update my view reactively.
If the [block] completes successfully or is cancelled due to reasons other than [LiveData]
becoming inactive, it will not be re-executed even after [LiveData] goes through active
inactive cycle.
Perhaps I'm missing something how I can reuse the same LiveDataScope to achieve this? Any help would be appreciated.
To do this with liveData { .. } block you need to define some source of commands and then subscribe to them in a block. Example:
MyViewModel() : ViewModel() {
val commandsChannel = Channel<Command>()
val liveData = livedata {
commandsChannel.consumeEach { command ->
// you could have different kind of commands
//or emit just Unit to notify, that refresh is needed
val newData = getSomeNewData()
emit(newData)
}
}
fun deleteUser() {
.... // delete user
commandsChannel.send(RefreshUsersListCommand)
}
}
Question you should ask yourself: Maybe it would be easier to use ordinary MutableLiveData instead, and mutate its value by yourself?
livedata { ... } builder works well, when you can collect some stream of data (like a Flow / Flowable from Room DB) and not so well for plain, non stream sources, which you need to ask for data by yourself.
I found a solution for this. We can use switchMap to call the LiveDataScope manually.
First, let see the official example for switchMap:
/**
* Here is an example class that holds a typed-in name of a user
* `String` (such as from an `EditText`) in a [MutableLiveData] and
* returns a `LiveData` containing a List of `User` objects for users that have
* that name. It populates that `LiveData` by requerying a repository-pattern object
* each time the typed name changes.
* <p>
* This `ViewModel` would permit the observing UI to update "live" as the user ID text
* changes.
**/
class UserViewModel: AndroidViewModel {
val nameQueryLiveData : MutableLiveData<String> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
name -> myDataSource.usersWithNameLiveData(name)
}
fun setNameQuery(val name: String) {
this.nameQueryLiveData.value = name;
}
}
The example was very clear. We just need to change nameQueryLiveData to your own type and then combine it with LiveDataScope. Such as:
class UserViewModel: AndroidViewModel {
val _action : MutableLiveData<NetworkAction> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = _action.switchMap {
action -> liveData(Dispatchers.IO){
when (action) {
Init -> {
// first network request or fragment reusing
// check cache or something you saved.
val cache = getCache()
if (cache == null) {
// real fecth data from network
cache = repo.loadData()
}
saveCache(cache)
emit(cache)
}
Reload -> {
val ret = repo.loadData()
saveCache(ret)
emit(ret)
}
}
}
}
// call this in activity, fragment or any view
fun fetchData(ac: NetworkAction) {
this._action.value = ac;
}
sealed class NetworkAction{
object Init:NetworkAction()
object Reload:NetworkAction()
}
}
First add implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file. Make your ViewModel as follows:
MyViewModel() : ViewModel() {
val userList = MutableLiveData<MutableList<User>>()
fun getUserList() {
viewModelScope.launch {
userList.postValue(usersRepo.getUser(userId))
}
}
}
Then onserve the userList:
viewModel.sessionChartData.observe(viewLifecycleOwner, Observer { users ->
// Do whatever you want with "users" data
})
Make an extension to delete single user from userList and get notified:
fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
if (!this.value.isNullOrEmpty()) {
val oldValue = this.value
oldValue?.removeAt(index)
this.value = oldValue
} else {
this.value = mutableListOf()
}
}
Call that extension function to delete any user and you will be notified in your Observer block after one user get deleted.
viewModel.userList.removeItemAt(5) // Index 5
When you want to get userList from data source just call viewModel.getUserList() You will get data to the observer block.
private val usersLiveData = liveData(Dispatchers.IO) {
val retrievedUsers = MyApplication.moodle.getEnrolledUsersCoroutine(course)
repo.users = retrievedUsers
roles.postValue(repo.findRolesByAll())
emit(retrievedUsers)
}
init {
usersMediator.addSource(usersLiveData){ usersMediator.value = it }
}
fun refreshUsers() {
usersMediator.removeSource(usersLiveData)
usersMediator.addSource(usersLiveData) { usersMediator.value = it }
The commands in liveData block {} doesn't get executed again.
Okay yes, the observer in the viewmodel holding activity get's triggered, but with old data.
No further network call.
Sad. Very sad. "Solution" seemed promisingly and less boilerplaty compared to the other suggestions with Channel and SwitchMap mechanisms.
You can use MediatorLiveData for this.
The following is a gist of how you may be able to achieve this.
class YourViewModel : ViewModel() {
val mediatorLiveData = MediatorLiveData<String>()
private val liveData = liveData<String> { }
init {
mediatorLiveData.addSource(liveData){mediatorLiveData.value = it}
}
fun refresh() {
mediatorLiveData.removeSource(liveData)
mediatorLiveData.addSource(liveData) {mediatorLiveData.value = it}
}
}
Expose mediatorLiveData to your View and observe() the same, call refresh() when your user is deleted and the rest should work as is.