Take snapshot of MutableStateFlow that doesn't change with the state in Jetpack Compose - kotlin

I'm trying to make an editor app that allows you to undo/redo changes. I want to achieve that by storing the ui state in stacks (ArrayDeque) and pop them back once the user hits undo/redo. But every time I stored the state in stack, after I made change to the state, the value in the stack is changed as well.
Is there a way to snapshot a state that won't be affected by future changes in the state flow?
My code looks something like this:
Data Class
class Data () {
var number: Int = 0
fun foo()
}
State Class
data class UiState(
val dataList: MutableList<Data> = mutableListOf(),
)
ViewModel
class UiViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
private val undoStack: ArrayDeque<UiState> = ArrayDeque()
fun makeChangeToState() {
saveStateToUndoStack(_uiState.value)
Log.i("Test", undoStack.last().dataList[0].number) // 0
val dataList= _uiState.value.dataList.toMutableStateList()
dataList[0].number = 1
_uiState.update { currentState ->
currentState.copy(
dataList = dataList,
)
}
Log.i("Test", undoStack.last().dataList[0].number) // 1 because the _uiState changed
}
fun undo() {
val lastState = undoStack.last()
// Won't work because the data in lastState has already been updated with _uiState
_uiState.update { lastState }
}
}
Things I've tried:
Use _uiState.value.copy
Call saveStateToUndoStack(uiState: UiState) from Composable functions and pass in the viewModel.uiState.collectAsState()
Both doesn't seem to work, I play around for a few hours but don't have a clue.

The reason why the old value got updated is because all the objects in the list are references, not related to MutableStateFlow. I just need a deep copy of the list, see this post here:
https://www.baeldung.com/kotlin/deep-copy-data-class
Another thread worth reading: Deep copy of list with objects in Kotlin

Related

API data takes 2 button clicks in order to display data - Kotlin

I've tried searching stackoverflow, but couldn't find the answer to my issue. When the search button is clicked, I want the app to display the data from the API. The issue I'm having is that it is taking 2 clicks of the search button in order to display the data. The first click displays "null" and the second click displays all data correctly. What am I doing wrong? What do I need to change in order to process correctly on the first click? Thanks in advance!
Pairing Fragment
package com.example.winepairing.view.fragments
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.activityViewModels
import com.example.winepairing.databinding.FragmentPairingBinding
import com.example.winepairing.utils.hideKeyboard
import com.example.winepairing.viewmodel.PairingsViewModel
class PairingFragment : Fragment() {
private var _binding: FragmentPairingBinding? = null
private val binding get() = _binding!!
private val viewModel: PairingsViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentPairingBinding.inflate(inflater, container, false)
val view = binding.root
val toolbar = binding.toolbar
(activity as AppCompatActivity).setSupportActionBar(toolbar)
binding.searchBtn.setOnClickListener {
hideKeyboard()
if (binding.userItem.text.isNullOrEmpty()) {
Toast.makeText(this#PairingFragment.requireActivity(),
"Please enter a food, entree, or cuisine",
Toast.LENGTH_SHORT).show()
} else {
val foodItem = binding.userItem.text.toString()
getWinePairing(foodItem)
pairedWinesList()
pairingInfo()
}
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun pairedWinesList() {
val pairedWines = viewModel.apiResponse.value?.pairedWines
var content = ""
if (pairedWines != null) {
for (i in 0 until pairedWines.size) {
//Append all the values to a string
content += pairedWines.get(i)
content += "\n"
}
}
binding.pairingWines.setText(content)
}
private fun pairingInfo() {
val pairingInfo = viewModel.apiResponse.value?.pairingText.toString()
binding.pairingInfo.setText(pairingInfo)
}
private fun getWinePairing(foodItem: String) {
viewModel.getWinePairings(foodItem.lowercase())
}
}
So, sorry!!! Here is the viewmodel
package com.example.winepairing.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.winepairing.BuildConfig
import com.example.winepairing.model.data.Wine
import com.example.winepairing.model.network.WineApi
import kotlinx.coroutines.launch
const val CLIENT_ID = BuildConfig.SPOONACULAR_ACCESS_KEY
class PairingsViewModel: ViewModel() {
private val _apiResponse = MutableLiveData<Wine>()
val apiResponse: LiveData<Wine> = _apiResponse
fun getWinePairings(food: String) {
viewModelScope.launch {
_apiResponse.value = WineApi.retrofitService.getWinePairing(food, CLIENT_ID)
}
}
}
Although you didn't share your ViewModel code, I'm guessing that your ViewModel's getWinePairings() function retrieves data asynchronously from an API and then updates a LiveData called apiResponse with the return value. Since the API response takes some time before it returns, your apiResponse LiveData is going to still be empty by the time you call the Fragment's pairedWinesList() function from the click listener.
Hint, any time you use the .value of a LiveData outside of the ViewModel that manages it, you are probably doing something wrong. The point of LiveData is to react to it's data when it arrives, so you should be calling observe() on it instead of trying to read its .value synchronously.
More information in this question about asynchronous calls.
You haven't posted your actual code for fetching data from the API (probably in viewModel#getWinePairings), but at a guess it's going like this in your button click listener:
you call getWinePairing - this kicks off an async call that will eventually complete and set data on viewModel.apiResponse, sometime in the future. It's initial value is null
you call pairedWinesList which references the current value of apiResponse - this is gonna be null until an API call sets a value on it. Since you're basically fetching the most recent completed search result, if you change your search data then you'll end up displaying the results of the previous search, while the API call runs in the background and updates apiResponse later
you call pairingInfo() which is the same as above, you're looking at a stale value before the API call returns with the new results
The problem here is you're doing async calls that take a while to complete, but you're trying to display the results immediately by reading the current value of your apiResponse LiveData. You shouldn't be doing that, you should be using a more reactive design that observes the LiveData, and updates your UI when something happens (i.e. when you get new results):
// in onCreateView, set everything up
viewModel.apiResponse.observe(viewLifeCycleOwner) { response ->
// this is called when a new 'response' comes in, so you can
// react to that by updating your UI as appropriate
binding.pairingInfo.setText(response.pairingInfo.toString())
// this is a way you can combine all your wine strings FYI
val wines = response.pairedWines?.joinToString(separator="\n") ?: ""
binding.pairingWines.setText(wines)
}
binding.searchBtn.setOnClickListener {
...
} else {
val foodItem = binding.userItem.text.toString()
// kick off the API request - we don't display anything here, the observing
// function above handles that when we get the results back
getWinePairing(foodItem)
}
}
Hopefully that makes sense - the button click listener just starts the async fetch operation (which could be a network call, or a slow database call, or a fast in-memory fetch that wouldn't block the thread - the fragment doesn't need to know the details!) and the observe function handles displaying the new state in the UI, whenever it arrives.
The advantage is you're separating everything out - the viewmodel handles state, the UI just handles things like clicks (updating the viewmodel) and displaying the new state (reacting to changes in the viewmodel). That way you can also do things like have the VM save its own state, and when it initialises, the UI will just react to that change and display it automatically. It doesn't need to know what caused that change, y'know?

