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.
Related
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())
}
I have recently completed this (links below) codelabs tutorial which walks through how to implement Room with LiveData and Databinding in Kotlin.
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin
Following on from this, I want to write some tests around the ViewModel, however, the GitHub repository where the code is stored does not contain any (it has a few tests around the DAO, not what I am interested in for now).
The ViewModel I am trying to test looks like this:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
My test class for the ViewModel looks like this:
#RunWith(JUnit4::class)
class WordViewModelTest {
private val mockedApplication = mock<Application>()
#Test
fun checkAllWordsIsEmpty() {
val vm = WordViewModel(mockedApplication)
assertEquals(vm.allWords, listOf<String>())
}
}
I get an error saying java.lang.IllegalArgumentException: Cannot provide null context for the database. This error then points to this line in the WordViewModel: val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao(). To get this to not crash, I believe I need to mock a lot of what is in the ViewModel, which I am fine with.
I would like to be able to run the test above and in the future, I would also like to mock return a list of data when repository.allWords is called. However, I am not sure how to do this. So my question is, how can I mock the following lines from WordViewModel to allow me to do this?
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
I have followed the Android Room with a View tutorial but have changed it to a single-activity app with multiple fragments. I have a fragment for inserting records. The save button calls the viewmodel save method and then pops the backstack to return to the previous (list) fragment. Sometimes this works but often the insert does not occur. I assume that this is because once the fragment is destroyed, the ViewModelScope cancels any pending operations so if the insert has not already occured, it is lost.
Fragment:
private val wordViewModel: WordViewModel by viewModel
...
private fun saveAndClose() {
wordViewModel.save(word)
getSupportFragmentManager().popBackStack()
}
Viewmodel:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
Repository:
suspend fun insert(word: Word) {
wordDao.insert(word)
}
How do I fix this? Should I be using GlobalScope instead of ViewModelScope as I never want the insert to fail? If so, should this go in the fragment or the viewmodel?
One option is to add NonCancellable to the context for the insert:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO + NonCancellable) {
repository.insert(word)
}
The approach recommended and outlined in this post is to create your own application-level scope and run your non-cancellable operations in it.
I need some clarification on how LiveData works with Android's Architecture components like Room.
Let's say I use this way of getting live data:
Dao:
#Query("SELECT * FROM check_table")
LiveData<List<DataItem>> getAllItems();
Repository Constructor:
private DataRepository(Application application) {
DataDatabase database = DataDatabase.getInstance(application);
dataDao = database.dataDao();
dataItems = dataDao.getAllData();
}
ViewModel Constructor:
public DataViewModel(#NonNull Application application) {
super(application);
repository = DataRepository.getInstance(application);
dataItems = repository.getDataItems();
}
Getter:
public LiveData<List<DataItem>> getDataItems() {
return dataItems;
}
Is the LiveData in ViewModel being updated everytime even when there are no active listeners?
I'm asking because I want to use the same data in pretty much all my fragments, and I want to know if the data has to be queried every time I add the listener to the data in one of my fragments or the LiveData object is updated in ViewModel and when i switch fragments and add listener to that LiveData in there, it just gets cached LiveData instead of querying for it once again
I am currently applying Room + ViewModel + LiveData to my project.
In my app, there is "obviously" observe data that is needed, but not all.
The code below is example code for category data. In my situation, category data does not change and always maintains the same value state (13 categories and content does not change). Categories are data that is loaded from the Database through the CategoryItemDao class.
Does category data need to be wrapped with livedata?
Or is there a reason enough to use LiveData in addition to its observerable feature?
I've read the guide to LiveData several times, but I do not understand the exact concept.
CategoryItemDao
#Dao
interface CategoryItemDao {
#Query("SELECT * FROM CategoryItem")
fun getAllCategoryItems(): LiveData<MutableList<CategoryItem>>
}
CategoryRepository
class CategoryRepository(application: Application) {
private val categoryItemDao: CategoryItemDao
private val allCategories: LiveData<MutableList<CategoryItem>>
init {
val db = AppDatabase.getDatabase(application)
categoryItemDao = db.categoryItemDao()
allCategories = categoryItemDao.getAllCategoryItems()
}
fun getAllCategories() = allCategories
}
CategoryViewModel
class CategoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository = CategoryRepository(application)
private val allCategories: LiveData<MutableList<CategoryItem>>
init {
allCategories = repository.getAllCategories()
}
fun getAllCategories() = allCategories
}
This is fine, but you can make a few changes:
Change LiveData<MutableList<CategoryItem>> to LiveData<List<CategoryItem>>. Don't use a MutableList unless you really have to. In your case, List would work fine.
In your CategoryRepository instead of fetching in init, do it during the getAllCategories() call. So change your code like this: fun getAllCategories() = categoryItemDao.getAllCategoryItems()
Similarly do the same in CategoryViewModel as well. Change you code to: fun getAllCategories() = repository.getAllCategories()
A common misconception is to use LiveData only when the data changes. But that's not true. Your 13 categories may not change, but that's in a database. So if you were to accomplish this without a LiveData you have to query the DB and populate the view in the main thread, or you need to wrap this around in a background thread. But if you do this via LiveData, you get the Asynchronous Reactive way of coding for free. Whenever possible, try to make your view observe a LiveData.