I am trying to use MapValueFactory on a TableView in Kotlin. I found a good example in Java at https://jenkov.com/tutorials/javafx/tableview.html
The following Java code works fine:
package com.example.tableview;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.MapValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
var tableView = new TableView();
TableColumn<Map, String> firstNameColumn = new TableColumn<>("firstName");
firstNameColumn.setCellValueFactory(new MapValueFactory<>("firstName"));
TableColumn<Map, String> lastNameColumn = new TableColumn<>("lastName");
lastNameColumn.setCellValueFactory(new MapValueFactory<>("lastName"));
tableView.getColumns().add(firstNameColumn);
tableView.getColumns().add(lastNameColumn);
ObservableList<Map<String, Object>> items = FXCollections.<Map<String, Object>>observableArrayList();
Map<String, Object> item1 = new HashMap<>();
item1.put("firstName", "Randall");
item1.put("lastName" , "Kovic");
items.add(item1);
Map<String, Object> item2 = new HashMap<>();
item2.put("firstName", "Irmelin");
item2.put("lastName" , "Satoshi");
items.add(item2);
tableView.getItems().addAll(items);
Scene scene = new Scene(tableView, 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
When I cut and paste that code into a Kotlin project in IntelliJ, it attempts to auto-translate from Java to Kotlin. However, the generated code won't compile. It says there is a Type Mismatch trying to add firstNameColumn to tableView.columns.
Type mismatch.
Required: Nothing!
Found: TableColumn<Map<*, *>, String>
Here is the generated Kotlin code that DOES NOT compile:
package com.example.tableview
import javafx.application.Application
import javafx.collections.FXCollections
import javafx.scene.Scene
import javafx.scene.control.TableColumn
import javafx.scene.control.TableView
import javafx.scene.control.cell.MapValueFactory
import javafx.stage.Stage
class HelloApplication : Application() {
override fun start(stage: Stage) {
val tableView: TableView<*> = TableView<Any?>()
val firstNameColumn = TableColumn<Map<*, *>, String>("firstName")
firstNameColumn.setCellValueFactory(MapValueFactory("firstName"))
val lastNameColumn = TableColumn<Map<*, *>, String>("lastName")
lastNameColumn.setCellValueFactory(MapValueFactory("lastName"))
tableView.columns.add(firstNameColumn)
tableView.columns.add(lastNameColumn)
val items = FXCollections.observableArrayList<Map<String, Any>>()
val item1: MutableMap<String, Any> = HashMap()
item1["firstName"] = "Randall"
item1["lastName"] = "Kovic"
items.add(item1)
val item2: MutableMap<String, Any> = HashMap()
item2["firstName"] = "Irmelin"
item2["lastName"] = "Satoshi"
items.add(item2)
tableView.items.addAll(items)
val scene = Scene(tableView, 320.0, 240.0)
stage.title = "Hello!"
stage.scene = scene
stage.show()
}
fun main() {
Application.launch(HelloApplication::class.java)
}
}
Any help would be much appreciated. I have gone down several rat holes on this trying to get Types correct...
CodePudding user response:
Compiler Error
Your compiler error comes from the fact you made tableView
a TableView<*>
. It doesn't know enough about the types to allow you to add anything to the columns list. Simply changing this line:
val tableView: TableView<*> = TableView<Any?>()
To one of the following:
val tableView: TableView<Map<*, *>> = TableView()
// or
val tableView = TableView<Map<*, *>>()
Will let your Kotlin code compile, and run as expected.
Problem With Your Java Code
Your Java code compiles because there you made tableView
the raw type TableView
. That means no generics associated with the TableView
are used. You should never use raw types unless you're interacting with legacy code older than Java 5.
You should change:
var tableView = new TableView();
To:
var tableView = new TableView<Map<?, ?>>();
Note you're also using raw types when parameterizing the TableColumn
s. The fix is similar, but unfortunately it breaks the ability to use MapValueFactory
due to the fact it declares a raw type when it implements Callback
. The real fix is to not use MapValueFactory
(see below).
Don't Use *ValueFactory
Classes
Classes such as PropertyValueFactory
and MapValueFactory
were added at a time when lambda expressions (in Java) were not a thing. That meant using an anonymous class. Anonymous classes can be pretty verbose, so they added convenience classes to make the code easier for developers. But they have two disadvantages: they rely on reflection and, more importantly, you lose all compile-time validations (e.g., whether the property exists, the type of the property, etc.).
In both Java and Kotlin, I would recommend using lambda expressions (while also never using raw types).
Java:
// Note everywhere is using a Map<String, String> now
var tableView = new TableView<Map<String, String>>();
TableColumn<Map<String, String>, String> firstNameColumn = new TableColumn<>("firstName");
firstNameColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().get("firstName")));
TableColumn<Map<String, String>, String> lastNameColumn = new TableColumn<>("lastName");
lastNameColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().get("lastName")));
Kotlin:
// Again, note everywhere is using Map<String, String>
val tableView = TableView<Map<String, String>>()
val firstNameColumn = TableColumn<Map<String, String>, String>("firstName")
firstNameColumn.setCellValueFactory { SimpleStringProperty(it.value["firstName"]) }
val lastNameColumn = TableColumn<Map<String, String>, String>("lastName")
lastNameColumn.setCellValueFactory { SimpleStringProperty(it.value["lastName"]) }
Although I made everything use Map<String, String>
in both versions, you can obviously parameterize the Map
differently to fit your needs.