I went threw plenty of similar posts on SO, but still I was not able to find the reason why I am not able to observe the LiveData on the UI.
I am working on a project for learning purposes. What I am trying to do is to fetch a value from my Roomdatabase and display it on the UI. For the sake of clearness, I am showing only the relevant code snippets.
Dao.kt
#Query("SELECT maxValue FROM DoItAgainEntity WHERE id = :id")
fun getMaxValue(id: Int): LiveData<Int>
Repository.kt
override fun getMaxValue(activityId: Int) = doItAgainDao.getMaxValue(activityId)
ViewModel.kt
private val mMaxValue = MutableLiveData<Int>()
override fun getMaxValue(id: Int): LiveData<Int> =
Transformations.switchMap(mMaxValue) {
repository.getMaxValue(id)
}
Fragment.kt
viewModel.getMaxValue(activitiesAdapter.activities[position].id)
viewModel.getMaxValue(activitiesAdapter.activities[position].id).observe(viewLifecycleOwner, Observer {
// THIS CODE IS NEVER CALLED
Log.d("getMaxValueObserver",it.toString())
}
How can I reach the Int value provided in the LiveData object? I know, this is almost a newbie question, but could you please explain me what I am missunderstanding?
Thanks a lot.
You do not get your LiveData updated because you do switchMap on mMaxValue. But that LiveData never changes (as I can see from your sample).
You actually do not need the property private val mMaxValue = MutableLiveData<Int>().
You can simplify your function to:
fun getMaxValue(id: Int): LiveData<Int> = repository.getMaxValue(id)
SwitchMap
Documentation
Returns a LiveData mapped from the input source LiveData by applying switchMapFunction to each value set on source.
There were a couple of steps that needed to be done to make this code working. Many thanks to #ChristianB for solving this problem. These were his suggestions:
If the Entity has more than one column/field, how should Room know which value you want when LiveData<Int> is returned? So, the whole row (which is definded as an Entity object) must be returned instead, this would be the entity where the ID matches the parameter :id.
Dao.kt
#Query("SELECT * FROM DoItAgainEntity WHERE id = :id")
fun getMaxValue(id: Int): LiveData<DoItAgainEntity>
in the Repository you can filter/map for any field you actually need. Or even map it to a complete different (domain) object.
Repository.kt
override fun getMaxValue(activityId: Int): LiveData<Int> = doItAgainDao.getMaxValue(activityId).map { entity -> entity.maxValue }
The switchMap(someLiveData) gets only called when the value of someLiveData changes, and then another liveData can be returned from it. But this was not my case. I just wanted to get a value from the LiveData, which goes down to my DAO. So switchMap is not needed here.
ViewModel.kt
override fun getMaxValue(id: Int) = repository.getMaxValue(id)
Finally, observing the LiveData on the UI works. The duplicate call to viewModel.getMaxValue(activitiesAdapter.activities[position].id) from the original question should be also removed.
Fragment.kt
viewModel.getMaxValue.observe(viewLifecycleOwner, Observer {
// THIS CODE HAS NOW BEEN CALLED :)
Log.d("getMaxValueObserver",it.toString())
return#Observer
})
Last but not least, If the Entity was updated like it was in my case, after creating the DB first, uninstall the app completely, do a proper DB version increase, so Room can run a migration on the DB! Clean & Rebuild the Project, Invalidate Caches / Restart.
In ViewModel
val mMaxValue = MutableLiveData<Int>()
override fun getMaxValue(id: Int){
repository.getMaxValue(id).observeForEver{it ->
mMaxValue.value = it
}
}
And in the Activity or Fragment :
viewModel.getMaxValue(activitiesAdapter.activities[position].id)
viewModel.mMaxValue.observe(viewLifecycleOwner, Observer {
// THIS CODE IS NEVER CALLED
Log.d("getMaxValueObserver",it.toString())
}
Related
I am using Room and I have written the Dao class as follows.
Dao
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
...etc
}
and this Flow is converted to LiveData through asLiveData() in ViewModel and used as follows.
ViewModel
#HiltViewModel
class MainViewModel #Inject constructor(
private val projectRepo: ProjectRepository
) : ViewModel() {
val allProjects = projectRepo.allProjects.asLiveData()
...
}
Activity
mainViewModel.allProjects.observe(this) { projects ->
adapter.submitList(projects)
...
}
When data change occurs, RecyclerView is automatically updated by the Observer. This is a normal example I know.
However, in my project data in Flow, what is the most correct way to get the data of the position selected from the list?
I have already written code that returns a value from data that has been converted to LiveData, but I think there may be better code than this solution.
private fun getProject(position: Int): Project {
return mainViewModel.allProjects.value[position]
}
Please give me suggestion
Room has in built support of flow.
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
//lets say you are saving the project from any place one by one.
#Insert()
fun saveProject(project :Project)
}
if you call saveProject(project) from any place, your ui will be updated automatically. you don't have to make any unnecessary call to update your ui. the moment there is any change in project list, flow will update the ui with new dataset.
to get the data of particular position, you can get it from adapter list. no need to make a room call.
Let's Say Here is Sample Code
LiveData Query
Query("SELECT IFNULL(COUNT(id),0) FROM Item WHERE status = :status")
fun getLiveData(status: Int): LiveData<Int>
Kotlin Flow Query
#Query("SELECT IFNULL(COUNT(id),0) FROM Item WHERE status = :status")
fun getFlowData(status: Int): Flow<Int>
So my Question is Flow gets new data if anything changes in the room database?
Yes Flow gets new data if anything changes in the room database if you collect that flow of course, like the example below:
val flow = getFlowData(2) // type Flow<Int>
flow.collect { data ->
// every time anything changes, the code inside collect is going to get called again
}
and also there is .first() that will give you only the latest data without live changes:
val data = getFlowData(2).first() // type Int
So it depends how you use Flow, and it depends on your needs.
I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}
Right now, my method of updating my jetpack compose UI on database update is like this:
My Room database holds Player instances (or whatever they're called). This is my PlayerDao:
#Dao
interface PlayerDao {
#Query("SELECT * FROM player")
fun getAll(): Flow<List<Player>>
#Insert
fun insert(player: Player)
#Insert
fun insertAll(vararg players: Player)
#Delete
fun delete(player: Player)
#Query("DELETE FROM player WHERE uid = :uid")
fun delete(uid: Int)
#Query("UPDATE player SET name=:newName where uid=:uid")
fun editName(uid: Int, newName: String)
}
And this is my Player Entity:
#Entity
data class Player(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "name") val name: String,
)
Lastly, this is my ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getDatabase(application)
val playerNames = mutableStateListOf<MutableState<String>>()
val playerIds = mutableStateListOf<MutableState<Int>>()
init {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().getAll().collect {
playerNames.clear()
playerIds.clear()
it.forEach { player ->
playerNames.add(mutableStateOf(player.name))
playerIds.add(mutableStateOf(player.uid))
}
}
}
}
fun addPlayer(name: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().insert(Player(name = name))
}
}
fun editPlayer(uid: Int, newName: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().editName(uid, newName)
}
}
}
As you can see, in my ViewHolder init block, I 'attach' a 'collector' (sorry for my lack of proper terminology) and basically whenever the database emits a new List<Player> from the Flow, I re-populate this playerNames list with new MutableStates of Strings and the playerIds list with MutableStates of Ints. I do this because then Jetpack Compose gets notified immediately when something changes. Is this really the only good way to go? What I'm trying to achieve is that whenever a change in the player table occurs, the list of players in the UI of the app gets updated immediately. And also, I would like to access the data about the players without always making new requests to the database. I would like to have a list of Players at my disposal at all times that I know is updated as soon as the database gets updated. How is this achieved in Android app production?
you can instead use live data. for eg -
val playerNames:Livedata<ListOf<Player>> = db.playerDao.getAll().asliveData
then you can set an observer like -
viewModel.playerNames.observe(this.viewLifecycleOwner){
//do stuff when value changes. the 'it' will be the changed list.
}
and if you have to have seperate lists, you could add a dao method for that and have two observers too. That might be way more efficient than having a single function and then seperating them into two different lists.
First of all, place a LiveData inside your data layer (usually ViewModel) like this
val playerNamesLiveData: LiveData<List<Player>>
get() = playerNamesMutableLiveData
private val playerNamesMutableLiveData = MutableLiveData<List<Player>>
So, now you can put your list of players to an observable place by using playerNamesLiveData.postValue(...).
The next step is to create an observer in your UI layer(fragment). The observer determines whether the information is posted to LiveData object and reacts the way you describe it.
private fun observeData() {
viewModel.playerNamesLiveData.observe(
viewLifecycleOwner,
{ // action you want your UI to perform }
)
}
And the last step is to call the observeData function before the actual data posting happens. I prefer doing this inside onViewCreated() callback.
I am creating an app with MVVM architecture and I ran into an issue of getting a list of LiveData to show in the View.
In my ViewModel I have a getAll() function that retrieves a list of strings from the database using Room. From there, I get the strings and call my Retrofit function to send each string individually to a web-server that returns an object. Here is where my issue occurs.
From the MVVM tutorials I see online, they usually have the LiveData> style but in this since I am getting each object individually, it becomes List> but I don't think this is the correct way of doing it because in my View I would need to do a ForEach loop to observe each LiveData object in the list.
I have tried other work arounds but it doesn't seem to work. Is there a better way of doing this?
DAO
#Query("SELECT * FROM table")
fun getAll(): LiveData<List<String>>
Repository
fun getAll(): LiveData<List<String>> {
return dao.getAll()
}
fun getRetrofitObject(s: String): LiveData<RetrofitObject> {
api = jsonApi.getRetrofitObjectInfo(s, API_KEY)
val retrofitObject: MutableLiveData<RetrofitObject> = MutableLiveData()
api.enqueue(object : Callback<RetrofitObject> {
override fun onFailure(call: Call<RetrofitObject>?, t: Throwable?) {
Log.d("TEST", "Code: " + t.toString())
}
override fun onResponse(call: Call<RetrofitObject>?, response: Response<RetrofitObject>?) {
if (response!!.isSuccessful) {
retrofitObject.value = response.body()
}
}
})
return retrofitObject
}
MainActivityViewModel (ViewModel)
var objectList ArrayList<LiveData<retrofitObject>> = ArrayList()
// This is getting objects using Retrofit
fun getRetrofitObject(s: String): LiveData<retrofitObject> {
return repo.getRetrofitObject(s)
}
// This is getting all the strings from the internal database
fun getAll(): ArrayList<LiveData<retroFitObject>> {
repo.getAll().value?.forEach {it ->
objectList.add(getRetrofitObject(it)) //How else would I be able to do this?
}
return objectList
}
MainActivity (View)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityViewModel.getAll().forEach {
it.observe(this, Observer {it ->
mainActivityViewModel.objectList.add(it) //Here is part of the issue since I don't want to use a forloop in the View
})
}
adapter.objectList = mainActivityViewModel.objectList
recyclerView.adapter = adapter
}
Thanks, let me know if there is anything else needed or confusion!
By looking at the above code you are trying to fetch a list of item for each table row from server and and trying to update the result to your recycler view. Your logic is little confusing..
So on your activity .. first init your adapter and recyclerview
Then call your viewmodel function to get all values inside table and make a loop to call your network thread fuction in background and store the values in a live data object.
Just observe this livedata in your activity/fragment and just pass the list to adapter and notify it.by doing this,whenever your livedata got a change your recyclerview also reflect the items
The problem with your code is, you are called a retrofit network function with enque option and its a background thread process.so, code wont wait for the network completion. And it will return the retrofitObject data.but it has not got the data yet.so this will make error.
There might be other methods exist I don't know about them.
But you can deal with situation using Transformations for more information please look at documentation page.
Transformations.switchMap(LiveData trigger, Function> func)
You don't have to put the live data observer inside for loop.