Room database update creates unexpected UI side effects - android

When I simply Insert my data for the first time, all is well and when I reload my tiles, they get saved in the database and the tiles reload normally - see Animation 1.
When I try to update my tiles, I have this weird behaviour, like a loop - see Animation 2.
I am wondering what could be causing this. Here is my code:
#Entity (tableName = "saved_values_table")
data class SavedValues (
#PrimaryKey()
var index: Int,
var visible: Boolean,
var enabled: Boolean,
var boardLetters: String,
var boardLettersValue: Int
)
Dao:
#Update
suspend fun updateValues(values: SavedValues)
Repo:
suspend fun updateValues(savedValues: SavedValues) {
savedValuesDao.updateValues(savedValues)
}
ViewModel:
fun saveValues(index: Int, boardLetters: Array<Letter>) {
val savedValues = readAllValues.value
if (savedValues != null) {
if (savedValues.isEmpty()) {
addValues(
SavedValues(
index,
visible = true,
enabled = true,
boardLetters = boardLetters[index].name,
boardLettersValue = boardLetters[index].value
)
)
}
else {
updateValues(
SavedValues(
index,
visible = true,
enabled = true,
boardLetters = boardLetters[index].name,
boardLettersValue = boardLetters[index].value
)
)
}
}
}
What am I doing wrong? Thanks for any help!
UPDATE
I am adding some more code here as the problem persists, contrary to what I said in my comment.
My whole Dao:
#Dao
interface SavedValuesDao {
#Insert
suspend fun addValues(values: SavedValues)
#Query("SELECT * FROM saved_values_table")
fun readAllValues(): LiveData<List<SavedValues>>
#Query("SELECT boardLetters FROM saved_values_table")
fun readBoardLetters(): LiveData<List<String>>
#Query("SELECT boardLettersValue FROM saved_values_table")
fun readBoardLettersValues (): LiveData<List<Int>>
#Update
suspend fun updateValues(values: SavedValues)
#Query("DELETE FROM saved_values_table")
suspend fun deleteAllValues()
}
My Database:
#Database(
entities = [Word::class, SavedValues::class, SavedWords::class],
version = 8,
exportSchema = false
)
abstract class WordDatabase : RoomDatabase() {
abstract fun wordDAO(): WordDao
abstract fun savedValuesDao(): SavedValuesDao
}
My Repository:
val readAllValues: LiveData<List<SavedValues>> = savedValuesDao.readAllValues()
val readBoardLetters: LiveData<List<String>> = savedValuesDao.readBoardLetters()
val readBoardLettersValues: LiveData<List<Int>> =
savedValuesDao.readBoardLettersValues()
suspend fun addValues(savedValues: SavedValues) {
savedValuesDao.addValues(savedValues)
}
suspend fun updateValues(savedValues: SavedValues) {
savedValuesDao.updateValues(savedValues)
}
suspend fun deleteAllValues() {
savedValuesDao.deleteAllValues()
}
Thanks for any help!

Related

Why did livedata builder not invoked when data inserted to room

