Change the value of a property of some data, but tableview doesn't update - kotlin

For a very simple table demo of TornadoFX, I just want to show some users in a view, and changes the names of them when clicking on a change button. But the problem is the modified data is not shown in table.
The main code is as following:
class User2(id: Int, name: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
}
val data2 = listOf(User2(111, "AAA"), User2(222, "BBB"), User2(333, "CCC")).observable()
vbox {
tableview(data2) {
column("id", User2::id)
column("name", User2::name).minWidth(200)
}
button("Modify data with type of User2").setOnAction {
data2.forEach { it.name += " changed!" }
}
}
Instead, if I define the User2 as following (also changed the class name to User1):
class User1(id: Int, name: String) {
val id = SimpleIntegerProperty(id)
val name = SimpleStringProperty(name)
}
Everything is working well.
I can't find why the first one is not working (as the code is most copied form the official guide)
Update:
A complete small demo to represent this question: https://github.com/javafx-demos/tornadofx-table-property-change-issue-demo

It looks like this issue is caused by confusing naming :-)
When refactoring User2 and renaming some fields it becomes evident that you were using the String and Int fields instead of the StringProperty and IntegerProperty, which obviously don't support binding.
If User2 looks like following:
class User2(id: Int, name: String) {
val idProperty = SimpleIntegerProperty(id)
var myId by idProperty
val nameProperty = SimpleStringProperty(name)
var myName by nameProperty
}
And the code changing it like following:
vbox {
tableview(data2) {
column("myId", User2::myId)
column("myName", User2::nameProperty).minWidth(200)
}
button("Modify data with type of User2").setOnAction {
data2.forEach { it.myName += " changed!" }
}
}
Everything seems to be working fine. As you can see before you were changing only the Int and String fields which doesn't lead to any updates.

For observable properties you must reference the property, not the getter. If you reference the getter you will see data, but it will never update. Go back to the way you defined the User object first, then reference User::idProperty and User::nameProperty for the columns.

Related

Filter Observed ViewModel data for Spinner based on another Spinners selection (LiveData)

I have two Spinners, each populated by ArrayList, which is observed in the ViewModel from the Fragment as below:
InventoryAddEdit Fragment
// Observe ProductGroups and populate Spinner
businessViewModel.allAppDataProductGroups.observe(viewLifecycleOwner, { productGroupArrayList -> // ArrayList<ProductGroupObject>
if (!productGroupArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productGroupArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, { productTypeArrayList -> // ArrayList<ProductTypeObject>
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productTypeArrayList)
binding.inventoryAddEditProductTypeSpinner.adapter = adapter
binding.inventoryAddEditProductTypeSpinner.setSelection(17) // Sets default value
}
})
I am trying to avoid keeping data in the Fragment (MVVM), so I am wondering how best to filter the second ArrayList based on the selection of the first.
I thought I could use the onItemSelected method on the first spinner to cancel the observer and reattach, but then filter the newly observed ArrayList by a selection of the first spinner. However, this seems a bit clunky. Another idea was to create another filtered list in the ViewModel, but that will mean more data in the ViewModel.
Is there another option I am missing, please?
For info, the ProductGroupObject and ProductTypeObject look like this:
ProductGroupObject
#IgnoreExtraProperties
data class ProductGroupObject (
//ProductGroup fields (1 fields)
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productGroupID: String = ""
) : Serializable {
override fun toString(): String {
return productGroup
}
}
ProductTypeObject
#IgnoreExtraProperties
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
fun detailsText(): String {
val detailsString = StringBuilder()
if(productTypeID.isNotEmpty()) detailsString.append("$productTypeID\n")
if(productType.isNotEmpty()) detailsString.append("$productType\n")
if(productGroup.isNotEmpty()) detailsString.append("$productGroup\n")
return detailsString.toString()
}
}
So the best solution I came up with was to create a 'full list' and 'filtered list' for each Spinner data set in the ViewModel + 'current selection' object for each Spinner (also kept in the ViewModel).
The 'full list' is populated by the Cloud Database on startup, the 'filtered lists' are filtered depending on the Spinner selection by way of the following code:
binding.inventoryAddEditProductGroupSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent:AdapterView<*>?, view: View?, position: Int, id: Long){
val productGroupObject = parent?.selectedItem as ProductGroupObject
if (productGroupObject.productGroupID.isNotEmpty()){
businessViewModel.updateCurrentProductGroupVMLiveData(productGroupObject.productGroupID)
}
}
This updates the 'filtered lists' (filtering the 'full list') which hold data to any of the linked Spinners as below:
fun updateCurrentProductGroupVMLiveData (currentProductGroupId: String) {
val newProductGroup = allAppDataProductGroups.value?.find { productGroup -> productGroup.productGroupID == currentProductGroupId }
_currentProductGroup.value = newProductGroup
if(newProductGroup?.productGroup != null) {
val filteredProductsList = allAppDataProductTypes.value?.filter { productTypeObject -> productTypeObject.productGroup == newProductGroup.productGroup} as ArrayList<ProductTypeObject>
_filteredAppDataProductTypes.value = filteredProductsList
}
// UPDATE OTHER SPINNERS HERE
Log.d(TAG, "updateCurrentProductGroupVMLiveData(): '_currentProductGroupId.value' updated ($currentProductGroupId)")
}
One issue I faced was that the View was nullable because of how LiveData works, which was solved in the following post: Spinner Listener LiveData Issue

