くろニャァ ~ 最初の一歩

前置き

ClojureでAndroidアプリを開発するシリーズ。今回は、最初の一歩です。

ClojureでAndroidアプリを開発するには、nekoとLeiningenプラグインのlein-droidを使います。

nekoの特徴は…

  • 元々Daniel Solano Gómez氏が開発したもの
  • 現在はAlexander Yakushev氏が中心
  • https://github.com/clojure-android
  • Android APIのラッパ
  • REPLサーバを内蔵

特に重要なのは最後の点です。アプリと一緒にREPLサーバが動くので、開発マシンからREPLへつなげば、アプリを動的に制御できます。

[2014-01-26]スクリーンキャストも公開しました。



準備

本記事ではWindowsを使用します。また、Android開発環境とClojureの環境はセットアップ済みと想定します。Clojure環境については、「Clojure開発環境」を参照して下さい。

まずはLeiningenにプラグインとAndroid SDKのパスを設定します。コンフィグファイルは、%LEIN_HOME%\profiles.cljです。環境変数LEIN_HOMEを定義してない場合は、%USERPROFILE%\.lein\profiles.cljになります。

profiles.clj
{
  :user {
    :plugins [[lein-droid "0.2.0"]          ; lein-droidプラグイン。
              [lein-midje "3.1.1"]]
    :android {:sdk-path "c:/misc/adk/"}     ; Android SDKへのパス。
  }
}

プロジェクト「くろニャァ」

コマンドプロンプトで適当なディレクトリへcdし、以下のコマンドを実行するとプロジェクトが生成されます。

プロジェクト生成
D:\etude\clj\lein>lein droid new clonya jp.dip.gpsoft.clonya :activity MainActivity :target-sdk 19 :app-name Clonya

newコマンドの詳細はhelpで見ることができます(たいして詳しくもないですが)。

D:\etude\clj\lein>lein help droid new
Creates new Android project given the project's name and package name.

Arguments: ([project-name package-name & options])

上記のコマンドの場合、オプションとしてactivityのクラス名とビルドに使うSDKのバージョンを指定しています。

project.cljの内容を少し修正します。

project.clj(抜粋)
(defproject clonya/clonya "0.0.1"
  :description "An android app in Clojure"
  :url "http://gpsoft.dip.jp/wiki/"

  ...

  :dependencies [[org.clojure-android/clojure "1.5.1-jb" :use-resources true]
                 [neko/neko "3.0.0"]]

  ...

  :android {:dex-opts ["-JXmx2048M"]
            ;; :force-dex-optimize true
            :target-version "19"
            :aot-exclude-ns ["clojure.parallel" "clojure.core.reducers"]})
  • :dex-optsはAndroidのdxコマンド用のオプション
  • メモリ不足になることがあるので、-JXmxオプションでメモリを増やしておく
  • :target-versionにより、使用するbuild-toolsのバージョンを指定

お好みで、minSdkVersionも変更します。

AndroidManifest.xml(抜粋)
  <uses-sdk android:minSdkVersion="15" />

もし開発の途中でtarget-versionやminSdkVersionを変更した場合は、全体をリビルドするために、一旦 lein clean した方が良いでしょう。また、target-versionとminSdkVersionは揃えておいた方が良いかもしれません(あとで分かったのですが、Rev.19のBuild-toolsでビルドしたapkは、APIレベル15のPlatformでは動きませんでした)。

せっかくなので、日本語用の文字列リソースも定義しておきます(端末の言語設定が「日本語」になっている前提です)。

clonya\res\values-ja\strings.xml(新規ファイル)
<?xml version="1.0" encoding="utf-8"?>
<resources>

  <string name="app_name">くろニャァ</string>

</resources>

PCに実機を接続するか、エミュレータを起動しておきましょう。ここではNexus7(2013)を使いました。脱線しますが、Nexus7のUSB接続は不安定な気がします。つながっているように見えても、実はadbが認識してないとか。USBデバッグ設定のON/OFFを繰り返したり、adbを再起動したり、いろいろやってると、そのうち認識してくれますが…。

