Android room query is empty when using Flow - android

I am confused about using Flow with Room for database access. I want to be able to observe a table for changes but also access it directly.
However, when using a query that returns a Flow, the result always seems to be null although the table is not empty. A query that returns a List directly, seems to work.
Can someone explain the difference or show me which part of the documentation I might have missed?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db_button.setOnClickListener {
val user_dao = UserDatabase.getInstance(this).userDatabaseDao
lifecycleScope.launch {
user_dao.insertState(State(step=4))
val states = user_dao.getAllState().asLiveData().value
if (states == null || states.isEmpty()) {
println("null")
} else {
val s = states.first().step
println("step $s")
}
val direct = user_dao.getStatesDirect().first().step
println("direct step $direct")
}
}
}
}
#Entity(tableName = "state")
data class State(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
#ColumnInfo(name = "step")
var step: Int = 0
)
#Dao
interface UserDatabaseDao {
#Insert
suspend fun insertState(state: State)
#Query("SELECT * FROM state")
fun getAllState(): Flow<List<State>>
#Query("SELECT * FROM state")
suspend fun getStatesDirect(): List<State>
}
Output:
I/System.out: null
I/System.out: direct step 1

In Room, we use Flow or LiveData to observe changes in a query result. So Room queries the db asynchronously and when you are trying to retrieve the value immediately, it is highly probable to get null.
As a result, if you want to get the value immediately, you shouldn't use Flow as the return type of a room query function, just like what you did on getStatesDirect(): List<State>. On the other hand, if you want to observe data changes, you should use the collect terminal function on the Flow to receive its emissions:
lifecycleScope.launch {
user_dao.insertState(State(step=4))
val direct = user_dao.getStatesDirect().first().step
println("direct step $direct")
}
lifecycleScope.launch {
user_dao.getAllState().collect { states ->
if (states == null || states.isEmpty()) {
println("null")
} else {
val s = states.first().step
println("step $s")
}
}
}

Related

LiveData list of objects from Room query not showing up in the view

