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

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

Related

execute room db queries async using coroutine

what is the problem with the codes
db queries freeze ui.
every thing works , but freezing ui when running db query
what is the point that i dont know .
all queries make ui freeze,
when executing Delete query , recycleview animation freezing ,but after commenting the line that execute query , recycleview work smoothly
hilt Module:
#Module
#InstallIn(SingletonComponent::class)
object DatabaseModule {
#Provides
fun provideCardDao(passwordDatabase: PasswordDatabase): CardDao {
return passwordDatabase.cardDao()
}
#Singleton
#Provides
fun providesCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
#Provides
#Singleton
fun providePasswordDatabase(
#ApplicationContext appContext: Context,
coroutineScope: CoroutineScope
): PasswordDatabase {
return Room.databaseBuilder(
appContext,
PasswordDatabase::class.java,
"mydb.db"
)
.addMigrations()
.addCallback(MYDatabaseCallback(coroutineScope))
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.fallbackToDestructiveMigration()
.build()
}
}
card Dao
#Dao
interface CardDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCardDetails(cardDetails : BanksCardModel)
#Query("DELETE FROM CardDetailsTable WHERE id = :id")
suspend fun deleteCardDetails(id : Int)
#Query("SELECT * FROM CardDetailsTable WHERE id = :id")
fun getOneCardDetails(id : Int):LiveData<BanksCardModel>
#Query("SELECT * FROM CardDetailsTable")
fun getAllCardDetails() : LiveData<List<BanksCardModel>>
#Query("SELECT * FROM CardDetailsTable WHERE username LIKE '%' || :str || '%' OR bank_name LIKE '%' || :str || '%' "
)
fun queryOnCards(str:String): LiveData<List<BanksCardModel>>
}
password db
#Database(entities = [ BanksCardModel::class],version = 15,exportSchema = false)
abstract class PasswordDatabase : RoomDatabase() {
abstract fun cardDao() : CardDao
}
repository
#Singleton
class Repository #Inject constructor(private val cardDao: CardDao) {
suspend fun insertCardDetails(cardDetailsItem: BanksCardModel){
cardDao.insertCardDetails(cardDetailsItem)
}
suspend fun deleteCardDetails(id : Int){
cardDao.deleteCardDetails(id)
}
fun getOneCardDetails(id : Int):LiveData<BanksCardModel>{
return cardDao.getOneCardDetails(id)
}
fun getAllCardDetails() : LiveData<List<BanksCardModel>>{
return cardDao.getAllCardDetails()
}
fun queryOnCards(str : String) : LiveData<List<BanksCardModel>>{
return cardDao.queryOnCards(str)
}
}
viewModel
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val repository: Repository
) : ViewModel() {
private var cardDetailsList : LiveData<List<BanksCardModel>>
init {
cardDetailsList = repository.getAllCardDetails()
}
fun insertCardDetails(cardDetailsItem: BanksCardModel)= viewModelScope.launch {
repository.insertCardDetails(cardDetailsItem)
}
fun deleteCardDetails(id : Int)= viewModelScope.launch {
repository.deleteCardDetails(id)
}
fun getOneCardDetails(id : Int):LiveData<BanksCardModel>{
return repository.getOneCardDetails(id)
}
fun getAllCardDetails() : LiveData<List<BanksCardModel>>{
return cardDetailsList
}
fun queryOnCards(str:String) : LiveData<List<BanksCardModel>>{
return repository.queryOnCards(str)
}
}
////edited
query in searchview
override fun onQueryTextChange(newText: String?): Boolean {
lifecycleScope.launch {
viewModel.queryOnCards(newText?:"").collect{
val searchedList = it.toMutableList()
showingEmptyListAnnounce(searchedList)
adapter.updateList(searchedList)
}
}
return true
}
the update list function in onQueryTextChange -- its using DiffUtils
fun updateList( updatedList : MutableList<BanksCardModel>){
val callback = CustomCallback(mList, updatedList)
val result = DiffUtil.calculateDiff(callback)
mList.clear()
mList.addAll(updatedList)
result.dispatchUpdatesTo(this)
}
video of screen
LiveData is outdated. Please consider to use Kotlin Flow.
Here is your use case:
lifecycleScope.launch {
dao.getData().collect { data ->
//handle data here
}
}
#Dao
interface MyDao {
#Query("SELECT * FROM somedata_table")
fun getData(): Flow<List<SomeData>>
}
Reference
There is a good Google codelab to practice Kotlin coroutine and Room queries
Also please consider to use StrictMode - it will show you this and similar issues in future.

Room database update creates unexpected UI side effects

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!

Insert data with room from view : Cannot access database on the main thread

