MutableStateFlow does not recomposes value - kotlin

I have a method that updates local value if I got a spicific json:
override fun onGameStartOpenWord(startOpenWord: StartOpenWord) {
var word = _state.value.gameState!!.words.find { startOpenWord.word.id == it.id } ?: return
val tempCopy = word.copy()
word = word.copy(
animationStart = startOpenWord.word.times.first().toULong(),
animationEnd = startOpenWord.word.times.last().toULong()
)
val words = _state.value.gameState!!.words.toMutableList()
val index = words.indexOf(tempCopy)
logger.debug("Animation should start at ${word.animationStart}, updating word ${tempCopy.id} with index $index")
words[index] = word
val gameState = _state.value.gameState?.copy(words = words)
_state.update { it.copy(gameState = gameState) }
logger.debug(_state.value.gameState!!.words[index].animationStart)
}
The State created like this:
private val _state = reset() //MutableStateFlow<GameData>
val state = _state.asStateFlow()
Using state like this:
val model = ViewModelsStore.mainFrameViewModel //compose-jb does not have method viewModel<>(), using object to store
val data by model.state.collectAsState()
val stateWord = data.gameState!!.words.find { it.id == word.id }!!
val animationStart = stateWord.animationStart
val animationEnd = stateWord.animationEnd
if (animationStart != null) {
//do animation stuff
}
Everything else updates correctly (recomposition starts), but the method above does not. However _state does actually update, logs show that clearly.
Debbuger showed me that before update() method gameState != _state.value.gameState, but after update() it is.
But recomposition didn't happen and animation didn't start. What is the problem?

Be careful with listeners. It was my mistake and gameState was overwritten by listener. So recompose didn't happen, because value changed too fast.

Related

Jetpack Compose Textfield value not getting updated