Kotlin Stateflow get one-but-last value

In my viewModel i have a StateFlow<Destination>, with those values:
enum class Destination {
FIRST_SCREEN,
SECOND_SCREEN,
THIRD_SCREEN,
FOURTH_SCREEN,
}
Somewhere in my activity i use this flow for navigation:
viewModel.destinations[...]
.collect { destination ->
fragmentManager.navigateTo(
when(destination){
FIRST_SCREEN -> FirstScreenFragment(),
SECOND_SCREEN -> SecondScreenFragment(),
THIRD_SCREEN -> ThirdScreenFragment(),
FOURTH_SCREEN -> FourthScreenFragment(),
}
)
}
...where navigateTo() is very simple in the moment
fun FragmentManager.navigateTo(fragment: Fragment) {
beginTransaction()
.replace(R.id.contentFragment, fragment)
.commit()
}
I would now like to add transitions ~ like in a viewPager.
The animations themselves are a piece of cake, but i need to know in which direction to animate:
Am i moving "forward" or "backward", which boils down to:
Is my current destination < or > my new destination?
i could use the fragment-backstack (but i and a few others hate it)
i could simply use a variable in my activity storing the last screen we navigated to, but that feels hacky
i could try to use flows for that, but i have no real idea how to do that. Any suggestions?
An optimal usage would look like:
viewModel.destinations[...]
.rememberHistory()
.collect { currentDestination, lastDestination ->
}
For those wondering how i solved this:
I used runningFold
data class DestinationHistory(val previous: Destination?, val current: Destination)
val destinationHistory: Flow<DestinationHistory> = destination.runningFold(
initial = DestinationHistory(previous = null, current = Destination.FIRST_SCREEN)
) { history, newDestination ->
DestinationHistory(
previous = history.current,
current = newDestination
)
}
Effectively this maps the destination-flow to a "pair" of previous & current destination.

How to modify variables outside of their scope in kotlin?

