I have allRecords - Live Data values obtained from the Room, through the Repository.
I want the handleSelectedItem method to change the values of one item in the LiveData<List<...>> if it is matched by id.
I tried to do this with Transformation.map (), but this code does not work
class RecordListViewModel #Inject constructor(val repository: RecordRepository): ViewModel() {
private var allRecords : LiveData<List<RecordItem>> = Transformations.map(repository.getAllRecording()){
records -> return#map records.map{ it.toItem()}
}
fun getAllRecords() : LiveData<List<RecordItem>>{
return allRecords
}
fun handleSelectedItem(id : Int) {
Log.d("HandleSelectedItem", "Test1")
allRecords = Transformations.map(allRecords) { records ->
return#map records.map {
if (it.id == id){
Log.d("HandleSelectedItem", "Test2")
it.copy(isSelected = true)
}
else{
Log.d("HandleSelectedItem", "Test3")
it.copy(isSelected = false)
}
}
}
}
}
Help to solve this problem
Update
Here offered MutableLiveData instead of LiveData.
Then both Repository and Dao should return MutableLiveData
Repository
fun getAllRecording(): MutableLiveData<List<RecordEntity>> =
appDatabase.getRecordDao().getAllRecording()
Dao
#Query("SELECT * FROM record")
fun getAllRecording() : MutableLiveData<List<RecordEntity>>
But Room database cannot return MutableLiveData
Error
D:\Project\VoiceRecording\app\build\tmp\kapt3\stubs\debug\ru\ddstudio\voicerecording\data\database\daos\RecordDao.java:17: error: Not sure how to convert a Cursor to this method's return type (androidx.lifecycle.MutableLiveData<java.util.List<ru.ddstudio.voicerecording.data.database.entities.RecordEntity>>).
public abstract androidx.lifecycle.MutableLiveData<java.util.List<ru.ddstudio.voicerecording.data.database.entities.RecordEntity>> getAllRecording();
Update2
private val allRecords = MediatorLiveData<List<RecordItem>>().apply {
val recordsRepository = repository.getAllRecording().map { records -> records.map { it.toItem() } }
addSource(recordsRepository)
}
Error addSource()
None of the following functions can be called with the arguments supplied:
#MainThread public final fun <S : Any!> addSource(#NonNull p0: LiveData<List<RecordItem>!>, #NonNull p1: (List<RecordItem>!) -> Unit): Unit defined in androidx.lifecycle.MediatorLiveData
#MainThread public open fun <S : Any!> addSource(#NonNull p0: LiveData<List<RecordItem>!>, #NonNull p1: Observer<in List<RecordItem>!>): Unit defined in androidx.lifecycle.MediatorLiveData
Your LiveData objects should be val. Use MutableLiveData/ MediatorLiveData and setValue or postValue to change the value in LiveData
class RecordListViewModel #Inject constructor(val repository: RecordRepository): ViewModel() {
private val allRecords = MediatorLiveData<List<RecordItem>>().apply {
val recordsLiveData = repository.getAllRecording().map { records -> records.map { it.toItem() } }
addSource(recordsLiveData) { records ->
value = records
}
}
fun getAllRecords() : LiveData<List<RecordItem>> {
return allRecords
}
fun handleSelectedItem(id : Int) {
Log.d("HandleSelectedItem", "Test1")
allRecords.value?.let { records ->
allRecords.value = records.map { it.copy(isSelected = it.id == id) }
}
}
}
Then both Repository and Dao should return MutableLiveData
No, it shouldn't. Use LiveData in your DAO and Repository
Related
Im creating an android app using room database with MVVM pattern, the problem is that i cant use multiple queries when fetching data. I can fetch data once, but then i cant do it anymore.
DAO interface:
#Dao
interface StockDao {
#Insert
suspend fun insert(stock:Stock)
#Update
suspend fun update(stock:Stock)
#Delete
suspend fun delete(stock:Stock)
#Query("DELETE FROM stock_table")
suspend fun deleteAll()
#Query("SELECT * FROM stock_table")
fun selectAll():Flow<List<Stock>>
#Query("SELECT * FROM stock_table WHERE isFinished = 0")
fun selectAllUnfinished(): Flow<List<Stock>>
#Query("SELECT * FROM stock_table WHERE isFinished = 1")
fun selectAllFinished():Flow<List<Stock>>
#Query("SELECT * FROM stock_table ORDER BY totalSpent DESC")
fun selectAllOrderByDesc():Flow<List<Stock>>
#Query("SELECT * FROM stock_table ORDER BY totalSpent ASC")
fun selectAllOrderByAsc():Flow<List<Stock>>
}
Repository:
class StockRepository(private val stockDao: StockDao) {
private lateinit var allStock: Flow<List<Stock>>
suspend fun insert(stock: Stock) {
stockDao.insert(stock)
}
suspend fun update(stock: Stock) {
stockDao.update(stock)
}
suspend fun delete(stock: Stock) {
stockDao.delete(stock)
}
suspend fun deleteAll() {
stockDao.deleteAll()
}
fun selectAll(): Flow<List<Stock>> {
allStock = stockDao.selectAll()
return allStock
}
fun selectAllOrderByDesc(): Flow<List<Stock>> {
allStock = stockDao.selectAllOrderByAsc()
return allStock
}
fun selectAllOrderByAsc(): Flow<List<Stock>> {
allStock = stockDao.selectAllOrderByAsc()
return allStock
}
fun selectAllFinished(): Flow<List<Stock>> {
allStock = stockDao.selectAllFinished()
return allStock
}
fun selectAllUnfinished(): Flow<List<Stock>> {
allStock = stockDao.selectAllUnfinished()
return allStock
}
}
Viewmodel class:
class StockViewModel(private val repo: StockRepository) : ViewModel() {
companion object {
const val ALL = 0
const val ORDER_BY_DESC = 1
const val ORDER_BY_ASC = 2
const val FINISHED = 3
const val UNFINISHED = 4
}
var allStocks = repo.selectAll().asLiveData()
fun insert(stock: Stock) = viewModelScope.launch {
repo.insert(stock)
}
fun update(stock: Stock) = viewModelScope.launch {
repo.update(stock)
}
fun delete(stock: Stock) = viewModelScope.launch {
repo.delete(stock)
}
fun deleteAll() = viewModelScope.launch {
repo.deleteAll()
}
fun selectAllStockWithFilter(filter: Int): LiveData<List<Stock>> {
when (filter) {
ALL -> allStocks = repo.selectAll().asLiveData()
ORDER_BY_DESC -> allStocks = repo.selectAllOrderByDesc().asLiveData()
ORDER_BY_ASC -> allStocks = repo.selectAllOrderByAsc().asLiveData()
FINISHED -> allStocks = repo.selectAllFinished().asLiveData()
UNFINISHED -> allStocks = repo.selectAllUnfinished().asLiveData()
}
return allStocks
}
class StockViewModelFactory(private val repo: StockRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StockViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return StockViewModel(repo) as T
}
throw IllegalArgumentException("Unknown viewModel class")
}
}
}
Application class:
class FinanceApplication :Application(){
private val database by lazy { FinanceDatabase.getInstance(this)}
val stockRepository by lazy { StockRepository(database.stockDao()) }
}
Activity using this viewmodel :
class StocksActivity : AppCompatActivity() {
//Layout components
private lateinit var binder: ActivityStocksBinding
private lateinit var recyclerView: RecyclerView
private lateinit var recyclerViewAdapter: StockAdapter
//ViewModel
private val viewModel: StockViewModel by viewModels {
StockViewModel.StockViewModelFactory((application as FinanceApplication).stockRepository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binder = ActivityStocksBinding.inflate(layoutInflater)
setContentView(binder.root)
fetchStocks()
}
private fun fetchStocks() {
viewModel.allStocks.observe(this) {
recyclerViewAdapter.submitList(it)
}
}
private fun initRecyclerViewLayout() {
val recyclerViewLayoutBinder = binder.includedLayout
recyclerView = recyclerViewLayoutBinder.stocksRecyclerView
recyclerViewAdapter = StockAdapter(this)
recyclerView.adapter = recyclerViewAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_stock_toolbar, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_stock_toolbar_filter_all -> viewModel.selectAllStockWithFilter(StockViewModel.ALL)
R.id.menu_stock_toolbar_filter_maior_menor -> viewModel.selectAllStockWithFilter(StockViewModel.ORDER_BY_DESC)
R.id.menu_stock_toolbar_filter_menor_maior -> viewModel.selectAllStockWithFilter(StockViewModel.ORDER_BY_ASC)
R.id.menu_stock_toolbar_filter_finalized -> viewModel.selectAllStockWithFilter(StockViewModel.FINISHED)
R.id.menu_stock_toolbar_filter_opened -> viewModel.selectAllStockWithFilter(StockViewModel.UNFINISHED)
}
return true
//NOTHIN HAPPENS AFTER CHOOSING ONE
}
}
When i enter the activity, all the data is fetched normally, but when i click on a menu item to apply some filter on it, nothing happens, the data doesnt change. How can i fix this?
allStocks may seem dynamic because it's LiveData, but remember that it's still a reference to an object in memory. When StocksActivity is created, it observes allStocks in it's initial state. For the sake of simplicity, let's say allStocks is pointing to an object in memory with the address of "A". When selectAllStockWithFilter() is eventually invoked, the allStocks handle is updated to point to a new instance of LiveData living in memory at address "B". The problem you're facing is that StocksActivity is still observing "A". Nothing communicated that the allStocks handle itself has been changed.
One way to resolve this would be to change allStocks into an instance of MutableLiveData. Subsequently, whenever the contents of this allStocks should be updated, instead of reassigning allStocks, you would update it's internal "value". This allows the ViewModel to pump new/updated values through the same LiveData object instance that StocksActivity is observing.
Something like this:
class StockViewModel(private val repo: StockRepository) : ViewModel() {
...
val allStocks = MutableLiveData<List<Stock>>().apply { value = repo.selectAll() }
...
fun selectAllStockWithFilter(filter: Int) {
when (filter) {
ALL -> allStocks.postValue(repo.selectAll())
ORDER_BY_DESC -> allStocks.postValue(repo.selectAllOrderByDesc())
ORDER_BY_ASC -> allStocks.postValue(repo.selectAllOrderByAsc())
FINISHED -> allStocks.postValue(repo.selectAllFinished())
UNFINISHED -> allStocks.postValue(repo.selectAllUnfinished())
}
}
...
}
I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.
I want to implement an android app with the MVVM model. The problem is that I can't find a way to update the data in the ViewModel with a Firestore call that contains a parameter. The repository class is responsible for loading the data, which either fetches it from the local room-database (works) or otherwise from firestore.
My question:
Is my implementation correct? Getting the data from the room-database works. But loading the data from Firestore doesn't work. I think the problem is that the firestore-query has not been executed yet when the getFoods function in the FirestoreFoodWebservice-Class returns. What can I do to fix this?
My architecture looks like this:
MainActivity -> MainActivitiyViewModel -> FoodRepository ->
(
if (data is in room database) return from database
else (get data from firestore webservice)
)
MainActivity:
mainActivityViewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
mainActivityViewModel.getFoodsFilteredByName().observe(this, Observer { foods ->
run {
searchResultAdapter.setData(foods)
progressIndicator.visibility = View.INVISIBLE
}
})
SearchResultAdapter:
fun setData(foods : List<Food>) {
if (searchResult.isEmpty()) {
searchResult = ArrayList(foods);
notifyItemRangeInserted(0, searchResult.size)
return
}
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return searchResult.size
}
override fun getNewListSize(): Int {
return foods.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return searchResult[oldItemPosition].foodName === foods[newItemPosition].foodName
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val newFood = foods[newItemPosition]
val oldFood = searchResult[oldItemPosition]
return newFood == oldFood
}
})
searchResult = ArrayList(foods)
result.dispatchUpdatesTo(this)
}
MainActivityViewModel:
private val foodsFilteredByName: LiveData<List<Food>>
private val filterName = MutableLiveData<String>()
init {
val foodsDao = FoodRoomDatabase.getDatabase(application, viewModelScope).foodDao()
val webservice = FirestoreFoodWebservice()
repository = FoodRepository(foodsDao, webservice)
foodsFilteredByName = Transformations.switchMap(filterName) { c -> repository.getFoods(c) }
}
...
fun setFilter(name: String ) {
filterName.value = name
}
FoodRepository:
fun getFoods(foodName: String): MutableLiveData<List<Food>> {
// if data exists in room database return the data
// else:
var foods: List<Food> = webservice.getFoods(foodName)
val data = MutableLiveData<List<Food>>()
data.value = foods
return data
}
FirestoreFoodWebservice:
(Before switching to MVVM I had the query in the MainActivity. There the query worked.)
fun getFoods(foodName: String): List<Food> {
val query = // create firestore query
query.get().addOnSuccessListener { result ->
for (document in result) {
var food = Food()
// fill food with data from result
foods.add(food)
}
}.addOnFailureListener { /* ToDo: Implement */ }
return foods
// Edit: Changed foodNames to foods
}
I've a Room database of Assignment and want to get loadAllByIds so I wrote the below codes, in the last one I'm trying to print the returned result but it fails.
#Entity
data class Assignment(
#PrimaryKey(autoGenerate = true) val uid: Int,
#ColumnInfo(name = "name") val name: String?,
#ColumnInfo(name = "title") val title: String? // ,
)
#Dao
interface AssignmentDao {
#Query("SELECT * FROM assignment WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<Assignment>
}
And
class AppRepository(private val assignmentDao: AssignmentDao) {
#WorkerThread
fun loadAllByIds(userIds: IntArray) {
assignmentDao.loadAllByIds(userIds)
}
}
And
class AppViewModel(application: Application) : AndroidViewModel(application) {
#WorkerThread
fun loadAllByIds(userIds: IntArray) = viewModelScope.launch(Dispatchers.IO) {
repository.loadAllByIds(userIds)
}
}
And
class AssignmentsAdapter(private val context: Context, private val chaptersList: ArrayList<Assignment>) :
RecyclerView.Adapter<AssignmentsAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.chapterName.setOnClickListener {
printAll(appViewModel.loadAllByIds(intArrayOf(position)))
}
}
fun printAll(strings: Collection<String>) {
for(s in strings) print("$s ")
println()
}
}
For printAll(appViewModel.loadAllByIds(intArrayOf(position))) I get an error:
Required: Collection
Found: Job
How can I fix it?
Definitely it won't gonna work and give you the compilation error as you are using coroutines viewModelScope.launch which returns Job.
What would I suggest you to add one more method in view model class which just returns the LiveData
Step 1: Modify your view model class like below
class AppViewModel(application: Application) : AndroidViewModel(application) {
private val assignments = MutableLiveData<List<Assignment>>()
#WorkerThread
fun loadAllByIds(userIds: IntArray) = viewModelScope.launch(Dispatchers.IO) {
assignments.postValue(repository.loadAllByIds(userIds))
}
fun getAssignments(): MutableLiveData<List<Assignment>> {
return assignments
}
}
Step 2: Then call below methods from fragment/activity.
for example calling from onCreate method
appViewModel.getAssignments().observe(this, Observer { assignments ->
printAll(assignments) // You can pass assignments object to your AssignmentsAdapter
})
appViewModel.loadAllByIds(intArrayOf(0,1,2))
Modified repository class
class AppRepository(private val assignmentDao: AssignmentDao) {
#WorkerThread
fun loadAllByIds(userIds: IntArray): List<Assingment> {
return assignmentDao.loadAllByIds(userIds)
}
}
I have a AmbassadorDAO that has a getAll() : List<Ambassador> that return correctly the list of Ambassadors.
The problem becomes when I refactory my existent code to use DataSource.Factory to paginate my list
Here is the code
Presation Module
Activity
class AmbassadorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val viewModel by viewModel<AmbassadorViewModel>()
val adapter = AmbassadorAdapter(this)
list_of_ambassadors.adapter = adapter
viewModel.ambassadors.observe(this, Observer { adapter.submitList(it) })
viewModel.listAmbassadors()
...
}
...
}
Viewmodel
class AmbassadorViewModel(
...,
private val getAllAmbassadorInteractor: GetAllAmbassadorInteractor
) : ViewModel() {
...
// not working
private val _ambassadors = MutableLiveData<PagedList<Ambassador>>()
// it's working
//private val _ambassadors = MutableLiveData<List<Ambassador>>()
...
// not working
val ambassadors : LiveData<PagedList<Ambassador>>
get() = _ambassadors
// it's working
//val ambassadors : LiveData<List<Ambassador>>
// get() = _ambassadors
...
fun listAmbassadors() {
viewModelScope.launch {
try {
...
// not working
// the data not return anything
// the livedata is notified with null
val data = getAllAmbassadorInteractor.exec()
_ambassadors.value = LivePagedListBuilder(data, 20).build().value
// it's working
//_ambassadors.value = getAllAmbassadorInteractor.exec()
} catch (e: Exception) {
e.printStackTrace()
} finally {
...
}
}
}
}
Domain Module
Boundary between PRESENTATION (my usecase interface)
interface GetAllAmbassadorInteractor {
//suspend fun exec() : List<Ambassador>
suspend fun exec() : DataSource.Factory<Int, Ambassador>
}
Usecase implementation
class GetAllAmbassadorInteractorImpl(
private val repository: AmbassadorRepository
) : GetAllAmbassadorInteractor {
override suspend fun exec() = withContext(Dispatchers.IO) { repository.getAll() }
}
Boundary between DATA (my repository interface)
interface AmbassadorRepository {
...
//suspend fun getAll() : List<Ambassador>
suspend fun getAll() : DataSource.Factory<Int, Ambassador>
...
}
Data Module
Repository implementation
class AmbassadorRepositoryImpl(
private val ambassadorDAO: AmbassadorDAO
) : AmbassadorRepository {
...
override suspend fun getAll() = ambassadorDAO.getAll().map { it.toDomain() }
...
}
My DAO
#Dao
interface AmbassadorDAO {
...
#Query("SELECT * FROM ${AmbassadorEntity.TABLE_NAME} ORDER BY name DESC")
fun getAll(): DataSource.Factory<Int, AmbassadorEntity>
//fun getAll(): List<AmbassadorEntity>
...
}
Where am I doign wrong?
I guess your mistake is on this line in AmbassadorViewModel class:
_ambassadors.value = LivePagedListBuilder(data, 20).build().value
Instead of that use:
_ambassadors.value = LivePagedListBuilder(data, 20).build()
Also refer to this post, maybe it will help.
With the support of Kotlin extension (LifecycleScope) we can easily connect LiveData with Coroutine and you don't need to use backing properties like _ambassadors and make it MutableLiveData.
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 or higher.
Like this is a function which is using Coroutine and returning a LiveData
/**
* Get all news rows livedata pageList from DB using Coroutine.
*/
suspend fun getAllNewsLiveData(): LiveData<PagedList<News>> {
return withContext(Dispatchers.IO) {
val data = mDao.getAllNews()
LivePagedListBuilder(data, Constants.PAGINATION_SIZE).build()
}
}
Now in UI class we can simply call this function using lifescope extension
lifecycleScope.launchWhenStarted {
newsViewModel.getNews()?.observe(this#NewsActivity, Observer { pagedNewsList -> pagedNewsList.let { newsAdapter.submitList(pagedNewsList) } })
}