im really new on AAC and repository.
I have made an app with MVVM and repository.
Activity
class UserTestActivity : AppCompatActivity() {
private val userViewModel : UserViewModel by viewModel<UserViewModel>()
private lateinit var button : AppCompatButton
private var count : Int =0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_test
button = findViewById(R.id.testButton)
val userObserver = Observer<MutableList<UserModel>> { newUserList ->
Log.d("room-db-status", "size: "+newUserList.size)
}
userViewModel._user.observe(this, userObserver)
button.setOnClickListener(View.OnClickListener {
count++
Toast.makeText(this, "updated: "+count, Toast.LENGTH_SHORT).show()
userViewModel.insertUser(UserModel(
uid = count.toString(),
nickName = "Alexar",
gender ="female",
age = 22,
birth ="19990901",
mainResidence= "Seoul",
subResidence = "???",
tripWish = mutableListOf("!!!","!!?"),
tripStyle = mutableListOf("!!!","!!?"),
selfIntroduction = "hi -_-",
uriList = mutableListOf("!!!","!!?"),
geohash = "none",
latitude = 37.455,
longitude = 124.890,
mannerScore = 4.5,
premiumOrNot = false,
knock = 0
))
})
}
}
this is ViewModel
class UserViewModel (
private val userRepository : UserRepository): ViewModel() {
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
fun insertUser (userModel: UserModel) {
viewModelScope.launch(Dispatchers.IO) {
userRepository.insertUser(userModel)
}
}
}
Repositoty
interface UserRepository {
suspend fun getAllUser () : MutableList<UserModel>
suspend fun insertUser (userModel: UserModel)
}
RepositoryImpl
class UserRepositoryImpl (
private val localDataSource : UserLocalDataSource,
private val userMapper: UserMapper) :UserRepository{
override suspend fun getAllUser() : MutableList<UserModel> {
val userList : List<UserEntity> = localDataSource.getAllUser()
var temp = mutableListOf<UserModel>()
for (user in userList)
temp.add(userMapper.entityToModel(user))
return temp
}
override suspend fun insertUser(userModel: UserModel) {
return localDataSource.insertUser(userMapper.modelToEntity(userModel))
}
}
UserLocalDataSource
interface UserLocalDataSource {
suspend fun getAllUser () : MutableList<UserEntity>
suspend fun insertUser (userEntity: UserEntity)
}
UserLocalDataSourceImpl
class UserLocalDataSourceImpl(
private val appDatabase: AppDatabase) : UserLocalDataSource {
override suspend fun getAllUser() : MutableList<UserEntity> {
return appDatabase.userEntityDao().getAllUser()
}
override suspend fun insertUser(userEntity: UserEntity) {
appDatabase.userEntityDao().insertUser(userEntity)
}
}
UserEntityDao
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : MutableList<UserEntity>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) :UserEntity
#Insert (onConflict = REPLACE)
suspend fun insertUser (user : UserEntity)
#Query("DELETE FROM user WHERE uid = (:uid)")
suspend fun delete(uid : String)
}
there are also Mapper and Koin injection.
when trying to insert user data to room, it was successful. but
after that, liveData Builder
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
not invoked...
Of course, that builder is invoked only once when app started haha
who knows why??
I do not know.
You shouldn't rely on the liveData builder to be invoked when data is inserted/updated in DB. Please refer to this doc on how to work with liveData builder.
To achieve the case when an observer of LiveData is invoked when data is inserted/updated in DB, methods must return LiveData object in UserEntityDao:
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : LiveData<List<UserEntity>>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) : LiveData<UserEntity>
// ...
}

Android Room - Trying to query a single row based on Primary ID

