-
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 of 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 are 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).
The main building block of neko.ui
is a UI tree. It is a vector that has
the following syntax:
[element-type attributes-map & inside-elements]
inside-elements
are the same vectors, hence the whole thing is a tree. You
can pass such UI tree to set-content-view!
, use in adapters, or on the
lowest level pass it to neko.ui/make-ui
which will return a View object
constructed from this UI tree.
For example:
;; `this` is our Activity object
(set-content-view! this [:linear-layout {:orientation :vertical}
[:edit-text {:id ::name-et
:hint "Put your name here"}]
[:button {:text "Submit"
:on-click (fn [_]
(let [name-et (find-view this ::name-et)]
(submit-name (.getText name-et))))}]])
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 inspect 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 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 to
be treated as UI definition, or any other object (or nil, then it
will simply be ignored).
(make-ui context (concat [:linear-layout {}]
(map (fn [i]
[:button {:text (str i)}])
(range 10))))
In this example UI tree will first be constructed from 10 buttons
that are stuffed inside :linear-layout
container, and then
make-ui
will generate this layout.
You can also insert an arbitrary, already created View inside your UI tree.
(let [cancel-button (Button. context)]
(.setText cancel-button "Cancel")
(make-ui context [:linear-layout {}
[:button {:text "OK"}]
cancel-button]))
Many properties for Android UI elements follow 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. Passing
additional arguments to a constructor can be done 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 context [:foo {:constructor-args [1 2]}])
You also might end up in a situation where you have to construct
an element in a completely different way (for example, by using
proxy
or reify
). For this case you can use
:custom-constructor
attribute which value should be a function
that takes a context and any other set of arguments. This way you
can combine :custom-constructor
and :constructor-args
to
create any object you want.
(make-ui [:image-view {:custom-constructor
(fn [ctx foo bar]
(proxy [android.widget.ImageView] [ctx]
(onDraw [^Canvas canvas]
...)
(onTouchEvent [^MotionEvent e]
...)))
:constructor-args ["foo" 42]}])
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 accepts element’s attributes map, takes out the attributes it should work on and generates 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.
-
: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.Options
:layout-height
and:layout-width
can have two special values::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 cooperation withneko.find-view/find-view
function. Together they allow to obtain references to child views from the parent view.You can see an example of how it is used in neko.find-view description.
(let [contact-item (make-ui context [:linear-layout {:id-holder true}
[:linear-layout {:orientation :vertical}
[:text-view {:id ::name}]
[:text-view {:id ::email}]]
[:button {:id ::submit}]])]
(find-view contact-item ::email) => returns TextView object)
- various listeners
Most functions in
neko.listeners.*
have respective traits. For example,:on-click
attribute takes a function and wraps it intoon-click-call
automatically.
[:button {:on-click (fn [_] (toast this "Clicked!"))}]
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 :another-trait]
:values {:fit-xy ImageView$ScaleType/FIT_XY
:matrix ImageView$ScaleType/MATRIX}
:attributes {:image-resource android.R$drawable/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
— 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, the 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 its job. -
: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 a trait functions take a widget, an attributes map and an 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. But occasionally 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 these two maps, your trait body should return a map with
:attributes-fn
and/or:options-fn
values.For new widgets to be able to use your trait you should list it in
:traits
as desribed here. If you want to enable new trait for existing widget types, you should call the following code before using them:
(neko.ui.mapping/add-trait! :trait-kw :widget-kw)
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 internal traits can use options values to implement custom behavior, 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 on the options map. Later the :id
trait
(which is called on elements with this attribute) will take the container
from options map and the child element 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 attributes options]
(.setFooBar wdg (:foo attributes)))
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 and turns all remaining attributes 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
":foobar trait consumes :foo and :bar attributes, and only if
both of them are present"
{: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