JavaFX GUI architecture with Clojure core.async @friemens
Jul 15, 2015
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
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