What am I trying to achieve ?
Get a single row of data which has the id I need. The SQL equivalent of SELECT * FROM favs WHERE link='link'. I have written a fun named getOneFav() which for this. I am following the tutorial https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0 and code from https://github.com/android/sunflower
What have I setup so far ?
Entity
#Entity(tableName = "favs")
data class Favorite(
#PrimaryKey #ColumnInfo(name = "link") val link : String,
#ColumnInfo(name = "keywords") val keywords : String
)
DAO
#Dao
interface FavDAO {
#Query("SELECT * FROM favs")
fun getAllFavsLive(): Flow<List<Favorite>>
#Query("SELECT * FROM favs WHERE link = :link")
fun getOneFav(link: String): Favorite
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(link: Favorite)
}
Repository
class FavRepo (private val favDao: FavDAO) {
val allFavs: Flow<List<Favorite>> = favDao.getAllFavsLive()
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(link: Favorite) {
favDao.insert(link)
}
fun getOneFav(link: String) = favDao.getOneFav(link)
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun delete(link: String) {
favDao.delete(link)
}
}
ViewModel
class FavViewModel (private val repository: FavRepo) : ViewModel() {
val allFavs: LiveData<List<Favorite>> = repository.allFavs.asLiveData()
fun insert(link: Favorite) = viewModelScope.launch {
repository.insert(link)
}
fun getOneFav(link: String) = repository.getOneFav(link)
}
class FavViewModelFactory(private val repository: FavRepo) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(FavViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return FavViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
What problems am I facing ?
I am receiving an error saying
java.lang.RuntimeException: Unable to start activity ComponentInfo{[package name removed].MainActivity}: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
What have I tried so far ?
I have tried -
Adding suspend in front of the function getOneFav in DAO and Repository
Made the function run inside viewModelScope. It gave the same error as above. Also, this way the function returned a Job instead of the 'Favorite' data class object.
fun getOneFav(link: String) = viewModelScope.launch {
repository.getOneFav(link)
}
Followed this method here - How to implement a Room LiveData filter which even though worked, which seemed like an overkill for something so simple. Also despite the fact that the code is using MutableLiveData, I wasn't able to see any triggers when the insert happened.
You should run your queries in a different context:
class FavRepo (private val favDao: FavDAO) {
val allFavs: Flow<List<Favorite>> = withContext(Dispatchers.IO) {
favDao.getAllFavsLive()
}
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(link: Favorite) = withContext(Dispatchers.IO) {
favDao.insert(link)
}
fun getOneFav(link: String) = withContext(Dispatchers.IO) {
favDao.getOneFav(link)
}
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun delete(link: String) = withContext(Dispatchers.IO) {
favDao.delete(link)
}
}

Room not updating entities with #Update(onConflict = OnConflictStrategy.REPLACE)

My application uses Google Places API which data I later use to get weather from openweather.
I have a SearchFragment with RecyclerView where this happens.
Inside SearchFragment I observe the list I'm getting:
viewModel.predictions.observe(viewLifecycleOwner) {
citiesAdapter.submitList(it)
}
<...>
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment_weather, menu)
<...>
searchView.onQueryTextChanged {
viewModel.searchQuery.value = it
}
}
My viewModel:
class SearchViewModel #Inject constructor(
private val repository: AutocompleteRepository,
private val weatherRepository: WeatherRepository
) : ViewModel() {
fun provideClient(client: PlacesClient) {
repository.provideClient(client)
}
val searchQuery = MutableStateFlow("")
private val autocompleteFlow = searchQuery.flatMapLatest {
repository.getPredictions(it)
}
val predictions = autocompleteFlow.asLiveData()
fun onAddPlace(place: PlacesPrediction, added: Boolean) {
viewModelScope.launch {
repository.update(place, added)
if (added) weatherRepository.addWeather(place)
else weatherRepository.delete(place)
}
}
fun onDestroy() = viewModelScope.launch {repository.clearDb()}
}
Inside adapter I bind my items like this:
inner class CityViewHolder(private val binding: ItemCityToAddBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
btnAdd.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val place = getItem(position)
btnAdd.animate().rotation(if (place.isAdded) 45f else 0f).start()
println("Current item state (isAdded): ${place.isAdded}")
listener.onAddClick(place, !place.isAdded)
}
}
}
}
fun bind(prediction : PlacesPrediction) {
binding.apply {
val cityName = prediction.fullText.split(", ")[0]
locationName.text = cityName
fullName.text = prediction.fullText
btnAdd.animate().rotation(if (prediction.isAdded) 45f else 0f).start()
}
}
}
Where listener is passed to my adapter as a parameter from my fragment:
override fun onAddClick(place: PlacesPrediction, isAdded: Boolean) {
viewModel.onAddPlace(place, isAdded)
println("Parameter passed to onClick: $isAdded, placeId = ${place.placeId}")
}
<...>
val citiesAdapter = CitiesAdapter(this)
My repository's update() method looks like this:
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().update(place.copy(isAdded = added))
And finally, my dao's update:
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(prediction: PlacesPrediction)
This is all tied up on PlacesPrediction class, an here it is:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
val isAdded: Boolean = false
) {
#PrimaryKey(autoGenerate = true) var id: Int = 0
}
So, my problem is that PlacesPredictions entries in my database are not getting updated. Actually, the only field I want to update with the code provided above is isAdded, but it stays the same after I press btnAdd of my list item. I used Android Studio's Database Inspector to verify that.
I tried using #Insert instead like so:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(prediction: PlacesPrediction)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added))
But strangely it only inserts a copy of place, the original item I clicked on stays the same.
Workaround
I get the desired behavior only if I hack my way to it:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
var isAdded: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added, id = place.id))
And I don't like this soution at all. So my question is: how do I make #Update work?
As you probably already understood, the generated copy method of data classes ignores all members declared outside the constructor. So place.copy(isAdded = added) will generate a copy of all constructor parameters, but leave the id as the default 0, meaning a new element should be inserted, instead of updating an existing one.
Now this is my personal opinion:
Having the id as constructor parameter is the most elegant solution, as updates will work out of the box.
However if you dislike it that much, maybe an extension function might help you:
inline fun PlacesPrediction.preserveId(copyBuilder: PlacesPrediction.() -> PlacesPrediction): PlacesPrediction{
val copy = copyBuilder(this)
copy.id = this.id
return copy
}
//usage
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.preserveId { copy(isAdded = added) })