I'm currently trying to use a SQLite database via the Room library on my Jetpack Compose project to create a view that does the following:
display a list of entries from the database that are filtered to only records with the current user's ID
allow the user to create new records and insert those into the database
update the list to include any newly created records
My issue is that I cannot get the list to show when the view is loaded even though the database has data in it and I am able to insert records into it successfully. I've seen a lot of examples that show how do this if you are just loading all the records, but I cannot seem to figure out how to do this if I only want the list to include records with the user's ID.
After following a few of tutorials and posts it is my understanding that I should have the following:
A DAO, which returns a LiveData object
A repository which calls the DAO method and returns the same LiveData object
A viewholder class, which will contain two objects: one private MutableLiveData variable and one public LiveData variable (this one is the one we observe from the view)
My view, a Composable function, that observes the changes
However, with this setup, the list still will not load and I do not see any calls to the database to load the list from the "App Inspection" tab. The code is as follows:
TrainingSet.kt
#Entity(tableName = "training_sets")
data class TrainingSet (
#PrimaryKey() val id: String,
#ColumnInfo(name = "user_id") val userId: String,
TrainingSetDao.kt
#Dao
interface TrainingSetDao {
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(trainingSet: TrainingSet)
#Query("SELECT * FROM training_sets WHERE user_id = :userId")
fun getUserTrainingSets(userId: String): LiveData<List<TrainingSet>>
}
TrainingSetRepository.kt
class TrainingSetRepository(private val trainingSetDao: TrainingSetDao) {
fun getUserTrainingSets(userId: String): LiveData<List<TrainingSet>> {
return trainingSetDao.getUserTrainingSets(userId)
}
suspend fun insert(trainingSet: TrainingSet) {
trainingSetDao.insert(trainingSet)
}
}
TrainingSetsViewModel.kt
class TrainingSetsViewModel(application: Application): ViewModel() {
private val repository: TrainingSetRepository
private val _userTrainingSets = MutableLiveData<List<TrainingSet>>(emptyList())
val userTrainingSets: LiveData<List<TrainingSet>> get() = _userTrainingSets
init {
val trainingSetDao = AppDatabase.getDatabase(application.applicationContext).getTrainingSetDao()
repository = TrainingSetRepository(trainingSetDao)
}
fun getUserTrainingSets(userId: String) {
viewModelScope.launch {
_userTrainingSets.value = repository.getUserTrainingSets(userId).value
}
}
fun insertTrainingSet(trainingSet: TrainingSet) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.insert(trainingSet)
} catch (err: Exception) {
println("Error!!!!: ${err.message}")
}
}
}
}
RecordScreen.kt
#Composable
fun RecordScreen(navController: NavController, trainingSetsViewModel: TrainingSetsViewModel) {
// observe the list
val trainingSets by trainingSetsViewModel.userTrainingSets.observeAsState()
// trigger loading of the list using the userID
// note: hardcoding this ID for now
trainingSetsViewModel.getUserTrainingSets("20c1256d-0bdb-4241-8781-10f7353e5a3b")
// ... some code here
Button(onClick = {
trainingSetsViewModel.insertTrainingSet(TrainingSet(// dummy test data here //))
}) {
Text(text = "Add Record")
}
// ... some more code here
LazyColumn() {
itemsIndexed(trainingSets) { key, item ->
// ... list row components here
}
}
NavGraph.kt** **(including this in case it's relevant)
#Composable
fun NavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screens.Record.route,
) {
composable(route = Screens.Record.route) {
val owner = LocalViewModelStoreOwner.current
owner?.let {
val trainingSetsViewModel: TrainingSetsViewModel = viewModel(
it,
"TrainingSetsViewModel",
MainViewModelFactory(LocalContext.current.applicationContext as Application)
)
// note: I attempted to load the user training sets here in case it needed to be done before entering the RecordScreen, but that did not affect it (commenting this line out for now)
// trainingSetsViewModel.getUserTrainingSets("20c1256d-0bdb-4241-8781-10f7353e5a3b")
RecordScreen(
navController = navController,
trainingSetsViewModel= TrainingSetsViewModel,
)
}
}
}
}
What somewhat worked...
I was able to get the list to eventually load by making the following two changes (see comments in code), but it still did not load in the expected sequence and this change did not seem to align from all the examples I've seen. I will note that with this change, once the list showed up, the newly created records would be properly displayed as well.
*TrainingSetsViewModel.kt *(modified)
private val _userTrainingSets = MutableLiveData<List<TrainingSet>>(emptyList())
/ ***************
// change #1 (change this variable from a val to a var)
/ ***************
var userTrainingSets: LiveData<List<TrainingSet>> = _userTrainingSets
... // same code as above example
fun getUserTrainingSets(userId: String) {
viewModelScope.launch {
// ***************
// change #2 (did this instead of: _userTrainingSets.value = repository.getUserTrainingSets(userId).value)
// ***************
userTrainingSets = repository.getUserTrainingSets(userId)
}
}
... // same code as above example

java.util.ConcurrentModificationException while inserting data into room database

I am trying to download data from firebase firestore and insert it into the room DB for some offline use and avoid time-lag using the MVVM architecture pattern but when I do that I get an java.util.ConcurrentModificationException error I am inserting the data into the room DB inside a coroutine.
My code
class HomeFragmentViewModel(application: Application): AndroidViewModel(application) {
private var mDatabase: AppDatabase = AppDatabase.getInstance(application)!!
private val postListRoom: MutableList<PostRoomEntity> = mutableListOf()
private val postList: LiveData<MutableList<PostRoomEntity>>? = getPostList2()
private val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
private val db: FirebaseFirestore = FirebaseFirestore.getInstance()
private val myTAG: String = "MyTag"
#JvmName("getPostList")
fun getPostList(): LiveData<MutableList<PostRoomEntity>>? {
return postList
}
#JvmName("getPostList2")
fun getPostList2(): LiveData<MutableList<PostRoomEntity>>? {
var postsDao: PostsDao? = null
Log.d(myTAG, "postDao getPost is " + postsDao?.getPosts())
return mDatabase.postsDao()?.getPosts()
// return postList
}
fun loadDataPost() {
val list2 = mutableListOf<PostRoomEntity>()
db.collection("Posts")
.addSnapshotListener { snapshots, e ->
if (e != null) {
Log.w(myTAG, "listen:error", e)
return#addSnapshotListener
}
for (dc in snapshots!!.documentChanges) {
when (dc.type) {
DocumentChange.Type.ADDED -> {
dc.document.toObject(PostRoomEntity::class.java).let {
list2.add(it)
}
postListRoom.addAll(list2)
viewModelScope.launch(Dispatchers.IO) {
mDatabase.postsDao()?.insertPost(postListRoom)
}
// mDatabase.let { saveDataRoom(postListRoom, it) }
}
DocumentChange.Type.MODIFIED -> {
}
DocumentChange.Type.REMOVED -> {
Log.d(myTAG, "Removed city: ${dc.document.data}")
}
}
}
}
}
}
PostsDao
#Dao
interface PostsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertPost(PostEntity: MutableList<PostRoomEntity>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllPosts(PostEntity :List<PostRoomEntity>)
#Query("Select * from PostRoomEntity")
fun getPosts(): LiveData<MutableList<PostRoomEntity>>
// #Query("SELECT * FROM notes WHERE id= :id")
// open fun getNoteById(id: Int): NoteEntity?
}
It is very error-prone to use MutableLists with asynchronous tasks or to expose them to outside functions. You are doing both, and this can result in them being modified from two different places in code simultaneously, which can cause a ConcurrentModificationException.
You should use read-only Lists to eliminate this risk. For example, use vars of type List instead of vals of type MutableList.
Some other issues with your code:
You are adding the whole contents of the list to the main list on each step of iteration, so the last item is added once, the second-to-last item is added twice, and so on. You are also inserting that whole exploded list in your local database on each step of iteration, so it is even more exponentially multiplied with redundancies. If you are just trying to update your local database with changes, you should only be inserting a single row at a time anyway.
Unnecessary nullability used in a few places. There's no reason for the DAO or your LiveData to ever be null.
Unnecessary intermediate variables that serve no purpose. Like you create a variable var postsDao: PostsDao? = null and log the null value and never use it.
Redundant and non-idiomatic getters for properties you could expose as public directly.
Redundant backing property for the value that's already held in a LiveData.
You can make your DAO functions suspend so you don't have to worry about which dispatchers you're using to call them.
There's no reason for the DAO to have an insert overload for a MutableList instead of a List. I think the parameter should just be a single item.
You can have a single coroutine iterate the list of changes instead of launching separate coroutines to handle each individual change.
I also recommend not mixing Hungarian and non-Hungarian member names. Actually I don't recommend using Hungarian naming at all, but it's a matter of preference.
And it's a little confusing that you have two databases, but there is nothing about their names to distinguish them.
Fixing these problems, your code will look like this, but there might be other issues because I can't test it or see what it's hooked up to. Also, I don't use Firebase, but I feel like there must be a more robust way of keeping your local database in sync with Firestore than trying to make individual changes with a listener.
class HomeFragmentViewModel(application: Application): AndroidViewModel(application) {
private val localDatabase: AppDatabase = AppDatabase.getInstance(application)!!
private val mutablePostList = MutableLiveData<List<PostRoomEntity>>()
val postList: LiveData<List<PostRoomEntity>> = mutablePostList
private val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance()
private val myTAG: String = "MyTag"
fun loadDataPost() {
db.collection("Posts")
.addSnapshotListener { snapshot, e ->
if (e != null) {
Log.w(myTAG, "listen:error", e)
return#addSnapshotListener
}
mutablePostList.value = snapshot!!.documents.map {
it.toObject(PostRoomEntity::class.java)
}
viewModelScope.launch {
for (dc in snapshot!!.documentChanges) {
when (dc.type) {
DocumentChange.Type.ADDED -> {
val newPost = dc.document.toObject(PostRoomEntity::class.java)
localDatabase.postsDao().insertPost(newPost)
}
DocumentChange.Type.MODIFIED -> {
}
DocumentChange.Type.REMOVED -> {
Log.d(myTAG, "Removed city: ${dc.document.data}")
}
}
}
saveDataRoom(postListRoom, localDatabase) // don't know what this does
}
}
}
}
#Dao
interface PostsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(postEntity: PostRoomEntity)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllPosts(postEntity: List<PostRoomEntity>)
#Query("Select * from PostRoomEntity")
fun getPosts(): LiveData<MutableList<PostRoomEntity>>
// #Query("SELECT * FROM notes WHERE id= :id")
// open fun getNoteById(id: Int): NoteEntity?
}

Caching is not working in Android Paging 3

I have implemented application using codelabs tutorial for new Paging 3 library, which was release week ago.
The problem is application is not working in offline mode. It does not retrieve data from Room database.
Tutorial Repo link :- https://github.com/googlecodelabs/android-paging
Code:-
RepoDao.kt
#Dao
interface RepoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(repos: List<Repo>)
#Query("SELECT * FROM repos WHERE " +
"name LIKE :queryString OR description LIKE :queryString " +
"ORDER BY stars DESC, name ASC")
fun reposByName(queryString: String): PagingSource<Int, Repo>
#Query("DELETE FROM repos")
suspend fun clearRepos()
}
GithubRepository.kt
class GithubRepository(
private val service: GithubService,
private val database: RepoDatabase
) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
val dbQuery = "%${query.replace(' ', '%')}%"
val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) }
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = GithubRemoteMediator(
query,
service,
database
),
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 50
}
}
SearchRepositoriesViewModel.kt
#ExperimentalCoroutinesApi
class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() {
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<Repo>>? = null
fun searchRepo(queryString: String): Flow<PagingData<Repo>> {
val lastResult = currentSearchResult
if (queryString == currentQueryValue && lastResult != null) {
return lastResult
}
currentQueryValue = queryString
val newResult: Flow<PagingData<Repo>> = repository.getSearchResultStream(queryString).cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
}
SearchRepositoriesActivity.kt
#ExperimentalCoroutinesApi
class SearchRepositoriesActivity : AppCompatActivity() {
.....
private lateinit var viewModel: SearchRepositoriesViewModel
private val adapter = ReposAdapter()
private var searchJob: Job? = null
// this is where adapter get flow data from viewModel
// initially this is called with **Android** as a query
private fun search(query: String) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModel.searchRepo(query).collectLatest {
adapter.submitData(it)
}
}
}
.....
}
Output:- It is just showing the empty recyclerview when application is open in offline mode.
If you're able to share your code or how you reached that conclusion I could probably help pinpoint the problem a bit better, but the codelab does load data from Room on the branch: step13-19_network_and_database
There are two components here:
PagingSource: Provided by Room by declaring a #Query with a PagingSource return type, will create a PagingSource that loads from Room. This function is called in the pagingSourceFactory lambda in Pager which expects a new instance each call.
RemoteMediator: load() called on boundary conditions where the local cache is out of data, this will fetch from network and store in the Room db, which automatically propagates updates to PagingSource implementation generated by Room.
One other issue you might be seeing could be related to loadStateListener/Flow, essentially the codelab shows an error state by checking for CombinedLoadStates.refresh, but this always defers to the RemoteMediator's load state when available and if you want to show the locally cached data, even when RemoteMediator errors out, you'll need to disable hiding of the list in that case.
Note that you can access individual LoadState with CombinedLoadStates.source or CombinedLoadStates.mediator.
Hopefully this is enough to help you, but it's hard to guess your issue without some more concrete example / information about what you're seeing.
Edit: While the above are still good things to check for, it looks like there's an underlying issue with the library that I'm chasing down here: https://android-review.googlesource.com/c/platform/frameworks/support/+/1341068
Edit2: This is fixed now and will be released with alpha02.

