clojure-android / neko

The Clojure/Android Toolkit
Other
297 stars 36 forks source link

Add reactivity #25

Open BartAdv opened 10 years ago

BartAdv commented 10 years ago

Hi,

So I've been experimenting with references and watches, and I came with idea of a trait that allows you to hook to the reference and alter the widget when reference value changes:

(deftrait :ref-text [^android.widget.TextView wdg {:keys [ref-text]}]
  (add-watch ref-text nil
             (fn [_ _ o n] (post wdg (.setText wdg n)))))

This helped me to refrain from fetching widgets by ids/vars and just allowed to focus on the values prepared for them:

(def txt (atom nil))

(def ui [:linear-layout {:id-holder true}
         [:text-view {:text "Hello from Clojure!!!"}]
         [:edit-text {:text "Initial text" :ref-text txt}]])

(reset! txt "Some other text")

There is one issue here: I haven't hooked to the widget destroy event to release the reference that watcher holds (I haven't yet found the way to do it, am not very knowledgeable in android SDK ;) ), but I'm pretty sure it can easily be done.

Real question is, do you think it would be feasible to alter existing traits to work this way if the value specified is a reference (just additional case)? Many traits would benefit from such thing, and it would give neko that 'reactive' feel (possibility of using dataflow programming a'la javelin for example, is so tempting). Or whether it's better as an 'add-on' (like I did in my code)?

BartAdv commented 10 years ago

I think weak references could help here - watcher' callback wouldn't capture direct reference to widget, and would just do nothing when it's null (possibly uninstall itself)

BartAdv commented 10 years ago

Started experimenting here: https://github.com/BartAdv/neko/commit/8a6c5b0ed0b68ae6baa4d95552eb51a8776f36b5

alexander-yakushev commented 10 years ago

Hello Bartosz,

This certainly looks interesting. Possibilities for reactive programming were requested a couple of times from people trying Clojure/Android. I myself think this is a great way to go. Although I'm still not sure if Neko should enforce this kind of UI architecture. This approach needs evaluation to see if all the attributes can be described this way, will there be much performance overhead, can the ordinary traits and reactive traits coexist. I encourage you to continue your experiment and see what can come out of it.

I'm leaving this issue open as work in progress. Thanks for your efforts and hope you enjoy CoA!

ghost commented 10 years ago

Just to add my $.02:

I've been working on a (by now) quite substantially-sized app, and had non-obvious references to UI components fall on my feet more than once. Besides, attaching UI elements directly to unfiltered application state only ever works at the beginning... until something flickers ... or gets to large for the widget size ... or is nil ... ;-)

What seems to work well for now, is to keep state local, and publish updates via event buses (realized trivially with core.async and pubs), to which my activities or fragments subscribe in onResume, and from which they unsubscribe in onPause. The subscriptions themselves are kept in an atom that is local to the Activity/Fragment (following the same pattern as in for example Nightweb), so even if I forget to unsubscribe, all references to views that may be kept by the subscribers are going down with the containing object.

alexander-yakushev commented 10 years ago

@vschlecht Can you please take a look at the solution here #23 (last comment)? It is not reactive, but at least it is not locally-bound and should be much safer than keeping references in namespace vars.

ghost commented 10 years ago

Honestly, I fail to see the point of using (*a) in this particular example: after all the reference to the Activity is available directly as "this" in the on-create fn.

What I really like about (*a) is that it's quite an elegant solution to be used in REPL development that probably won't break things just by remaining in production code in the same way that :def a did.

However I do not think that it should be used outside of the REPL for the following reasons:

IMHO by design, the use of (*a) will pollute every function that uses it with system-mutable global state.

alexander-yakushev commented 10 years ago

Actually, I pointed to the example that shows usage of find-view to obtain references to UI widgets. *a was only incidental there, but since you touched that topic we can discuss it as well.

I agree that (*a) is better be used only for REPL development. But it is still better than :def. We should encourage in docs to change calls to (*a) with true activity references, but if users don't the results won't still be as destructive (at least memory-wise).

As for the first point, to distinguish two activities in the same namespace :key attribute was added to defactivity which allows to specify a custom alias for the activity rather than its namespace. Say:

(defactivity foo.bar.BazActivity
  :key :baz
  .....)

(*a :baz) => yields BazActivity instance
BartAdv commented 10 years ago

As for the original point, I had experimented bit more back then, and it quickly became hairy as soon as I tried it to be bound two-way. It's pretty raw, uses quite low level primitives (watchers), and it tries to connect two distinct values: the original value, and the value kept by the widget, which does not map 1:1 (TextViews use entirely different structure than string for example).

That's why it's just unfeasible to add such trivial reactivity on top of neko (which design goals are to sit on top of original SDK), and it would rather require something entirely different.

As for the other point, I haven't yet given it a go.