I have a Jetpack Compose Picture App where the user views Cached Photos from ROOM Database. The database has a isFavorite column which stores Booleans and is updated when the user clicks to like the photo.
This is my dao
#Query("SELECT * FROM astrophotoentity")
suspend fun getSavedAstroPhotos(): Flow<List<AstroPhotoEntity>>
#Query("UPDATE astrophotoentity SET isFavorite =:isFavorite WHERE id=:id")
suspend fun updateIsFavoriteStatus(id: String,isFavorite:Boolean)
This is my Repository code
override suspend fun updateIsFavoriteStatus(photo: AstroPhoto, isFavorite:Boolean) {
dao.updateIsFavoriteStatus(photo.date,isFavorite)
}
I am using a use case class for clean architecture.
class UpdateIsFavoriteStatusUseCase (private val repository: AstroRepository) {
suspend operator fun invoke(photo: AstroPhoto, isFavorite:Boolean){
repository.updateIsFavoriteStatus(photo, isFavorite)
}
}
I also use a simple data class to hold state for the ViewModel.
data class PhotoState(
val astroPhotos: List<AstroPhoto> = emptyList(),
val isPhotosListLoading: Boolean = false,
val errorMessage: String? = null)
This is the ViewModel which initializes the Photo State list to a list fetched from the DB. I use mutableStateOf() to hold the photo objects list. Here I update isFavorite column in the database depending on the passed event.
#HiltViewModel
class OverviewViewModel #Inject constructor(
private val useCaseContainer: UseCaseContainer
) : ViewModel() {
var state by mutableStateOf(PhotoState())
private set
init {
//set the state to the retrieved list of photo objects
... getPhotosListFromDb()
}
fun onEvent(event: OverviewEvent) { ....
is OverviewEvent.OnMarkFavorite -> { ....
useCaseContainer.updateIsFavoriteStatus(event.photo, event.isFavorite)}
is OverviewEvent.OnRemoveFromFavorites -> { ....
useCaseContainer.updateIsFavoriteStatus(event.photo, event.isFavorite)}
I pass the Photos list to the below composable.
#Composable
fun AstroPhotoComposable(
photo: AstroPhoto,
onMarkAsFavorite: () -> Unit,
onRemovePhoto: () -> Unit = {}
) {
//retrieve the isFavorite state using photo.isFavorite and cache it
var isFavorite by remember{ mutableStateOf(photo.isFavorite) ... }
The issue I am facing is that when the photo changes from isFavorite/!isFavorite the state is not reflecting live on the UI.
I have read somewhere about cold and hot kotlin flows but I'm yet to wrap my head around this. I also tried to replace mutableStateOf with MutableStateFlow but the state is not updating on Database Changes.
Basically, I am looking to observe for database changes and get an up-to-date state similar to LiveData.
Any help is appreciated.
Have you ever tried updating the whole entity?
#Update
suspend fun update(myEntity: myEntity): Int
And in my opinion it has to be without suspend
#Query("SELECT * FROM astrophotoentity")
fun getSavedAstroPhotos(): Flow<List<AstroPhotoEntity>>
I'd do it like that.
fun getSavedAstroPhotos() = dao.getSavedAstroPhotos().map{ it.toPhotoState() }
And collect it in view model
init {
viewModelScope.launch {
getSavedAstroPhotos().collect{
state = it
}
}
}
Perhabs it helps you link
Related
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
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'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
...
}
}
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)
}
}