Skip to content
This repository was archived by the owner on Apr 25, 2024. It is now read-only.

User interface

jjpe edited this page Feb 5, 2013 · 21 revisions

User interface

Introduction

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.
  • Slightly slow. The process of transforming XML layouts into real Android views (also called inflating) actually happens at the moment of Activity initialization. Hence for some very complex layouts this can notably increase the loading time of the activity.

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.
  • No performance overhead. The transformation from declarative neko.ui code to Java constructors and setters happen in real time. It means that neko.ui is even faster than XML-based layouts.

    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).

Using neko.ui

Basics

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.

Injecting already created elements into the layout

By default, make-ui expects every element in the UI tree to be a vector. If it is not, then the element is eval’ed until it finally becomes a vector.

(make-ui (vec (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”.

If you want to insert an arbitrary, already created View inside your UI tree, you have to explicitly mark it with single quote symbol.

(let [cancel-button (Button. context)]
  (.setText cancel-button "Cancel")
  (make-ui [:linear-layout {}
            [:button {:text "OK"}]
            'cancel-button]))

Attributes

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/HORIZONTAL), which is exactly what we need.

Special values

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).

Custom context and constructor arguments

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]}])

Traits

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.

List of most useful traits

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 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 var ok-button which stores the button object.

    Note though that before the make-ui is executed, ok-button is known only as an unbound var. That means that any code calls methods on it will use reflection. 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 R$string/app_name}].

  • :layout-params, used by :view-group.

    Operates on the following attributes: :layout-height, :layout-width, :layout-weight. Creates an appropriate LayoutParams object based on these attributes and the type of

    the container, and applies it to the current element.

    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.

    Actually it consists of two separate traits: :id and :id-holder. These trait 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 calling setTag. Next time to get the inside elements you only have to call getTag 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 to true, and then set :id attribute for the subelements. In the end the container will have a map available via getTag 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
        

Ways of extension

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.

Defining new elements

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.

Creating new traits

First lets recall what a trait is. Trait is a special function that turns some attribute(s) into Java interop code. 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? match-predicate? codegen-fn]

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.
  • match-predicate (optional argument) is a function that takes an attribute map and returns if the trait should be applied on this element. Usually it should just check if the needed attribute is present on the map, so this behavior is used by default (where checked attribute is the same as trait’s name. So, for instance, trait named :text will be only applied if element’s attribute map contains :text attribute). So you only have to specify this predicate only for more complicated traits (e.g. you need to check if two or more attributes are present).
  • codegen-fn is the main part of the trait. It is a function that takes four arguments: object symbol, attributes map, code generated so far and an options map. This function is relatively complex so it deserves a separate section following by.

Code generating function

The usual life cycle of a codegen function looks like this: take necessary attributes from the attribute map, generate the interop code from it, append it to the code generated earlier by other traits and clean up attribute map from processed attributes.

In most of the cases the attribute being processed matches the trait’s name. Because this is the default behavior, codegen-fn can return only the generated code. If so, attribute with trait’s name will be automatically removed from the attributes map.

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"
  (fn [obj attrs code options]
    (conj code
          `(.setFooBar ~obj ~(: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 cases your codegen function should return a map with :code value and optional values :attributes-fn and :options-fn.

(deftrait :bar
  #(every #{:foo :bar} %) ;; It's a match-predicate that checks if both attributes are present
  (fn [obj attrs code options]
    {:attributes-fn #(dissoc % :foo :bar) ;; Remove both attributes in the end
     :code `(conj code
                  `(.setFooBar ~obj ~(:foo attrs) ~(:bar attrs)))}))

Options

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.

Clone this wiki locally