Kotlin Stateflow get one-but-last value - kotlin

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.

Related

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

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

Trying to get a when expression to show the result on the ui in kotlin

I have been stuck making this metric converter app for over a week I keep going back to the previous codelabs that explain how to make a tip calculator but when I try to apply the same method to this app that I'm trying to make it doesn't have any errors but the app crashes whenever I try to test it to see if I can get the functionality to work this is my last resort. I've tried to do when statements but I kept getting the error that certain branches would never be reached. if anyone can point me in the right direction as to what I'm doing wrong or what's missing from my code. I'm all ears.
package com.example.metricconversion
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.metricconversion.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(R.layout.activity_main)
setContentView(binding.root)
binding.conversionTo.setOnClickListener { convertMetric() }
}
fun convertMetric() {
val stringInTextField = binding.howMuchStuffWeTalking.text.toString()
val amountIQ = stringInTextField.toDouble()
val selectedId = binding.unitThatNeedsToBeConverted.checkedRadioButtonId
val selectedId2 = binding.toBeConverted.checkedRadioButtonId
// val grainToQtr = R.id.Grain and R.id.quarter1
//val grainToSt= R.id.Grain and R.id.quarter1
//val grainToLb = R.id.Grain and R.id. pound1
//val grainToTon= R.id.Grain and R.id.Ton1
val st = R.id.Grain and R.id.stone1
// code to convert grain to the other options
//code to convert grain to the other options
when (selectedId and selectedId2) {
R.id.Grain and R.id.Ounce1 -> {
amountIQ / 437.5
}
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.quarter1 -> amountIQ * 0.000005714
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.pound1 -> amountIQ * 0.0001429
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.Ton1 -> amountIQ / 1.4e+7
}
when (selectedId and selectedId2) {
st -> {
amountIQ * 0.0000102
}
}
binding.metricConverted.text=getString(R.string.end_result,convertMetric())
}
}
Problems with your code:
Using and to check two values for equality at the same time is not viable. The and function does a bitwise operation to merge the two numbers together. There are many possible inputs that could merge to the same solution, so you will get false positives. Instead you could use the to infix function to combined the two numbers into a Pair wrapper object so they are both preserved for the comparison.
You have a series of individual when statements, each with only a single condition. It doesn't make sense to use a when statement with a single condition. Usually a single condition would be represented with an if statement instead of when statement. But I'm guessing you didn't mean for these to be separate when statements, based on what I'm seeing.
Your when statements are not actually doing anything. Their branches resolve to a number, but you're not doing anything with that number (not assigning it to a variable or logging it or showing it in the UI, etc.). So they are useless. Your statement at the bottom isn't getting the result of any of the when statements, but is instead recursively calling the same function again. This function doesn't return anything, so that's useless. Even if it did return a number, the recursive call will create an infinite loop, resulting in a stack overflow.
So first, to just fix your code (we can discuss a better way of doing it later):
We replace and with to.
We merge all the when statements into a single statement.
We store the result of the when statement in a variable and use that variable to set the text in the last line of the function. When you use a when statement with a subject (something in parentheses that is compared for each branch) or to get a result, you have to cover every possible case, so an else branch must also be added.
fun convertMetric() {
val stringInTextField = binding.howMuchStuffWeTalking.text.toString()
val amountIQ = stringInTextField.toDouble()
val selectedId = binding.unitThatNeedsToBeConverted.checkedRadioButtonId
val selectedId2 = binding.toBeConverted.checkedRadioButtonId
// code to convert grain to the other options
val result = when (selectedId to selectedId2) {
R.id.Grain to R.id.Ounce1 -> amountIQ / 437.5
R.id.Grain to R.id.quarter1 -> amountIQ * 0.000005714
R.id.Grain to R.id.pound1 -> amountIQ * 0.0001429
R.id.Grain to R.id.Ton1 -> amountIQ / 1.4e+7
R.id.Grain to R.id.stone1 -> amountIQ * 0.0000102
else -> error("Unsupported selection pair")
}
binding.metricConverted.text = getString(R.string.end_result, result.toString())
}
Above, the error() call will crash your app. You need to make sure you cover every possible combination that could occur. During development, this is suitable, but for production you might want to change the behavior so it shows an error message in the UI of the app and doesn't crash.
Now, regarding the overall design, it is quite fragile because you have UI layout details so tightly coupled to your app's behavior. All these formula calculations should probably be defined in a separate class, possibly an Enum class. You could create a Map in your Activity file that links the UI elements to the behavior class(es), and then in your when statement, you could use the UI elements to pull the associated behavior from the map. This would be more maintainable, and make it easier for you to avoid forgetting something as you add/modify functionality. I say this just to get you thinking about it, but it's probably too much for a beginner project right now. I don't have time to explain in detail how I would do all of that for your case.

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
});

Handle To Remove/Add Live Data Observer to Observe On Button Click

I want to to observe a row in room database. it change after some period. but when we stop button click it need to be stop observing form database and when click start button it start observing again.
My current code is
To create Observer
private lateinit var recordObserver: Observer<Ride>
recordObserver= Observer<Ride> { rides ->
if (rides != null)
updateData(rides)
else
setDataToZero()
}
when(isState){
Constants.isrunning->{//need to start observer}
Constants.Stop->{//need to stop observer}
}
In order to start/stop observing LiveData you should use observe() / removeObserver() methods. As simple as that. If you have access to LifecycleOwner (Fragment, Activity) use fun observe(), if not - use fun observeForever().
Your code will look like this:
val liveData = database.observeRides() // get your live data
when(isState){
Constants.isrunning -> {
liveData.observe(this, recordObserver)
}
Constants.Stop -> {
liveData.removeObserver(recordObserver)
}
}

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.