in my ViewModel:
private val _itemList = mutableStateListOf<Post>()
val itemList: List<Post> = _itemList
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy(isLiked = true)
}
Here my Post data class:
data class Post(
val id: Int,
val name: String,
val isLiked: Boolean = false,
)
And here my Composable:
val postList = viewModel.itemList
LazyRow(content = {
items(postList.size) { i ->
val postItem = postList[i]
PostItem(
name = postItem.name,
isLiked = postItem.isLiked,
likePost = { viewModel.likePost(postItem)}
)
}
})
The change does not update in the UI instantly, I first have to scroll the updated item out of the screen so it recomposes or switch to another Screen and go back to see the change.
For some reason it doesn't like updating, it will add and delete and update instantly. You have to do it this way when updating for our to update the state.
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy()
_itemList[index].isLiked = true
}
You are returning a List<> effectively and not MutableStateList from your ViewModel.
If you want the list to not be mutable from the view, I happen to use MutableStateFlow<List<>> and return StateFlow<List<>>. You could also just convert it to a list in your composable.
Edit:
//backing cached list, or could be data source like database
private val deviceList = mutableListOf<Device>()
private val _deviceListState = MutableStateFlow<List<Device>>(emptyList())
val deviceListState: StateFlow<List<BluetoothDevice>> = _deviceListState
//manipulate and publish
fun doSomething() {
_deviceListState.value = deviceList.filter ...
}
In your UI
val deviceListState = viewModel.deviceListState.collectAsState().value
Related
Below is my viewmodel class body
private var _movieState = mutableStateOf(false)
val movieState = _movieState
private val query = if (_movieState.value) ListState.POPULAR_PLAYING else ListState.ALL_PLAYING
val moviesData: Flow<PagingData<Movie>> = Pager(PagingConfig(pageSize = 10)) {
MoviePagingSource(movieRepository, query.string)
}.flow
And I have Two function which populate the Boolean value
fun setListToPopular(){
_movieState.value = true
}
fun setListToAllNowPlaying(){
_movieState.value = false
}
which will be triger from UI
Now i have one mistake once the code of viewmodel runs my if block is over but i want to observe that Boolean state within it.
how should i do that ?
I'm trying to display a 4x4 grid with values that change depending on user input. To achieve that, I created mutableStateListOf that I use in a ViewModel to survive configuration changes. However, when I try to replace a value in that particular list using button onClick, it keeps doing that until app crashes. I can't understand why is onReplaceGridContent looping after clicking the button once. Currently, my code looks like this:
ViewModel:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
fun initialize(){
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Button:
Button(
onClick = {
onReplaceGridContent(GameUtils.getRandomTileNumber(),GameUtils.getRandomTilePosition(gameGridContent))},
colors = Color.DarkGray
){
Text(text = "Add number to tile")
}
Activity Composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
gameViewModel.initialize()
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
Your initialize will actually run on every recomposition of gameScreen:
You click on a tile - state changes causing recomposition.
initializa is called and changes the state again causing recomposition.
Step 2 happens again and again.
You should initialize your view model in its constructor instead (or use boolean flag to force one tim initialization) to make it inly once.
Simply change it to constructor:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
init {
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Now you don't need to call initialize in the composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
I'm have a LazyColumn that renders a list of items. However, I now want to fetch more items to add to my lazy list. I don't want to re-render items that have already been rendered in the LazyColumn, I just want to add the new items.
How do I do this with a StateFlow? I need to pass a page String to fetch the next group of items, but how do I pass a page into the repository.getContent() method?
class FeedViewModel(
private val resources: Resources,
private val repository: FeedRepository
) : ViewModel() {
// I need to pass a parameter to `repository.getContent()` to get the next block of items
private val _uiState: StateFlow<UiState> = repository.getContent()
.map { content ->
UiState.Ready(content)
}.catch { cause ->
UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
initialValue = UiState.Loading
)
val uiState: StateFlow<UiState>
get() = _uiState
And in my UI, I have this code to observe the flow and render the LazyColumn:
val lifecycleAwareUiStateFlow: Flow<UiState> = remember(viewModel.uiState, lifecycleOwner) {
viewModel.uiState.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val uiState: UiState by lifecycleAwareUiStateFlow.collectAsState(initial = UiState.Loading)
#Composable
fun FeedLazyColumn(
posts: List<Post> = listOf(),
scrollState: LazyListState
) {
LazyColumn(
modifier = Modifier.padding(vertical = 4.dp),
state = scrollState
) {
// how to add more posts???
items(items = posts) { post ->
Card(post)
}
}
}
I do realize there is a Paging library for Compose, but I'm trying to implement something similar, except the user is in charge of whether or not to load next items.
This is the desired behavior:
I was able to solve this by adding the new posts to the old posts before emitting it. See comments below for the relevant lines.
private val _content = MutableStateFlow<Content>(Content())
private val _uiState: StateFlow<UiState> = repository.getContent()
.mapLatest { content ->
_content.value = _content.value + content // ADDED THIS
UiState.Ready(_content.value)
}.catch { cause ->
UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
initialValue = UiState.Loading
)
private operator fun Content.plus(content: Content): Content = Content(
posts = this.posts + content.posts,
youTubeNextPageToken = content.youTubeNextPageToken
)
class YouTubeDataSource(private val apiService: YouTubeApiService) :
RemoteDataSource<YouTubeResponse> {
private val nextPageToken = MutableStateFlow<String?>(null)
fun setNextPageToken(nextPageToken: String) {
this.nextPageToken.value = nextPageToken
}
override fun getContent(): Flow<YouTubeResponse> = flow {
// retrigger emit when nextPageToken changes
nextPageToken.collect {
emit(apiService.getYouTubeSnippets(it))
}
}
}
For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())
The fragment I am coding right now is supposed to give the user a calendaric overview of his meal planning schedule. So via date picker, he can choose a time period and the program will show the user which recipes he has chosen for the chosen weekdays.
So I build a nested RecyclerView with the weekdays as parent layer and corresponding recipes as a child layer. The data class for the weekday layer looks like this :
data class Weekday (
val weekday : String,
val listWithRecipes : List<Recipe>?
)
The class for the Recipe entity looks like this:
#Entity(tableName = "Recipe")
#Parcelize
data class Recipe(
#PrimaryKey var recipeName : String,
var description : String?,
var serving : Int,
var preparationTime : Int?
) : Parcelable
The Adapter for the top Recycler View like this :
class MealPlanAdapter(private var mealplan: List<Weekday>) :
RecyclerView.Adapter<MealPlanAdapter.MealPlanViewHolder>(), RecipeAdapter.OnItemClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MealPlanViewHolder {
return MealPlanViewHolder(
DailyMealplanItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun getItemCount() = mealplan.size
override fun onBindViewHolder(holder: MealPlanViewHolder, position: Int) {
val weekday = mealplan[position]
val recipeAdapter = RecipeAdapter(this)
recipeAdapter.submitList(weekday?.listWithRecipes)
holder.dayOfWeek.text = weekday.weekday
val recipeLayoutManager = LinearLayoutManager(holder.recyclerView.context,RecyclerView.VERTICAL, false)
recipeLayoutManager.initialPrefetchItemCount = 4
holder.recyclerView.apply{
layoutManager = recipeLayoutManager
adapter = recipeAdapter
}
}
fun setSchedule(mealplan : List <Weekday>){
this.mealplan = mealplan
notifyDataSetChanged()
}
inner class MealPlanViewHolder(val binding: DailyMealplanItemBinding) :
RecyclerView.ViewHolder(binding.root) {
val recyclerView: RecyclerView = binding.rvRecyclerView
val dayOfWeek: TextView = binding.tvDayOfWeek
}
override fun onItemClick(recipe: Recipe) {
TODO("Not yet implemented")
}
}
Whenever the user changes the time period, the setScheduled() method in the adapter gets called in the fragment.
materialDatePickerStartDate.addOnPositiveButtonClickListener(
MaterialPickerOnPositiveButtonClickListener<Any?> { selection ->
_binding.viewmodel!!.startDateInUTCFormat = selection as Long
_binding.tvStartDate.setText(materialDatePickerStartDate.headerText)
adapter.setSchedule(
_binding.viewmodel!!.returnListWithWeekDaysAndCorrespondingRecipes(
_binding.viewmodel!!.startDateInUTCFormat,
_binding.viewmodel!!.endDateInUTCFormat
)
)
}
)
The viewmodel looks like this :
#HiltViewModel
class MealplanViewModel #Inject constructor(
val mealPlanRepository: MealPlanRepository
) : ViewModel() {
private lateinit var _binding: FragmentMealPlanBinding
var startDateInUTCFormat: Long = System.currentTimeMillis()
var endDateInUTCFormat: Long = System.currentTimeMillis()
fun returnListWithWeekDaysAndCorrespondingRecipes(
startDate: Long,
endDate: Long
): ArrayList<Weekday> {
var startDate = Date(startDateInUTCFormat)
var endDate = Date(endDateInUTCFormat)
var startDateCalendar = dateToCalendar(startDate)
var endDateCalendar = dateToCalendar(endDate)
val calendarDays = createListWithCalendarDates(startDateCalendar, endDateCalendar)
return createListWithWeekDaysAndCorrespondingRecipes(calendarDays)
}
fun dateToCalendar(date: Date): Calendar {
var calInstance = Calendar.getInstance()
calInstance.setTime(date)
return calInstance
}
fun createListWithCalendarDates(
startDateCalendar: Calendar,
endDateCalendar: Calendar
): ArrayList<Calendar> {
var listWithCalendarDates = arrayListOf<Calendar>()
while (startDateCalendar <= endDateCalendar) {
listWithCalendarDates.add(startDateCalendar.clone() as Calendar)
startDateCalendar.add(Calendar.DATE, 1)
}
return listWithCalendarDates
}
fun createListWithWeekDaysAndCorrespondingRecipes(calendarDays: ArrayList<Calendar>): ArrayList<Weekday> {
var dayOfWeekAsString: String
var listWithDaysOfWeeksAndRecipes = arrayListOf<Weekday>()
var flattenedListWithRecipes: List<Recipe>?
for (i in 0 until calendarDays.size) {
var dayOfWeekAsInt = calendarDays[i].get(Calendar.DAY_OF_WEEK)
dayOfWeekAsString = when (dayOfWeekAsInt) {
1 -> "Sunday"
2 -> "Monday"
3 -> "Tuesday"
4 -> "Wednesday"
5 -> "Thursday"
6 -> "Friday"
else -> "Saturday"
}
var calendarDateInString =
transformCalendarDateIntoRequiredStringFormat(calendarDays[i])
var listWithDateAndCorrespondingRecipes: List<MealplanScheduleWithRecipes> =
listOf()
var liveDatalistWithDateAndCorrespondingRecipes =
mealPlanRepository.getMealplanScheduleWithRecipes(calendarDateInString)
liveDatalistWithDateAndCorrespondingRecipes.observeForever() { list ->
listWithDateAndCorrespondingRecipes = list
var listWithRecipes = listWithDateAndCorrespondingRecipes?.map { it.recipes }
flattenedListWithRecipes = listWithRecipes?.flatten()
var wochentag = dayOfWeekAsString
listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes))
}
}
return listWithDaysOfWeeksAndRecipes
}
fun transformCalendarDateIntoRequiredStringFormat(calendarDate: Calendar): String {
var year = calendarDate.get(Calendar.YEAR)
var month = transformCalendarMonthFormatToCorrectMonth(calendarDate)
var day = calendarDate.get(Calendar.DAY_OF_MONTH)
return "$day" + "$month" + "$year"
}
fun transformCalendarMonthFormatToCorrectMonth(calendarDate: Calendar): String {
var monthCalendarFormat = calendarDate.get(Calendar.MONTH)
var monthCorrectFormat = when (monthCalendarFormat) {
0 -> "1"
1 -> "2"
2 -> "3"
3 -> "4"
4 -> "5"
5 -> "6"
6 -> "7"
7 -> "8"
8 -> "9"
9 -> "10"
10 -> "11"
else -> "12"
}
return monthCorrectFormat
}
fun datesAreReasonable(startDate: Long, endDate: Long): Boolean {
return (startDate <= endDate)
}
}
My problem is the list that is passed to the RecyclerView Adapter consists of Weekday objects, which consist of the name of the weekday and the corresponding recipes (see data class "weekday" on top).
In the method "createListWithWeekDaysAndCorrespondingRecipes" in the viewmodel I create this list in a for loop that gets all weekdays between given Dates and their corresponding recipes. However, the recipes are LiveData fetched asynchronously via Room database query while the names of the weekdays are derived synchronously in the main thread. At the end however when I create the Weekday object
(see listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes) at the end of the for loop) I need them together at the same time. I haven't found a way how I can coordinate this successfully. At the moment the logics for adding the object to the list is in the asynchronous "observeForever" block.
See here:
liveDatalistWithDateAndCorrespondingRecipes.observeForever() { list ->
listWithDateAndCorrespondingRecipes = list
var listWithRecipes = listWithDateAndCorrespondingRecipes?.map { it.recipes }
flattenedListWithRecipes = listWithRecipes?.flatten()
var wochentag = dayOfWeekAsString
listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes))
}
This creates wrong results, probably because the coordination between main thread and the observer thread doesn't work.
If I however take the logics of adding out of the observer block, the list with recipes will give me null, because of the asynchronous character of the query.
I know that I described the problem very badly. Maybe still someone got a grasp of it and can help?
You should try to avoid using observeForever, I expect you are using this inside a fragment or an activity which actually has a lifecyclescope that your observer can use.
Your observer should look something like this
liveDataList.observe(viewLifecycleOwner, { list ->
// The way I do it at the moment I just set the recyclerViews adapter and layoutManager here
// This is not the best way to do it, so please keep that in mind
recyclerView.apply {
adapter = MyAdapter(list)
layoutManager = LinearLayoutManager(requireContext())
}
})
// Or if used inside an activity
liveDataList.observe(this, {})
This way your observer will be attached to your lifecycle and "die" together with your view. Whenever that list changes, you will show all entities in the recyclerView. HOWEVER, when you use an Array together with LiveData the LiveData object never "updates" when you just add something to that value, since the array only is a memory reference to the start of the array.
To counter this whenever you add something to your array you need to refresh the LiveData object in order to trigger an update and all observers.
myLiveDataObject.value.add(someOtherObject)
myLiveDataObject.value = myLiveDataObject.value
myLiveDataObject.value = myLiveDataObject.value triggers all observers that there has been a change, annoying I know
If you use it inside a viewHolder or adapter simply pass the lifecycle along with the list
I am also quite new to kotlin, keep that in mind and I guarantee you there is a better way to do this, but hope it helps