Hi i have a list of items that i retrieve from a data source and then i apply a map on the observable to store the data into ROOM.
It manages to add it to the table but when i try to retrieve a LiveData of it, It doesnt seem to notify my observer of the results.
There is definetly data in the table and my query works as i changed the return time from LiveData to simple a List and that worked perfectly fine
Here is my data class
#Entity
data class Item(
#PrimaryKey
val id:String,
val title: String,
val description: String,
val imgUrl: String,
val usageRules : List<String>,
)
Here is my DAO that exposes a func that add all the list of Items
#Dao
interface MyDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun saveAllItems(itemList: MutableList<Item>)
#Query("SELECT * FROM items")
fun getAllItems(): LiveData<MutableList<Item>>
}
My DB class
#Database(entities = arrayOf(Item::class), version = 1, exportSchema = false)
#TypeConverters(UsageRulesTypeConverter::class)
abstract class MyDatabase : RoomDatabase() {
abstract fun getProductDao(): MyDao
}
Below is how i insert data into the database:
#Inject
lateInt val database : MyDatabase
override fun getAllItems(): Single<LiveData<MutableList<Item>>> {
//retrieve new data using rertrofit
networkController.getItems().map { responseItems ->
saveAllItems(responseItems )
getItems()
}
#Transaction
fun saveAllItems(allItems: ItemsDataSource) {
database.getProductDao().saveAllItems(allItems.loadedItems)
database.getProductDao().saveAllItems(allItems.savedItems)
database.getProductDao().saveAllItems(allItems.expiredItems)
database.getProductDao().saveAllItems(allItems.unloadedItems)
}
fun getItems() : LiveData<MutableList<Item>>{
return database.getProductDao().getAllItems()
}
My data source retrieved 4 lists of Items that i then save all of them in one entity/table but LiveData doesnt notify my UI about it?
ViewModel:
override fun getUnloadedOffers(): LiveData<MutableList<ProductOffer>> {
if (!this::itemsLiveData.isInitialized) {
itemsLiveData= MutableLiveData()
//get data from network or database
itemDelegator.getItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ items->
itemsLiveData= items
})
}
return itemsLiveData
}
UI
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
viewModel.getItems().observe(this, Observer {
items->
items?.let {
adapter = SomeAdapter(items)
itemsRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
}
})
Does your function getUnloadedOffers() return empty mutableList? I'm not sure if passing items to itemsLiveData is going to work, since observing LiveData is running on bg thread (if I'm not mistaken)
Try to use vararg instead of List when saving collection of data.
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun saveAllItems(vararg itemList: Item)
And then you can call this method using * (also known as Spread Operator) on your list like this
saveAllItems(*responseItems.toTypedArray())
Related
I'm trying to get data from server and cache into database and return new fetched list to user. I'm getting response form server and saving it to the local database but when im trying to observer it from composable function it showing list is empty.
When i try to debug and collect flow data in myViewModel class it showing but it not showing is composable function.
dao
#Dao
interface CategoryDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(categories: List<Category>)
#Query("SELECT * FROM categories ORDER BY name")
fun read(): Flow<List<Category>>
#Query("DELETE FROM categories")
suspend fun clearAll()
}
repository class:
suspend fun getCategories(): Flow<List<Category>> {
val categories = RetrofitModule.getCategories().categories
dao.insert(categories)
return dao.read()
}
myViewModel
fun categoriesList(): Flow<List<Category>> {
var list: Flow<List<Category>> = MutableStateFlow(emptyList())
viewModelScope.launch {
list = repository.getCategories().flowOn(Dispatchers.IO)
}
return list
}
Observing from:
#Composable
fun StoreScreen(navController: NavController, viewModel: CategoryViewModel) {
val list = viewModel.categoriesList().collectAsState(emptyList())
Log.d("appDebug", list.value.toString()) // Showing always emptyList []
}
current response :
2021-05-15 16:08:56.017 5125-5125/com.demo.app D/appDebug: []
You are never updating the value of MutableStateFlow which has been collected as state in the Composable function.
Also you are assigning a Flow type object to a MutableStateFlow variable.
We can just update the value of the collected flow in the compose using:-
mutableFlow.value = newValue
We need to change the type of list to MutableStateFlow<List<Category>> instead of Flow<List<Category>>
Try this:-
var list: MutableStateFlow<List<Category>> = MutableStateFlow(emptyList()) // changed the type of list to mutableStateFlow
viewModelScope.launch {
repository.getCategories().flowOn(Dispatchers.IO).collect { it ->
list.value = it
}
}
I have an Activity where I have a LiveData and I wanna display it in a ListView.
My code snippet:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_records)
this.recordListView.emptyView = recordListEmptyView
viewModel = ViewModelProvider(this).get(RecordsViewModel::class.java)
recordDAO = AppDatabase.getDb(this).recordDao()
recordDAO.findAllSync().observe(this) {
//display livedata in listview after getting added
//adapter = ArrayAdapter(this,list_items,syncedRecords)
//listview.adapter = adapter
}
}
After I added a record I wanna display the records in the place where I have the comment.
I could make it work with a normal findAll() and set my adapter to the return of findAll() but I could not make it work with LiveData.
My DAO class:
#Dao
interface RecordDAO{
#Update(onConflict = REPLACE)
fun update(record:Record) : Int
#Insert(onConflict = IGNORE)
fun persist(record: Record): Long
#Delete
fun delete(record: Record): Int
#Delete
fun deleteAll(records: List<Record>)
#Query("SELECT * FROM record")
fun findAll(): List<Record>
#Query("SELECT * FROM record")
fun findAllSync(): LiveData<List<Record>>
#Query("SELECT * FROM record WHERE id = :id")
fun findById(id: Int): Record?
}
Here is how it should work. Since you did not inclue the viewmodel code, it is difficult to know the exact error. Here you observe the result of a function (recordDAO.findAllSync().observe), so I guess that the livedata is a variable only in the scope of findAllSync.
This is not how it works. Live data should be a member variable of the viewModel
class RecordsViewModel : ViewModel() {
val records = MutableLiveData<RecordDAO>()
// this function gives the records to the live data. No need for a return value
fun findAllSync() {
// Retrieve the records somehow, here this should be edited by you
records.value = someService.getRecords()
}
}
Then in the activity/fragment, you observe the live data and request a record update (You can also request the update directly from the init of the viewmodel depending on how/when you want to get the data)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_records)
this.recordListView.emptyView = recordListEmptyView
viewModel = ViewModelProvider(this).get(RecordsViewModel::class.java)
// THIS CHANGED
viewModel.findAllSync()
viewModel.records.observe(this) { records ->
val adapter = ArrayAdapter(this,list_items, records)
listview.adapter = adapter
}
}
Note: I used a MutableLiveData. The cleaner way is to have a private MutableLiveData and a public LiveData
private val _records = MutableLiveData<RecordDAO>()
val records: LiveData<RecordDAO> = _records
The vm uses _records.value to set the data but since the Activity can only read the non mutable one, it cannot edit the value, only the viewmodel can. You don't need to do it this way it but it's always nice to know
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))
}
I'm using the PagedList architectural components using a Room database and I am having trouble returning results to the observe method.
Here is my Dao:
#Dao
interface WorkPackageStorageDao {
#Query("SELECT * from workpackages where id = :id")
fun getById(id: String): Workpackage
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(workpackage: Workpackage)
#Query("Select * from workpackages WHERE id LIKE '%' || :searchString || '%' order by :orderBy")
fun searchWorkpackages(searchString : String, orderBy : String) : DataSource.Factory<Int, Workpackage>
#Query("SELECT * FROM workpackages")
fun searchWorkPackgesTest() : List<Workpackage>
#Query("Select * from workpackages WHERE id LIKE '%' || :searchString || '%' order by :orderBy")
fun searchWorkPackgesTestQuery(searchString : String, orderBy : String) : List<Workpackage>
#Query("DELETE from workpackages")
fun deleteAll()
}
My repository:
fun getAllWorkPackagesTestQuery() : List<Workpackage> {
return workpackagesDao.searchWorkPackgesTestQuery("",SortedBy.WorkPackageNumber.type)
}
fun getAllWorkPackages() : DataSource.Factory<Int, Workpackage> {
return getSortedAndSearchedWorkPackages("",
SortedBy.WorkPackageNumber
)
}
fun getSortedAndSearchedWorkPackages(searchString : String, sortBy: SortedBy) : DataSource.Factory<Int, Workpackage> {
return workpackagesDao.searchWorkpackages(searchString,sortBy.type)
}
Here is the method in my view model:
suspend fun fetchWorkPackagesInitial(
workWeek: Configurations.AppWeek,
pagingLimit: Int
) {
coroutineScope {
withContext(Dispatchers.IO) {
val factory: DataSource.Factory<Int, Workpackage> =
workPackageRepository.getAllWorkPackages()
val pagedListBuilder =
LivePagedListBuilder<Int, Workpackage>(factory, pagingLimit)
workPackagesList = pagedListBuilder.build()
val list = workPackageRepository.getAllWorkPackagesTestQuery() //27 Items returned, query is fine.
}
}
}
Here is my fragment:
mainViewModel.week.observe(this, Observer {
it ?: return#Observer
launch { workPackagesViewModel.fetchWorkPackagesInitial(it, PAGING_LIMIT) }
})
//Observe never called.
workPackagesViewModel.workPackagesList?.observe(this, Observer { wpList ->
wpList ?: return#Observer
adapter = WorkPackagesRecyclerAdapter(this)
adapter.submitList(wpList)
binding.workPackagesRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
})
As a test to my query, I've implemented:
val list = workPackageRepository.getAllWorkPackagesTestQuery()
which returns 27 items, so query is fine. Am I setting up the the Dao wrong, the LivePagedListBuilder wrong? Why is observe not called?
You're not getting items because it's a PagedList. You need to trigger the load in order to obtain pages.
That is why giving the PagedList to a PagedListAdapter via submitList will eventually load the data.
You also don't need to manually invoke adapter.notifyDataSetChanged() when you're using a PagedListAdapter, because the DiffUtil will handle that internally.
However, you should definitely be retrieving the DataSource.Factory and the LivePagedListBuilder and the PagedList on the UI thread (Dispatcher.MAIN), because threading is handled by the Paging lib's Room integration internally. Observing (and invoking getItem( on an unloaded element) will trigger the load, and the load will be executed asynchronously by the DataSource out of the box.
The way to use Paging is like this:
class MyViewModel(
private val workPackageStorageDao: WorkPackageStorageDao
): ViewModel() {
private val searchQuery: MutableLiveData<String> = MutableLiveData("")
val workPackages: LiveData<PagedList<WorkPackage>> = Transformations.switchMap(searchQuery) { searchText ->
val factory = workPackageStorageDao.searchWorkPackages(searchText, SortedBy.WorkPackageNumber)
val pagedListBuilder = LivePagedListBuilder<Int, WorkPackage>(factory, pagingLimit)
pagedListBuilder.build()
}
}
Then in Fragment:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... setup RecyclerView, etc
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java, viewModelFactory)
viewModel.workPackages.observe(viewLifecycleOwner, Observer { pagedList ->
adapter.submitList(pagedList)
})
}
And in adapter:
class MyAdapter: PagedListAdapter<WorkPackage, MyAdapter.ViewHolder>(WorkPackage.ITEM_CALLBACK) {
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val workPackage = getItem(position)
if(workPackage != null) {
holder.bind(workPackage)
}
}
}
Long story short, you don't need coroutines here. Use the Paging Library and LiveData, and it will load on the correct threads.
I'm having trouble figuring out how I'm supposed to update data in a Room Database after a change in RecyclerView item order. How am I supposed to update LiveData items in Room based on user action?
Using ItemTouchHelper.Callback I've set up an onMove callback that can make changes to the order of items presented to the user (on drag and drop), but when I make a call to update the order of items in the Room Database, using a ViewModel object, the user can then only move items one at a time. So if you drag an item, it will only move one space.
This is the onMove function I have defined in the ListAdapter, which implements ItemTouchHelperAdapter for the callback.
override fun onMove(
recyclerView: RecyclerView,
fromViewHolder: RecyclerView.ViewHolder,
toViewHolder: RecyclerView.ViewHolder
): Boolean {
d(this.TAG, "swap viewHolders: " + fromViewHolder.adapterPosition + " to " + toViewHolder.adapterPosition)
val workoutRoutine1 = workoutRoutines[fromViewHolder.adapterPosition]
val workoutRoutine2 = workoutRoutines[toViewHolder.adapterPosition]
workoutRoutine1.orderNumber = toViewHolder.adapterPosition.toLong()
workoutRoutine2.orderNumber = fromViewHolder.adapterPosition.toLong()
//this.workoutRoutinesViewModel.update(workoutRoutine1)
//this.workoutRoutinesViewModel.update(workoutRoutine2)
notifyItemMoved(fromViewHolder.adapterPosition, toViewHolder.adapterPosition)
return true
}
This is my DAO object
#Dao
interface WorkoutRoutineDAO {
#Insert
suspend fun insert(workoutRoutine: WorkoutRoutine)
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(workoutRoutine: WorkoutRoutine)
#Delete
suspend fun delete(workoutRoutine: WorkoutRoutine)
#Query("DELETE FROM workout_routine_table")
fun deleteAll()
#Query("SELECT * FROM workout_routine_table ORDER BY order_number ASC")
fun getAllWorkoutRoutines(): LiveData<List<WorkoutRoutine>>
#Query("SELECT COALESCE(MAX(order_number), -1) FROM workout_routine_table")
fun getLargestOrderNumber(): Long
}
This is my RoomDatabase object
#Database(entities = [WorkoutRoutine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun workoutRoutineDAO(): WorkoutRoutineDAO
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
.addCallback(WorkoutRoutineDatabaseCallback(scope))
.build()
INSTANCE = instance
instance
}
}
}
private class WorkoutRoutineDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
}
}
This is the ViewModel object I implemented.
class WorkoutRoutinesViewModel(application: Application) : AndroidViewModel(application) {
private val workoutRoutinesRepository: WorkoutRoutineRepository
val allWorkoutRoutines: LiveData<List<WorkoutRoutine>>
init {
// Get the DAO
val workoutRoutineDAO = AppDatabase.getDatabase(application, viewModelScope).workoutRoutineDAO()
// Build a new data repository for workout routines
workoutRoutinesRepository = WorkoutRoutineRepository(workoutRoutineDAO)
// Get a live view of the workout routines database
allWorkoutRoutines = workoutRoutinesRepository.allRoutines
}
fun insert(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.insert(workoutRoutine)
}
fun update(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.update(workoutRoutine)
}
fun delete(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.delete(workoutRoutine)
}
}
I expect the user to be able to move the item n spaces, then drop, and have the update to Room database execute when the user drops the item, but if I put the Room update in the onMove method, the user can only move the item once.
I'm trying to understand the right way to update the Room data when the order of objects change in the recycler view. I'm trying to get the order of those objects to persist even when the user exits the app or changes activities or whatever. How am I supposed to echo those changes back to the Room database, using LiveData?
You can follow this guide on Udacity. It is free, made by Google and uses Kotlin.