Why does my tornadoFX ObservableList not receive updates? - kotlin

I have a simple tornadoFX program that generates some circles in random locations on the screen. However, none of the circles get drawn. I've added some debug code to print a line when a circle is drawn, and it only prints once.
I would expect circles to appear at 100ms intervals, as well as when I click the "Add actor" button.
private const val WINDOW_HEIGHT = 600
private const val WINDOW_WIDTH = 1024
fun main(args: Array<String>) {
Application.launch(MainApp::class.java, *args)
}
class MainApp : App(WorldView::class, Stylesheet::class)
data class Actor(val x: Double, val y: Double)
class WorldView: View("Actor Simulator") {
override val root = VBox()
private val actors = ArrayList<Actor>(0)
init {
tornadofx.runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
actors.add(Actor(x, y))
Thread.sleep(100)
}
}
}
init {
with(root) {
stackpane {
group {
bindChildren(actors.observable()) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
also {
println("drew circle")
}
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}
}
}
Oddly, if I put a breakpoint during the circle draw code, circles will draw and the debug line will print.

Some observations:
Calling someList.observable() will create an observable list backed by the underlying list, but mutations on the underlying list will not emit events. You should instead initialize actors as an observable list right away.
Access to an observable list must happen on the UI thread, so you need to wrap mutation calls in runLater.
For people trying to run your example - you didn't include a stylesheet, but references one in your App subclass, so the IDEA will most probably import the TornadoFX Stylesheet class. This will not end well :)
The also call has no effect, so I removed it.
I updated your code to best practices here and there, for example with regards to how to create the root node :)
Updated example taking these points into account looks like this:
private const val WINDOW_HEIGHT = 600.0
private const val WINDOW_WIDTH = 1024.0
class MainApp : App(WorldView::class)
data class Actor(val x: Double, val y: Double)
class WorldView : View("Actor Simulator") {
private val actors = FXCollections.observableArrayList<Actor>()
override fun onDock() {
runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
runLater {
actors.add(Actor(x, y))
}
Thread.sleep(100)
}
}
}
override val root = stackpane {
setPrefSize(WINDOW_WIDTH, WINDOW_HEIGHT)
group {
bindChildren(actors) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
println("drew circle")
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}

Related

Expose value from SharedPreferences as Flow

I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}

Kotlin: Edit icon dashboard of icons between fragments

I'm trying to figure out the most efficient way to structure this problem..
I'd like to click on the 'EDIT' icon in the dashboard of the MainFragment, display a DialogFragment, allow user to select/deselect up to 5 icons, save the selection, close the DialogFragment, and update the MainFragment.
Should I use MutableLiveData/Observer from a ViewModel? Or is there a better approach? I currently cannot figure out how to use the ViewModel approach correctly...
So far, this is the code I have:
MainFragment: https://i.stack.imgur.com/5fRt2.png
DialogFragment: https://i.stack.imgur.com/ZvW3d.png
ViewModel Class:
class IconDashboardViewModel() : ViewModel(){
var liveDataDashIcons: MutableLiveData<MutableList<String>> = MutableLiveData()
var liveItemData: MutableLiveData<String> = MutableLiveData()
// Observer for live list
fun getLiveDataObserver(): MutableLiveData<MutableList<String>> {
return liveDataDashIcons
}
// Observer for each icon
fun getLiveItemObserver(): MutableLiveData<String> {
return liveItemData
}
// Set icon list
fun setLiveDashIconsList(iconList: MutableLiveData<MutableList<String>>) {
liveDataDashIcons.value = iconList.value
}
// Set data for data
fun setItemData(icon : MutableLiveData<String>) {
liveItemData.value = icon.toString()
}
var iconList = mutableListOf<String>()
}
MainFragment:
private fun populateIconList() : MutableLiveData<MutableList> {
var iconList = viewModel.liveDataDashIcons
// Roster icon
if (roster_dash_layout.visibility == View.VISIBLE) {
iconList.value!!.add(getString(R.string.roster))
} else {
if (iconList.value!!.contains(getString(R.string.roster))) {
iconList.value!!.remove(getString(R.string.roster))
}
}
}
DialogFragment:
private fun setIconList(iconList: MutableList){
var iconList = viewModel.iconList
Log.d(TAG, "viewModel iconList = " + iconList)
if (iconList.contains(getString(R.string.roster))) {
binding.radioButtonRosterPick.setBackgroundResource(R.drawable.icon_helmet_blue_bg)
}
}

LazyColumn with nested LazyRows - memory issue

