Jetpack Compose not updating / recomposing Flow List Values from Room DB when DB Data is getting changed - kotlin

I'm trying to show a List of Items in my Android App. I'm using Jetpack Compose, Flows and RoomDB.
When launching the Activity all Items are shown without any problems, the Flow get's items collected and they are displayed.
But when I change some properties of the Item in the Database, the changes are not displayed. In my case I change the item to deleted, but it's still displayed as not deleted.
When I look at the Database Inspector, the value is changed in the database and set to deleted.
When I log collecting the flow, the change is getting emitted (It says the Item is set to deleted)
But Jetpack Compose is not recomposing the change.
If I delete an element from / add an element to the List (in the DB) the UI gets updated and recomposed.
So I can only assume that the problem must lie in the recomposition or handling of the flow.
Here my Code:
My Activity:
#AndroidEntryPoint
class StockTakingHistoryActivity : ComponentActivity() {
private val viewModel: StockTakingHistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.stockList = ...
setContent {
LaunchedEffect(Unit) {
viewModel.getStockListItems(viewModel.stockList!!.uuid)
}
Surface(color = MaterialTheme.colors.background) {
Content(viewModel.stockListItems)
}
}
}
}
...
#Composable
private fun Content(items: List<StockListItem>) {
...
LazyColumn {
items(items) { item ->
HistoryItem(stockListItem = item)
}
}
}
}
...
#Composable
private fun HistoryItem(stockListItem: StockListItem) {
...
Text(text = stockListItem.deleted)
...
Button(onClick = {
viewModel.deleteItem(stockListItem)
}) {
Text(text = "Set to deleted!")
}
}
}
My ViewModel:
var stockListItems by mutableStateOf(emptyList<StockListItem>())
fun getStockListItems(uuid: String) {
viewModelScope.launch {
stockListItemRepository.findByUUID(uuid).collect { items ->
Log.d("StockTakingHistoryViewModel", "items changed! ${items.map { it.deleted }}")
stockListItems = items
}
}
}
fun deleteItem(stockListItem: StockListItem) {
viewModelScope.launch(Dispatchers.IO) {
stockListItemRepo.update(item.copy(deleted = true);
}
}
The Repository:
fun findByUUID(uuid: String): Flow<List<StockListItem>> {
return dao.findByUUID(uuid)
}
The Dao behind the Repository Request:
#Query("select * from StockListItem where stockListUUID = :uuid order by createdAt desc limit 30")
fun findByUUID(uuid: String): Flow<List<StockListItem>>
I would be very happy if someone could help me! Thank you!

Considering you can collect a flow as state (via collectAsState) I'd consider going that route for getting the list rather than calling collect in the viewModel and updating the stockListItems as there are fewer moving parts for things to go wrong.
For example something like the following:
setContent {
val stockListItems = viewModel.getStockListItemsFlow(uuid).collectAsState(initial = emptyList())
Surface(color = MaterialTheme.colors.background) {
Content(stockListItems)
}
}

Found the Problem: The equals() method of StockListItem only compared the primary key.

Related

How to list API call results in Jetpack Compose

I am trying to build a simple listing app and to fetch data from url I use retrofit2. And then, I store in MutableLiveData<Resource> object. How can I list retrofit2 results in LazyColumn?
My composable:
#ExperimentalAnimationApi
#Composable
fun CarsScreen(
viewModel: CarListViewModel = hiltViewModel()
){
viewModel.getCarsFromAPI()
val carsLiveData = viewModel.carsLiveData.observeAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
){
GreetingSection()
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
itemsIndexed( ){
//TODO
}
}
}
Viewmodel:
#HiltViewModel
class CarListViewModel #Inject constructor(
private val carRepository: CarRepository
): ViewModel() {
val carsLiveData:MutableLiveData<Resource<CarsResponse>> = MutableLiveData()
fun getCarsFromAPI() = viewModelScope.launch {
carsLiveData.postValue(Resource.Loading())
val response = carRepository.getCarsFromAPI()
carsLiveData.postValue(handleCarResponse(response))
}
private fun handleCarResponse(response: Response<CarsResponse>) : Resource<CarsResponse> {
if(response.isSuccessful){
response.body()?.let{resultResponse ->
return Resource.Success(resultResponse)
}
}
return Resource.Error(response.message())
}
}
observeAsState doesn't return a LiveData; it returns the data contained within the LiveData that you're observing.
Whenever that LiveData's value changes, recomposition is triggered, and you'll get a new value.
Change the name of property carsLiveData to just cars. You can use that directly as the items in your LazyColumn.
One other note - you're calling viewModel.getCarsFromAPI() inside the CarsScreen composable every time it's recomposed. You probably don't want to do that.
If you only want to get the list once, you could use a LaunchedEffect inside CarsScreen, something like:
// Copyright 2023 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#Composable
fun CarsScreen(...) {
LaunchedEffect(Unit) {
viewModel.getCarsFromAPI()
}
...
}
If you want to update that list, pass some state into LaunchedEffect instead of Unit - whenever that state changes, the LaunchedEffect will be canceled (if currently running) and restarted.

