JavaFX GUI architecture with Clojure core.async

Preview:

Citation preview

JavaFX GUI architecturewith Clojure core.async

@friemens

GUIs are challenging

GUI implementation causes significant LOC numbers

GUIs require frequent changes

Automatic GUI testing is expensive

GUI code needs a suitable architecture

Model Controller

View

Model Controller

View

MVC makes you think of mutable things

MVC Variations

MVP a.k.a

Passive View

View

Model Presenter ViewModel

View

Model

MVVM a.k.a

PresentationModel

A real-world OO GUI architecture

ControllerViewModel

ViewImplUI Toolkit Impl

UIView

Other parts of the system

two-waydatabinding

updates

actionevents

only data!

Benefits so far

ControllerViewModel

ViewImplUI Toolkit Impl

UIView

Other parts of the system

two-waydatabinding

updates

actionevents

only data!

Dumb Views => generated code

Dumb ViewModels => generated code

Controllers are unit-testable

Remaining annoyances

ControllerViewModel

ViewImplUI Toolkit Impl

UIView

Other parts of the system

two-waydatabinding

updates

actionevents

only data!

Unpredicatble execution paths

Coordination with long runnning code

Merging of responses into ViewModels

Window modality is based on a hack

Think again... what is a user interface?

events

state

1) is a representation of system state

A user interface ...

{:name {:value "Foo" :message nil} :addresses [{:name "Bar" :street "Barstr" :city "Berlin"} {:name "Baz" :street "Downstr" :city "Bonn"}] :selected [1]}

events

state

1) is a representation of system state2) allows us to transform system state

A user interface ...

{:name {:value "Foo" :message nil} :addresses [{:name "Bar" :street "Barstr" :city "Berlin"} {:name "Baz" :street "Downstr" :city "Bonn"}] :selected [1]}

2)

1)

A user interface ...

… consists of two functions ...

which – for technical reasons – need to be executed asynchronously.

[state] → ⊥ ;; update UI (side effects!)

[state event] → state ;; presentation logic

( )

Asynchronity in GUIs

GUI can become unresponsive

Java FX application thread

Event loop

Your code

Service call

What happensif a service call takes seconds?

Keep GUI responsive (callback based solution)

Service call

Your code 1

Your code 2

Use other thread

Java FX application thread

Event loop Some worker thread

Delegate execution

Schedule toevent loop

Meet core.async: channels go blocks+

Based on Tony Hoare's CSP* approach (1978).Communicating Sequential Processes*

(require '[clojure.core.async :refer [put! >! <! go chan go-loop]])

(def c1 (chan))

(go-loop [xs []] (let [x (<! c1)] (println "Got" x ", xs so far:" xs) (recur (conj xs x))))

(put! c1 "foo");; outputs: Got bar , xs so far: [foo]

a blocking read

make a new channelcreates a lightweight

process

async write

readwrite

The magic of go

sequential codein go block

read

write

macroexpansion

statemachine

code snippets

Keep GUI responsive (CSP based solution)core.async process

core.async process

Java FX application thread

Your code

Update UI

<! >!

<!put!

go-loop

one per viewexactly one

events

state

Expensive service call: it's your choice(def events (chan))

(go-loop [] (let [evt (<! events)] (case ((juxt :type :value) evt) [:action :invoke-blocking] (case (-> (<! (let [ch (chan)] (future (put! ch (expensive-service-call))) ch)) :state) :ok (println "(sync) OK") :nok (println "(sync) Error"))

[:action :invoke-non-blocking] (future (put! events {:type :call :value (-> (expensive-service-call) :state)})) [:call :ok] (println "(async) OK") [:call :nok] (println "(async) Error"))) (recur))

blocking

non-blocking

ad-hoc new channel

use views events channel

Properties of CSP based solution

„Blocking read“ expresses modality

A views events channel takes ALL async results✔ long-running calculations✔ service calls✔ results of other views

Each view is an async process

Strong separation of concerns

A glimpse ofhttps://github.com/friemen/async-ui

prototype

JavaFX + Tk-process + many view-processes

JavaFX

Many view processesOne toolkit oriented

process

(run-view)

(run-tk)

Event handler

(spec) (handler)

Each view has one events channel

Data representing view state

:id ;; identifier:spec ;; data describing visual components:vc ;; instantiated JavaFX objects:data ;; user data:mapping ;; mapping user data <-> VCs:events ;; core.async channel:setter-fns ;; map of update functions :validation-rule-set ;; validation rules:validation-results ;; current validation messages:terminated ;; window can be closed:cancelled ;; abandon user data

(spec) - View specification with data

(defn item-editor-spec [data] (-> (v/make-view "item-editor" (window "Item Editor" :modality :window :content (panel "Content" :lygeneral "wrap 2, fill" :lycolumns "[|100,grow]" :components [(label "Text") (textfield "text" :lyhint "growx") (panel "Actions" :lygeneral "ins 0" :lyhint "span, right" :components [(button "OK") (button "Cancel")])]))) (assoc :mapping (v/make-mapping :text ["text" :text]) :validation-rule-set (e/rule-set :text (c/min-length 1)) :data data)))attach more

configuration dataa map with initial user data

specify contents

(handler) - Event handler of a view

(defn item-editor-handler [view event] (go (case ((juxt :source :type) event) ["OK" :action] (assoc view :terminated true) ["Cancel" :action] (assoc view :terminated true :cancelled true) view)))

Using a view

(let [editor-view (<! (v/run-view #'item-editor-spec #'item-editor-handler {:text (nth items index)}))] . . .)

(defn item-editor-spec [data] (-> (v/make-view "item-editor" (window "Item Editor" :modality :window :content (panel "Content" :lygeneral "wrap 2, fill" :lycolumns "[|100,grow]" :components [(label "Text") (textfield "text":lyhint "growx") (panel "Actions" :lygeneral "ins 0" :lyhint "span, right" :components [(button "OK") (button "Cancel")])]))) (assoc :mapping (v/make-mapping :text ["text" :text]) :validation-rule-set (e/rule-set :text (c/min-length 1)) :data data)))

(defn item-editor-handler [view event] (go (case ((juxt :source :type) event) ["OK" :action] (assoc view :terminated true) ["Cancel" :action] (assoc view :terminated true :cancelled true) view)))

a map with initial user data

spec handler

calling view process waits for callee

You can easily build it yourself!

JavaFX API

updatebuild

Toolkit Impl

View process fns

Toolkit process fns

core.cljtk.clj

builder.clj

binding.cljbind

< 400 LOC

Wrap up

MVC leads to thinking in terms of mutation

UIs introduce asynchronity

UI is a reactive representation of system state

Thank you for listening!

Questions?

@friemenswww.itemis.de@itemis

https://github.com/friemen/async-ui