Room observable in Kotlin for single db entry - android

I am using Room for handling db entities and I am adapting the code from the WordRoom example from adroid developers.
I understand that in order to eprform operations that can take a long time I have to use coroutines, and this seems to work fine for inserting and deleting objects into the database. In the main activity I have a recyclerview that onCreate gets binded to its layoutmanager and to the ViewModelProvider.
In the adapter I set an onClick listener to get the current ID of the object in the recycled view>
holder.mealItemView.setOnClickListener {
(callerContext as MainActivity).getID(current)
}
the getID from the main activity starts a new activity that should retrieve the element from the database and display its properties:
fun getID(meal:Meal){
val intent = Intent(applicationContext, ActivtyViewMeal::class.java)
intent.putExtra("mealId", meal.id.toString())
startActivity(intent)
}
Then in the ActivityViewMeal in the oncreate I get the intent and add an observer to the variable that should store the Entity from the database:
mealViewModel = ViewModelProvider(this).get(MealViewModel::class.java)
mealViewModel.aMeal.observe(this, Observer {meal ->
meal?.let {Log.d(...)
dataIn.text = it.mealAddDate.toString()})
mealViewModel.getSingleContentById(mealID.toInt())
and after binding the variable with the observer I try to retrieve the data.
My issue is that the log never gets executed.
aMeal is declared inside the ViewModel, a separate kotlin class:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val allMeals: LiveData<List<Meal>>
var aMeal: LiveData<Meal>
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
allMeals = repository.allMeals
aMeal = repository.aMeal
}
...
fun getSingleContentById(id: Int)=viewModelScope.launch(Dispatchers.IO) {
repository.getSingleContentById(id)
}
}
I understand that the coroutine cannot return a value and shall not block.
The meal repository class is defined as follow:
/**
* Abstracted Repository as promoted by the Architecture Guide.
* https://developer.android.com/topic/libraries/architecture/guide.html
*/
class MealRepository(private val mealDao: MealDao) {
val allMeals: LiveData<List<Meal>> = mealDao.getAllContentById()
var aMeal: LiveData<Meal> = mealDao.getSingleContentById(0) //init to avoid null pointer errors
...
#Suppress("RedundantSuspendModifier")
#WorkerThread
fun getSingleContentById(id:Int){
aMeal = mealDao.getSingleContentById(id)
// no return from here
}
and to conclude, getSingleContentById is defined inside the DAO class, that is in another kotlin file.
#Query("SELECT * FROM meals_table WHERE id=:id")
fun getSingleContentById(id:Int):LiveData<Meal>
I have no hint on why I don't get the aMeal updated, while the variable allmeals gets updated correctly.
Any hint would be gladly appreciated.

You can change you DB operation to be suspend and return Meal object:
#Query("SELECT * FROM meals_table WHERE id=:id")
suspend fun getSingleContentById(id:Int): Meal
In your repository make getSingleContentById function suspend as well:
class MealRepository(private val mealDao: MealDao) {
suspend fun getSingleContentById(id: Int): Meal {
aMeal = mealDao.getSingleContentById(id)
return aMeal
}
}
In your MealViewModel make aMeal of type MutableLiveData and update it in getSingleContentById function:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val aMeal: MutableLiveData<Meal> = MutableLiveData()
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
}
fun getSingleContentById(id: Int) = viewModelScope.launch(Dispatchers.Main) {
val meal = repository.getSingleContentById(id)
aMeal.postValue(meal)
}
}

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

Kotlin Flows declaration in Compose function

In my task I need to fetch list of bikes.
I made BikesApi and put some bikes into a list.
In MovieRepository implementation I overrided function getBikes that returns now Flow<List>>.
val BikeZ : Flow<List<Bike>> = flow{
while(true){
val lastBikeX = bikeApi.getBikes()
emit(lastBikeX)
kotlinx.coroutines.delay(1000)
}
}
return BikeZ
In BikeViewModel I implemented it like this, It gets MovieRepository (Koin):
public fun getPopularBikes() = flow<Bike> {
val bikesList = repository.getPopularBikes().collect()
return#flow bikesList
}
And when I try in Compose function this:
val viewModel = viewModel<BikeViewModel>()
I got this error:
Cannot create an instance of class BikeViewModel.
java.lang.RuntimeException:
Cannot create an instance of class BikeViewModel
I'm using Koin.
I need list of Movies to implement them into UI.
EDIT:
In app module i did this:
single<BikeApi>{
BikeApiImpl()
}
single<BikeRepository>{
BikeRepositoryImpl(get())
}
viewModel { BikeViewModel(get()) }
Bike api:
class BikeApiImpl : BikeApi{
override suspend fun getPopularBikes(): List<Bike> {
return listOf(
Bike(
id = 1,
name = "Nakamura",
isCheckedOff = true,
bikeType = "MTB",
overview = "None",
picture = R.drawable.nakamura_1,
userScore = 72.0
)
I't wont return list of Bikes.
Initialize your viewModel like this if you are using Koin di:
val viewModel: BikeViewModel by viewModel()
also import
import org.koin.androidx.viewmodel.ext.android.viewModel

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?
}

Kotlin Flow in ViewModel doesn't emit data from Room table sometimes

I'm trying to combine three different flows in my ViewModel to make a list of items that will then be displayed on a RecyclerView in a fragment. I found out that when navigating to the screen, when there is no data in the table yet, the flow for testData1 doesn't emit the data in the table. Happens probably 1/5 of the time. I assume it's a timing issue because it only happens so often, but I don't quite understand why it happens. Also, this only happens when I'm combining flows so maybe I can only have so many flows in one ViewModel?
I added some code to check to see if the data was in the table during setListData() and it's definitely there. I can also see the emit happening but, there is no data coming from room. Any guidance would be greatly appreciated!
Versions I'm using:
Kotlin: 1.4.20-RC
Room: 2.3.0-alpha03
Here is my ViewModel
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.flowOn(Dispatchers.Default)
.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> {
// package the three data elements up to one list of rows
...
}
}
And here is my Repository/DAO layer (repeats for each type of data)
#Query("SELECT * FROM demo_data_1_table")
abstract fun getData1AsFlow() : Flow<List<DemoData1>>
I was able to get around this issue by removing flowOn in the combine function. After removing that call, I no longer had the issue.
I still wanted to run the setListData function on the default dispatcher, so I just changed the context in the setListData instead.
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> = withContext(Dispatchers.Default) {
// package the three data elements up to one list of rows
...
}
}

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

Categories

Resources