Can ViewModel observe its MutableLiveData property?

I'm trying to use ViewModels with LiveData and MutableLiveData in Android for the first time. I have not read all documentation yet.
My question:
How can I call fun applyFilters() when val filterByName = MutableLiveData<String>() was changed from outside (from a Fragment).
In .NET MVVM I would just add that call in the setter for filterByName property. But how to I do it in with MutableLiveData?
I think I should use MediatorLiveData, but this documentation does not explain much.
I was thinking about using the observer inside a viewmodel (so it could observe itself), but then I would have to use AndroidViewModel instead of ViewModel, to get lifecycleOwner which is required to observe MutableLiveData...
My code:
In my AssignUhfTagToInventoryItemViewModel() : ViewModel() (which is shared between few fragments) I have properties like this:
/* This is filled only once in SomeViewModel.init() function, always contains all data */
var _items = ArrayList<InventoryItemDto?>()
/* This is filled from _items, contains filtered data */
val items = MutableLiveData<ArrayList<InventoryItemDto?>>().apply { value = ArrayList() }
val filterByName = MutableLiveData<String>().apply { value = "" }
And a function like this:
fun applyFilters(){
Log.d(TAG, "ViewModel apply filters called. Current name filter: ${filterByName.value}")
items.value?.clear()
val nameFilterLowercase = filterByName.value.toString().lowercase()
if (!filterByName.value.isNullOrEmpty()) {
for (item in _items) {
val itemNameLowercase = item?.name?.lowercase()
if (itemNameLowercase?.contains(nameFilterLowercase) == true)
items.value?.add(item)
}
Log.d(TAG, "Filter is on, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
else {
items.value?.addAll(_items)
Log.d(TAG, "Filter is off, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
}
And this is part of my Fragment where I use that viewmodel:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentInventoryItemSelectBinding.bind(view)
val vm = ViewModelProvider(requireActivity())[AssignUhfTagToInventoryItemViewModel::class.java]
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, vm.items.value)
binding.lvItems.adapter = listAdapter // old school ListView
binding.tvFilterByName.addTextChangedListener(AfterTextChangedWatcher {
vm.filterByName.postValue(it)
/*
IN MY FRAGMENT I WANT TO AVOID FUNCTION CALL BELOW
*/
vm.applyFilters()
listAdapter.notifyDataSetInvalidated()
Log.d(TAG, "afterTextChanged: $it")
})
vm.items.observe(requireActivity()){
listAdapter.notifyDataSetInvalidated()
}
}
Technically? I guess you can but you can use Transformations.switchMap to take input from one LiveData and apply them to the source from another.
var _items = MutableLiveData(ArrayList<InventoryItemDto?>())
val filterByName = MutableLiveData<String>().apply { value = "" }
val items = Transformations.switchMap(filterByName) { filter ->
val nameFilterLowercase = filter.lowercase()
_items.map { mappedItems ->
mappedItems.filter { listItem ->
listItem.contains(nameFilterLowercase)
}
}
}
vm.items.observe(viewLifecycleOwner) { items ->
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, items)
binding.lvItems.adapter = listAdapter // old school ListView
}

Deleting an item from RecyclerView causes shuffling or duplicating other items

I have a recyclerView that users can add or delete item rows and that rows saving the Firebase Firestore. Adding function works fine but deleting function does not. I created an interface (Listener) through PairDetailRecyclerAdapter. It contains DeleteOnClick method which have myList and position parameters. Also I have a deleteData method through my viewModel for deleting documents from Firestore. When i clicked the delete button on Firebase side everything is OK but on recyclerView side items duplicating themselves or shuffling
Here is the codes:
Interface and onClickListener from PairDetailRecyclerAdapter :
interface Listener {
fun DeleteOnClick(list: ArrayList<AnalyzeDTO>, position: Int)
}
override fun onBindViewHolder(holder: AnalyzeViewHolder, position: Int) {
holder.itemView.rrRatioText.text = "RR Ratio: ${list[position].rrRatio}"
holder.itemView.resultText.text = "Result: ${list[position].result}"
holder.itemView.causeForEntryText.text = "Reason: ${list[position].reason}"
holder.itemView.conceptText2.text = "Concept: ${list[position].concept}"
if (list[position].tradingViewUrl != null && list[position].tradingViewUrl!!.isNotEmpty()) {
Picasso.get().load(list[position].tradingViewUrl)
.into(holder.itemView.tradingviewImage);
}
holder.itemView.imageView.setOnClickListener {
listener.DeleteOnClick(list, holder.layoutPosition)
}
deleteData from ViewModel :
fun deleteData(position: Int) {
var chosenPair = ozelSharedPreferences.clickiAl().toString()
val currentU = Firebase.auth.currentUser
val dbCollection = currentU?.let {
it.email.toString()
}
database.collection(dbCollection!!).document("Specified").collection("Pairs")
.document(chosenPair).collection("Analysis").get().addOnSuccessListener { result ->
val newList = ArrayList<String>()
if (result != null) {
for (document in result) {
newList.add(document.id)
database.collection(dbCollection!!).document("Specified").collection("Pairs")
.document(chosenPair).collection("Analysis").document(newList[position]).delete()
}
}
}
}
Overrided Listener in PairDetailActivity
override fun DeleteOnClick(list: ArrayList<AnalyzeDTO>, position: Int) {
viewModel.deleteData(position)
list.removeAt(position)
recyclerA.notifyItemRemoved(position)
}
#SuppressLint("NotifyDataSetChanged")
fun deleteItem(i: Int, context: Context) {
question = dataList[i] as Questionio //this my model
dataList.removeAt(i)
notifyDataSetChanged()
}
//
this code works for me
in fact, it would be better if you did it as a model and it would be suitable for mvvm architecture.

Why data in room automatically deleted when have a new data in Kotlin

I have a feature namely draft. I already success showing draft data from room to RV as a list. And also open that data into detail page with different id (primary key), this is a success too.
But the problem is, If I only have 1 data in the list draft, and then when I open it in detail page, it's showing. But when I have 2 data (for ex: data 1 and data 2), let's say data 1 it's pref draft and data 2 it's new draft. Then when I open data 1 in the detail page, the data from the room it's null/size == 0. But when I open data 2, the data it's not null.
So the problem is when I add a new draft data, the prev data data in the detail page always automatically deleted, but the new draft data it's not. Why it's happened?
This is my view model to show data in the list draft:
private val _listDraft = MutableLiveData<ResultState<List<PolicyHolderSwitchingEntity>>>()
val listDraft: LiveData<ResultState<List<PolicyHolderSwitchingEntity>>> get() = _listDraft
private fun setDraft(resultState: ResultState<List<PolicyHolderSwitchingEntity>>) {
_listDraft.postValue(resultState)
}
private suspend fun getDraft() : List<PolicyHolderSwitchingEntity> {
return withContext(Dispatchers.IO) {
repository.getSwitchingAndRedirectionDraft()
}
}
fun getListDraft() {
viewModelScope.launch {
val draft = getDraft()
if (draft.isNullOrEmpty()) {
setDraft(ResultState.NoData())
return#launch
}
setDraft(ResultState.HasData(draft))
}
}
This is my fragment to show list draft:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.shimmerStateView.setShimmerLayout(R.layout.shimmer_list_switching_redirection)
initRecyclerView()
vm.getListDraft()
vm.listDraft.observe(viewLifecycleOwner, Observer {
it?.let {
when (it) {
is ResultState.NoData -> {
binding.shimmerStateView.showNoData(R.string.no_switching_redirection_history)
binding.contentGroup.gone()
}
is ResultState.HasData -> {
adapter.notifyDataSetChanged()
binding.shimmerStateView.showHasData()
binding.contentGroup.visible()
adapter.submitList(it.data)
}
}
}
})
}
And this is my view model to show data in the detail page:
private val _draftSwitching = SingleLiveData<ResultState<PolicyHolderSwitchingEntity>>()
val draftSwitching: LiveData<ResultState<PolicyHolderSwitchingEntity>> get() = _draftSwitching
private fun setDraftSwitching(resultState: ResultState<PolicyHolderSwitchingEntity>) {
_draftSwitching.postValue(resultState)
}
fun getListSwitchingFromDraft(mptId: Long?) {
viewModelScope.launch(IO) {
try {
val switching = repository.showRecapSwitchingDraft(mptId)
setSwitchingForm(switching.policyHolder) // this is not deleted
setListInvestmentFund(switching.investmentFund) // but this is always deleted in the prev draft when a new draft added
setListFundSpinner(switching.listFund) // but this is always deleted in the prev draft when a new draft added
setDraftSwitching(ResultState.HasData(switching.policyHolder))
} catch (e: Throwable) {
Timber.e("Error getting switching : ${e.message}")
}
}
}
And this is my code in detail page:
private fun initDataListSwitchingDraft() {
vm.getListSwitchingFromDraft(mptId?.toLong())
vm.draftSwitching.observe(viewLifecycleOwner, Observer {
it?.let {
when (it) {
is ResultState.HasData -> {
val data = it.data
showContent()
binding.shimmerStateView.showHasData()
listSpinner.add(NominalType("0", NOMINAL))
listSpinner.add(NominalType("1", PERCENTAGE))
listSpinner.add(NominalType("2", UNIT))
val type = data.transferFrom
displayDataSpinnerType(listSpinner, type)
displayDataSourceFund(vm.currentSource.value)
}
}
}
})
}
And this is my Dao to show list draft:
#Transaction
#Query("SELECT * FROM policy_holder_switching WHERE is_draft=1")
fun getSwitchingAndRedirectionDrafts(): List<PolicyHolderSwitchingEntity>
And this is my Dao to show data in detail page:
#Transaction
#Query("SELECT * FROM policy_holder_switching WHERE mptIdSwitching=:mptIdSwitching AND is_draft=1")
fun fetchAllSwitchingFromDraft(mptIdSwitching: Long?): SwitchingFromAndDestination
Sorry, this is my fault. This because I forgot to use the primary key as auto-generated.

how Live data will be update in MVVM

I want to get input from the user using EditText and pass it to server and show the response to the user. I do this simply without any architecture but I would like to implement it in MVVM.
this is my repository code:
class Repository {
fun getData(context: Context, word: String): LiveData<String> {
val result = MutableLiveData<String>()
val request = object : StringRequest(
Method.POST,
"https://jsonplaceholder.typicode.com/posts",
Response.Listener {
result.value = it.toString()
},
Response.ErrorListener {
result.value = it.toString()
})
{
#Throws(AuthFailureError::class)
override fun getParams(): MutableMap<String, String> {
val params = HashMap<String, String>()
params["word"] = word
return params
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
return result
}
}
and these are my View Model codes:
class ViewModel(application: Application) : AndroidViewModel(application) {
fun getData(word: String): LiveData<String> {
val repository = Repository()
return repository.getData(getApplication(), word)
}
}
and my mainActivity would be like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val model = ViewModelProviders.of(this).get(ViewModel::class.java)
model.getData("test").observe(this, Observer {
Log.i("Log", "activity $it")
})
}
}
My layout has an EditText which I want to get user input and pass it to the server, how should i do that?
Here how i did it in my projet.
You can probably use android annotations.
It's gonna requiere you to put some dependencies and maybe change the class a bit, but then you gonna link your Viewmodel with your repository and then you gonna have to program the setter of the variable to do a notifyChange() by making the class herited from BaseObservable. Then in the xml, if you did the correctly, you should be able to do a thing like text:"#={model.variable}" and it should be updating at the same time.
A bit hard and explain or to show for me sorry, but i would look into Android Annotations with #DataBinding, #DataBound :BaseObservable
https://github.com/androidannotations/androidannotations/wiki/Data-binding-support
Hope that can help a bit!