I'm trying to create a display where users can input a duration composed of hours, minutes and seconds. I manage the state of the duration with a class i wrote called TimeData. I set the value for the textfield to the state values, however this does not get updated when it changes. I have tried alot and i can't seem to figure out why this does not work, as similar implementations work just fine.
I save the state in viewmodel, inject it into the screen (composable) the screen passes fields of the TimeData to the three composables for the hours, seconds and minutes. These components handle displaying the textfields and changing the values
I've tried changing state directly, saving state in screen file with by remember instead of in viewmodel with State, i've tried changing val to var and back in the TimeData object and it's children. And much more.
p.s. this is my first question here, so if anything is not clear, please let me know.
TimeData class (saved as state in viewmodel)
data class TimeData(
val hours: TimeUnit = TimeUnit(TimeUnits.HOURS),
val mins: TimeUnit = TimeUnit(TimeUnits.MINS),
val secs: TimeUnit = TimeUnit(TimeUnits.SECS),
) {
fun isDataEmpty() = hours.value == 0L && mins.value == 0L && secs.value == 0L
}
fun TimeData.toISOString(): String = "PT" +
"${hours.value}${hours.unit.firstLetter}" +
"${mins.value}${mins.unit.firstLetter}" +
"${secs.value}${secs.unit.firstLetter}"
fun TimeData.toDuration(): Duration = Duration.parse(this.toISOString())
TimeUnit class (this value is updated)
data class TimeUnit(
var unit: TimeUnits
) {
var value: Long = 0
set(value) {
val parsedValue = value
val min = 0
val max = when (unit) {
TimeUnits.HOURS -> 99
TimeUnits.MINS -> 60
TimeUnits.SECS -> 60
}
if (parsedValue in min..max) {
field = value
}
}
override fun toString(): String {
return if (value in 1..9) "0$value" else value.toString()
}
}
enum class TimeUnits(val firstLetter: String) {
HOURS("h"),
MINS("m"),
SECS("s")
}
State in viewmodel
private val _timeState = mutableStateOf(TimeData())
val timeState: State<TimeData> = _timeState
Texfield
BasicTextField(
modifier = Modifier.widthIn(1.dp),
value = time.value.toString(),
onValueChange = {
if (it.isNotBlank()) {
//changing state directly
time.value = it.toLong()
//letting viewmodel handel change
viewModel.onEvent(AddEditExerciseEvents.DurationValueChanged(time))
}
},
singleLine = true,
textStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Medium,
color = textColor,
textAlign = TextAlign.Center
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Changing state value in viewmodel
when (event) {
is AddEditExerciseEvents.DurationValueChanged -> {
when (event.value.unit) {
TimeUnits.HOURS -> {
_timeState.value = timeState.value.copy(
hours = event.value
)
}
TimeUnits.MINS -> {
_timeState.value = timeState.value.copy(
mins = event.value
)
}
TimeUnits.SECS -> {
_timeState.value = timeState.value.copy(
secs = event.value
)
}
}
}

find value in arraylist in kotlin

Hey I am working in kotlin. I am working on tree data structure. I added the value in list and now I want to find that value and modified their property. But I am getting the error.
VariantNode, StrengthNode, ProductVariant
StrengthNode.kt
class StrengthNode : VariantNode() {
var pricePerUnit: String? = null
var defaultValue = AtomicBoolean(false)
}
ActivityViewModel.kt
class ActivityViewModel : ViewModel() {
var baseNode: VariantNode = VariantNode()
private val defaultValueId = "12643423243324"
init {
createGraph()
}
private fun createGraph() {
val tempHashMap: MutableMap<String, VariantNode> = mutableMapOf()
val sortedList = getSortedList()
sortedList.forEach { productVariant ->
productVariant.strength?.let { strength ->
if (tempHashMap.containsKey("strength_${strength.value}")) {
baseNode.children.contains(VariantNode(strength.value)) // getting error
return#let
}
val tempNode = StrengthNode().apply {
value = strength
pricePerUnit = productVariant.pricePerUnit?.value
if (productVariant.id == defaultValueId) {
defaultValue.compareAndSet(false, true)
}
}
baseNode.children.add(tempNode)
tempHashMap["strength_${strength.value}"] = tempNode
}
productVariant.quantity?.let { quantity ->
if (tempHashMap.containsKey("strength_${productVariant.strength?.value}_quantity_${quantity.value}")) {
return#let
}
val tempNode = QuantityNode().apply {
value = quantity
}
val parent =
tempHashMap["strength_${productVariant.strength?.value}"] ?: baseNode
parent.children.add(tempNode)
tempHashMap["strength_${productVariant.strength?.value}_quantity_${quantity.value}"] =
tempNode
}
productVariant.subscription?.let { subscription ->
val tempNode = SubscriptionNode().apply {
value = subscription
}
val parent =
tempHashMap["strength_${productVariant.strength?.value}_quantity_${productVariant.quantity?.value}"]
?: baseNode
parent.children.add(tempNode)
}
}
baseNode
}
}
I am getting error on this.
I want to find that node value and modified other property.
Your class VariantNode only has a single no-arg constructor, but you're trying to call it with arguments, hence the error
Too many arguments for public constructor VariantNode() defined in com.example.optionsview.VariantNode
Either you have to provide a constructor, that matches your call, e.g.
open class VariantNode(var value: ProductValue?) {
var children: MutableList<VariantNode> = arrayListOf()
}
or you need to adjust your code to use the no-arg constructor instead.
val node = VariantNode()
node.value = strength.value
baseNode.children.contains(node)
Note however, that your call to contains most likely will not work, because you do not provide a custom implementation for equals. This is provided by default, when using a data class.
If you just want to validate whether baseNode.children has any element, where value has the expected value, you can use any instead, e.g.:
baseNode.children.any { it.value == strength.value }

CoroutineScope is skipped

For some reasom my CoroutineScope just stopped working, though, nothing crucial has been changed and errors don't show up. The program skips it and my progress bar remains unchanged. Do you have any idea what could have possibly caused this issue?
class HomeFragment : BaseFragment(R.layout.fragment_home) {
private var todayEventsCount: Int = 0
private var todayCheckedEventsCount: Int = 0
private val mHandler = Handler()
private var mAdapter: FirebaseRecyclerAdapter<EventModel, EventsHolder>? = null
private lateinit var mRunnable: Runnable
private lateinit var barList: ArrayList<BarEntry>
private lateinit var barDataSet: BarDataSet
private lateinit var barData: BarData
override fun onResume() {
super.onResume()
initFields()
}
override fun onPause() {
super.onPause()
home_events_list.removeAllViewsInLayout()
todayCheckedEventsCount = 0
}
#SuppressLint("SetTextI18n")
private fun initFields() {
val progress: ProgressBar = ev_progress_bar
val text: TextView = count
getTodayEvents()
CoroutineScope(Dispatchers.IO).launch {
mRunnable = Runnable {
if (mAdapter?.itemCount != 0) {
todayEventsCount = mAdapter?.itemCount!!
for (i in 0 until todayEventsCount) {
if (mAdapter?.getItem(i)!!.checked == "1") {
todayCheckedEventsCount++
progress.progress = todayCheckedEventsCount
text.text = progress.progress.toString() + "/" + progress.max.toString()
}
}
todayCheckedEventsCount = 0
}
mHandler.postDelayed(mRunnable, 30)
}
mHandler.post(mRunnable)
}
initChart()
}
There's no point in posting runnables if you're using a coroutine. Half the reason coroutines exist is to avoid this messy nested juggling of callbacks. There are also a few errors I see in your coroutine:
It runs on Dispatchers.IO, even though it never does anything blocking.
It runs on a throwaway CoroutineScope that is never cancelled. You should never use the CoroutineScope() constructor if you're not immediately assigning it to a variable or property through which you can cancel it at the appropriate time. You should be using viewLifeCycleOwner.lifecycleScope instead, since it's already set up to automatically cancel at the appropriate time to avoid crashes.
Your runnables you're posting are not canceled at the appropriate time, so even if you use the appropriate scope, they can still cause crashes. But as mentioned above, you don't need them in a coroutine.
(mAdapter?.itemCount != 0) will evaluate to true even if mAdapter is null, which will promptly cause a crash when you use mAdapter!!.
Fixed code looks like this. If this still doesn't do anything, you'll want to check to make sure your adapter is not null at the time this is called.
private fun initFields() {
val progress: ProgressBar = ev_progress_bar
val text: TextView = count
getTodayEvents()
viewLifeCycleOwner.lifecycleScope.launch {
val adapter = mAdapter ?: return
while (true) {
if (adapter.itemCount != 0) {
todayEventsCount = adapter.itemCount
for (i in 0 until todayEventsCount) {
if (adapter.getItem(i).checked == "1") {
todayCheckedEventsCount++
progress.progress = todayCheckedEventsCount
text.text = progress.progress.toString() + "/" + progress.max.toString()
}
}
todayCheckedEventsCount = 0
}
delay(30)
}
}
initChart()
}
I didn't try to follow your logic of what these todayEventsCount and todayCheckedEventsCount properties are. They probably should be locally defined variables in the coroutine.
You're also going to need some condition in the while loop that breaks you out of it. I didn't look closely enough to see what that condition should be. Your original code doesn't break the loop of reposting the runnable forever.

Kotlin Compose Search Bar

I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.
Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out

How do I bind a custom property to a textfield bidirectionally?

I have a complex object that I want to display in a textfield. This is working fine with a stringBinding. But I don't know how to make it two-way so that the textfield is editable.
package com.example.demo.view
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
class MainView : View("Hello TornadoFX") {
val complexThing: Int = 1
val complexProperty = SimpleObjectProperty<Int>(complexThing)
val complexString = complexProperty.stringBinding { complexProperty.toString() }
val plainString = "asdf"
val plainProperty = SimpleStringProperty(plainString)
override val root = vbox {
textfield(complexString)
label(plainProperty)
textfield(plainProperty)
}
}
When I run this, the plainString is editable and I see the label change because the edits are going back into the property.
How can I write a custom handler or what class do I need to use to make the stringBinding be read and write? I looked through a lot of the Property and binding documentation but did not see anything obvious.
Ta-Da
class Point(val x: Int, val y: Int) //You can put properties in constructor
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
if(string.isNullOrBlank()) return null //Empty strings aren't valid
val xy = string.split(",", limit = 2) //Only using 2 coordinate values so max is 2
if(xy.size < 2) return null //Min values is also 2
val x = xy[0].trim().toIntOrNull() //Trim white space, try to convert
val y = xy[1].trim().toIntOrNull()
return if(x == null || y == null) null //If either conversion fails, count as invalid
else Point(x, y)
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6) //Probably doesn't need to be its own member
val pointProperty = SimpleObjectProperty<Point>(point)
val pc = PointConverter()
override val root = vbox {
label(pointProperty, converter = pc) //Avoid extra properties, put converter in construction
textfield(pointProperty, pc)
}
}
I made edits to your converter to "account" for invalid input by just returning null. This is just a simple band-aid solution that doesn't enforce correct input, but it does refuse to put bad values in your property.
This can probably be done more cleanly. I bet there is a way around the extra property. The example is fragile because it doesn't do input checking in the interest of keeping it simple. But it works to demonstrate the solution:
class Point(x: Int, y: Int) {
val x: Int = x
val y: Int = y
}
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
val xy = string?.split(",")
return Point(xy[0].toInt(), xy[1].toInt())
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6)
val pointProperty = SimpleObjectProperty<Point>(point)
val pointDisplayProperty = SimpleStringProperty()
val pointStringProperty = SimpleStringProperty()
val pc = PointConverter()
init {
pointDisplayProperty.set(pc.toString(pointProperty.value))
pointStringProperty.set(pc.toString(pointProperty.value))
pointStringProperty.addListener { observable, oldValue, newValue ->
pointProperty.set(pc.fromString(newValue))
pointDisplayProperty.set(pc.toString(pointProperty.value))
}
}
override val root = vbox {
label(pointDisplayProperty)
textfield(pointStringProperty)
}
}