how to save response to room database correctly? - android

Hi all I am developing news app I have implemented room database with kotlin coroutines what I want to achieve I want to fetch data first then that respose I want to save to room database but data not showing at all below my database implementation
#Database(entities = [Article::class], version = 1, exportSchema = false)
#TypeConverters(SourceTypeConverters::class)
abstract class SportNewsDatabase : RoomDatabase() {
abstract fun sportNewsDao(): SportNewsDao
companion object {
private var instance: SportNewsDatabase? = null
fun getInstance( context: Context): SportNewsDatabase? {
if (instance == null) {
synchronized(SportNewsDatabase::class.java) {
instance = Room.databaseBuilder(context.applicationContext, SportNewsDatabase::class.java, "article_database")
.fallbackToDestructiveMigration()
.build()
}
}
return instance
}
}
}
SportNewsDao.kt
#Dao
interface SportNewsDao {
#Query("SELECT * FROM Article")
fun getAllData(): LiveData<List<Article>>
#Insert
suspend fun addAll(article: List<Article>)
#Update
suspend fun updateArticle(article: Article)
#Delete
suspend fun deleteArticle(article: Article)
}
below NewsRepository.kt
class NewsRepository(private val sportNewsApi: SportNewsInterface, private val sportNewsDao: SportNewsDao) {
val data = sportNewsDao.getAllData()
suspend fun refresh() {
withContext(Dispatchers.IO) {
val articles = sportNewsApi.getNewsAsync().body()?.articles
if (articles != null) {
sportNewsDao.addAll(articles)
}
}
}
}
below MainViewModel.kt
#Suppress("UNCHECKED_CAST")
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
// Coroutine's background job
val job = Job()
// Define default thread for Coroutine as Main and add job
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
val showLoading = MutableLiveData<Boolean>()
val sportList = MutableLiveData<List<Article>>()
val showError = SingleLiveEvent<String>()
fun loadNews() {
// Show progressBar during the operation on the MAIN (default) thread
showLoading.value = true
// launch the Coroutine
launch {
// Switching from MAIN to IO thread for API operation
// Update our data list with the new one from API
val result = withContext(Dispatchers.IO) {
newsRepository?.data
newsRepository.refresh()
}
}
}
}
I want to know where I am making mistake what I have to do in order to save response to database correctly and show data to my android application.

This my help you debug what is going on.
You can get the created DB file on your phone with the Device File Explorer
View -> Tool Windows -> Device File Explorer.
Then navigate to where your database is and download them on your pc.
Download a SQLite DB browser and inspect the DB.

Related

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

How to implement room database with coroutines correctly?

I am developing news android and I already implemented database logic with room what I want to achieve I want fetch data first and then save to room database but I have implemented that logic already when I run the application it is showing white screen only progress bar loading
below screenshot of android app
below my MainViewmModel.kt class
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
// Coroutine's background job
val job = Job()
// Define default thread for Coroutine as Main and add job
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
val showLoading = MutableLiveData<Boolean>()
val sportList = MutableLiveData<List<Article>>()
val showError = SingleLiveEvent<String>()
fun loadNews() {
// Show progressBar during the operation on the MAIN (default) thread
showLoading.value = true
// launch the Coroutine
launch {
// Switching from MAIN to IO thread for API operation
// Update our data list with the new one from API
val result = withContext(Dispatchers.IO) {
newsRepository?.data
newsRepository.refresh()
}
}
}
}
below my NewsRepository where I am fetching data and saving it to database
class NewsRepository(private val sportNewsApi: SportNewsInterface, private val sportNewsDao: SportNewsDao) {
val data = sportNewsDao.getAllData()
suspend fun refresh() {
withContext(Dispatchers.IO) {
val articles = sportNewsApi.getNewsAsync().body()?.articles
if (articles != null) {
sportNewsDao.addAll(articles)
}
}
}
}
below NewsDao.kt
#Dao
interface SportNewsDao {
#Query("SELECT * FROM Article")
fun getAllData(): LiveData<List<Article>>
#Insert
suspend fun addAll(article: List<Article>)
#Update
suspend fun updateArticle(article: Article)
#Delete
suspend fun deleteArticle(article: Article)
}
below SportNewsDatabase.kt
#Database(entities = [Article::class], version = 1, exportSchema = false)
#TypeConverters(SourceTypeConverters::class)
abstract class SportNewsDatabase : RoomDatabase() {
abstract fun sportNewsDao(): SportNewsDao
companion object {
private var instance: SportNewsDatabase? = null
fun getInstance( context: Context): SportNewsDatabase? {
if (instance == null) {
synchronized(SportNewsDatabase::class.java) {
instance = Room.databaseBuilder(context.applicationContext, SportNewsDatabase::class.java, "article_database")
.fallbackToDestructiveMigration()
.build()
}
}
return instance
}
}
}
I want to know where I am making mistake why app showing empty white screen

