Umi Uyuraのブログ

プログラミング関連の作業ログ

Alloy Modelの学習

実は、これまではAlloyといってもView/Controllerしか使っておらず、データベースを使う場合も Titanium.Database を使ってしまっていたので、ちゃんとModelを活用したことがありませんでした。

というわけで、今更ながらにAlloy Modelを触り始めてみました。

今回作るもの

Alloyの基本的な動きを把握するため、SQLiteデータベースを使う、簡単なメモツールのようなものを作りました。

Android

f:id:umi-uyura:20160217235441p:plain f:id:umi-uyura:20160217235451p:plain

iOS

f:id:umi-uyura:20160217235502p:plain f:id:umi-uyura:20160217235511p:plain

ソースは umi-uyura/AlloyModelStudy です。

仕様的なもの

  • シンプルに入力欄とリストのみ
  • 入力欄に入力したテキストを、順番にリストに登録していくだけ
  • リストの項目をタップすると、操作メニューを表示
  • 操作メニューからは、お気に入り登録(するとメモの頭に * をつける)もしくはメモの削除ができる
  • アプリを一旦終了して再度起動しても、登録したデータが表示されている

という感じで、Alloy Modelを使ったCRUDとListViewへのData Bindingをざっと触れる想定です。

実装

プロジェクトの作成

CLIベースで開発しているため、ターミナルからiPhoneAndroidを対象にしたAlloyプロジェクトを作成。

$ titanium create --type app --id com.example.titanium.alloymodelstudy --name AlloyModelStudy --platforms iphone,android --url http://www.example.com --workspace-dir .
$ alloy new AlloyModelStudy

Modelを作成

メモを保存するためのModelを作成します。

テーブルの構造は超シンプルに以下のような感じとしました。

内容 カラム 主キー
レコードの識別子 id INTEGER AUTOINCREMENT
メモの内容 contents TEXT - -

これをもとに alloy generate model でModelのコードを生成します。モデル名につづいて sql を指定することで、SQLite向けの構成になります。

$ alloy generate model memo sql 'id:integer primary key autoincrement' contents:text

プロジェクト内の app/models/memo.js に生成されました。

生成されたコードに1点手を加える必要があり、テーブル内のレコードを識別するためのカラムを設定するために、 config.adapter.idAttribute を追加して id を指定しました。

できあがったモデルのコードは以下のような感じ。

app/models/memo.js

exports.definition = {
    config: {
        columns: {
            "id": "integer primary key autoincrement",
            "contents": "text"
        },
        adapter: {
            type: "sql",
            collection_name: "memo",
            idAttribute: "id"
        }
    },
    extendModel: function(Model) {
        _.extend(Model.prototype, {
            // extended functions and properties go here
        });

        return Model;
    },
    extendCollection: function(Collection) {
        _.extend(Collection.prototype, {
            // extended functions and properties go here

            // For Backbone v1.1.2, uncomment the following to override the
            // fetch method to account for a breaking change in Backbone.
            /*
           fetch: function(options) {
               options = options ? _.clone(options) : {};
               options.reset = true;
               return Backbone.Collection.prototype.fetch.call(this, options);
           }
           */
        });

        return Collection;
    }
};

UIを実装

私のふだんのAlloy開発スタイルどおり、UIはJade&Stylusで作っています。もしプロジェクトを動かす方がいたら、JadeとStylusをグローバルにインストールしておいてください。

umi-uyura.hatenablog.com

また、iPhoneAndroid両方でナビゲーションバー/アクションバーを表示するために、Fokke Zandbergen氏が作っている xp.ui を使っています。これは、本来iOSでしか使えないNavigationWindowのダミーを用意することで、Androidとコードを共通化できる便利なライブラリです。

そんな感じで、スタイルの方はソースを見ていただくとして、Viewとなる index.jade は以下のようにしました。

app/view/index.jade

Alloy
  Collection(src="memo")/
  NavigationWindow(module="xp.ui")
    Window.container(title="Alloy Model Study", onOpen="doOpen")
      View.controller
        TextField#memoText
        Button#addButton(title="Add", onClick="addText")
      ListView#memoList(onItemclick="clickItem")
        ListSection(dataCollection="memo")
          ListItem(title="{contents}", itemId="{id}")/

まずは、このViewで利用するCollectionを Alloy タグの直下に指定。

Alloy
  Collection(src="memo")/

さらに、後述するData Bindingのために、ListSectionに利用するCollectionの指定を、またListItemに表示する memo Modelのフィールドを指定しています。

ListSection(dataCollection="memo")
  ListItem(title="{contents}", itemId="{id}")/