lein-droidのdoallコマンドを使うと、ソースのコンパイルから、APKのパック、インストール、実行などを全て実施することができます。

実行
D:\etude\clj\lein>cd clonya
D:\etude\clj\lein\clonya>lein droid doall

スプラッシュスクリーン(10秒程度)に続いて、MainActivityが表示されます。

splashmain-act

MainActivityのスケルトン

MainActivityのソースを見ておきます。

main.clj
(ns jp.dip.gpsoft.clonya.main
  (:use
    [neko.activity :only [defactivity set-content-view!]]
    [neko.threading :only [on-ui]]
    [neko.ui :only [make-ui]]))

(defactivity jp.dip.gpsoft.clonya.MainActivity
  :def a
  :on-create
  (fn [this bundle]
    (on-ui
      (set-content-view! a
        (make-ui [:linear-layout {}
                  [:text-view {:text "Hello from Clojure!"}]])))))

nekoの関数/マクロを4つ(defactivity、on-ui、set-content-view!、make-ui)使っています。

  • defactivityで、activityを定義
  • activityの内容は、キーと値のペアで指定
  • :on-createキーの値に指定した無名関数が、Activity#onCreate()に対応
  • on-uiは、UIスレッド上でコードを実行するためのマクロ
  • set-content-view!は、Activity#setContentView()に対応
  • make-uiで、viewオブジェクトを作る

ちなみに、スプラッシュスクリーンのアニメを出すコードは、Javaで書かれています(SplashActivity.java)。

REPる

REPってみましょう。

アプリ内で動いているREPLサーバへリモート接続するには、EclipseとCounterclockwiseを使います(Emacsでもいけるようです)。

Eclipseのメニューから[Window]→[Connect to REPL]を実行しましょう。

con-repl

REPLサーバのアドレスとポート番号は、デフォのままでOKです。

repl-port

無事つながれば、REPLビューが開きます。

repl

アプリを再起動するとREPL接続も切れてしまいますが、そんなときは、このビューのツールバーにある"Reconnect"ボタンを押せば再接続されます。

では、いよいよ、REPL経由でアプリを制御してみましょう。まずは、アプリの名前空間に移るため、以下のコードをREPLに打ち込みます。

(in-ns 'jp.dip.gpsoft.clonya.main)

ログでも出してみましょうか。

(use 'neko.log)
(d "foo")
(d "bar" :tag "clonya")

2番目の例に指定した :tag はオプションです。ログを見るには、adbか、Android Debug Monitorか、EclipseのDDMSを使えば良いでしょう。

ところで、REPLでコードを実行するには、2つの方法があります。

  • コードを直接REPLに打ち込む
  • Eclipseのエディタで、評価したい式にカーソルを置いて、Ctrl+Enter

後者の方法を使えば、ソースを修正して、簡単にその結果を試すことができます。また、よく使うコマンドをファイルに書いてエディタで開いておけば、REPLのヒストリ機能を使うより便利かもしれませんね。

試しに、main.cljを修正してみましょう。

main.cljの修正
(ns jp.dip.gpsoft.clonya.main
  (:use
    [neko.activity :only [defactivity set-content-view!]]
    [neko.threading :only [on-ui]]
    [neko.ui :only [make-ui]]
    [neko.log :as log]                      ; ←
    ))

(defactivity jp.dip.gpsoft.clonya.MainActivity
                                            ; :def aを削除。
  :on-create
  (fn [this bundle]
    (log/d "on-create" :tag "clonya")       ; ←
    (on-ui
      (set-content-view! this               ; ←
        (make-ui [:linear-layout {}
                  [:text-view {:text "Hello from Clojure!"}]])))))

変更点は、以下の2つです。

  • :on-create時にlogを出す
  • defactivityから:defを削除(aの代わりにthisを使う)

":def a"を削除したのは、このままではリリースモードでのビルド時にエラーになってしまうためです。:defはデバッグモード専用の機能で、例えば :def a としておけば、当該activityオブジェクトをグローバル変数aで参照できるようになります。便利ではありますが、activityオブジェクトがGCされなくなるので、リリースモードではdisableされているようです。

さて、修正したコードを試すためは、nsフォームとdefactivityフォームにカーソルを置いて、それぞれCtrl+Enterを押します。さらに、端末の向きを縦から横に変えてみると……。

log

期待通りのログが出ました。

端末の向きを変えたことにより、AndroidシステムがActivity#onCreate()をコールバックしてくれるので、上記コードの:on-createで指定した無名関数が呼ばれ、ログが出たわけです。ちなみに、エミュレータで画面を回転するには、Ctrl+F11かCtrl+F12です。ただし、左側のCtrlキーを使わないといけないとか、連続して回転するときは、一旦、Ctrlキーを離さないといけないとか、動きにクセがあるようです。

次は、MainActivityのレイアウトを変更してみます。

レイアウト変更
(ns jp.dip.gpsoft.clonya.main
  (:use
    [neko.activity :only [defactivity set-content-view!]]
    [neko.threading :only [on-ui]]
    [neko.ui :only [make-ui]]
    [neko.log :as log]
    [neko.resource :only [get-string]]
    ))

(defn main-layout []
  [:linear-layout {:orientation :vertical}
   [:text-view {:text :welcome :text-size [24 :sp]}]
   [:edit-text {:hint (get-string :hint-edit1) :layout-width :fill}]
   [:button {:text :caption-bt1} ]
   ])

(defactivity jp.dip.gpsoft.clonya.MainActivity
  :on-create
  (fn [this bundle]
    (log/d "on-create" :tag "clonya")
    (on-ui
      (set-content-view! this
        (make-ui (main-layout))))))

make-uiへ渡すレイアウト情報を、main-layout関数で生成するようにしました。レイアウトも複雑化して、いくつか新しい文字列リソースが必要になっています。

strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

  <string name="app_name">くろニャァ</string>
  <string name="welcome">ClojureとNekoでAndroidアプリ開発ニャ</string>
  <string name="hint_edit1">おなまえ</string>
  <string name="caption_bt1">ファイヤー</string>

</resources>

キーワードとリソース名が微妙に違うことに注意して下さい。ハイフン(-)を好む文化と、アンダースコア(_)を好む文化の違いはnekoが吸収してくれています。また、:hintがsetHint()になったり、:fillがFILL_PARENTになったりと、nekoの舞台裏で様々な変換処理が行われます。

残念ながら、xmlの修正をREPL経由でアプリに反映することはできないので、ここでは、lein droid doallする必要があります(再コンパイル、再インストール、再起動)。

main-layout

あとは、main-layout関数の中身を修正しては、Ctrl+EnterでREPLに打ち込むのを繰り返し、レイアウトを微調整していくことができます。毎回、端末の向きを変えるのが面倒なら、以下のコードを実行するだけでもOKです。

(on-ui (set-content-view! mainActivity (make-ui (main-layout))))

mainActivityというシンボルが使えるのは、defactivityで :def を指定しなかった場合、デフォルトでは :def mainActivity を指定したことになるためです(ただしデバッグモード時のみ)。activity名の先頭を小文字にするだけです。

微調整後
(ns jp.dip.gpsoft.clonya.main
  (:use
    [neko.activity :only [defactivity set-content-view!]]
    [neko.threading :only [on-ui]]
    [neko.ui :only [make-ui]]
    [neko.log :as log]
    [neko.resource :only [get-string]]
    )
  (:import
    [android.view Gravity]))

(defn main-layout []
  [:linear-layout {:orientation :vertical}
   [:text-view {:text :welcome
                :text-size [24 :sp]
                :layout-width :fill
                :layout-margin-top [30 :px]
                :layout-margin-bottom [30 :px]
                :gravity Gravity/CENTER_HORIZONTAL}]
   [:edit-text {:hint (get-string :hint-edit1) :layout-width :fill}]
   [:button {:text :caption-bt1} ]
   ])

;(on-ui (set-content-view! mainActivity (make-ui (main-layout))))

(defactivity jp.dip.gpsoft.clonya.MainActivity
  :on-create
  (fn [this bundle]
    (log/d "on-create" :tag "clonya")
    (on-ui
      (set-content-view! this
        (make-ui (main-layout))))))

TextViewのマージンやアライメントを調整してみました。

main-layout2

ひとまず、最初の一歩はこんなとこです。

最後に、デバッグ時に役立つ機能について簡単に説明しておきます。

デバッグ

neko.debug

REPL経由で試行錯誤していると、ときどきアプリが落ちてしまうことがあります。そうなると、アプリを再起動させたり、Ctrl+Enterをやり直したりと、面倒な作業が必要になります。そういうわけで、なるべくアプリを落としたくないのですが、失敗を恐れずに試行錯誤できるのがREPL経由のコーディングの利点でもあります。

こんなジレンマを解決してくれるのが、neko.debugのsafe-for-uiマクロです。

落ちそうなコードをsafe-for-uiマクロで囲んでおけば、すべての例外をキャッチして、toastに表示してくれます。

(safe-for-ui (/ 1 0))

起きた例外の内容は、(ui-e) で見ることができますし、スタックトレースはログにも出力されます。

neko.log

neko.log名前空間は、android.util.Logクラスのd()やi()などに相当する関数を提供します。

  • 関数名は、d、e、i、v、w
  • 引数に指定された値をstr関数で文字列化して、それぞれのログレベルで出す
  • :tagオプションでタグを指定可能
  • 例外の内容をログ出力したいときは、:exオプション
(d "The map is" {:a "ok" :b 123} "and the list is" '(:true :false))
(i "タグ付きで" :tag "情報")
(try
  (conj)
  (catch Exception ex
    (e "例外発生:" :exception ex)))

ログは、こんな感じになります。:tagを指定しなかった場合は、名前空間をタグとして使うようですね。

D/user(5576): The map is {:a "ok", :b 123} and the list is (:true :false)
I/情報(5576): タグ付きで
E/user(5576): 例外発生:
E/user(5576): clojure.lang.ArityException: Wrong number of args (0) passed to: core$conj
              ... 以下、スタックトレース ...

特定のログレベルについてログ出力を抑止したいときは、project.clj(またはLeiningenのコンフィグファイル)に、ログレベルを指定します。

project.clj
 :android {
      :ignore-log-priority [:debug :verbose]
  }

指定できる値は、:verbose, :debug, :info, :warning, :errorです。

パフォーマンスチューニング

Clojureは動的型付け言語ですが、メタデータを使ってデータの型を指定することができます。型指定はコンパイラへのヒントとなり、生成されるバイトコードの改善が期待できます。たとえばClojureからJavaのメソッドを呼ぶとき、普通なら実行時にメソッド名と引数の数をキーにしてリフレクションによりメソッドを特定する必要がありますが、型を指定していればコンパイル時にメソッドが特定できるかもしれません。よって、高速化やメモリ節約につながります。

私の経験では、ListViewをpopulateするときにGCが多発するせいで、リストのスクロールが引っかかることがありました。原因は、(.setText view caption) のような何気ないコードで、これを、(.setText ^TextView view ^String caption) と変えればスッキリ解決しました。

doallしたときのwarningメッセージに注意しておけば、どこでリフレクションを使っているか分かります。

Last modified:2014/01/28 16:53:56
Keyword(s):
References:[Clojure meets Android]
This page is frozen.