In my app there's a LazyColumn that contains nested LazyRows. I have a memory issue - when there are 30-40 rows and about 10-20 elements per row in the grid, it's possible to reach Out-of-Memory (OOM) by simply scrolling the list vertically up and down about 20 times. An item is a Card with some Boxes and texts. It seems that the resulting composable for each of the items is stored, even when the item is out of composition.
Here is a sample that demonstrates this. It shows a simple grid of 600 elements (they are just Text) and on my emulator gets to a memory usage of about 200 MB. (I use Android TV emulator with landscape, 120 elements are visible at once).
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyColumnTestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
runTest()
}
}
}
}
}
#Composable
fun runTest() {
var itemsState: MutableState<List<TestDataBlock>> = remember {
mutableStateOf(listOf())
}
LaunchedEffect(Unit) {
delay(1000)
itemsState.value = MutableList<TestDataBlock>(30) { rowIndex ->
val id = rowIndex
TestDataBlock(id = id.toString(), data = 1)
}
}
List(dataItems = itemsState.value)
}
#Preview
#Composable
fun List(
dataItems: List<TestDataBlock> = listOf(TestDataBlock("1",1), TestDataBlock("2",2))
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
itemsIndexed(items = dataItems,
key = { _, item ->
item.id
}
) { _, rowItem ->
drawElement(rowItem)
}
}
}
#Composable
fun drawElement(rowItem: TestDataBlock) {
Text(text = "${rowItem.id}")
LazyRow() {
itemsIndexed(items = rowItem.testDataItems,
key = { _, item ->
item.id
}
) { _, item ->
Text(text = "${item.id }", color = Color.Black, modifier = Modifier.width(100.dp))
}
}
}
TestDataBlock.kt
#Immutable
data class TestDataBlock(
val id: String,
val data: Int,
) {
val testDataItems: List<TestDataItem> = (0..20).toList().map{ TestDataItem(it.toString()) }
}
TestDataItem.kt
#Immutable
data class TestDataItem(
val id: String
)

How do I display a new image in tornadofx imageview?

I want to display a WritableImage in imageview, but I want that image to change when the user loads in a new file from the file browser. I know that there is a bind() function for strings that change over time, but I could not find a similar option for images. I could solve the problem for images that are the same size as the default loaded one (with writing through the pixels), but that only works if they are the same size, since I cant modify the size of the image that I displayed.
My Kotlin code so far:
class PhotoView : View("Preview") {
val mainController: mainController by inject()
override val root = hbox {
imageview{
image = mainController.moddedImg
}
hboxConstraints {
prefWidth = 1000.0
prefHeight = 1000.0
}
}
class ControlView: View(){
val mainController: mainController by inject()
override val root = hbox{
label("Controls")
button("Make BW!"){
action{
mainController.makeBW()
}
}
button("Choose file"){
action{
mainController.setImage()
mainController.update()
}
}
}
}
class mainController: Controller() {
private val ef = arrayOf(FileChooser.ExtensionFilter("Image files (*.png, *.jpg)", "*.png", "*.jpg"))
private var sourceImg=Image("pic.png")
var moddedImg = WritableImage(sourceImg.pixelReader, sourceImg.width.toInt(), sourceImg.height.toInt())
fun setImage() {
val fn: List<File> =chooseFile("Choose a photo", ef, FileChooserMode.Single)
sourceImg = Image(fn.first().toURI().toString())
print(fn.first().toURI().toString())
}
fun makeBW() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color.grayscale())
}
}
}
fun update() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color)
}
}
}
}
ImageView has a property for the image that you can bind:
class PhotoView : View("Preview") {
val main: MainController by inject()
val root = hbox {
imageview { imageProperty().bind(main.currentImageProperty) }
...
}
...
}
class MainController : Controller() {
val currentImageProperty = SimpleObjectProperty<Image>(...)
var currentImage by currentImageProperty // Optional
...
}
From there, any time you set the currentImage in MainController, it will update in the PhotoView.

Progressbar TaskStatus does listen to all async runs

I made a small example for you guyes to see what i mean.
By Running it you will see that on clicking into the Yellow area the progress bar is shown... But i only want it to be shown if the Green area is clicked.
Am i doing sth wrong or is this actually an expected behavior?
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.VBox
import tornadofx.*
class RunAsyncExample : View() {
override val root = VBox()
private val runAsyncOne: RunAsyncOne by inject()
private val runAsyncTwo: RunAsyncTwo by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 300.0
add(runAsyncOne)
add(runAsyncTwo)
}
}
}
class RunAsyncOne : View() {
override val root = AnchorPane()
init {
with(root) {
var checker = false
minWidth = 400.0
minHeight = 150.0
style {
backgroundColor += c("YELLOW")
}
setOnMouseClicked {
checker = !checker
runAsync {
while (checker) {
Thread.sleep(100)
println("AsyncOne")
}
}
}
}
}
}
class RunAsyncTwo : View() {
override val root = VBox()
private val status: TaskStatus by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync {
var b = 0.0
while (b < 100.0) {
b+=1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen { status.running }
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}
}
When i click into the green area:
When i click into the yellow area while the AsyncTwo is running,
the progress bar is changing but i dont want that...
When you inject a TaskStatus object into your View, you will get a "global" TaskStatus object that by default will report status of any async calls. What you want is to create a local TaskStatus object for the RunAsyncTwo View and pass that to the runAsync call.
If you don't pass a TaskStatus object to runAsync, the default for your scope will be used, hence the behavior you're seeing. Here is an example with a local TaskStatus object for the second view. Please also note that I'm using a more sensible syntax for defining the root node, you should absolutely pick this pattern up :)
class RunAsyncTwo : View() {
private val status = TaskStatus()
override val root = vbox {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync(status) {
var b = 0.0
while (b < 100.0) {
b += 1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen(status.running)
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}