Room Database entries in RecyclerView

I want to show Room Database entries in a RecyclerView. So far I have the Room skeleton and I can show some dummy content (not from Room) in the RecyclerView:
However I have struggles showing Room DB entries instead of the dummy content. In Arduino the EEPROM I/O stuff used to be almost a oneliner but within Android Room this conceptually easy task seems to be a code-intense and not-so-forward challenge. This brings me to my first question:
1) As in my case the database is pretty slim and simple, is there any simpler approach than Room using less overhead and classes?
Regarding the Room approach, I believe that I am pretty close. I have difficulties implementing the following:
2) How can I substitute the for-loop in init DummyContent by the Room-DB entries (allJumps from ViewModel)?
Here is what I got so far (I didn't post anything below the ViewModel such as Repository and DAO's as it should not of interest right now):
DummyItems (dummy contents to be replaced by Room DB entries)
object DummyContent {
// An array of sample (dummy) items.
val ITEMS: MutableList<DummyItem> = ArrayList()
// A map of sample (dummy) items, by ID.
val ITEM_MAP: MutableMap<String, DummyItem> = HashMap()
private val COUNT = 25
init {
// Add some sample items.
// TO BE REPLACED BY ROOM DB ENTRIES <----------------------------------------------------
for (i in 1..COUNT) {
addItem(createDummyItem(i))
}
}
private fun addItem(item: DummyItem) {
ITEMS.add(item)
ITEM_MAP.put(item.id, item)
}
private fun createDummyItem(position: Int): DummyItem {
return DummyItem(position.toString(), "Item " + position, makeDetails(position))
}
private fun makeDetails(position: Int): String {
val builder = StringBuilder()
builder.append("Details about Item: ").append(position)
for (i in 0..position - 1) {
builder.append("\nMore details information here.")
}
return builder.toString()
}
// A dummy item representing a piece of content.
data class DummyItem(val id: String, val content: String, val details: String) {
override fun toString(): String = content
}
}
allJumps / JumpData
// allJumps is of type LiveData<List<JumpData>>
#Entity
data class JumpData (
#PrimaryKey var jumpNumber: Int,
var location: String?
}
ViewModel
class JumpViewModel(application: Application) : AndroidViewModel(application) {
// The ViewModel maintains a reference to the repository to get data.
private val repository: JumpRepository
// LiveData gives us updated words when they change.
val allJumps: LiveData<List<JumpData>>
init {
// Gets reference to WordDao from WordRoomDatabase to construct
// the correct WordRepository.
val jumpsDao = JumpRoomDatabase.getDatabase(application, viewModelScope).jumpDao()
repository = JumpRepository(jumpsDao)
allJumps = repository.allJumps // OF INTEREST <----------------------------------------------------
}
fun insert(jump: JumpData) = viewModelScope.launch {
repository.insert(jump)
}
fun getJumps() : LiveData<List<JumpData>> {
return allJumps
}
}
You can try to add this to object DummyContent
object DummyContent {
val jumpsLiveData = MutableLiveData<List<JumpData>>()
private val observedLiveData: LiveData<List<JumpData>>? = null
private val dataObserver = object : Observer<List<JumpData>> {
override fun onChanged(newList: List<JumpData>) {
// Do something with new data set
}
}
fun observeJumpsData(jumpsLiveData: LiveData<List<JumpData>>) {
observedLiveData?.removeObserver(dataObserver)
observedLiveData = jumpsLiveData.apply {
observeForever(dataObserver)
}
}
}
And this to viewModel's init block:
init {
val jumpsDao = JumpRoomDatabase.getDatabase(application, viewModelScope).jumpDao()
repository = JumpRepository(jumpsDao)
allJumps = repository.allJumps
DummyContent.observeJumpsData(getJumps())
}
By this code, DummyContent will automatically subscribe to new data after ViewModel creation
And in 'Activity', where you created RecyclerView, add this text to end of onCreate:
override fun onCreate(savedState: Bundle?) {
DummyContent.jumpsLiveData.observe(this, Observer {
recyclerAdapter.changeItemsList(it)
}
}
changeItemsList - method that changes your recycler's data, i believe, you already created it

LiveData + ViewModel + Room: Exposing a LiveData returned by query which changes over time (Through a fts search)

I have an FTS query in my DAO which I'd like to use to provide search in my App. The activity passes the query to view model each time the search text is changed.
The problem is that, Room returns a LiveData every single time the query is executed while I'd like to get same LiveData object updated when I run the query.
I was thinking about copying data from the LiveData which room returns into my dataSet (see the code below). Would it be a good approach? (And if yes, how would I actually do that?)
Here's my work so far:
In my Activity:
override fun onCreate(savedInstanceState: Bundle?) {
//....
wordViewModel = ViewModelProviders.of(this).get(WordMinimalViewModel::class.java)
wordViewModel.dataSet.observe(this, Observer {
it?.let {mRecyclerAdapter.setWords(it)}
})
}
/* This is called everytime the text in search box is changed */
override fun onQueryTextChange(query: String?): Boolean {
//Change query on the view model
wordViewModel.searchWord(query)
return true
}
ViewModel:
private val repository :WordRepository =
WordRepository(WordDatabase.getInstance(application).wordDao())
//This is observed by MainActivity
val dataSet :LiveData<List<WordMinimal>> = repository.allWordsMinimal
//Called when search query is changed in activity
//This should reflect changes to 'dataSet'
fun searchWord(query :String?) {
if (query == null || query.isEmpty()) {
//Add all known words to dataSet, to make it like it was at the time of initiating this object
//I'm willing to copy repository.allWordsMinimal into dataSet here
} else {
val results = repository.searchWord(query)
//Copy 'results' into dataSet
}
}
}
Repository:
//Queries all words from database
val allWordsMinimal: LiveData<List<WordMinimal>> =
wordDao.getAllWords()
//Queries for word on Room using Fts
fun searchWord(query: String) :LiveData<List<WordMinimal>> =
wordDao.search("*$query*")
//Returns the model for complete word (including the definition for word)
fun getCompleteWordById(id: Int): LiveData<Word> =
wordDao.getWordById(id)
}
DAO:
interface WordDao {
/* Loads all words from the database */
#Query("SELECT rowid, word FROM entriesFts")
fun getAllWords() : LiveData<List<WordMinimal>>
/* FTS search query */
#Query("SELECT rowid, word FROM entriesFts WHERE word MATCH :query")
fun search(query :String) :LiveData<List<WordMinimal>>
/* For definition lookup */
#Query("SELECT * FROM entries WHERE id=:id")
fun getWordById(id :Int) :LiveData<Word>
}
val dataSet :LiveData<List<WordMinimal>>
val searchQuery = MutableLiveData<String>()
init {
dataSet = Transformations.switchMap(searchQuery) { query ->
if (query == null || query.length == 0) {
//return WordRepository.getAllWords()
} else {
//return WordRepository.search(query)
}
}
}
fun setSearchQuery(searchedText: String) {
searchQuery.value = searchedText
}

Categories

Resources