bekwam courses

ListView Navigation App

September 3, 2018

This article presents a navigation technique common to mobile apps as applied to the desktop with TornadoFX. An initial screen displays a collection. Selecting an item in the collection brings up a details view. The view change is executed through a sliding transition. To return to the collection from the details view, a button is used that reverses the slide. The interaction between both views is implemented using a shared object called an ItemViewModel.

The following video shows the collection view. Pressing on the plus button adds rows to a ListView. Double-clicking on an item slides the collection view off the screen, replacing it with the details view. The details view contains the data from the selected record. A back button returns to the collection view. Editing the fields of the selected record is reflected in an updated collection view.

The program starts with a data structure Todo which is a JavaFX Property-enabled object representing a task that the user wants to track. It is paired with a similar class that extends ItemViewModel. While the properties in the ItemViewModel mirror those found in the Todo class, ItemViewModel tracks changes. These changes can be deferred until an explicit commit() is called. The code in the init block opts to commit the model state when any of the properties change.

	
class Todo(descr : String = "New Item", completed : Boolean = false, completedBy : LocalDate? = null) {
    val descrProperty = SimpleStringProperty(descr)
    val completedProperty = SimpleBooleanProperty(completed)
    val completedByProperty = SimpleObjectProperty<LocalDate>(completedBy)
}
	
class TodoItemViewModel : ItemViewModel<Todo>() {
    val selectedDescr  = bind(Todo::descrProperty)
    val selectedCompleted  = bind(Todo::completedProperty)
    val selectedCompletedBy = bind(Todo::completedByProperty)

    init {
        autocommitProperties.addAll(selectedDescr, selectedCompleted, selectedCompletedBy)
    }
}

I use a TornadoFX Controller to manage the list of items. I also provide a function for adding a new item. This demo app doesn't have a delete function, but if it did, it would belong in the Controller.


class ListNavController : Controller() {

    val data = mutableListOf<Todo>().observable()

    fun newItem() {
        data.add( Todo())
    }
}

View

ListNavView is the collection view of the data. It is a VBox containing a + Button and a ListView. The globally-available todoItemViewModel is injected. ListNavView will communicate with the details view, ListDetailsView, through this object. There is a reference to the singleton ListDetailsView too.

The ListView uses a special factory to create its ListCells. TornadoFX provides the convenient cellFragment() function which helps to divide the view into smaller pieces for easier maintenance. cellFragment() is passed a class which will be a small section of a Scene Graph to be used for the ListCell.

	
class ListNavView : View("List Nav") {

    val c : ListNavController by inject()
    val detailsView : ListDetailsView by inject()
    val todoItemViewModel : TodoItemViewModel by inject()

    override val root = vbox {
        button("+") { action { c.newItem() } }
        listview(c.data) {
            bindSelected(todoItemViewModel)
            onDoubleClick {
                this@ListNavView.replaceWith(detailsView, ViewTransition.Slide(Duration.seconds(0.5)))
            }
            cellFragment(TodoListItemFragment::class)
            vgrow = Priority.ALWAYS
        }

        prefHeight = 480.0
        prefWidth = 320.0
        spacing = 4.0
        paddingTop = 4.0
    }
}	

When there is a double-click on a selectedItem, the replaceWith() function is called on this and the current collection view is replaced with the details view. A Slide Transition is used make the user aware that a new view is presented. I find replaceWidth() too abrupt. The following image shows the two screens and the segues between them.

Screen Shot of App
Screen Navigation from Collection to Details (and Back)

This is the Fragment that encapsulates the UI controls in a ListCell. An HBox holds a Checkbox and a Label pair. One of the Labels is based on a LocalDate and requires a converter to be rendered into a String.


class TodoListItemFragment : ListCellFragment<Todo>() {
    val model = TodoItemViewModel().bindTo(this)

    override val root = hbox {
        checkbox(property = model.selectedCompleted)
        vbox {
            label(model.selectedDescr)
            label(model.selectedCompletedBy, converter = javafx.util.converter.LocalDateStringConverter())
        }
    }
}

Like the collection view, the details view maintains a reference to the other view (the collection) and to the shared ItemViewModel object. The ItemViewModel object will have been set by ListNavView through a selection. The changed item is bound to the fields of the details form. Because of the autocommit in the ItemViewModel, any change to the form fields will result in a change to the ItemViewModel. Because the ItemViewModel itself contains references to the original data in the ListView, that will have updated the ListView contents upon returning.


class ListDetailsView : View("List Nav - Details") {

    val mainView : ListNavView by inject()
    val todoItemViewModel : TodoItemViewModel by inject()

    var tf : TextField by singleAssign()

    override val root = vbox {

        button("<") {
            action {
                this@ListDetailsView.replaceWith(
                        mainView,
                        ViewTransition.Slide(Duration.seconds(0.5), tornadofx.ViewTransition.Direction.RIGHT)
                )
            }
        }
        form {
            fieldset {
                field("Descr") {
                    tf = textfield(todoItemViewModel.selectedDescr)
                }
                field("Completed") {
                    checkbox {
                        bind(todoItemViewModel.selectedCompleted)
                    }
                }
                field("Completed By") {
                    datepicker(todoItemViewModel.selectedCompletedBy)
                }
            }
        }

        spacing = 4.0
        paddingTop = 4.0
    }

    override fun onDock() = tf.requestFocus()
}

The onDock() method is overridden to make sure that the user can begin editing the form's name right away. Otherwise, the default focus will be on the return nav button.

This example showed a typical mobile navigation from a collection view to a details view. TornadoFX providers smooth transitions that enhance the transition by keeping the user aware of the action as it's proceeding. (No sudden jumps to a new screen.) ItemViewModel, though similar to the original domain model, is used to pass parameters from one view to the next and to defer changes unless otherwise specified.

There is a trivial App and main provided in the source link

The complete ListNavApp.kt file can be downloaded here.


Headshot of Carl Walker

By Carl Walker

President and Principal Consultant of Bekwam, Inc