Home > Blockchain >  JavaFX - How do you use MapValueFactory in Kotlin?
JavaFX - How do you use MapValueFactory in Kotlin?

Time:05-11

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 TableColumns. 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.

  • Related