how save data to database after fetching data from server using room in kotlin coroutines?

I am developing news app I want to save data to database after fetching data from server using room but I am just confused I have followed tutorial but their logics is different how can I save data properly so that after getting data from server and I can save data to database?
below my SportNewsDatabase.kt
#Database(entities = [Article::class], version = 1, exportSchema = false)
abstract class SportNewsDatabase : RoomDatabase() {
abstract fun sportNewsDao(): SportNewsDao
companion object {
private var instance: SportNewsDatabase? = null
fun getInstance( context: Context): SportNewsDatabase? {
if (instance == null) {
synchronized(SportNewsDatabase::class.java) {
instance = Room.databaseBuilder(context.applicationContext, SportNewsDatabase::class.java, "article_database")
.fallbackToDestructiveMigration()
.build()
}
}
return instance
}
}
}
below SportNewsDao.kt
#Dao
interface SportNewsDao {
#Query("SELECT * FROM article")
fun getAllData(): LiveData<List<Article>>
#Insert
suspend fun addAll(article: List<Article>)
#Update
suspend fun updateArticle(article: Article)
#Delete
suspend fun deleteArticle(article: Article)
}
below my repository class
interface NewsRepository {
// Suspend is used to await the result from Deferred
suspend fun getNewsList(): UseCaseResult<List<Article>>
var sportNewsDao: SportNewsDao
}
fun insertArticle(article: List<Article>){
val insertArticles
}
#Suppress("UNCHECKED_CAST")
class NewsRepositoryImpl(private val sportsNewsApi: SportNewsInterface) : NewsRepository {
override suspend fun getNewsList(): UseCaseResult<List<Article>> {
return try {
val result = sportsNewsApi.getNewsAsync().body()!!.articles
UseCaseResult.Success(result)
} catch (ex: Exception) {
UseCaseResult.Error(ex)
}
}
}
below MainViewModel.kt where I am using Kotlin coroutines
#Suppress("UNCHECKED_CAST")
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
// Coroutine's background job
val job = Job()
// Define default thread for Coroutine as Main and add job
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
val showLoading = MutableLiveData<Boolean>()
val sportList = MutableLiveData<List<Article>>()
val showError = SingleLiveEvent<String>()
fun loadNews() {
// Show progressBar during the operation on the MAIN (default) thread
showLoading.value = true
// launch the Coroutine
launch {
// Switching from MAIN to IO thread for API operation
// Update our data list with the new one from API
val result = withContext(Dispatchers.IO) {
newsRepository?.getNewsList()
}
// Hide progressBar once the operation is done on the MAIN (default) thread
showLoading.value = false
when (result) {
is UseCaseResult.Success<*> -> {
sportList.value = result.data as List<Article>
}
is Error -> showError.value = result.message
}
}
}
override fun onCleared() {
super.onCleared()
// Clear our job when the linked activity is destroyed to avoid memory leaks
job.cancel()
}
}
#sashabeliy
Hi, i think i have the solution of your problem but i am not a expert in mvvm and coroutine so if someone have a better solution i take it :)
For my app i launch the the download in the main thread and only when i get the response i use Dispatchers.IO for insert in Room the data received.
In ViewModel
fun downLoadPDV(last_update : String) : LiveData<List<PDV>> {
return repositoryPDV.downloadPDV(last_update)
}
In Repository
fun downloadPDV(last_update : String ): LiveData<List<PDV>> {
val mutableLiveData: MutableLiveData<List<PDV>> = MutableLiveData()
val apiService: ApiInterface = RetrofitClient.getRetrofitClient().create(ApiInterface::class.java)
apiService.CallListPDV(last_update)?.enqueue(object :
Callback<List<PDV>> {
override fun onResponse(
call: Call<List<PDV>>,
response: Response<List<PDV>>
) {
Log.e(ContentValues.TAG, "Download PDV Success => $response")
if (response.isSuccessful && response.body() != null) {
mutableLiveData.setValue(response.body());
GlobalScope.launch(Dispatchers.IO) {
for (pdv in response.body()!!) {
insertPDV(pdv)
}
}
}
}
override fun onFailure(call: Call<List<PDV>>, t: Throwable) {
Log.e(ContentValues.TAG, "[Error] Download PDV Fail --> $call")
}
})
return mutableLiveData
}
I return the Data download for update the UI and inform the user of how many items was downloaded