`java.lang.StackOverflowError` when accessing Kotlin property

I got this (contrived) sample from Packt's "Programming Kotlin" on using secondary constructor with inheritance.
Edit: from the answer it is clear that the issue is about backing field. But the book did not introduced that idea, just with the wrong example.
open class Payment(val amount: Int)
class ChequePayment : Payment {
constructor(amount: Int, name: String, bankId: String) : super(amount) {
this.name = name
this.bankId = bankId
}
var name: String
get() = this.name
var bankId: String
get() = this.bankId
}
val c = ChequePayment(3, "me", "ABC")
println("${c} ${c.amount} ${c.name}")
When I run it this error is shown.
$ kotlinc -script class.kts 2>&1 | more
java.lang.StackOverflowError
at Class$ChequePayment.getName(class.kts:10)
at Class$ChequePayment.getName(class.kts:10)
at Class$ChequePayment.getName(class.kts:10)
Line 10 does seems to be a infinite recursion, how to solve it?
You have a recursion in your code:
class ChequePayment : Payment {
constructor(amount: Int, name: String, bankId: String) : super(amount) {
this.name = name
this.bankId = bankId
}
var name: String
get() = this.name // recursion: will invoke getter of name (itself)
var bankId: String
get() = this.bankId // recursion: will invoke getter of bankId (itself)
}
If you don't need custom logic for your getter, just leave your properties like this:
var name: String
var bankId: String
They will have a default getter, which does nothing more than returning the value of the backing field.
Note: The code as it is can/should be refactored to this:
class ChequePayment(amount: Int, var name: String, var bankId: String) : Payment(amount) {
// ...
}
This uses the primary constructor and is much less redundant.
To access the backing field you have to use the keyword field instead of this.name see https://kotlinlang.org/docs/reference/properties.html#backing-fields
this.name references the getter, which references this.name which is an infinite recursion, as you already noted. In code:
var name: String
get() = field
var bankId: String
get() = field
Side note: Android Studio and Idea will complain rightfully that you don't need a getter in this case. So you can simplify even more:
var name: String
var bankId: String

Deserialize a nested json field with Jackon in Kotlin

I've already deserialized some nested field in the past in Java, following instructions from https://www.baeldung.com/jackson-nested-values (section 5) :
#JsonProperty("brand")
private void unpackNested(Map<String,Object> brand) {
this.brandName = (String)brand.get("name");
Map<String,String> owner = (Map<String,String>)brand.get("owner");
this.ownerName = owner.get("name");
}
ownerName being a field in the bean.
Now, I need to do something similar in Kotlin, but I am not happy with what I have so far. Assuming I have a MyPojo class that has a createdAt field, but in the JSON that represents it, the field is nested under a metadata attribute:
data class MyPojo(var createdAt: LocalDateTime = LocalDateTime.MIN) {
#JsonProperty("metadata")
private fun unpackNested(metadata: Map<String, Any>) {
var createdAtAsString = metadata["createdAt"] as String
this.createdAt = LocalDateTime.parse(createdAtAsString,DateTimeFormatter.ISO_DATE_TIME)
}
}
One of the thing I don't like here is that I am forced to make createdAt a var, not a val.
Is there a Kotlin trick to make things overall better here?
For the sake of simplicity, I used Int as type for createdAt.
You could do it like this:
class JsonData(createdAt: Int = 0) {
private var _createdAt: Int = createdAt
val createdAt: Int
get() = _createdAt
#JsonProperty("metadata")
private fun unpackNested(metadata: Map<String, Any>) {
_createdAt = metadata["createdAt"] as Int
}
}
createdAt will be a parameter with a default value. Since a data classe's constructor can only have properties (var/val) you will loose the advantages of a data class (toString() out of the box etc.).
You will assign this parameter to a private var _createdAt when the class is instantiated.
The only thing that will be exposed to the outside is a property without a backing field createAt (just a getter in Java terms). So, _createdAt cannot be changed after instantiation.
There are two cases now:
If you instantiate the class, _createdAt will be set to the value you specify.
If Jackson instantiates the class the value of _createdAt will be overwritten by the unpackNested call.
Here is an example:
val jsonStr = """{
"metadata": {
"createdAt": 1
}
}
""".trimIndent()
fun main() {
val objectMapper = ObjectMapper()
// Jackson does instantiation
val jsonData = objectMapper.readValue(jsonStr, JsonData::class.java)
// you do it directly
JsonData(5)
}