データベース操作の実装

app/controllers/index.js に、Data Bindingを利用したデータベース操作を実装しました。

レコードの読み込み、ListViewへの表示

アプリ起動時に、データベースからレコードを読み込みます。

これは Collection.fetch() で読み込むことができるので、Windowの open イベント内で呼び出しておきます。これによって、画面が開かれたタイミングで、データベースにレコードがあれば、それが読み込まれることになります。

var memos = Alloy.Collections.memo;

function doOpen() {
  memos.fetch();
}

さらに、View内でListSectionに memo Collectionを紐付けている( dataCollection)ので、読み込まれたレコード分、そのListSectionのListItemとして生成されます。

ListItemには title として memo モデルの contents フィールドを指定しているので、そこに登録されているレコードがListItemに表示されることになります。

レコードの追加

TextFieldに文字を入力して Add ボタンを押すと、新しいレコードを登録します。

その処理は以下のように実装。

function addText() {
  var txt = $.memoText.getValue();

  var m = Alloy.createModel('memo', {
    contents: txt
  });

  memos.add(m);
  m.save();
  memos.fetch();

  $.memoText.setValue('');
}

新しいレコードを作成するには、 Alloy.createModel で新しいModelオブジェクトを生成。

そのModelオブジェクトをCollectionにを追加して Model.save() を呼び出すことで、データベースにレコードが保存されます。

その後、再び Collection.fetch() を呼び出すと、追加したレコードに対応するListItemがリストに追加されます。

レコードを編集・更新

編集画面を別途作ろうかと思ったのですが、少々面倒なので中止。かわりに、お気に入りという扱いとして、登録した文字列の先頭に * を付与するというだけの仕様にしています。

ListViewの click イベントでOptionDialogを表示して、お気に入り登録か削除かをするようにしました。

どのListItemに対する操作なのかを判別するための情報は、イベントの引数から取得できます。

function clickItem(e) {
  var secIdx = e.sectionIndex;
  var itemIdx = e.itemIndex;
  var itemId = e.itemId;
  var text = $.memoList.sections[e.sectionIndex].items[e.itemIndex].properties.title;
   
  if (OS_ANDROID) {
    itemId = parseInt(itemId);
  }
  ...

ここでひとつハマったのが、レコードのキーとなる itemId

memo modelの id を紐付けているのですが、元の id は数値型( integer )にしていたのに対して、 itemIdAPIドキュメントによると文字列型( string )でした。

この点、iOSではうまいこと型変換してくれているらしく問題はなかったのですが、Androidに関してはドキュメントどおり文字列型で取れるため、このあとの Collection.where で型不一致のせいかデータが取れないという問題がありました。

もしかしたら、数値型IDは itemId として使わないほうが良いのかもしれませんが、今回はAndroidの場合 parseInt することで対処しています。

その後、既存レコードを編集します。

var m = memos.where({id: itemId});
if (m.length) {
  var contents = '*' + m[0].get('contents');
  m[0].set({contents: contents});
  m[0].save();
}

Collection.where() は配列を返すので、 Array.length で存在を確認してから処理を実施。

変更内容を Model.set() して、 Model.save() を呼ぶことで、Data Bindingのおかげで、すぐに変更がリストにも反映されます。

ちなみに、Alloy Modelの仕組みのベースになっているのはBackbone.jsですが、Alloyに含まれているBackbone.jsは 0.9.2 と古いバージョンです。このBackbone.jsのバージョンを 1.1.2 にすると単独のModelを返す Collection.findWhere() が使えるようになるので、上記のようなわかりにくさが解消できそうです。

Backbone.jsのバージョンを変える方法は Alloy Backbone Migration を参照。

レコードを削除

レコードの削除処理は以下のように実装。

var dialogs = require('alloy/dialogs');
...
dialogs.confirm({
  title: 'Do you want to delete ?',
  message: '"' + text + '"',
  callback: function() {
    var m = memos.where({id: itemId});
    if (m.length) {
      m[0].destroy();
    }
  }
});

削除を確認するダイアログを出したのち、削除を実行します。

レコードの編集と同様の方法で既存レコードを取得し、 Model.destroy() で、呼出し後リストから該当項目が消えました。

まとめ

簡単なサンプルですが、最低限のAlloy Modelの使い方は理解できたかな、と思います。

今まで使う機会がなかったのですが、やはりData Bindingは便利ですね。

ここ を見ると、ListView(正確にはListSection)以外にもData BindingできるViewはありますが、やはり一番使いどころがあるのはListViewだと思いますし、さらにListItemのテンプレートの仕組みと組み合わせると、いろんな表現ができそう。

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition