くろニャァ ~ Activityとリソース

前置き

ClojureでAndroidアプリを開発するシリーズ。今回の主題はactivityです。

コンテキスト(android.content.Context)

Contextクラスにはアプリ環境に関する情報が格納されており、AndroidのAPIを使うときは至る所でこれを要求されます。そこでnekoでは、コンテキストをグローバル変数で持つようになっています。

  • シンボル名は、neko.context/context
  • publicなVarなので、どこからでも参照可能
  • SplashActivity(アプリ起動時にアニメを出すactivity)の中で、初期化される
  • 初期値は、applicationオブジェクト(ApplicationはContextの派生クラス)

applicationオブジェクトで初期化されるという点に注意して下さい。activityオブジェクトをコンテキストとして使いたい場合は、バインドを変更する必要があります。

(alter-var-root #'neko.context/context (constantly act))   ; actはactivityオブジェクト。

例えば、applicationオブジェクトをコンテキストに使うと、Androidのバージョンによってはスピナをタッチした瞬間に落ちることがあるようです(4.0.3とか)。そんなケースでは、activityをコンテキストに使えばOKです。

いっそのこと、activityが切り替わるたびに(例えばonResumeで)、当該activityをcontextにバインドしておけば安心かもしれません。

neko.context名前空間には、Context#getSystemService()相当のマクロget-serviceも定義されています。以下のコードは同じ意味です。

(get-service :alarm)
(.getSystemService context Context/ALARM_SERVICE)

ClojureのキーワードからJavaの定数への変換はnekoがやってくれます。変換ルールは容易に想像できるでしょう。例えば、:layout-inflaterは、LAYOUT_INFLATER_SERVICEになります。

android.app.Activity

neko.activity

activity関連の関数は、neko.activity名前空間に定義されています。

  • defactivity ...Activityの派生クラスを定義
  • set-content-view! ...Activity#setContentView()相当
  • request-window-features! ...Activity#requestWindowFeature()相当

Activity派生クラスの定義

defactivityには、パッケージ+クラス名とオプションを指定します。

(defn on-create-fn      ; Activity#onCreate()相当。
  [this bundle]
  ...)

(defactivity
  jp.dip.gpsoft.clonya.MainActivity    ; パッケージ+クラス名。
  :on-create on-create-fn)             ; :on-createオプション

この例の場合、Activityから派生したMainActivityクラスを定義し、onCreate()をオーバーライドしたことになります。onCreate()はBundleを引数に取りますが、:on-createに指定する関数はthisとbundleを引数に取ります。thisにはactivityオブジェクトが指定されます。

メソッドのオーバーライド

オーバーライド用のオプションには、:on-createの他にも、:on-pause、:on-resumeなどがあります。使えるオプション名と関数の引数は以下の通りです。オプション名を見ればメソッド名が類推できるでしょう。

オプション引数
:on-create[this bundle]
:on-create-options-menu[this menu]
:on-options-item-selected[this item]
:on-activity-result[this requestCode resultCode intent]
:on-new-intent[this intent]
:on-start[this]
:on-restart[this]
:on-resume[this]
:on-pause[this]
:on-stop[this]
:on-destroy[this]

実は、:on-pauseなどのオプションを使わなくてもメソッドのオーバーライドは可能です。例えば上記のコードに対して、onPause()をオーバーライドしたいなら、以下の関数を定義するだけでOKです。

(defn MainActivity-onPause
  [this]
  ...
)

デフォルトでは、上記のように関数名を、[クラス名] + "-" + [オーバーライドしたいメソッド名] とする必要があります。これがイヤなら、:prefixオプションでカスタマイズすることも可能です。この辺の仕様は、clojure.core/gen-classに由来します(defactivityの中ではgen-classを使っている)。

(defactivity
  jp.dip.gpsoft.clonya.MainActivity
  :prefix "main-"
  :on-create on-create-fn)

(defn main-onPause [this] ...)

上表に無いメソッドでも、一応、オーバーライドは可能です。例えば、main-onSaveInstanceState関数を定義すれば、Activity#onSaveInstanceStateをオーバーライドできます。ただ残念ながらこのケースでは、後述する、「親クラスのメソッドを呼ぶ」ことはできません。

親クラスのメソッドを呼ぶ

仮に、:on-pauseオプションでオーバーライドする方式をオプション方式と呼び、main-onPauseのように関数名の規則でオーバーライドする方式をgen-class方式と呼ぶことにしましょう。オプション方式には、gen-class方式に比べて利点が2つあります。

1つめは、必要に応じて親クラスのメソッドを自動的に呼んでくれることです。

例えば、onPause()をオーバーライドする場合、「その中で親クラスのonPause()を呼べ」というのがAndroidのルールです。呼ばないと例外が発生します。オプション方式の場合はnekoがこの面倒を見てくれますが、gen-class方式の場合は、プログラマが面倒を見る必要があります。

(defn main-onPause
  [this]
  ...
  (.superOnPause this))      ; 親クラスのメソッドを呼ぶ。

onPauseに対して、親クラスのメソッド名はsuperOnPauseになります。他のメソッドの場合も同じネーミングルールです。ただし、これはオプション方式でサポートされているメソッドのみの話しです。例えば、前述のActivity#onSaveInstanceStateに対するsuperOnSaveInstanceStateは存在しません。

利点の2つめは、:on-createオプション特有のものです。それは、activityオブジェクトをdefすることです。

activityオブジェクトのdef

nekoには、activityオブジェクトをバインドしてグローバル変数を自動的に定義する機能があります。

  • defactivityに:on-createオプションを指定したときのみ有効
  • デバッグビルドのときのみ有効
  • デフォルトのシンボル名は、クラス名の先頭を小文字にしたもの(mainActivityとか)
  • defactivityの:defオプションで、シンボル名をカスタマイズ可能
  • declareしておけば、ソースファイルのdefactivityフォームより前方で参照することも可能
(defactivity
  jp.dip.gpsoft.clonya.MainActivity
  :prefix "main-"
  :def main-act
  :on-create on-create-fn)

デバッグビルドに限定しているのはメモリリーク対策のようです(不要になったactivityオブジェクトがいつまでもGCされずに残ってしまう)。ただ、defactivityの:defと似たような仕組みがview関連の機能(neko.ui/make-ui関数)にもあり、そっちはリリースモードでも有効なので、一貫性に欠ける気がしないでもないです。

set-content-view!とrequest-window-features!

set-content-view!もrequest-window-features!も、ほぼ同名のメソッドがActivityクラスにありますね。

set-content-view!の引数には、activityとviewを指定します。view引数には、viewオブジェクトか、レイアウトのリソースID(Javaで言うR.layout.xxx)を指定できます。viewオブジェクトをどうやって作るか、という話は、別の記事でやります。

request-window-features!の引数には、activityとfeature(いくつでも)を指定します。featureの指定にはキーワードが使え、例えば :no-title なら、android.view.Window/FEATURE_NO_TITLE と解釈されます。

リソース

neko.resource名前空間は、各種リソースやJavaのRクラスへアクセスする手段を提供します。

最も基本的な関数が get-resource で、これによりリソースIDを取得できます。引数にはリソースのタイプと名前をキーワードで指定します。

(get-resource :drawable :ic-launcher)
  ; jp.dip.gpsoft.clonya.R$drawable/ic_launcherのこと。
(get-resource :layout :android/simple-list-item-1)
  ; android.R$layout/simple_list_item_1のこと。

get-resourceとは別に、リソースのタイプごとに専用関数が用意されています。

(get-id :splash_app_name)
(get-layout :splashscreen)
(get-drawable :ic-launcher)
(get-string :app-name)

これらの関数の多くはリソースIDを返しますが、get-drawbleとget-stringは例外です。get-drawableはdrawableオブジェクトを返し、get-stringは文字列を返します。get-stringの第2引数以降には、フォーマット引数を指定することもできます。

get-xxx関数は、実行時にリソースIDを取得するので、パフォーマンスへのインパクトが気になるかもしれません。そんな場合は、resolve-xxxを使いましょう。これなら、コンパイル時にリソース名をリソースIDへ解決します。ただし、resolve-drawableもresolve-stringもリソースIDを返すので、単純にget-drawableやget-stringと置き換えることはできません。

ややトリッキーですが、リソース解決用のリーダマクロも用意されています。

  • #res/id
  • #res/layout
  • #res/string
  • #res/drawable

これらをリソース名のキーワードの前に置けば、リード時にリソースIDへ解決されます。

例として、ランチャアイコンを得る方法を4通り示します。

(.getDrawable (.getResources act) (get-resource :drawable :ic-launcher))
(.getDrawable (.getResources act) #res/drawable :ic-launcher)
(get-drawable :ic-launcher)
(get-drawable #res/drawable :ic-launcher)

くろニャア

くろニャアにactivityを追加してみましょう。それには、AndroidManifest.xmlの修正も必要です。また、MainActivityのレイアウトも変更しました。

AndroidManifest.xml
<activity android:name=".KuActivity" />
<activity android:name=".TaActivity" />
UI部品を作る関数(main.clj)
(defn- make-header-label
  [caption]
  [:text-view {:text caption
               :text-size [24 :sp]
               :layout-width :fill
               :layout-margin-top [30 :px]
               :layout-margin-bottom [30 :px]
               :gravity Gravity/CENTER_HORIZONTAL}])
  
(defn- make-transit-button
  [act caption act-name & [ct]]
  [:button {:text caption
            :on-click (fn [v] (start-activity act act-name :clear-top ct))}])
activityを開始する関数(ui_helper.clj)
(defn start-activity
  "activityを開始する。
   from-actは遷移元のactivityオブジェクト。
   act-nameは遷移先のクラス名。
   :clear-topをtrueにすると、遷移先より上にあるactivityをクリアする。"
  [^Activity from-act ^String act-name & {ct :clear-top}]
  (let [package (.getPackageName (.getApplicationContext from-act))
        i (Intent.)]
    (doto i
      (.setFlags Intent/FLAG_ACTIVITY_NEW_TASK)    ; このフラグは不要かも。
      (.setClassName package (str package "." act-name)))
    (when ct (.addFlags i Intent/FLAG_ACTIVITY_CLEAR_TOP))
    (.startActivity from-act i)))

[2014-05-23]IntentにFLAG_ACTIVITY_NEW_TASKフラグを付けると、startActivityの代わりにstartActivityForResultを使った場合にうまく動きません。このフラグにより、遷移先のActivityが遷移元とは別のタスク上に作られるためです(タスクについては別の記事で解説してます)。ちなみに、ActivityのlaunchModeがsingleTopやsingleInstanceの場合も、startActivityForResultは期待通りに動きません。Activityをstartすると同時にonActivityResultがRESULT_CANCELEDでコールバックされてしまいます。

ログ関数(utils.clj)
(defmacro logd
  "マクロ。デバッグログを出す。引数は、空白区切りで連結。"
  [& args]
  `(log/d ~@args :tag "CLONYA"))
レイアウト(main.clj)
(defn- main-layout [act]
  [:linear-layout {:orientation :vertical}
   (make-header-label :welcome)
   (make-transit-button act :caption-ku-bt "KuActivity")
   (make-transit-button act :caption-ta-bt "TaActivity")
   ])

(defn- ku-layout [act]
  [:linear-layout {:orientation :vertical}
   (make-header-label :ku-name)
   (make-transit-button act :caption-clo-bt "MainActivity")
   (make-transit-button act :caption_cleartop_bt "MainActivity" true)
   (make-transit-button act :caption-ta-bt "TaActivity")
   ])

(defn- ta-layout [act]
  [:linear-layout {:orientation :vertical}
   (make-header-label :ta-name)
   (make-transit-button act :caption-clo-bt "MainActivity")
   (make-transit-button act :caption_cleartop_bt "MainActivity" true)
   (make-transit-button act :caption-ku-bt "KuActivity")
   ])
activity定義(main.clj)
(defactivity jp.dip.gpsoft.clonya.MainActivity
  :prefix "main-"
  :on-create
  (fn [this bundle]
    (logd "main::on-create" this bundle)
    (on-ui
      (set-content-view! this
        (make-ui (main-layout this))))))

(defactivity jp.dip.gpsoft.clonya.KuActivity
  :prefix "ku-"
  :on-create
  (fn [this bundle]
    (logd "ku::on-create" this bundle)
    (request-window-features! this :no-title)
    (on-ui
      (set-content-view! this
        (make-ui (ku-layout this))))))

(defactivity jp.dip.gpsoft.clonya.TaActivity
  :prefix "ta-"
  :on-create
  (fn [this bundle]
    (logd "ta::on-create" this bundle)
    (request-window-features! this :no-title)
    (on-ui
      (set-content-view! this
        (make-ui (ta-layout this))))))
activityの状態遷移をトレースする(main.clj)
(defn main-onPause [this]
  (logd "main::on-pause" this)
  (.superOnPause this))

(defn main-onStart [this]
  (logd "main::on-start" this)
  (.superOnStart this))

(defn main-onResume	[this]
  (logd "main::on-resume" this)
  (.superOnResume this))

(defn main-onRestart [this]
  (logd "main::on-restart" this)
  (.superOnRestart this))

(defn main-onStop [this]
  (logd "main::on-stop" this)
  (.superOnStop this))

(defn main-onDestroy [this]
  (logd "main::on-destroy" this)
  (.superOnDestroy this))

せっかくなので、スクリーンショットを。

main main main

ログは、以下のようになります。

; 起動時。
01-28 15:36:31.062: D/CLONYA(5429): main::on-create jp.dip.gpsoft.clonya.MainActivity@b5e76608 
01-28 15:36:31.072: D/CLONYA(5429): main::on-start jp.dip.gpsoft.clonya.MainActivity@b5e76608
01-28 15:36:31.082: D/CLONYA(5429): main::on-resume jp.dip.gpsoft.clonya.MainActivity@b5e76608
; KuActivityへ遷移。
01-28 15:37:58.164: D/CLONYA(5429): main::on-pause jp.dip.gpsoft.clonya.MainActivity@b5e76608
01-28 15:37:58.254: D/CLONYA(5429): ku::on-create jp.dip.gpsoft.clonya.KuActivity@b6033fe8 
01-28 15:37:58.594: D/CLONYA(5429): main::on-stop jp.dip.gpsoft.clonya.MainActivity@b5e76608
; TaActivityへ遷移。
01-28 15:38:37.105: D/CLONYA(5429): ta::on-create jp.dip.gpsoft.clonya.TaActivity@b60165e0 
; MainActivityへCLEAR_TOPで戻る。
01-28 15:39:04.805: D/CLONYA(5429): main::on-destroy jp.dip.gpsoft.clonya.MainActivity@b5e76608
01-28 15:39:04.826: D/CLONYA(5429): main::on-create jp.dip.gpsoft.clonya.MainActivity@b6059850 
01-28 15:39:04.855: D/CLONYA(5429): main::on-start jp.dip.gpsoft.clonya.MainActivity@b6059850
01-28 15:39:04.956: D/CLONYA(5429): main::on-resume jp.dip.gpsoft.clonya.MainActivity@b6059850
; 画面を横向きに。
01-28 15:39:43.186: D/CLONYA(5429): main::on-pause jp.dip.gpsoft.clonya.MainActivity@b6059850
01-28 15:39:43.207: D/CLONYA(5429): main::on-stop jp.dip.gpsoft.clonya.MainActivity@b6059850
01-28 15:39:43.236: D/CLONYA(5429): main::on-destroy jp.dip.gpsoft.clonya.MainActivity@b6059850
01-28 15:39:43.312: D/CLONYA(5429): main::on-create jp.dip.gpsoft.clonya.MainActivity@b5f54ab0
      Bundle[{android:viewHierarchyState=Bundle[{android:Panels=android.util.SparseArray@b5cb6fe0, 
      android:views=android.util.SparseArray@b5cb9520, android:ActionBar=android.util.SparseArray@b5caab18}]}]
01-28 15:39:43.316: D/CLONYA(5429): main::on-start jp.dip.gpsoft.clonya.MainActivity@b5f54ab0
01-28 15:39:43.326: D/CLONYA(5429): main::on-resume jp.dip.gpsoft.clonya.MainActivity@b5f54ab0
Last modified:2014/05/23 18:31:28
Keyword(s):
References:[Clojure meets Android]
This page is frozen.