I understand that in Kotlin there is no such thing as "Non-local variables" or "Global Variables" I am looking for a way to modify variables in another "Scope" in Kotlin by using the function below:
class Listres(){
var listsize = 0
fun gatherlistresult(){
var listallinfo = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
listallinfo.addOnSuccessListener {
listResult -> listsize += listResult.items.size
}
}
}
the value of listsize is always 0 (logging the result from inside of the .addOnSuccessListener scope returns 8) so clearly the listsize variable isn't being modified. I have seen many different posts about this topic on other sites , but none fit my usecase.
I simply want to modify listsize inside of the .addOnSuccessListener callback
This method will always be returned 0 as the addOnSuccessListener() listener will be invoked after the method execution completed. The addOnSuccessListener() is a callback method for asynchronous operation and you will get the value if it gives success only.
You can get the value by changing the code as below:
class Demo {
fun registerListResult() {
var listallinfo = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
listallinfo.addOnSuccessListener {
listResult -> listsize += listResult.items.size
processResult(listsize)
}
listallinfo.addOnFailureListener {
// Uh-oh, an error occurred!
}
}
fun processResult(listsize: Int) {
print(listResult+"") // you will get the 8 here as you said
}
}
What you're looking for is a way to bridge some asynchronous processing into a synchronous context. If possible it's usually better (in my opinion) to stick to one model (sync or async) throughout your code base.
That being said, sometimes these circumstances are out of our control. One approach I've used in similar situations involves introducing a BlockingQueue as a data pipe to transfer data from the async context to the sync context. In your case, that might look something like this:
class Demo {
var listSize = 0
fun registerListResult() {
val listAll = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
val dataQueue = ArrayBlockingQueue<Int>(1)
listAll.addOnSuccessListener { dataQueue.put(it.items.size) }
listSize = dataQueue.take()
}
}
The key points are:
there is a blocking variant of the Queue interface that will be used to pipe data from the async context (listener) into the sync context (calling code)
data is put() on the queue within the OnSuccessListener
the calling code invokes the queue's take() method, which will cause that thread to block until a value is available
If that doesn't work for you, hopefully it will at least inspire some new thoughts!

How to correctly update value of IntegerProperty in the view?

I am building simple application that will track a certain tabletop game of 2 players.
I have a view called MatchView
class MatchView : View() {
// data
private var currentRound = SimpleIntegerProperty(0)
private var currentTurn = SimpleIntegerProperty(0)
override val root = borderpane {
center = label(currentRound.stringBinding{ "Round %d".format(it) })
// other layout-related stuff
subscribe<EndTurnEvent> {
endPlayerTurn()
}
}
private fun endPlayerTurn() {
// increment turn
currentTurn.plus(1)
currentRound.plus(currentTurn.value % 2)
}
}
that is subscribed to EndTurnEvent - event emitted by one of the fragments used by view.
The called method is supposed to increment value of currentTurn and if needed currentRound (round increments after second player ends their turn)
However neither the value of currentRound nor the one of currentTurn are getting increased when i call .plus() method.
I have tried editting values differently :
private fun endPlayerTurn() {
// increment turn
currentTurn.value = currentTurn.value + 1
currentRound.value = currentTurn.value % 2
}
But this throws me java.lang.IllegalStateException: Not on FX application thread
I am aware that putting properties into views is anti-pattern, but since I just want to keep track of 2 properties, I thought I could put them directly into View
Platform.runLater(() -> {
// Update GUI from another Thread here
});

How to inform a Flux that I have an item ready to publish?

I am trying to make a class that would take incoming user events, process them and then pass the result to whoever subscribed to it:
class EventProcessor
{
val flux: Flux<Result>
fun onUserEvent1(e : Event)
{
val result = process(e)
// Notify flux that I have a new result
}
fun onUserEvent2(e : Event)
{
val result = process(e)
// Notify flux that I have a new result
}
fun process(e : Event): Result
{
...
}
}
Then the client code can subscribe to EventProcessor::flux and get notified each time a user event has been successfully processed.
However, I do not know how to do this. I tried to construct the flux with the Flux::generate function like this:
class EventProcessor
{
private var sink: SynchronousSink<Result>? = null
val flux: Flux<Result> = Flux.generate{ sink = it }
fun onUserEvent1(e : Event)
{
val result = process(e)
sink?.next(result)
}
fun onUserEvent2(e : Event)
{
val result = process(e)
sink?.next(result)
}
....
}
But this does not work, since I am supposed to immediately call next on the SynchronousSink<Result> passed to me in Flux::generate. I cannot store the sink as in the example:
reactor.core.Exceptions$ErrorCallbackNotImplemented:
java.lang.IllegalStateException: The generator didn't call any of the
SynchronousSink method
I was also thinking about the Flux::merge and Flux::concat methods, but these are static and they create a new Flux. I just want to push things into the existing flux, such that whoever holds it, gets notified.
Based on my limited understanding of the reactive types, this is supposed to be a common use case. Yet I find it very difficult to actually implement it. This brings me to a suspicion that I am missing something crucial or that I am using the library in an odd way, in which it was not intended to be used. If this is the case, any advice is warmly welcome.