-
Notifications
You must be signed in to change notification settings - Fork 36
User interface
Android SDK comes with a declarative way to define your UI in XML. This approach is much simpler and more convenient than writing tons of Java boilerplate (like in Swing). Although having your UI in entirely different language has its shortcomings. Here is the brief list of them:
- It’s static. Since we aim at dynamic development (the way Clojure allows) we want the ability to dynamically modify every part of our application, and native UI tools stand in the way for that.
- XML. Enough said.
- Different language. Like if the previous point wasn’t bad enough, declaring user interface in XML is even more disadvantageous because it’s a separate language and a separate compilation stage. You can’t manipulate things written in XML from the Java side, and the possibilities for extending the existing behavior of XML config transformation is very limited.
On the other hand, neko provides it’s own solution to describe user interface declaratively. Among other advantages it brings similar to Android’s native approach it is also:
- Dynamic. You can recompile your UI definition at any time at the REPL.
- You write Clojure. Neko’s UI element descriptions are just Clojure’s native data structures (vectors and maps). It means that you can use Clojure’s ordinary data-processing facilities to manipulate these definitions in any way (for example, generate repetitive UI elements or reuse some common idioms).
-
Highly extensible. Neko provides you all tools to write your own
elements and attribute transformers.
This allows neko.ui to be considered a viable replacement for the original UI framework when writing applications in Clojure. However you can fall back to defining XML layouts any time it feels appropriate (for example, if you want to use WYSIWYG GUI tools like the one Eclipse provides).
Your main tool for creating user interfaces is make-ui
macro. It
takes one argument - the UI tree, which in turn is a vector that
has the following syntax:
[element-type attributes-map & inside-elements]
and returns a View object which you can call set-content-view!
on or use like any ordinary View object.
For example:
(make-ui [:linear-layout {:orientation :vertical}
[:edit-text {:hint "Put your name here"}]
[:button {:text "Submit"
:on-click (fn [_] (submit-name))}]])
Let’s examine in detail every part of this example.
Element type is a keyword that represents Android UI view. Every
element type has its respective class name, attributes it can
contain, default attribute values and other parameters. You can
see a list of all available elements by calling
(neko.doc/describe)
. Also you can expect each element separately
by providing an element type to describe
.
The second value in UI tree is an attribute map. It consists of pairs where key is a keyword representing an attribute, and value is the value for this attribute. Many elements have their default attribute values for cases if you don’t provide them, for instance, a button’s default text is “Default button”. If you don’t want to provide any attributes to an element, put an empty map there anyway.
After that comes an optional number of elements that should go inside the current element. This only makes sense for elements that extend the ViewGroup class, so they contain other elements. Every sub-element definition is a vector itself and follows the same rules. You can see that vectors for EditText and Button have only two values each inside, since they can’t serve as containers to other elements.
make-ui
expects every element in the UI tree to be a
sequence (or nil, then it will simply be ignored).
(make-ui (concat [:linear-layout {}]
(map (fn [i]
[:button {:text (str i)}])
(range 10))))
In this example make-ui
will evaluate every form until it
becomes a vector, so what we’ll get is a linear layout with ten
buttons, named from “0” to “9”.
You can also insert an arbitrary, already created View inside your UI tree.
(let [cancel-button (Button. context)]
(.setText cancel-button "Cancel")
(make-ui [:linear-layout {}
[:button {:text "OK"}]
cancel-button]))
Many properties for Android UI elements follows a convention of
having a separate dedicated setter. This allows us to omit
explicit description of most attributes for every element. By
default, attribute definition is transformed to code in the
following way: :attribute-key attribute-value
becomes
(.setAttributeKey obj attribute-value)
. As you can see,
attribute key is transformed into a setter by removing dashes,
turning the string into camelCase and putting “set” at the
beginning.
If value is also a Clojure keyword, it is perceived as a static
field of the element class and transformed as well; thus
:attribute-value
becomes ElementClassName/ATTRIBUTE_VALUE
. In
this case the rule is somewhat different: all letters are
uppercased and dashes are replaced with underscores.
By using this feature we didn’t even need to have an explicit
:orientation
attribute handler for linear layout in the previous
example. The attribute pair :orientation :vertical
was turned
into (.setOrientation LinearLayout/VERTICAL)
, which is exactly
what we need.
Sometimes it is useful to define custom keyword values for
attributes that don’t exactly match a static field. For example,
ProgressDialog’s progress style attribute can have two values:
STYLE_HORIZONTAL
and STYLE_SPINNER
. Of course, you can set it
like this: :progress-style :style-spinner
. On the other hand an
element can contain a mapping of special keywords to values:
;; somewhere in :progress-dialog definition
:values {:horizontal ProgressDialog/STYLE_HORIZONTAL
:spinner ProgressDialog/STYLE_SPINNER}
This allows you to specify the attribute like
:progress-style :spinner
instead.
You can view the map of special values for element by calling
(neko.doc/describe element-keyword)
.
Most elements have a constructor that takes one argument - context. This constructor is used by default in neko.ui, and application context is passed to it as an argument.
However some elements doesn’t have this type of constructor, or
require you to provide some values that you can’t later set with a
setter. Also there are elements that require a richer Context
object (like Activity or Service). The latter issue can be solved
by using a two-arguments version of make-ui
that takes a context
object as its first argument. Example:
;; main-activity is an Activity object
(make-ui main-activity [:progress-dialog {:style :spinner}])
Passing additional arguments to a constructor can be dove via
:constructor-args
attribute which takes a list of arguments.
Note that you have to specify only additional arguments as the
first argument (a context) is provided automatically.
(make-ui [:foo {:constructor-args [1 2]}])
Many attributes have their setter counterparts but some of them don’t. Or there are some attributes that you want process simultaneously. You might even want to introduce some special behavior via attributes that isn’t possible with setters.
To be able to do such things neko.ui has a concept of traits. A trait is a special function that takes element’s attributes map, takes the attributes it should work on and generate Clojure code from them. Each element has its own list of traits, and also it inherits its parent traits.
describe
when called on the element keyword prints all traits
for this element with the detailed description. You can also call
describe
on the trait name itself to see the documentation for it.
Every trait has a name that is a Clojure keyword. Usually a trait seeks for attribute with the same name as trait’s name. This is considered a default behavior unless stated otherwise.
If you see that some trait is used by :view
, for example, it
means, that every element that inherits from :view
also gets
this trait.
-
:def, used by
:view
Takes a namespace-qualified symbol provided to
:def
attribute and binds the object to it. Under the hood it generates a call to(def _value_ _current-object_)
.Example:
[:button {:def `ok-button}]
defines a varok-button
in the current namespace to store the button object.Note though that before the
make-ui
is executed,ok-button
is not known to the rest of the code. This will lead to compilation errors during AOT-compilation. To avoid this,declare
all vars with necessary type hints at the beginning of the namespace.
(declare ^android.widget.Button ok-button)
(defn get-text [] (.getText ok-button)) ;; No reflection!
...
(make-ui [:button {:def `ok-button ....
-
:text, used by
:button
,:edit-text
,:text-view
.Sets the element’s text to a string, integer ID or a keyword representing the string resource provided to
:text
attribute.This only differs from the default
.setText
setter because it resolves the string resource if keyword is provided as a value.Example:
[:button {:text :app-name}]
. -
:layout-params, used by
:view-group
.Operates on the vast number of attributes:
:layout-height
,:layout-width
by default,:layout-weight
and:layout-gravity
for LinearLayout, all types of relative descriptions for RelativeLayout,:layout-margin-top/left/right/bottom
for both Linear and RelativeLayout. Creates an appropriate LayoutParams object based on these attributes and the type of the container.Values for
:layout-height
and:layout-width
can have two special ones::fill
and:wrap
which correspond to FILL_PARENT and WRAP_CONTENT respectively. If not provided:wrap
is used by default.Example:
[:linear-layout {}
[:button {:layout-width :fill
:layout-height :wrap
:layout-weight 1}]]
-
:id, used by
:view
.First of all, this trait sets ID of the widget to the value of
:id
attribute (by calling.setId
method. But its primary goal lies in conjunction with:id-holder
trait. These two traits are used to imitate a ViewHolder pattern.ViewHolder is usually implemented for quick access to inside elements of the container. Here’s the use case: suppose you have a ListView that consists of complex Views (a LinearLayout with a TextView and a button inside). When updating the Views while scrolling you are given a View to reuse and set new values for its internals.
How to get subelements (Button and TextView) from a container (LinearLayout)? When using XML layouts you can call
findViewById
method on the top-level container and provide subelement’s update. However scanning through XML takes time, which is obviously undesirable. Here’s where ViewHolder kicks in. It looks like this: for every top-level container, a special Holder object is created that stores references to the subelement of this container, and this object is saved into container by callingsetTag
. Next time to get the inside elements you only have to callgetTag
which returns the Holder, from where you can get the subelements directly.In Java world Android developers implement this from scratch every time they need it. That’s why it is called a “pattern” in the first place. Neko.ui offers a much easier solution for this. You can set
:id-holder
in the top-level container totrue
, and then set:id
attribute for the subelements. In the end the container will have a map available viagetTag
which stores the mapping of IDs to elements.Example:
(def contact-item [:linear-layout {:id-holder true}
[:linear-layout {:orientation :vertical}
[:text-view {:id ::name}]
[:text-view {:id ::email}]]
[:button {:id ::submit}]])
(::email (.getTag contact-item)) => returns TextView object
Neko.ui initially provides a small set of elements a traits. Its main goal is to let user create new UI entities and behaviors whenever he needs them, and do it easily.
You can define new elements in any part of your program (but
obviously prior to using them) with neko.ui.mapping/defelement
function. It takes an element’s name (which should be a keyword)
and optional key-value arguments. Here’s the list of them:
- :classname - a class of a real Android UI element. This option is obligatory for every element that you plan to use in UI tree directly (so you can omit it for abstract elements that you plan only to inherit from).
- :inherits - parent element’s name that you want to inherit from. Traits, special values and default attributes are inherited. It is suggested that you inherit your elements at least from :view to gain the most common traits.
- :traits - a list of traits to be supported by this element. You don’t have to specify traits that are already inherited from the parent element.
- :values - a map of special value keywords to actual values.
- :attributes - a map of default attribute values that will be used if attribute is not provided.
Example:
(defelement :image-view
:classname android.widget.ImageView
:inherits :view
:traits [:specific-image-trait :some-more-trait]
:values {:fit-xy ImageView$ScaleType/FIT_XY
:matrix ImageView$ScaleType/MATRIX}
attributes {:image-resource android.R$sym_def_app_icon})
Here we define an element called :image-view
which represents an
original ImageView. We inherit it from :view
which automatically
gives our new elements some useful traits. Then we provide
additional traits via :traits
option. :values
allows to
specify convenient keyword aliases for values hidden behind
ScaleType class. Finally, using :attributes
we define the
default image for the element which will be used if user doesn’t
provide this attribute.
First, let us recall what a trait is. Trait is a special function that takes some attribute(s) out of the attribute map and performs specific actions based on their values. Since most of the attributes are covered by the default transformation into a setter, traits are only necessary for more complex cases.
There is a special macro called neko.ui.traits/deftrait
for
creating traits. Here is how its arguments look like:
[trait-name docstring? param-map? args-vector & body]
Let’s describe them one by one:
- trait-name is a keyword that will represent this trait. This name is to be added to UI elements’ trait list.
-
docstring (optional argument) is a way to add some info about
the trait, and can be later accessible via
neko.doc/describe
. -
param-map (optional argument) is a function that takes a map
with certain trait parameters. The following parameters are
supported:
-
:attributes
- should be a vector of keywords that denote attributes, which trait is applied to. By default, trait is looking for the attribute with the same name as itself. Hence, trait named:text
will be only applied if element’s attribute map contains:text
attribute. But if a trait operates on more than one attribute, this parameter allows to specify it. Also this parameter is used to determine which attributes should be removed from the map after trait finishes. -
:applies?
- if you need even more complex method to determine whether trait should be applied to the given widget and attributes, you can put it into this parameter. This should be an expression that returns a boolean value. The expression can use all variables from args-vector.
-
- args-vector is a binding vector for the trait function. Remember that trait functions take widget, attributes map and options map, but it’s up to you how to destructure it.
-
body is the main part of the trait. It operates on passed UI
widget based on attributes’ values.
Usually trait’s body doesn’t have to return anything. Also you might want to change what happens to the attribute map after the trait finishes (by default, the used attributes are dissoc’ed from it). Same for options map. To provide your custom update functions for this two maps, your trait body should return a map with
:attributes-fn
and/or:options-fn
values.
So far I didn’t tell you what are these “options” and how do they differ from attributes. Options map is an internal way to pass values between traits, from higher-level elements to their subelements.
If this still doesn’t make sense, look how it works. A trait of some container element (the one that contains other elements, like a LinearLayout) can put some values on the options map. These values will become visible for all traits that are called on the inside elements of the container. These traits can use these values to generate code, and modify the options map themselves for their own subelements.
:id-holder
and :id
traits are an example of options usage. If
:id-holder
attribute is true for some container, the respective
trait puts this container’s object symbol on the options map.
Later the :id
trait (which is called on elements with this
attribute) will take the container symbol from options map and
generate the code that puts current’s element reference to the
container’s tag.
By default (if :options-fn
is not specified in the return value
of codegen-fn) options map is not changed as a result of trait’s
activity. :options-fn
if provided takes options map as an
argument and can put new values to it or remove existing ones.
In this example we create a trait named :foo
that generates a
call to .setFooBar
with the value of :foo
attribute. The
attribute will be automatically removed from the map in the end.
(deftrait :foo
"You may put docs here"
[wdg attrs opts]
(.setFooBar wdg (:foo attrs)))
Why do we need to remove anything from the attribute map? If you remember, after all traits are applied the default attribute transformer kicks in that turns all attributes that remained into simple setters. Since a trait already processed its attribute, we don’t want the default transformer to do this again (besides, incorrectly).
Although there might be cases where you need to clean attribute map in a more complex way (e.g. you processed several attributes and want to remove them all). For this you can specify a list of attributes in the parameters map. You can also modify the resulting options map by returning a map like following:
(deftrait :foobar
{:attributes [:foo :bar]
:applies? (every #{:foo :bar} attrs)} ;; Trait will only apply if
;; both attributes are present
[wdg attrs options]
(.setFooBar wdg (:foo attrs) (:bar attrs))
{:options-fn #(assoc % :cached-foo foo)}) ;; Put value of foo onto
;; the options map
Namespaces
- neko.action-bar
- neko.activity
- neko.context
- neko.data
- neko.data.shared-prefs
- neko.debug
- neko.dialog.alert
- neko.find-view
- neko.intent
- neko.listeners
- neko.log
- neko.notify
- neko.resource
- neko.threading
- neko.ui
- neko.ui.mapping
- neko.ui.listview
- neko.ui.adapters
User interface
Action bar
SQLite
Logging