Room Database Operation wont crash after callling on main thread

I am using Coroutines to perform db operations on my Room database
I have create a helper for Coroutines as follows
object Coroutines {
fun main(work: suspend(() -> Unit)) = CoroutineScope(Dispatchers.Main).launch {
work()
}
fun io(work: suspend(() -> Unit)) = CoroutineScope(Dispatchers.IO).launch {
work()
}
}
Following is the code where i call my insert operation on the main thread
class LocalListViewModel(private val localVideoRepository: LocalVideoRepository) : ViewModel() {
val localVideos = MutableLiveData<ArrayList<LocalVideo?>?>()
fun insertAllLocalVideo() {
Coroutines.main {
localVideoRepository.insertLocalVideo(localVideos.value)
}
}
}
class LocalVideoRepository(private val db: AppDatabase) {
suspend fun insertLocalVideo(localVideos: ArrayList<LocalVideo?>?) =
db.getLocalVideoDao().insertLocalVideos(localVideos)
}
#Dao
interface LocalVideoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLocalVideos(localVideoList: ArrayList<LocalVideo?>?)
}
#Database(entities = [LocalVideo::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getLocalVideoDao(): LocalVideoDao
companion object {
#Volatile
private var instance: AppDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also { instance = it }
}
private fun buildDatabase(context: Context) = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
context.getString(R.string.psplayer_db)
).build()
}
}
What i don't understand is even after calling on main thread of Coroutines, the data gets inserted successfully,instead of crashing?
For an in-depth answer look at the CoroutinesRoom.execute helper:
suspend fun <R> execute(
db: RoomDatabase,
inTransaction: Boolean,
callable: Callable<R>
): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
// use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
Unless it already is in a transaction it always moves the execution of the actual query onto a IO-optimized dispatcher. So regardless of the calling context a suspend room definition will never execute on the application thread.

Android app retrieve data from server, save in database and display to user