I am a total beginner with kotlin and android development. I followed Persist data with Room tutorial and I have a popular problem to save my data:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
I understand why by reading other responses. But I don't understand how to deal with it.
My entity and its DAO :
#Entity
data class AccountConfiguration(
#PrimaryKey val server_address: String,
#ColumnInfo(name = "user_name") val user_name: String,
#ColumnInfo(name = "password") val password: String, // FIXME : secure storage
#ColumnInfo(name = "notify_hungry") val notify_hungry: Boolean,
#ColumnInfo(name = "notify_thirsty") val notify_thirsty: Boolean,
#ColumnInfo(name = "notify_ap") val notify_ap: Boolean,
#ColumnInfo(name = "network_grab_each") val network_grab_each: Int,
)
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun get(): Flow<AccountConfiguration>
#Query("DELETE FROM accountconfiguration")
fun clear()
#Insert
fun insert(account_configuration: AccountConfiguration)
}
Its view model :
class AccountConfigurationViewModel(private val repository: AccountConfigurationRepository) : ViewModel() {
val accountConfiguration: LiveData<AccountConfiguration> = repository.accountConfiguration.asLiveData()
fun insert(account_configuration: AccountConfiguration) = viewModelScope.launch {
repository.update(account_configuration)
}
fun isEntryValid(server_address: String, user_name: String, password: String, network_grab_each: Int): Boolean {
if (server_address.isBlank() || user_name.isBlank() || password.isBlank() || network_grab_each < 5 * 60) {
return false
}
return true
}
}
The database and Application.
Then, concerned part of the view (entire file here) :
class AccountConfigurationFragment : Fragment() {
private var _binding: AccountConfigurationFragmentBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: AccountConfigurationViewModel by activityViewModels {
AccountConfigurationViewModelFactory(
(activity?.application as RollingDashboardApplication).account_configuration_repository
)
}
lateinit var accountConfiguration: AccountConfiguration
// [...] hidden code
private fun save() {
Toast.makeText(context, R.string.saving, Toast.LENGTH_LONG).show()
if (isEntryValid()) {
val networkGrabEach = 3600 // FIXME : determine value for real
viewModel.insert(
AccountConfiguration(
server_address = binding.textInputServerAddress.text.toString(),
// [...] hidden code
network_grab_each = networkGrabEach,
)
)
Toast.makeText(context, R.string.account_configuration_saved, Toast.LENGTH_LONG).show()
findNavController().navigate(R.id.action_AccountConfigurationFragment_to_DashboardFragment)
} else {
Toast.makeText(context, R.string.wrong_inputs, Toast.LENGTH_LONG).show()
}
}
// [...] hidden code
}
I don't understand how this call, viewModel.insert( can be asynchronous. Note AccountConfigurationRepository.update is already a suspend function. I tried by modifying like this without success (same error) :
- fun insert(account_configuration: AccountConfiguration) = viewModelScope.launch {
+ suspend fun insert(account_configuration: AccountConfiguration) = viewModelScope.launch {
lifecycleScope.launch { // coroutine on Main
viewModel.insert(
// ...
How can I make this database insert non blocking for UI ?
You have to make your DAO methods suspend, so they don't block the UI thread
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun get(): Flow<AccountConfiguration>
#Query("DELETE FROM accountconfiguration")
suspend fun clear() //make this suspend
#Insert
suspend fun insert(account_configuration: AccountConfiguration) //make this suspend
}
I've tried your github code, and you've to uncomment implementation "androidx.room:room-runtime:$room_version".
I think there's a bug in Room 2.3.0 as it's giving Not sure how to handle query method's return type (java.lang.Object). error, on adding suspend keyword to DAO. You should use Room 2.4.0-beta02
def room_version = "2.4.0-beta02"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
Room DAO functions should always suspend so it can't block the main UI thread, you can refer this google recommended example:
https://developer.android.com/codelabs/android-room-with-a-view-kotlin#5

Save remote data separately related tables when using NetworkBoundRepository with coroutines (android)

I want to use Single source of truth principle in my application. How can I add multiple table when using NetworkBoundRepository.
MainApi.kt
interface MainApi {
#GET("main")
suspend fun getMain(): Response<MainResponse>
}
MainResponse.kt
#JsonClass(generateAdapter = true)
data class MainResponse(
#Json(name = "categories") val categoryList: List<Category>,
#Json(name = "locations") val locationList: List<Location>,
#Json(name = "tags") val tagList: List<Tag>
)
NetworkBoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<Resource<RESULT>> {
emit(Resource.Success(fetchFromLocal().first()))
val apiResponse = fetchFromRemote()
val remoteCategories = apiResponse.body()
if (apiResponse.isSuccessful && remoteCategories != null) {
saveRemoteData(remoteCategories)
} else {
emit(Resource.Failed(apiResponse.message()))
}
emitAll(
fetchFromLocal().map {
Resource.Success<RESULT>(it)
}
)
}.catch { e ->
emit(Resource.Failed("Network error! Can't get latest categories."))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal(): Flow<RESULT>
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
MainRepository.kt
#ExperimentalCoroutinesApi
class MainRepository #Inject constructor(
private val mainApi: MainApi,
private val categoryDao: CategoryDao,
private val locationDao: LocationDao,
private val tagDao: TagDao
) {
suspend fun getMain(): Flow<Resource<List<Category>>> {
return object : NetworkBoundRepository<List<Category>, List<Category>>() {
override suspend fun saveRemoteData(response: List<Category>) = categoryDao.insertList(response)
override fun fetchFromLocal(): Flow<List<Category>> = categoryDao.getList()
override suspend fun fetchFromRemote(): Response<List<Category>> = mainApi.getMain()
}.asFlow()
}
}
Currently NetworkBoundRepository and MainRepository only works with categories. I want to fetch some data from internet and save each data to related tables in database. App must be offline first.
How can I add locationDao, tagDao to MainRepository?
I don't quite follow your question. You are adding locationDao and tagDao to MainRepository already here:
class MainRepository #Inject constructor(
...
private val locationDao: LocationDao,
private val tagDao: TagDao
)
If you are asking how to provide them in order for them to injectable via Dagger2 you have to either define dao constructor as #Inject or add #Provides or #Binds annotated methods with the relevant return type to the needed #Module, and tangle them in the same #Scope - more here
If you asking how to use those repos in your functions it is also easy:
object : NetworkBoundRepository<List<Category>, MainResponse>() {
override suspend fun saveRemoteData(response: MainResponse) = response?.run{
categoryDao.insertList(categoryList)
locationDao.insertList(locationList)
tagDao.insertList(tagList)
}
override fun fetchCategoriesFromLocal(): Flow<List<Category>> = categoryDao.getList()
override fun fetchLocationsFromLocal(): Flow<List<Location>> = locationDao.getList()
override fun fetchTagsFromLocal(): Flow<List<Tag>> = tagDao.getList()
override suspend fun fetchFromRemote(): Response<MainResponse> = mainApi.getMain()
//This function is not tested and written more like a pseudocode
override suspend fun mapFromLocalToResponse(): Flow<MainResponse> = fetchCategoriesFromLocal().combine(fetchLocationsFromLocal(), fetchTagsFromLocal()){categories, locations, tags ->
MainResponse(categories,locations,tags)
}
}
Maybe some more adjustments will be needed. But the main problem of your code is that you are trying to combine all the different entities into one repo and it is not very good(and the request that returns all the stuff under one response is not good either) - I would suggest to split it somehow not to mix it all.

Using the repository functions in the viewModel with coruntineScope in android Room

Working with Androind and Room for the first time, and i was able to follow a few codelabs and tutorials to achieve inserting and showing a list of my entities, but i cant seem to be able to use my other Repository methods in my ViewModel due to a type mismatch, here is my ViewModel file
class CustomerViewModel(application: Application) : AndroidViewModel(application) {
// The ViewModel maintains a reference to the repository to get data.
private val repository: CustomerRepository
// LiveData gives us updated words when they change.
val allCustomers: LiveData<List<Customer>>
init {
// Gets reference to Dao from db to construct
// the correct repo.
val customerDao = AppDatabase.getInstance(application).customerDao()
repository = CustomerRepository(customerDao)
allCustomers = repository.getCustomers()
}
fun insert(customer: Customer) = viewModelScope.launch {
repository.insert(customer)
}
}
and im trying to add a method like
fun find(id: Int) = viewModelScope.launch {
return repository.getCustomerByLocalId(id)
}
but the ide says there's a type mismatch here? Required: Customer, Found: Job
here is my repository:
class CustomerRepository(private val customerDao: CustomerDao) {
fun getCustomers(): LiveData<List<Customer>> = customerDao.getAlphabetizedCustomers()
suspend fun getCustomerByLocalId(local_Id: Int): Customer =
customerDao.customerByLocalId(local_Id)
suspend fun insert(customer: Customer) {
customerDao.insert(customer)
}
companion object {
// For Singleton instantiation
#Volatile
private var instance: CustomerRepository? = null
fun getInstance(customerDao: CustomerDao) =
instance ?: synchronized(this) {
instance ?: CustomerRepository(customerDao).also { instance = it }
}
}
}
methods in CustomerDao
#Query("SELECT * from customers ORDER BY name ASC")
fun getAlphabetizedCustomers(): LiveData<List<Customer>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(customer: Customer)
#Query("SELECT * FROM customers WHERE local_id = :localId")
suspend fun customerByLocalId(localId: Int): Customer
EDIT
I tried #lena-bru 's suggestion but the error is still there, there appears to be 2 different ones, the type mismatch and that there should not be a return. are you supposed to create this method in a different location?
The IDE error
change this:
fun find(id: Int) = viewModelScope.launch {
return repository.getCustomerByLocalId(id)
}
to this:
fun find(id: Int): Customer = viewModelScope.launch {
withContext(Dispatchers.IO){
repository.getCustomerByLocalId(id)
}
}
Your find method as defined above is void, it needs to return type Customer
Also you need to provide a context, and remove the return keyword

Categories

Resources