I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.
Related
I'm still a bit of a beginner with Jetpack compose and understanding how re-composition works.
So I have a piece of code calls below inside a ViewModel.
SnapshotStateList
var mutableStateTodoList = mutableStateListOf<TodoModel>()
private set
during construction of the view model, I execute a room database call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList = listTypeTodo.toMutableStateList()
}
}
}
then I have an action from a the ui that triggers adding of a new Todo to the list and expecting a re-composition from the ui that shows a card composable
fun onFabClick() {
todoList.add(TodoModel())
}
I can't figure out why it doesn't trigger re-composition.
However if I modify the init code block below, and invoke the onFabClick() action, it triggers re-composition
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList.addAll(listTypeTodo)
}
}
}
or this, taking out the re-assigning of the mutableStateList outside of the coroutine scope also works (triggers re-composition).
init {
// just trying to test a re-assigning of the mutableStateList property
mutableStateTodoList = emptyList<TodoModel>().toMutableStateList()
}
Not quite sure where the problem if it is within the context of coroutine or SnapshotStateList itself.
Everything is also working as expected when the code was implemented this way below, using standard list inside a wrapper and performing a copy (creating new reference) and re-assigning the list inside the wrapper.
var todoStateWrapper by mutableStateOf<TodoStateWrapper>(TodoStateWrapper)
private set
Same init{...} call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
todoStateWrapper = todoStateWrapper.copy (
todoList = listTypeTodo
)
}
}
}
To summarize, inside a coroutine scope, why this works
// mutableStateList
todoList.addAll(it)
while this one does not?
// mutableStateList
todoList = it.toMutableStateList()
also why does ordinary list inside a wrapper and doing copy() works?
The mutable state in Compose can only keep track of updates to the containing value. Here is simplified code on how MutableState could be implemented:
class MutableState<T>(initial: T) {
private var _value: T = initial
private var listeners = MutableList<Listener>
var value: T
get() = _value
set(value) {
if (value != _value) {
_value = value
listeners.forEach {
it.triggerRecomposition()
}
}
}
fun addListener(listener: Listener) {
listeners.add(listener)
}
}
When the state is used by some view, this view subscribes to updates of this particular state.
So, if you declare the property as follows:
var state = MutableState(1)
and try to update it with state = 2.toMutableState() (this is analogous to your mutableStateTodoList = listTypeTodo.toMutableStateList()), triggerRecomposition cannot be called because you create a new object which resets all the listeners. Instead, to trigger recomposition you should update it with state.value = 2.
With mutableStateList, the analog of updating value is any method of MutableList interface that updates containing list, including addAll.
Inside init it works because no view is subscribed to this state so far, and that's the only place where methods such as toMutableStateList should be used.
It is important to always define mutable states as immutable property with val in order to prevent such mistakes. To make it mutable only from view model, you can define it like this, and make updates on _mutableStateTodoList:
private val _mutableStateTodoList = mutableStateListOf<TodoModel>()
val mutableStateTodoList: List<TodoModel> = _mutableStateTodoList
The only exception when you can use var is using mutableStateOf with delegation - this is where you can use it with private set because in that case the delegation does the work for you by not modifying the container, but only it's value property. Such method cannot be applied to mutableStateListOf, because there's no single value field that's responsible for the data in case of list.
var someValue by mutableStateOf(1)
private set
The following content is from the article.
1: I don't understand fully if I can use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change?
2: I'm very strange why State<List<T>> and the immutable listOf() can be observed by Compose to trigger recomposition when they change but in fact List<T> and immutable listOf() are immutable, could you give me some sample codes?
Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.
Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.
Instead of using
non-observable mutable objects, we recommend you use an observable
data holder such as State<List> and the immutable listOf().
Image
The core concept is
Recomposition happens only when an observable state change happens.
For mutable objects, we have options to use add(), remove() and other methods and modify the object directly.
But the change would not trigger a recomposition as the change is not observable. (The object instance is NOT changed)
Even for mutable objects, we can trigger proper recomposition by assigning them to a new object instance. (The object instance is changed)
Hence using mutable objects is error-prone.
We can also, see a lint error due to this problem.
On the other hand, an immutable object like list can not be modified. They are replaced with a new object instance.
Hence they are observable and proper recomposition happens. (The object instance is changed)
Use this as an example to understand the concept.
#Composable
fun ComposeListExample() {
var mutableList: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var mutableList1: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var arrayList: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var arrayList1: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var list: MutableState<List<String>> = remember {
mutableStateOf(listOf())
}
Column(
Modifier.verticalScroll(state = rememberScrollState())
) {
// Uncomment the below 5 methods one by one to understand how they work.
// Don't uncomment multiple methods and check.
// ShowListItems("MutableList", mutableList.value)
// ShowListItems("Working MutableList", mutableList1.value)
// ShowListItems("ArrayList", arrayList.value)
// ShowListItems("Working ArrayList", arrayList1.value)
// ShowListItems("List", list.value)
Button(
onClick = {
mutableList.value.add("")
arrayList.value.add("")
val newMutableList1 = mutableListOf<String>()
mutableList1.value.forEach {
newMutableList1.add(it)
}
newMutableList1.add("")
mutableList1.value = newMutableList1
val newArrayList1 = arrayListOf<String>()
arrayList1.value.forEach {
newArrayList1.add(it)
}
newArrayList1.add("")
arrayList1.value = newArrayList1
val newList = mutableListOf<String>()
list.value.forEach {
newList.add(it)
}
newList.add("")
list.value = newList
},
) {
Text(text = "Add")
}
}
}
#Composable
private fun ShowListItems(title: String, list: List<String>) {
Text(title)
Column {
repeat(list.size) {
Text("$title Item Added")
}
}
}
P.S: Use mutableStateListOf if you have a list of items that needs to be modified as well as trigger recomposition properly.
I managed to do like this:
#Composable
fun ComposeListExample(
allObjects: List<Object>,
selectedObjects: List<Object>
) {
val selectedItems = remember {
mutableStateListOf<Object>().apply { addAll(selectedObjects) }
}
Column {
allObjects.forEach { item ->
SomeView(
title = item.title,
onSelect = {
if (selectedItems.contains(item)) {
selectedItems.remove(item)
} else {
selectedItems.add(item)
}
})
}
}
}
I have a controller with a string variable, and I would like the text value of a TextArea to change when the controller's string variable changes.
class MyView: View() {
...
button("Run Test").action {
runAsync {
for(test in testList){
controller.updateText = "running" + test.name
run(test)
}
}
}
...
scriptRanArea = textarea {
text = controller.updateText
}
...
}
This is the fastest way I know how to accomplish this, but I don't really know which design pattern you want to use:
class MyView: View() {
val controller: MyController by inject()
override val root = vbox {
textarea(controller.myTextProperty)
}
}
class MyController: Controller() {
val myTextProperty = SimpleStringProperty()
}
The inject method automatically finds the controller within the TornadoFX Scope, or creates one if not found, when it is first referenced. The TornadoFX text area builder function binds the string property from the controller to the TextArea when it is passed in as a param. Keep in mind though, writing in the text area will now automatically change the value in the controller's property and vice versa. If you don't want that functionality, you will have to update your question to be more specific to your needs.
Good day.
I am trying to preserve a property of ItemViewModel via config helper. I am able to successfully save the property (conf directory with appropriate .properties file is generated), however upon next start, the property does not restore its value, just remains null. Here's a sample code to demonstrate my issue:
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
data class Foo(val doNotPreserveMe: String, val preserveMe: String)
class FooModel : ItemViewModel<Foo>() {
val doNotPreserveMe = bind { item?.doNotPreserveMe?.toProperty() }
val preserveMe = bind { SimpleStringProperty(item?.preserveMe, "pm", config.string("pm")) }
}
class FooApp : App(FooView::class)
class FooView : View() {
private val model = FooModel()
override val root = form {
fieldset {
field("Do not preserve me") { textfield(model.doNotPreserveMe).required() }
field("Preserve me") { textfield(model.preserveMe).required() }
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
}
}
}
Any ideas on why the model is not restoring the value?
Each Component has it's own config store, which is backed by a separate file. Either make sure to use the same config file, or the app global config file.
You can refer to other component's config store, so one solution would be to let the View access the ViewModel's config store like this:
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(model.config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
However, there is a much simpler and more contained solution, which is simply to handle save in the FooModel's onCommit callback
override fun onCommit() {
with(config) {
set("pm" to preserveMe.value)
save()
}
}
In this case you'd simply call model.commit() in the button callback.
You can also use a common, or even global config object. Either use a Controller's config store, or the global store. To use the global config object, just refer to app.config in both the model and the view.
I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}