I'm rewriting an app that involves retrieving data from a server via REST, saving that to the database on each Android device, and then displaying that data to the user. The data being retrieved from the server has a "since" parameter, so it won't return all data, just data that has changed since the last retrieval.
I have the retrieval from the server working fine, but I'm not sure the best way to save that data to the database, then show it to the user. I'm using Kotlin, Retrofit, Room and LiveData.
The code below is a simplified version of what I'm actually doing, but it gets the point across.
MyData.kt (model)
#Entity(tableName = "MyTable")
data class MyData(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var id Int? = null,
#SerializedName("message")
#ColumnInfo(name = "message")
var message: String? = null
) {
companion object {
fun fromContentValues(values: ContentValues): MyData {
val data = MyData()
// Do this for id and message
if (values.containsKey("id") {
data.id = values.getAsInteger("id")
}
}
}
}
DataViewModel.kt
class DataViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DataRepository()
fun data(since: Long) =
liveData(Dispatchers.IO) {
val data = repository.getDataFromServer(since)
emit(data)
}
fun saveData(data: List<MyData>) =
liveData(Dispatchers.Default) {
val result = repository.saveDataToDatabase(data)
emit(result)
}
fun data() =
liveData(Dispatchers.IO) {
val data = repository.getDataFromDatabase()
emit(data)
}
}
DataRepository.kt
class DataRepository(application: Application) {
// I won't add how the Retrofit client is created, it's standard
private var client = "MyUrlToGetDataFrom"
private var myDao: MyDao
init {
val myDatabase = MyDatabase.getDatabase(application)
myDao = myDatabase!!.myDao()
}
suspend fun getDataFromServer(since: Long): List<MyData> {
try {
return client.getData(since)
} catch (e: Exception) {
}
}
fun getDataFromDatabase(): List<MyData> = myDao.getAll()
suspend fun insertData(data: List<MyData>) =
myDao.insertData(data)
}
MyDao.kt
#Dao
interface PostsDao {
#Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
suspend fun getAllData(): List<MyData>
#Insert
suspend fun insertData(data: List<MyData>)
}
ListActivity.kt
private lateinit var mDataViewModel: DataViewModel
override fun onCreate(savedInstanceBundle: Bundle?) {
super.onCreate(savedInstanceBundle)
mDataViewModel = ViewModelProvider(this, DataViewModelFactory(contentResolver)).get(DataViewModel::class.java)
getData()
}
private fun getData() {
mDataViewModel.data(getSince()).observe(this, Observer {
saveData(it)
})
}
private fun saveData(data: List<MyData>) {
mDataViewModel.saveData(data)
mDataViewModel.data().observe(this, Observer {
setupRecyclerView(it)
})
}
ListActivity.kt, and possibly the ViewModel and Repository classes where it uses coroutines, are where I'm stuck. getData() retrieves the data from the server without a problem, but when it comes to saving it in the database, then taking that saved data from the database and displaying it to the user I'm unsure of the approach. As I mentioned I'm using Room, but Room will not let you access the database on the main thread.
Remember, I have to save in the database first, then retrieve from the database, so I don't want to call mDataViewModel.data().observe until after it saves to the database.
What is the proper approach to this? I've tried doing CoroutineScope on the mDataViewModel.saveData() then .invokeOnCompletion to do mDataViewModel.data().observe, but it doesn't save to the database. I'm guessing I'm doing my Coroutines incorrectly, but not sure where exactly.
It will also eventually need to delete and update records from the database.
Updated Answer
After reading comments and updated question I figured out that you want to fetch a small list of data and store it to database and show all the data stored in the database. If this is what you want, you can perform the following (omitted DataSouce for brevity) -
In PostDao You can return a LiveData<List<MyData>> instead of List<MyData> and observe that LiveData in the Activity to update the RecyclerView. Just make sure you remove the suspend keyword as room will take care of threading when it returns LiveData.
#Dao
interface PostsDao {
#Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
fun getAllData(): LiveData<List<MyData>>
#Insert
suspend fun insertData(data: List<MyData>)
}
In Repository make 2 functions one for fetching remote data and storing it to the database and the other just returns the LiveData returned by the room. You don't need to make a request to room when you insert the remote data, room will automatically update you as you are observing a LiveData from room.
class DataRepository(private val dao: PostsDao, private val dto: PostDto) {
fun getDataFromDatabase() = dao.getAllData()
suspend fun getDataFromServer(since: Long) = withContext(Dispatchers.IO) {
val data = dto.getRemoteData(since)
saveDataToDatabase(data)
}
private suspend fun saveDataToDatabase(data: List<MyData>) = dao.insertData(data)
}
Your ViewModel should look like,
class DataViewModel(private val repository : DataRepository) : ViewModel() {
val dataList = repository.getDataFromDatabase()
fun data(since: Long) = viewModelScope.launch {
repository.getDataFromServer(since)
}
}
In the Activity make sure you use ListAdapter
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(getSince())
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}
Initial Answer
First of all, I would recommend you to look into Android Architecture Blueprints v2. According to Android Architecture Blueprints v2 following improvements can be made,
DataRepository should be injected rather than instantiating internally according to the Dependency Inversion principle.
You should decouple the functions in the ViewModel. Instead of returning the LiveData, the data() function can update an encapsulated LiveData. For example,
class DataViewModel(private val repository = DataRepository) : ViewModel() {
private val _dataList = MutableLiveData<List<MyData>>()
val dataList : LiveData<List<MyData>> = _dataList
fun data(since: Long) = viewModelScope.launch {
val list = repository.getData(since)
_dataList.value = list
}
...
}
Repository should be responsible for fetching data from remote data source and save it to local data source. You should have two data source i.e. RemoteDataSource and LocalDataSource that should be injected in the Repository. You can also have an abstract DataSource. Let's see how can you improve your repository,
interface DataSource {
suspend fun getData(since: Long) : List<MyData>
suspend fun saveData(list List<MyData>)
suspend fun delete()
}
class RemoteDataSource(dto: PostsDto) : DataSource { ... }
class LocalDataSource(dao: PostsDao) : DataSource { ... }
class DataRepository(private val remoteSource: DataSource, private val localSource: DataSource) {
suspend fun getData(since: Long) : List<MyData> = withContext(Dispatchers.IO) {
val data = remoteSource.getData(since)
localSource.delete()
localSource.save(data)
return#withContext localSource.getData(since)
}
...
}
In your Activity, you just need to observe the dataList: LiveData and submit it's value to ListAdapter.
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(since)
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}

Categories

Resources