I can't insert data one table to another table in room

Ín my app's room database I have a table called movie_table. When the user clicks the item, it is saved to another table called favorite_table. I tried almost two days but I can't solve it. I am new to MVVM.
For better understanding please see the code:
FavoriteMovieDao.kt
#Dao
interface FavoriteMovieDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFavMovie(favorite: Favorite)
#Query("SELECT * FROM favorite_table WHERE id LIKE :id")
suspend fun getFavoriteMovieById(id:Int): Favorite
#Query("SELECT * FROM favorite_table")
suspend fun getAllFavoriteMovie(): List<Favorite>
#Delete
suspend fun deleteFavorite(favorite: Favorite)
}
Repository.kt
class Repository(context: Context) {
private val favoriteMovieDao: FavoriteMovieDao = MovieDatabase.invoke(context).getFavoriteMovieDao()
suspend fun insertFavMovie(favorite: Favorite){
favoriteMovieDao.insertFavMovie(favorite)
}
MovieDetailsViewModel.kt
class MovieDetailsViewModel(private val repository: Repository):ViewModel() {
private val favMovieResponse:MutableLiveData<List<Favorite>> = MutableLiveData()
val actorResponse:MutableLiveData<List<Movie>> = MutableLiveData()
private val insertFavMovie:MutableLiveData<Favorite> = MutableLiveData()
fun actorDetail(){
viewModelScope.launch {
val actor = repository.getAllMovieDB()
actorResponse.value = actor
}
}
fun insertFavMovie(favorite: Favorite){
viewModelScope.launch {
//i can't insert this
val insertFav = repository.insertFavMovie(favorite)
insertFavMovie.value = insertFav
}
}
}
}
MovieDetailsActivity.kt
class MovieDetailsActivity : AppCompatActivity(){
private lateinit var actorViewModel: MovieDeatilsViewModel
private val actorAdapter by lazy { ActorAdapter() }
private var isFav: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_details)
val repository = Repository(this)
val viewModelFactory = MovieDetailsViewModelFactory(repository)
actorViewModel = ViewModelProvider(this,viewModelFactory).get(MovieDeatilsViewModel::class.java)
setUpPostRecyclerView()
actorViewModel.actorDetail()
actorViewModel.actorResponse.observe(this, Observer {actorList ->
actorAdapter.setData(actorList)
})
initBundle()
favBtn.setOnClickListener {
if (isFav){
isFav = false
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#E4C1C1"))
Log.d("msg","Not Favorite!")
}else{
isFav = true
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#FFC107"))
Log.d("msg","Favorite!")
}
}
}
private fun initBundle() {
val bundle:Bundle? = intent.extras
movieTitle.text = bundle!!.getString("title")
directorbc.text = bundle.getString("director")
genre.text = bundle.getString("genre")
releaseYear.text = bundle.getInt("year").toString()
language.text = bundle.getString("language")
country.text = bundle.getString("country")
rating.text = bundle.getString("rating")
plotText.text = bundle.getString("plot")
//moviePosterD.setImageDrawable(bundle!!.getString("image"))
Glide.with(this).load(bundle.getString("image")).into(moviePosterD);
val player = SimpleExoPlayer.Builder(this).build()
player.preparePlayer(movieView)
player.setSource(applicationContext, "http://html5videoformatconverter.com/data/images/happyfit2.mp4")
}
private fun setUpPostRecyclerView(){
actorRecyclerview.adapter = actorAdapter
actorRecyclerview.layoutManager = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false)
}
}
How can I insert it? Please help me. Thank you
In Movies table, to handle the favorite movies, you can make use of a variable in each movie item which you can for example set to true or false. This allows you to query using filters to show them on the screen. I don't think you need another table to store Favourites. I hope that helps! I can help you with the design if you share more details there.
#Entity
data class Movie( val title: String, val isFavourite: Boolean = false)
Update 1:
When user clicks on favButton, in MovieViewModel update the Movie entity like this:
fun setMovie(movie: Movie){
_movie.value = movie
}
fun updateMovie(movie: Movie) {
movie.isFavourite = !movie.isFavourite
viewModelScope.launch {
repository.updateMovie(movie)
setMovie(movie)
}
}
in MovieDao
#Update
suspend fun updateMovie(movie: Movie)
Update 2:
In MoviesDao
#Update
suspend fun updateMovie(movie: Movie)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAll(vararg movie: Movie)
#Query("select * from movies_table where isFavourite = 1")
fun getFavMovies(): LiveData<List<Movie>>
#Query("Select * from movies_table")
fun getAllMovies(): LiveData<List<Movie>>
In Repository
val favMovies: LiveData<List<Movie>> = moviesDao.getFavMovies()
Update 3:
favBtn.setOnClickListener {
_viewModel.updateMovie(movie)
}
_viewModel.movie.observe(viewLifecycleOwner, Observer {
//Update UI based on isFavourite value on Movie
})

