くろニャァ ~ BundleとSharedPreferences

前置き

ClojureでAndroidアプリを開発するシリーズ。今回のテーマは、Androidでキー&値ペア形式のデータを扱うときに使う、BundleとSharedPreferencesです。

Bundle

Clojureのキー&値ペアと言えばマップなので、BundleやSharedPreferencesがマップとして使えたら便利ですね。neko.data/like-map関数を使うと、Bundle、Intent、SharedPreferencesオブジェクトからマップ的なオブジェクトを作ることができます。

ただし、あくまでもマップ「的」であって、マップその物ではありません。厳密に言えば、clojure.lang.Associativeです。getやcontains?やDestructuringは使えますが、keysやvalsには使えません。また、関数として使うこともできません。

(let [i (Intent.)]
  (.putExtra i "a" 100)
  (.putExtra i "b" "hello")
  (let [m (like-map i)]
    (println (get m "a"))        ;=> 100
    (println (contains? m "c"))  ;=> false
    (let [{:strs [a b]} m]
      [a b])))                   ;=> [100 "hello"]

Intentオブジェクトをlike-mapするのは、Intent#getExtras()が返すBundleオブジェクトをlike-mapするのと同じことです。

Clojureのマップはimmutableなので、Bundleを変更するにはJavaのメソッドを使う必要があります。Intent#putExtra()やBundle#putString()などですね。

SharedPreferences

SharedPreferencesに関するオペレータも、neko.data名前空間に定義されています。

まず、SharedPreferencesオブジェクトを得るには、neko.data/get-shared-preferences関数を使います。引数はファイル名とモードです。

(get-shared-preferences "my_preferences" :private)

モードには、:private、:world-readable、:world-writeable、あるいはContext/MODE_XXX定数を指定します。ただし、MODE_WORLD_READABLEとMODE_WORLD_WRITEABLEはAPI17でdeprecateされたので、実質的には、MODE_PRIVATEの一択でしょう。もし複数プロセス間で共有したいなら、MODE_MULTI_PROCESSをORした方がいいみたいです。

読み書き(型限定)

nekoが標準でサポートするデータの型は、boolean、float、double、int、long、Stringの6つです。

これらをSharedPreferencesへ書き込むには、neko.data/assoc! 関数を使います。clojure.coreにも同名の関数があるので注意して下さい。またassoc! の引数は、SharedPreferencesではなく、SharedPreferences.Editorオブジェクトです。

(require '[neko.data :as data])

(let [prefs (get-shared-preferences "my_preferences" :private)
      editor (.edit prefs)]
  (.commit (data/assoc! editor "a" 100))
  (get (like-map prefs) "a"))              ;=> 100

SharedPreferencesはxmlファイルに出力されます。

my_preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<long name="a" value="100" />
</map>

読み書き(自由型)

nekoが標準でサポートしてない型のデータでも、文字列にシリアライズすることで読み書きできるようになります。そのための関数が、neko.data/assoc-arbitrary! とneko.data/get-arbitraryです。内部では、それぞれpr-strとread-stringを使ってシリアライズ/デシリアライズしています。

(require '[neko.data :as data])

(let [prefs (get-shared-preferences "my_preferences" :private)
      editor (.edit prefs)]
  (.commit (data/assoc-arbitrary! editor "c" {:a 100 :b "hello"}))
  (data/get-arbitrary (like-map prefs) "c"))   ;=> {:a 100, :b "hello"}
my_preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="c">{:a 100, :b &quot;hello&quot;}</string>
<long name="a" value="100" />
</map>

くろニャア

くろニャアにSharedPreferencesを組み込んでみます。

MainActivityからActivity#startActivityForResult()でTaActivityを開き、好きな食べ物の名前を入力してOKすると、SharedPreferencesに保存してからMainActivityへ戻るようにしましょう。

まずActivityを開始する関数を改良します。

ui_helper.clj
(ns jp.dip.gpsoft.clonya.ui-helper
  (:require
    [neko.context :as context]
    [neko.log :as log])
  (:import
    [android.content Intent]
    [android.app Activity PendingIntent Notification Notification$Builder]
    ))

(defn start-activity
  [^Activity from-act ^String act-name & {ct :clear-top req-code :req-code}]
  (let [package (.getPackageName (.getApplicationContext from-act))
        i (Intent.)]
    (doto i
;      (.addFlags Intent/FLAG_ACTIVITY_NEW_TASK)
      (.setClassName package (str package "." act-name)))
    (when ct (.setFlags i Intent/FLAG_ACTIVITY_CLEAR_TOP))
    (if req-code
      (.startActivityForResult from-act i req-code)
      (.startActivity from-act i))))

start-activity関数に:req-codeオプションが指定されたときは、startActivityForResultを使うようにしました。また、IntentにFLAG_ACTIVITY_NEW_TASKフラグは付けません。これを付けると遷移先Activityが別タスクになるので、startActivityForResultが期待通りに動きません(そもそも、このフラグを立てていた理由が今となっては思い出せない…)。

MainActivity(main.clj)
(def ^:private req-code-fav-food 100)
(def ^:private prefs-name "clonya_preferences")

(defn- make-transit-button
  [act caption act-name & [ct req]]
  [:button {:text caption
            :on-click (fn [v] (start-activity act act-name :clear-top ct :req-code req))}])

(defn- main-layout [act]
  ...
    (make-transit-button act :caption-ta-bt "TaActivity" false req-code-fav-food)
  ...
  )

(defn main-onActivityResult [this req res intent]
  (logd "main::on-activity-result" this req res intent)
  (if (= req req-code-fav-food)
    (if (= res Activity/RESULT_OK)
      (let [prefs (get-shared-preferences prefs-name :private)]
        (toast (str "ターちゃんは"
                 ((data/get-arbitrary (like-map prefs) "fav-foods") :ta)
                 "が大好きニャ"))))
    (.superOnActivityResult this req res intent)))

遷移先Activityが終了すると、MainActivity#onActivityResult()がコールバックされるので、そこでSharedPreferenceを読んでトーストを表示するようにしました。

TaActivity(main.clj)
(defn- on-ta-ok-fn [act v]
  (let [ll (.getParent (.getParent v)) ; linear-layout
        et (::fav-food (.getTag ll))   ; edit-text
        prefs (get-shared-preferences prefs-name :private)
        editor (.edit prefs)]
    (->> et
      .getText
      str
      (assoc (data/get-arbitrary (like-map prefs) "fav-foods") :ta)
      (data/assoc-arbitrary! editor "fav-foods"))
    (.commit editor)
    (.setResult act Activity/RESULT_OK))
  (.finish act))

(defn- on-ta-back-fn [act v]
  (.setResult act Activity/RESULT_CANCELED)
  (.finish act))

(defn- ta-layout [act]
  [:linear-layout {:orientation :vertical
                   :id-holder true}
   (make-header-label :ta-name)
    [:edit-text {:hint (get-string :hint-fav-food)
                 :layout-width :fill
                 :id ::fav-food}]
    [:linear-layout {:orientation :horizontal}
     [:button {:text :caption-ok-bt
               :on-click #(on-ta-ok-fn act %)}]
     [:button {:text :caption-back-bt
               :on-click #(on-ta-back-fn act %)}]]
   ])

prefs

clonya_preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="fav-foods">{:ta &quot;クリスピーキッス&quot;}</string>
</map>
Last modified:2014/05/23 18:21:52
Keyword(s):
References:[Clojure meets Android]
This page is frozen.