Simple casting in Kotlin/Java

I have an object User defined as below
class User(){
var id: Int? = null
var name: String? = null}
For certain reasons, I have to create new object User of same parameters and I have to copy data from old to new type.
class UserNew(){
var id: Int? = null
var name: String? = null}
I was looking for easiest way to convert from old type to a new one. I want to do simply
var user = User()
var userNew = user as UserNew
But obviously, I am getting This cast can never succeed. Creating a new UserNew object and set every parameter is not feasible if I have a User object with lots of parameters. Any suggestions?
as is kotlin's cast operator. But User is not a UserNew. Therefore the cast fails.
Use an extension function to convert between the types:
fun User.toUserNew(): UserNew {
val userNew = UserNew()
userNew.id = id
userNew.name = name
return userNew
}
And use it like so
fun usingScenario(user: User) {
val userNew = user.toUserNew()
If you don't want to write a boilerplate code, you can use some libraries that will copy values via reflection (for example http://mapstruct.org/), but it's not the best idea.
To achieve you can Simply use Gson and avoid boilerplate code:
var user = User(....)
val json = Gson().toJson(user)
val userNew:UserNew =Gson().fromJson(json, UserNew::class.java)
you should follow this logic for this case.
note: #Frank Neblung answer i implemented
fun main(args: Array<String>) {
val user = User()
user.id = 10
user.name = "test"
var userNew = user.toUserNew()
println(userNew.id) // output is 10
println(userNew.name)// output is test
}
class User()
{
var id: Int? = null
var name: String? = null
fun toUserNew(): UserNew {
val userNew = UserNew()
userNew.id = id
userNew.name = name
return userNew
}
}
class UserNew() {
var id: Int? = null
var name: String? = null
}
You have two options. Either create interface and implement it in both classes. then you can use this interface in both places (User,UserNew) If this is not what you want, i would use copy constructor in UserNew taking User as parameter, You can create new
NewUser nu = new UserNew(userOld)
if you have lots of properties answer from ppressives is way to go
To achieve that you can use the concept of inheritance:
https://www.programiz.com/kotlin-programming/inheritance
Example:
open class Person(age: Int) {
// code for eating, talking, walking
}
class MathTeacher(age: Int): Person(age) {
// other features of math teacher
}

How to show pojo properties in TornadoFX tableview?

I'm writing a very simple TornadoFX table demo, trying to display the properties of some pojos in a table, but the cells are all empty.
The main code is:
data class User(val id: Int, val name: String)
private val data = listOf(User(111, "AAA"), User(222, "BBB"), User(333, "CCC"), User(444, "DDD")).observable()
class HelloWorld : View() {
override val root = vbox {
tableview(data) {
column("id", User::id.getter)
column("name", User::name.getter)
}
}
}
I use User::id.getter to make it compiling, but the cells are empty.
I did a lot of search, but can't find code to work with current latest tornado (1.7.16)
Here is a complete demo for this: https://github.com/javafx-demos/tornadofx-table-show-pojo-demo
You need to reference the property, not the getter, ie. User::id. To reference immutable properties you need to use the readonlyColumn builder:
readonlyColumn("id", User::id)
readonlyColumn("name", User::name)
That said, you really should use JavaFX properties in your domain objects instead. Not doing so in a JavaFX based application just makes everything harder, and you loose out on a lot of benefits, or at the very least you have to jump through hoops.
Here is the complete application written with observable JavaFX properties. Note that you would then access the idProperty and nameProperty properties instead. With this approach, changes to the underlying data item would automatically be visible in the tableview as well:
class User(id: Int, name: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
}
private val data = listOf(User(111, "AAA"), User(222, "BBB"), User(333, "CCC"), User(444, "DDD")).observable()
class HelloWorld : View() {
override val root = vbox {
tableview(data) {
column("id", User::idProperty)
column("name", User::nameProperty)
}
}
}