Kotlin observeForever every time is null

i'm trying realise some global object for my app and observe inside to some LiveData from my DAO. Here is how it looks:
object appCommon {
#Volatile
lateinit var config: Config
suspend fun initAppCommon() {
GlobalScope.launch(Dispatchers.Main) {
Database_mW.getDBInstance(AppController.appInstance).ConfigDAO().getLive()
.observeForever(Observer {
config = it
})
}
initConfig()
}
private suspend fun initConfig() {
withContext(Dispatchers.IO) {
if (Database_mW.getDBInstance(AppController.appInstance).ConfigDAO().get() == null) {
Database_mW.getDBInstance(AppController.appInstance).ConfigDAO().insert(Config(getDeviceSerialNumber(), "", "", ""))
}
}
}
}
Here is DAO:
#Dao
interface Config_dao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(config: Config)
#Update
fun update(config: Config)
#Query("DELETE FROM Config")
fun clear()
#Query("SELECT * from Config LIMIT 1")
fun get(): Config
#Query("SELECT * from Config LIMIT 1")
fun getLive(): LiveData<Config>
#Query("update Config set device_secret = :secret")
fun updateSecret(secret: String)
}
So when i'm calling ConfigDAO().insert, observer is invoking but it = null.
Can somebody explain how to get latest inserted object inside observer for to update my global var?

Categories

Resources