I am new to jetpack compose,
I am showing a dataset to Lazycolumn that works fine.
When I try to fliter i.e. replace the original dataset with different dataset,
my Lazycolumn initially shows it the replaced one but in a flash it goes back to original dataset again.
Here some snippets to what I have done, I suspect my compose logic and could not able to find out
// The main composeable where I am observing the changes and calling ShowList to populate
#Composeable
fun SomeScreen(viewModel : TestviewModel){
val stateCountryCodeMap = remember { mutableStateOf(mapOf<String?, List<CountryInfo>>()) }
// observe and retrieve the dataset.
testViewModel.stateMapCountryInfo.collectAsState().value.let {
stateCountryCodeMap.value = it
}
// Some Test to buttom to load a different data set
someRandomeButtom.click{
viewModel. filterCountryList()
}
// request to load original data set
testViewModel.fetchCountryList()
ShowList(
currentSelected = stateCountryCodeSelectedIndex.value,
groupedCountryInfo = stateCountryCodeMap.value,
testViewModel = testViewModel
)
}
// The ShowList function to display
#Composable
private fun ShowList(
currentSelected: Pair<String, Int>,
groupedCountryInfo: Map<String?, List<CountryInfo>>,
testViewModel: TestViewModel
) {
// LazyColumn stuff to render itmems from map dataset
}
// and TestviewModel
val stateMapCountryInfo = MutableStateFlow(mapOf<String?, List<CountryInfo>>())
val stateSortedCountryInfo = MutableStateFlow(listOf<CountryInfo>())
fun fetchCountryList() {
// some IO operation which gives result
when (val result = getCountryListRepo.invoke()) {
is Result.Success -> {
val countryInfoResultList = result.data
// sort the list by country name and store
stateSortedCountryInfo.value = countryInfoResultList.sortedBy { it.countryName }
// save it to map
stateMapCountryInfo.value = stateSortedCountryInfo.value.groupBy { it.countryName?.get(Constants.ZERO).toString() }
}
}
val stateFilteredCountryInfo = MutableStateFlow(listOf<CountryInfo>())
fun filterCountryList() {
// some IO operation
// filter on the sorted array, // results 2 items - India, Indonesia
val filteredList = stateSortedCountryInfo.value.filter {
it.countryName?.contains("Ind") == true
}
// store the filtered result
stateFilteredCountryInfo.value = filteredList
// now assing it to map
stateMapCountryInfo.value = stateFilteredCountryInfo.value.groupBy { it.countryName?.get(Constants.ZERO).toString() }
}
}
}
Till this point, it is a straight forward display of items in ShowList method.
Now, back to SomeScreenMethod(..),
now If I click on that random button, which gives me a different/filtered list as expected
and LazyColumn updates it but then again goes back to original state.
Can you pinpoint where it went wrong?
it seems when recomposition is happening, getting triggered multiple times.
testViewModel.fetchCountryList()
can you try this and check
LaunchedEffect(Unit) {
testViewModel.fetchCountryList()
}
Related
I’ve got a problem with a LazyColumn of elements that have a favourite button: basically when I tap the favourite button, the item that is being favourited (a document in my case) is changed in the underlying data structure in the VM, but the view isn’t updated, so I never see any change in the button state.
class MainViewModel(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
var documentList = emptyList<PDFDocument>().toMutableStateList()
....
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
}
}
The composables are:
#Composable
fun DocumentRow(
document: PDFDocument,
onDocumentClicked: (String, Boolean) -> Unit,
onFavoriteValueChange: (Uri) -> Unit
) {
HeartIcon(
isFavorite = document.favorite,
onValueChanged = { onFavoriteValueChange(document.uri) }
)
}
#Composable
fun HeartIcon(
isFavorite: Boolean,
color: Color = Color(0xffE91E63),
onValueChanged: (Boolean) -> Unit
) {
IconToggleButton(
checked = isFavorite,
onCheckedChange = {
onValueChanged()
}
) {
Icon(
tint = color,
imageVector = if (isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
}
}
Am I doing something wrong? because when I call the toggleFavouriteDocument in the ViewModel, I see it’s marked or unmarked as favorite but there is no recomposition at all anywhere.
I might be missing it because you didn't post the rest of your code, but your documentList in the VM isn't observable, so how would the Composable know that it got changed? It needs to be something like Flow or LiveData, and it needs to be observed in the Composable. Something like this:
in ViewModel:
val documentList = MutableLiveData<List<PDFDocument>>()
in Composable:
val documentList by viewModel.documentList.observeAsState(List<PDFDocument>())
And you'll probably have to change the way you modify items in documentList. LiveData is weird about mutable collections inside MutableLiveData, and modifying individual items doesn't trigger a state change. You have to create a copy of the list with the modified items, and then re-port the whole list to the LiveData variable:
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.value?.let { oldList ->
// create a copy of existing list
val newList = mutableListOf<PDFDocument>()
newList.addAll(oldList)
// modify the item in the new list
newList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
// update the observable
documentList.postValue(newList)
}
}
Edit: There's also a potential problem with the way that you're trying to update the favorite value in the existing list. Without knowing how PDFDocument is implemented, I don't know if you can use the = operator. You should test that to make sure that newList.find { it == pdfDocument } actually finds the document
I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.
I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}
I have a search fragment that shows list of searched items.
if user type something, I pass that string to url as new query parameter and get new list using paging 3 library.
first solution is:
//viewModel
lateinit var postListUrl: String
val postList: Flow<PagingData<Post>> = Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, postListUrl)
}.flow.cachedIn(viewModelScope)
//fragment
fun showPostList(url: String) {
postListAdapter.submitData(lifecycle, PagingData.empty())
viewModel.postListUrl = url
viewLifecycleOwner.lifecycleScope.launch {
viewModel.postList.collectLatest {
postListAdapter.submitData(it)
}
}
}
by this solution by changing url (showPostList(newUrl), list remain without any changes. maybe using cached list in viewModel.
another solution is:
using showPostList(initUrl) in onViewCreated of fragment and then using blew method by changing parameter:
//fragment
fun changePostList(url: String) {
viewModel.postListUrl = url
postListAdapter.refresh()
}
this work but if old list and new list have common item, new list show on last common visible item.
for example if 5th position item of old list is same as 7th of new list, then on after list change to show new list, it start from 7th position not first item.
I found another solution here:
//viewModel
val postListUrlFlow = MutableStateFlow("")
val postList = postListUrlFlow.flatMapLatest { query ->
Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, query)
}.flow.cachedIn(viewModelScope)
}
//fragment
fun showPostList(url: String) {
postListAdapter.submitData(lifecycle, PagingData.empty())
viewModel.postListUrlFlow.value = url
viewLifecycleOwner.lifecycleScope.launch {
viewModel.postList.collectLatest {
postListAdapter.submitData(it)
}
}
}
but by using this list refresh on back to fragment and sometimes Recyclerview state changing.
class MyViewModel:ViewModel(){
private val _currentQuery = MutableLiveData<String>()
val currentQuery:LiveData<String> = _currentQuery
val users = _currentQuery.switchMap { query->
Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, query)
}.livedata.cachedIn(viewModelScope)
}
fun setCurrentQuery(query: String){
_currentQuery.postValue(query)
}
}
By the use of
SwitchMap
you can get new results every time query is chnaged and it will replace the old data .
Inside my ViewModel class i have defined my paged list configuration
private val pagedListConfig: PagedList.Config = PagedList.Config.Builder().apply {
setEnablePlaceholders(true)
setInitialLoadSizeHint(10)
setPageSize(10)
}.build()
After that i retrieve from my Room database the messages that i want to show in my chatRoom Activity given to the groupId which i also take it from database and i make a switchMap Transformation
private var groupChatItem = MutableLiveData<GroupChatItem>()
var chatRoomGroupMessages: LiveData<PagedList<MessageWithMsgQueueAccount>> =
Transformations.switchMap(groupChatItem) {
it?.let {
LivePagedListBuilder(
messagesRepository.retrieveChatRoomGroupMessages(
chatRoomServerId,
it.groupId
), pagedListConfig
).build()
}
}
All good up to now. Here i want to transform the List to expose a list of List, so basically i want to convert every element to a element through a function.
So what i need is a Transformation.map() to the first LiveData so i can change it to another LiveData. But the problem is that i want to do it with Paged List. How can i do this?
var messageChatItems: LiveData<List<MessageChatItem>> = Transformations.map(chatRoomGroupMessages, messageChatItem -> {
// Here is where i need to call the function
})
fun convertGroupItemToMessageItem(): MessageChatItem {
// here i make the convertion
}
So i get this to work as below
var chatRoomGroupMessages: LiveData<PagedList<MessageItem>> = Transformations.switchMap(groupChatItem) {
it?.let {
// Here is the messages from database
val groupItemFactory = messagesRepository.getChatRoomMessages()
.map { messageItem: ChatMessageItem? ->
// Here i transform them
toMessageChatItem(messageItem, it.accountId) }
LivePagedListBuilder(
groupItemFactory, pagedListConfig
).build()
}
}
And the transform function is the "toMessageItem()" function