Alloy Modelの学習・その4(REST API)
引き続き、Alloy Modelを勉強中。
クロスプラットフォームなUIを素早く構築しつつWeb APIと連携するというのは、Titaniumが活用できるパターンだと思います。
というわけで、今回はREST APIとのData Bindingを試してみました。
REST API Sync Adapter
標準で用意されているSync Adapterは、SQLite用のSql AdapterとTitanium.App.Propertiesを使うProperties Adapterの2種類しかありませんが、Sync Adapterは自作することもできます。
Custom Sync Adapters - Alloy Sync Adapters and Migrations
REST API用のSync Adapterを作ってくれている人がいますので、これを採用することにします。
viezel/napp.alloy.adapter.restapi
今回作るもの
アプリ自体は簡単にするため、 その2 で作ったものと同じ仕様とします。
それとは別に、連携先のAPIが必要です。
適当なWeb APIを使っても良かったのですが、CRUDを一通り触りたかったのと、サーバー側に渡ってくるリクエストの内容も把握したかったので、今回はスタブAPIサーバーを立てることにしました。
スタブAPIサーバー
スタブAPIサーバーについては、シンプルで使いやすそうだったMockyを使ってみることにしました。
okv/mocky: http mocking server with simple config written on nodejs
テーブル
スタブなので実際のデータベースは用意しませんが、 その2 と同じ仕様ということで、以下のようなテーブル構造を扱うAPIを用意します。
内容 | カラム | 型 |
---|---|---|
レコードの識別子 | id | INTEGER |
メモの内容 | contents | TEXT |
メモの優先度 | priority | TEXT |
API
上記のテーブルをもとに、CRUDをひととおり呼び出せるよう、以下のようなAPIにすることにしました。
操作 | Method | URL |
---|---|---|
メモを読み込む | GET | http://127.0.0.1:4321/memos |
メモを追加 | POST | http://127.0.0.1:4321/memos |
メモの内容を更新 | PUT | http://127.0.0.1:4321/memos/{ID} |
メモを削除 | DELETE | http://127.0.0.1:4321/memos/{ID} |
スタブAPIPサーバー自体の実装は省略しますが、ダミーデータをメモリ上に保持しておいて、APIの呼び出しに合わせてダミーデータを操作しています。
例えばメモの読み込みAPIを叩くと、こんな感じにJSONのレスポンスを得られる感じです。
データベースを使っていないので、サーバーを再起動するとデータは初期状態に戻ります。
ソースは umi-uyura/AlloyModelStudyRestStub にありますので、こちらをご覧ください。
Qiitaにあった mocky を使ってちょっと賢いスタブAPIサーバを作る - Qiita や Node.js の mocky を使用し スタブREST API を作成してみる、その1 - Qiita あたりを読めば簡単に作れました。
アプリ
アプリ側のソースは umi-uyura/AlloyModelStudyRestApi です。
プロジェクト作成
毎度おなじみCLIでプロジェクト作成。
$ titanium create --type app --id com.example.titanium.alloymodelstudyrestapi --name AlloyModelStudyRestApi --platforms iphone,android --url http://www.example.com --workspace-dir . $ alloy new AlloyModelStudyRestApi
これをベースに、今回もUIはJade & Stylusに、FokkeZB/UTiL/xp.ui を使っています。
REST API Sync Adapter (napp.alloy.adapter.restapi) の準備
GitHubのリポジトリから restapi.js をダウンロードして、プロジェクト内の app/assets/alloy/sync/
へ配置しておきます。
モデル作成
先の想定テーブル構造から、ベースとなるモデルを生成します。このとき、 restapi
タイプというのは Alloy
コマンドがサポートしていないため、いったん sql
として生成しました。
$ alloy generate model memo sql 'id:integer' contents:text priority:text
生成されたモデルファイルをもとに、REST API用に編集します。
exports.definition = { config: { columns: { "id": "integer", "contents": "text", "priority": "text" }, adapter: { type: "restapi", collection_name: "memo", "idAttribute": "id" }, "URL": "http://127.0.0.1:4321/memos", "debug": 1 }, extendModel: function(Model) { _.extend(Model.prototype, { }); return Model; }, extendCollection: function(Collection) { _.extend(Collection.prototype, { comparator: function(data) { return (0 - data.get('priority').length); } }); return Collection; } };
編集した箇所としては、以下のとおりです。
config.adapter.type
をrestapi
に変更URL
を追加し、スタブAPIのURLを設定debug
を追加、これでAPIを使う際のリクエスト/レスポンスがデバッグ出力されてわかりやすい
他に headers
や parentNode
をといった設定もありますが、今回は未使用。
以下はデバッグ出力の例。
Data Binding
ビューの部分は 前回 と同じもので、コントローラもほぼ一緒で動きました。
一箇所修正したのは、メモを追加したあとに呼び出していた memos.fetch()
の部分で、通信が発生してタイミングが変わるせいか、これを呼び出してしまうと追加したはずのメモが消えてしまう現象が発生しました。
その上の行の memos.add(m)
までで、ListViewには新しいメモが追加されていたので、 memos.fetch()
は削除することにしました。
app/controllers/index.js
... function addText() { var txt = $.memoText.getValue(); var params = { contents: txt, priority: '' }; var m = Alloy.createModel('memo', params); m.save(); memos.add(m); //memos.fetch(); <== 削除した $.memoText.setValue(''); } ...
エラーハンドリングについて
今回やっていないこととして、通信のエラーハンドリングがあります。
データ構造を同じにしたこともありますが、SQL用からREST API用にAdapterを入れ替えて、ほぼそのまま動かせてしまいました。
しかし、ローカルにあるデータベースなどと比べて、インターネットを介した通信は不安定だったり、APIサーバー側で何らかの問題が発生することなども考えられます。
そのため、napp.alloy.adapter.restapiには、以下のような感じで通信成功時とエラー時で処理をハンドリングできるようになっています。
var collection = Alloy.createCollection("MyCollection"); //or model //the fetch method is an async call to the remote REST API. collection.fetch({ success : function(){ _.each(collection.models, function(element, index, list){ // We are looping through the returned models from the remote REST API // Implement your custom logic here }); }, error : function(){ Ti.API.error("hmm - this is not good!"); } });
napp.alloy.adapter.restapiを使ってきちんと実装する場合は、この点も考慮する必要があると思います。
参考
- viezel/napp.alloy.adapter.restapi
- AlloyのModelでREST APIからデータを取得する - Qiita
- Titanium + Alloy + napp.alloy.adapter.restapiで作る簡単Qiitaビューワーアプリ - Qiita
- Titanium + Alloy + napp.alloy.adapter.restapiで作る簡単Qiitaビューワーアプリ応用編(ページネーション機能の実装) - Qiita
モックサーバー
- okv/mocky: http mocking server with simple config written on nodejs
- mocky を使ってちょっと賢いスタブAPIサーバを作る - Qiita
- Node.js の mocky を使用し スタブREST API を作成してみる、その1 - Qiita
- 【図解】RESTful WebサービスにおけるHTTPステータスコード : アジャイル株式会社
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
Alloy Modelの学習・その3(Properties Adapter)
引き続き、Alloy Modelを深ぼってます。
標準で用意されているSync Adapterには、SQLite用のSQL Adapterの他に、Titanium.App.Propertiesを使うProperties Adapterがあります。
※以前はもう一つ、Mobile Web用にlocalStorage adapterというのがあったようですが、Alloy 1.5.0で非推奨になってしまったようです。
Alloy Model、特にData Bindingを使う場面としては、SQLiteとの組み合わせで使うことの方が多い気がしますが、アプリのちょっとした設定を保持しておくのにTitanium.App.Propertiesを使うこともあるので、せっかくなので試しておくことにしました。
今回作るもの
設定画面で使う用途を想定して、1画面にいくつか入力項目があるもの、ということで、単純に名前・性別・国籍を登録しておく画面という、あまり実用性のないものになりました。
すべてテキスト入力だとつまらないので、性別だけOptionDialogを使って選択式にしています。
ソースは umi-uyura/AlloyModelStudyProps にあがっているものです。
プロジェクト作成
例によってCLIでプロジェクト作成。
$ titanium create --type app --id com.example.titanium.alloymodelstudyprops --name AlloyModelStudyProps --platforms iphone,android --url http://www.example.com --workspace-dir . $ alloy new AlloyModelStudyProps
これをベースに、今回もUIはJade & Stylusに、FokkeZB/UTiL/xp.ui を使っています。
モデル作成
上記の画面通り、以下のようなテーブルにします。
内容 | カラム | 型 |
---|---|---|
ID | id | String |
名前 | name | String |
性別 | gender | String |
国籍 | nationality | String |
これをもとに alloy generate
コマンドを使ってモデルを生成。
$ alloy generate model settings properties id:string name:string gender:integer nationality:string
今回想定している設定画面の場合は複数レコードが発生することはないため、レコードを識別する id
は不要な気がしますが、Data Binding(というよりはBackbone?)の仕組み上、キーとなる属性が必要なようです。キーとなる属性がない場合、設定を更新しようとしたときに該当レコードが見つけられず、新しいレコードが作られてしまいます。
生成されたモデルを、最終的に以下のようにしました。
app/models/settings.js
exports.definition = { config: { columns: { "id": "string", "name": "string", "gender": "string", "nationality": "string" }, defaults: { "id": "settingsId", "name": "", "gender": "", "nationality": "" }, adapter: { type: "properties", collection_name: "settings", 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; } };
加えたのは config.defaults
で、特に id
には何か初期値を入れておかないと(つまりあらかじめキーを登録しておく)、先ほど述べたレコードが見つからない状態となってしまうようです。
画面とData Binding
今回もスタイルはソースを参照してもらうとして、Viewの index.jade は以下のようにしました。
app/views/index.jade
Alloy Model(src="settings")/ NavigationWindow(module="xp.ui") Window.container(title="Alloy Model Study: Properties", onOpen="doOpen") View.line Label.label Name: TextField#textName.textfield(value="{settings.name}") View.line Label.label Gender: TextField#textGender.textfield(value="{settings.gender}", onClick="doGenderClick") View.line Label.label Nationality: TextField#textNationality.textfield(value="{settings.nationality}") View.controll Button#saveButton(onClick="doSaveClick") Save
前回のData Bindingと少し異なるのは、設定情報は1レコードでCollectionは不要なため、 <Alloy>
タグ直下に記述するBindingする対象に関して、 <Model>
タグで指定してみました。
各TextFieldにData Bindingする項目を設定し、これをもとにコントローラ側も実装。
app/controllers/index.js
var settings = Alloy.Models.settings; function doOpen() { settings.fetch(); } function doSaveClick(e) { var name = $.textName.getValue(); var gender = $.textGender.getValue(); var nationality = $.textNationality.getValue(); settings.save({ name: name, gender: gender, nationality: nationality }); } ...
Viewの open
イベントで Model.fetch()
してデータを読み込み、画面に配置したSaveボタンのクリック時に Model.save()
してデータを保存しています。
Modelオブジェクト単独を扱っているため、Collectionから該当レコードを検索する処理がないこともあり、簡潔に書くことができました。
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
Alloy Modelの学習・その2(マイグレーション)
前回の学習内容をもとに、せっかくなのでマイグレーション機能も試してみることにしました。
今回のサンプルは前回と同じプロジェクトの step2
というブランチにあります。
umi-uyura/AlloyModelStudy at step2
マイグレーションを試す場合は、まず master
ブランチをビルドして動かしてから、 step2
ブランチをビルドして実行することで発動します。
マイグレーションする内容
マイグレーションとは、テーブルにカラムを追加したいといった場合に、データベースを作りなおすのではなく、データを保持したまま変更を加える方法。
というわけで、前回のプロジェクトで作成したテーブルを加工してみることにしました。
ただ、新しい項目を加えるとUIも変更しなくてはならなくなり少々面倒なので、既存データの扱いを変更することで試すことに。
前回時のデータベースの構造は以下のとおり。
内容 | カラム | 型 | 主キー | 他 |
---|---|---|---|---|
レコードの識別子 | id | INTEGER | ◯ | AUTOINCREMENT |
メモの内容 | contents | TEXT | - | - |
変更内容としては、お気に入りということで文字列の先頭につけていた *
を分離する、というものにしました。また *
の登録
できる数には制限をかけていなかったこともあり、その数が多いほど優先度が高い扱いにすることにしました。
そこで、マイグレーション後のテーブル構造は以下のように変更することにしました。
内容 | カラム | 型 | 主キー | 他 |
---|---|---|---|---|
レコードの識別子 | id | INTEGER | ◯ | AUTOINCREMENT |
メモの内容 | contents | TEXT | - | - |
メモの優先度 | priority | TEXT | - | - |
よってマイグレーションで実施する処理としては、 contents
から先頭の *
部分を切り出して、 priority
へセットする、というものになります。
マイグレーション処理の実装
マイグレーションファイルの生成
マイグレーションの内容はJavaScriptで実装できます。
そのファイル名は、モデル名をベースに、マイグレーションの世代管理のための日時情報を YYYYMMDDHHmmss_
として接頭部に付与したファイルを用意します。
この雛形は、 alloy generate
コマンドで生成することもできます。
$ alloy generate migration memo [INFO] Generated migration named 201602180643382_memo
上記のコマンドを実行した例としては、 app/migrations
フォルダ内に 201602180643382_memo.js が生成されます。
マイグレーションファイルの構造
生成されたマイグレーションファイルは、以下の2つの関数によって構成されています。
migration.up = function(migrator) { }; migration.down = function(migrator) { };
どちらも migrator
というオブジェクトを引数に取っていますが、この中にデータベースオブジェクトの参照や、テーブル名などの情報、そしてデータベース操作用の関数を持っています。この migrator
オブジェクトを使って、マイグレーション処理を実装します。
migrator
が持っている機能は以下のとおり。
キー | 説明 |
---|---|
db | Ti.Databaseインスタンス ※このインスタンスを閉じたり、別のDBを開いたりするのはNG! |
dbname | データベース名 |
table | テーブル名。 config.adapter.collection_name と同じもの。 |
idAttribute | データベースのプライマリーキーとなる属性名 |
createTable | データベースにテーブルを作成する。Alloy Modelの columns と同じ構造でテーブル定義を渡す。 |
dropTable | データベースからテーブルを削除する |
insertRow | テーブルにレコードを追加する |
deleteRow | テーブルからレコードを削除する |
migration.up()
は前回のバージョンからデータベースをアップグレードする処理を、 migration.down()
は変更点をロールバックして前回のバージョンに戻す処理を実装します。
ちなみに、そもそもですが、マイグレーション機能はsql sync adapterでのみ利用することができる機能のようです。
例)新しいテーブルを追加するマイグレーション
今回は既存のテーブルに変更を加えるパターンですが、新しいテーブルを追加する場合の例です。
この場合は、 migrator.createTable()
と migrator.dropTable()
を使ってマイグレーションおよびロールバック処理を書くようです。
Appceleratorドキュメントの例によると、以下のような感じです。
var preload_data = [ {title: 'To Kill a Mockingbird', author:'Harper Lee'}, {title: 'The Catcher in the Rye', author:'J. D. Salinger'}, {title: 'Of Mice and Men', author:'John Steinbeck'}, {title: 'Lord of the Flies', author:'William Golding'}, {title: 'The Great Gatsby', author:'F. Scott Fitzgerald'}, {title: 'Animal Farm', author:'George Orwell'} ]; migration.up = function(migrator) { migrator.createTable({ "columns": { "book": "TEXT", "author": "TEXT" } }); for (var i = 0; i < preload_data.length; i++) { migrator.insertRow(preload_data[i]); } }; migration.down = function(migrator) { migrator.dropTable(); };
migrator.createTable()
に、モデルの columns
と同じようにテーブル定義情報を渡すことで新しいテーブルが作成されるので、必要に応じてさらに新しいデータを migrator.insertRow()
で突っ込んでいます。
ロールバックする場合は、そのテーブル自体を migrator.dropTable()
で削除してしまえば、もとに戻せるということのようです。
カラムを追加するマイグレーション
今回のようなカラムを追加するようなマイグレーションの場合は、直接SQLを発行して処理します。
今回の場合、
- 優先度カラムを追加
- 既存データから優先度を判定してデータを更新
という処理が必要になるので、以下のような感じに実装してみました。
app/migrations/201602180643382_memo.js
migration.up = function(migrator) { var db = migrator.db; var table = migrator.table; migrator.createTable({ columns: { 'id': 'integer primary key autoincrement', 'contents': 'text' } }); db.execute('ALTER TABLE ' + table + ' ADD COLUMN priority STRING DEFAULT ""'); var rows = db.execute('SELECT * FROM ' + table + ';'); while (rows.isValidRow()) { var contents = rows.fieldByName('contents'); var priorityPos = contents.lastIndexOf('*'); if (0 > priorityPos) { rows.next(); continue; } var priorityString = contents.substring(0, priorityPos + 1); var contentsBody = contents.substring(priorityPos + 1); var updateQuery = 'UPDATE ' + table + ' SET priority = "' + priorityString + '", contents = "' + contentsBody + '" WHERE id = ' + rows.fieldByName('id') + ';'; db.execute(updateQuery); rows.next(); } };
SQLiteは DROP COLUMN
が使えないので、直接テーブルからカラムを削除することができません。そこで、カラムが減った新しいテーブルを作成して、そこにデータを入れなおす必要があるようです。
最初に migrator.createTable()
をしていますが、これはこのマイグレーションを実施するバージョンのアプリを新規でインストールする場合、まだテーブルが作成されていない可能性があるからです。 migrator.createTable()
は内部で IF NOT EXISTS
を使っているので、すでにテーブルがある状態で呼び出しても問題ありません。
その後、 ALTER TABLE
で priority
カラムを追加してから、 contents
の先頭にある *
を取り出して priority
へ格納するという処理を、すべての行に対して実施しています。
今回は関係ありませんでしたが、マイグレーション時の注意事項として、もしモデルに idAttribute
を指定していない場合、 alloy_id
というカラムを自動的に追加しているらしので、これを含めるのを忘れないようにする必要があるようです。
contents
から *
を取り除いたので、画面側にも priority
を表示するため、ListItemの表示内容にも priority
を追加しました。
app/views/index.jade
ListItem(title="{priority} {contents}", itemId="{id}")/
ロールバックする( priority
カラムがない状態に戻す)
さて、マイグレーションが想定通り動くようになったので、次に migration.down()
にロールバック処理を実装してみることに。
これは、データベースをマイグレーション前に戻す処理になります。
そこで、テンポラリのテーブルを作って、既存のデータを避難させ、改めてマイグレーション前の構造のテーブルを作成し、そこにデータを加工しつつ投入する、という感じで実装してみました。
migration.down = function(migrator) { var db = migrator.db; var table = migrator.table; var backup_table = table + '_backup'; db.execute('CREATE TEMPORARY TABLE ' + backup_table + '(id,contents,priority);'); db.execute('INSERT INTO ' + backup_table + ' SELECT id,contents,priority FROM ' + table + ';'); migrator.dropTable(); migrator.createTable({ columns: { 'id': 'integer primary key autoincrement', 'contents': 'text' } }); var rows = db.execute('SELECT * FROM ' + backup_table + ';'); while (rows.isValidRow()) { var id = rows.fieldByName('id'); var contents = rows.fieldByName('contents'); var priority = rows.fieldByName('priority'); var updateQuery = 'INSERT INTO ' + table + ' (id,contents) VALUES (' + id + ',"' + priority + contents + '");'; db.execute(updateQuery); rows.next(); } db.execute('DROP TABLE ' + backup_table + ';'); };
で、ここまで実装して、ふと気付いたのですが、
・・・ロールバックってどうやって確認するんだろ?
Alloy Modelのマイグレーション処理で、migration.down()ってどういうときに呼ばれるものなのでしょうか? #titaniumjp
— Umi Uyura (@umi_uyura) February 19, 2016
声(ツイート)に出てた。
Appceleratorのドキュメントにもそのあたり発見できず、いろいろ探していたところ、最近Q&AフォーラムがAppcelerator公式サイトから移行したStack Overflowの方で、それらしい回答を発見しました。
appcelerator - Lifecycle of Alloy Migrations - Stack Overflow
それによると、 migration.down()
は何らかの方法で古いバージョンのアプリへアップグレード?したときに発動するもよう。
ただし、App Storeで公開しているものでは発動しないみたいで、これはアプリをダウングレードする手段が提供されていないから、ということかもしれません。
よって使える場面としては、テスト中やAd Hoc/Enterprise配布のときということになりそうです。
確かに本番リリースしたアプリでロールバックさせるような状況って思いつかない(必要なら再度アプリをアップデートする)ので、あまり使う機会ないのかも。
ついでに表示順序も改良
どうせ優先度をつけるなら、ListViewへの表示順序も優先度が高いとしているものから並ばせたい、と思ったので、その部分も合わせて変更することにしました。
Backbone.Collectionは、基本的にモデルを追加した順序が基準になるようです。
そこで、 priority
の数が多い順が基準となるように、 Collection.comparator
を実装しました。
app/models/memo.js
exports.definition = { ... extendCollection: function(Collection) { comparator: function(data) { return (0 - data.get('priority').length); } }); return Collection; } };
これで、 model.fetch()
してきたときに priority
順で表示されるようになりました。
もう1点、お気に入りの登録をして priority
が変化した時には、 Collection.sort()
を呼び出して再度並び替えを実施することで、更新後のお気に入り数に応じた順序で表示されるようになりました。
app/controllers/index.js
function clickItem(e) { var optionDialog = Ti.UI.createOptionDialog(opt); optionDialog.addEventListener('click', function(e) { switch (e.index) { case 0: var m = memos.where({id: itemId}); if (m.length) { var priority = '*' + m[0].get('priority'); m[0].set({priority: priority}); m[0].save(); memos.sort(); // <= 追加 } break; } }); optionDialog.show(); }
Data Bindingのおかげで、UI部分を操作するコードは書くことなく、Collectionの順序が画面に反映されました。
見た目的には前回と変わっていないのですが、お気に入り( *
)の数が多いものが、より上位に表示されるようになりました。また、お気に入りの数を増やした場合、より上位に移動します。
感想
今のところマイグレーションを使うような予定はないのですが、そういうのもあるのか、ということで。
参考
- Alloy Sync Adapters and Migrations
- appcelerator - Lifecycle of Alloy Migrations - Stack Overflow
- TitaniumでMigrationを行う - Qiita
- Alloyでのmigrationについて調べてみた - Give it a shot
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
Alloy Modelの学習
実は、これまではAlloyといってもView/Controllerしか使っておらず、データベースを使う場合も Titanium.Database
を使ってしまっていたので、ちゃんとModelを活用したことがありませんでした。
というわけで、今更ながらにAlloy Modelを触り始めてみました。
今回作るもの
Alloyの基本的な動きを把握するため、SQLiteデータベースを使う、簡単なメモツールのようなものを作りました。
ソースは umi-uyura/AlloyModelStudy です。
仕様的なもの
- シンプルに入力欄とリストのみ
- 入力欄に入力したテキストを、順番にリストに登録していくだけ
- リストの項目をタップすると、操作メニューを表示
- 操作メニューからは、お気に入り登録(するとメモの頭に
*
をつける)もしくはメモの削除ができる - アプリを一旦終了して再度起動しても、登録したデータが表示されている
という感じで、Alloy Modelを使ったCRUDとListViewへのData Bindingをざっと触れる想定です。
実装
プロジェクトの作成
CLIベースで開発しているため、ターミナルからiPhoneとAndroidを対象にした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をグローバルにインストールしておいてください。
また、iPhoneとAndroid両方でナビゲーションバー/アクションバーを表示するために、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
)にしていたのに対して、 itemId
はAPIドキュメントによると文字列型( 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
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
Titanium.UI.ListViewのイベントまわりを調べてみた
ふと試してみたところ、Titanium.UI.ListViewのドキュメントに掲載されていない swipe
イベントが取れたので、もしかして他の使えるのかも?と思って調べてみました。
ドキュメントに掲載されているクリックイベント以外だと、スワイプと長押しあたりが使えると広がるな―などと思って始めたのですが、、、まあ結論を書くと、swipe
イベント自体は発行されるものの、個々のListItemのアクションとしては使えないものだったのでした。
ということを調べたメモです。
確認した環境
確認したこと
- ListSection/ListItemでイベントが取れるのか?
- どのイベントを受け取ることができるのか?
ListSection/ListItemでイベントが取れるのか?
そもそもListSectionやListItemでイベントは取れないのか?という点を念のため再確認。
それぞれに addEventListener
をしたところ not a function
になってしまったので、ここはやはりListViewでハンドリングするしかなさそう。
もう半分答えが出た感じ。
どのイベントを受け取ることができるのか?
公式ドキュメント上で特定のListItemを識別するようなイベント itemclick
のみですが、他のViewなどで持っているものでListItemへの操作をハンドリングできそうなイベントを見繕って調べてみました。
Alloyベースのプロジェクトを作って、思いついたイベントハンドラを片っ端から追加して、ポチポチさわってみた結果は以下のような感じ。
イベント | iOS | Android | 補足 |
---|---|---|---|
itemclick | ◯ | ◯ | - |
swipe | △ | △ | イベントは発生するが、 sectionIndex や itemIndex は取れない ※1 |
click | - | - | 発生しない |
singleclick | - | - | 発生しない |
dblclick | - | △ | Androidは、ListItemがない部分でのみ発生する |
longclick | - | - | 発生しない |
longpress | △ | △ | iOSは発生するが、 itemclick も発生する Androidは、ListItemがない部分でのみ発生する |
singletap | △ | △ | iOSは発生するが、 itemclick も発生する Androidは、ListItemがない部分でのみ発生する |
doubletap | - | - | 発生しない |
twofingertap | △ | △ | iOSは発生するが、 itemclick も発生する Androidは、ListItemがない部分でのみ発生する |
というわけで、やはりListViewで特定のListItemへの操作に対応するためのイベントは itemclick
だけ。要するに、ListViewのイベントということですね。
ちなみに、Androidで受け取れる itemclick
以外のイベントで sectionIndex
や itemIndex
が取れることがあったのですが、その操作以前にクリックしているListItemの情報が取れてしまっているだけで、その操作に対応した情報ではありませんでした(紛らわしい!)
まとめ
標準のListViewのみだと、凝った操作はできない印象。
他の方法としては、ItemTemplateにいろいろViewを設置して、そこでイベントハンドリングすれば取れそうな気もしますが、もともとやろうとしていることから外れているので、いったんここまで。
調べるのに作ったプロジェクトは これ 。
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
コマンドラインのチートシート管理ツール「Cheat」
先日、Twitterを眺めていて、tldrというコマンドを知りました。
これは便利、とさっそく導入したのですが、どうせなら自分で使うパターンを登録できるものがないものかと思って探してみたところ、Cheatというツールを発見しました。というか、 tldrのGitHubリポジトリのREADME で紹介されていました。
基本的にはtldrと同様、指定したコマンドのよくある使い方のようなものを表示してくれるものですが、それ以外にも表示内容を編集したり、新しいコマンドの使い方を追加することができる、というものです。
インストール
Homebrewで入れることができます。
$ brew install cheat
コマンド自体はPythonで作られているようなので、 pip
でもインストールできるようです。
使い方
チートシートの表示 cheat <command>
引数として、使い方を知りたいコマンドを渡すことで、その主だった使い方を表示します。
例えば curl
の使い方例を観たい場合は、以下のように実行します。
$ cheat curl # Download a single file curl http://path.to.the/file # Download a file and specify a new filename curl http://example.com/file.zip -o new_file.zip ...
チートシートの編集 cheat -e <command>
エディタが起動して、表示内容を編集することができるようになります。
環境変数 EDITOR
を参照しているようです。
自分はよく curl
でリダイレクトも辿ってダウンロードをするオプションを忘れるので、 curl
のチートシートを編集して、以下の例を追記しておきました。
# Follow the redirect curl -L http://example.com/file
パーミッションに注意
私の環境では cheat -e
したときに、読み取り専用状態で開かれました。
Homebrewで導入すると、オリジナルのチートシートファイルは /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/
といったパスにコマンド単位で格納されているようなのですが、そのオリジナルを保護するためか、各ファイルは読み取り専用になっていましたので、そのためかと思います。
この場合は、編集後に別名保存して、必要なら chmod
で書き込み権限を付与すればOKです。
なお、編集した個人用のチートシートは、デフォルトでは ~/.cheat
というパスに保存されるようです。これを変更したい場合は、 DEFAULT_CHEAT_DIR
という環境変数を設定すれば良いようです。
チートシートの検索 cheat -s <keyword>
指定したキーワードを含むチートシート内を検索することができます。
例えば キーワードとして awk
を検索すると、以下のように出力されます。
$ cheat -s awk awk: printf '1\n2\n3\n' | awk '{ sum += $1} END {print sum}' printf '1:2:3' | awk -F ":" '{print $1+$2+$3}' seq 9 | sed 'H;g' | awk -v RS='' '{for(i=1;i<=NF;i++)printf("%dx%d=%d%s", i, NR, i*NR, i==NR?"\n":"\t")}' printf '1 2 3' | awk 'BEGIN {OFS=":"}; {print $1,$2,$3}' find: find . -type f -size +20000k -exec ls -lh {} \; | awk '{ print $9 ": " $5 }' history: history | awk '{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl | head -n10 netstat: netstat -pln | grep <port> | awk '{print $NF}' openssl: awk '/-----BEGIN/,/END CERTIFICATE-----/' | \
awk
の使用例のほか、内部的に awk
を使っているケースもヒットするので、コマンドの勉強にもなるかもしれません。
チートシートの一覧 cheet -l
こんな感じで、ズラズラっと表示されます。
7z /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/7z ab /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/ab apk /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/apk apparmor /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/apparmor apt-cache /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/apt-cache apt-get /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/apt-get aptitude /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/aptitude asciiart /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/asciiart asterisk /usr/local/Cellar/cheat/2.1.18/lib/python2.7/site-packages/cheat/cheatsheets/asterisk ...
上記だとチートシート自体のパスも出力されていて少し見にくいので、コマンドだけ抽出すると、2/9時点(Cheat 2.1.18)で、以下のとおりでした。
7z, ab, apk, apparmor, apt-cache, apt-get, aptitude, asciiart, asterisk, at, awk, bash, bower, chmod, chown, convert, crontab, csplit, cups, curl, cut, date, dd, df, dhclient, diff, distcc, dnf, docker, dpkg, du, emacs, ffmpeg, find, gcc, gdb, git, gpg, grep, gs, gzip, hardware-info, head, history, hub, ifconfig, indent, ip, iptables, irssi, iwconfig, journalctl, jq, jrnl, less, lib, ln, ls, lsof, lvm, man, markdown, mkdir, more, mount, mysql, mysqldump, nc, ncat, netstat, nmap, notify-send, nova, ntp, numfmt, od, openssl, p4, pacman, paste, pdftk, php, ping, ping6, pip, ps, python, readline, rm, route, rpm, rss2email, rsync, sam2p, scp, screen, sed, shred, smbclient, snmpwalk, sockstat, sort, split, sqlmap, ss, ssh, ssh-copy-id, ssh-keygen, stdout, strace, systemctl, systemd, tail, tar, tcpdump, tee, tmux, top, tr, truncate, udisksctl, uname, uniq, unzip, vagrant, vim, weechat, wget, xargs, yaourt, youtube-dl, yum, zip, zoneadm
面倒くさいので両方使いたい
ところで、同じタイミングのtldrの対応コマンドは以下のとおり。
※tldrは様々な言語・プラットフォームのものが用意されていますが、自分的に導入しやすいNode.js版を使っています。
7za, ab, ack, adb, ag, alias, apropos, ar, aria2c, arp, atom, autojump, autossh, awk, axel, bash, bashmarks, bc, bmaptool, bundle, cal, calibre-server, calibredb, cat, cd, chmod, chown, chsh, cksum, clang, comm, convert, convmv, cordova, cowsay, cp, csvclean, csvcut, csvformat, csvgrep, csvlook, csvpy, csvsort, csvstat, curl, cut, date, deluser, df, dhcpwn, diff, dig, docker, dokku, drush, ebook-convert, echo, electrum, emacs, enca, env, espeak, exiftool, fdupes, ffmpeg, file, find, for, fortune, fswebcam, fzf, gcc, gem, gifsicle, git-add, git-blame, git-branch, git-checkout, git-clone, git-commit, git-config, git-diff, git-fetch, git-init, git-log, git-merge, git-mv, git-pull, git-push, git-rebase, git-remote, git-rm, git-stash, git-status, git-svn, git-tag, git, glances, gpg, gradle, grep, gzip, handbrakecli, haxelib, history, host, iconv, if, ifconfig, in2csv, ionice, ioping, ipcs, jar, java, javac, kill, last, latexmk, less, license, ln, lp, lpstat, ls, lsof, lwp-request, mailx, make, man, mitmproxy, mkdir, mocha, montage, more, mount, mp4box, mtr, mv, mysql, mysqldump, nano, nc, nginx, nice, nix-env, nmap, node, nohup, npm, nvm, openssl, optipng, pandoc, parallel, pass, passwd, paste, patch, pdflatex, pg_dump, pg_restore, pgrep, php, phpize, phpunit, pigz, ping, pip, pkill, play, pngcrush, printf, ps, psql, pushd, pv, pwd, python, read, redis-cli, redshift, rename, renice, rm, rmdir, route, rsync, sails, salt-call, salt-key, salt-run, salt, sass, scp, screen, sed, seq, sftp, shred, skicka, sl, socat, sort, sox, split, srm, ssh-keygen, ssh, sshfs, strings, su, sudo, sum, svn, tabula, tac, tail, tar, tcpdump, tee, telnet, test, time, tldr, tldrl, tmux, touch, tr, traceroute, transcode, tree, ufraw-batch, umount, uname, uniq, unrar, unzip, uptime, vagrant, vim, vimtutor, virtualenv, w, watch, wc, wget, which, while, who, whoami, x_x, xargs, xz, yes, youtube-dl, zbarimg, zcat, zdb, zfs, zip, zless, zpool, airport, archey, base64, brew, caffeinate, dd, diskutil, ditto, drutil, du, head, hostname, locate, md5, mdfind, netstat, networksetup, nm, open, pbcopy, pbpaste, qlmanage, route, say, shutdown, sw_vers, sysctl, system_profiler, systemsetup, top, w, wacaw, xctool, xed, xsltproc
tldrの方がサポートしているコマンドは多く、またCheatと違ってチートシートの内容のみアップデートできる( tldr -u
)ので、今後もまだまだ増えていきそう。
よって普段使い的にはtldrの方がヒットする率が高そうですが、Cheatは自分用のチートシートを用意できるのが便利。
ということで、両方呼び出すシェルスクリプトを作って使うことにしました。
以下のgistを実行権限付きでPATHが通った場所に保存して、 how <command>
で使えます。
実行すると、以下のような感じで cheat
と tldr
両方の結果を出力します。
$ how curl ======================================= cheat # Download a single file curl http://path.to.the/file # Download a file and specify a new filename curl http://example.com/file.zip -o new_file.zip # Follow the redirect curl -L http://example.com/file ... ======================================== tldr curl Transfers data from or to a server. Supports most protocols including HTTP, FTP, POP. - Download a URL to a file: curl "URL" -o filename - Send form-encoded data: curl --data name=bob http://localhost/form - Send JSON data: curl -X POST -H "Content-Type: application/json" -d '{"name":"bob"}' http://localhost/login ...
参考
すべてのUNIXで20年動くプログラムはどう書くべきか デプロイ・保守に苦しむエンジニア達へ贈る[シェルスクリプトレシピ集]
- 作者: 松浦智之,USP研究所
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2015/07/25
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
ti-deploygate-hookをアップデートした(0.2.0)
ti-deploygate-hook は、Titanium CLIでビルド後、DeployGateへアップロードするためのプラグインです。
変更点
- Titanium SDK 5.x対応
- dgコマンド対応
Titanium SDK 5.x対応
Titanium SDK 5.xで、iOS版をビルドしたときのフォルダ構造が変わりました。
Titanium SDK 4.xまでは
build/iphone/build/Debug-iphoneos
というような階層だったものが、
build/iphone/build/Products/Debug-iphoneos
と、2つ目の build
フォルダの下に Products
というフォルダが加わるようになっていたので、それに合わせてビルド時のSDKバージョンによって、.ipaのパス指定を切り替えるようにしました。
なお、Android版は変わっていないようです。
dgコマンド対応
これまでのti-deploygate-hookは、内部でDeployGateが提供しているCLIインターフェイスの dgate コマンドを使っていたのですが、新しく公開された dg (deploygate-cli) コマンドも使えるようにしました。
参考: 開発中のアプリを1コマンドで共有する方法 | DeployGate Blog
dgateの方はアップデートも停止され、今後の機能追加はdgコマンドで行うようなので、移行してしまうのが良さそうです。
dgコマンドを使う場合は、これまで使っていた --dgate
のかわりに --dg
を指定すればOKです。
$ titanium build -p ios --target device --dg
--dg
のうしろにメッセージを指定することもできます。
$ titanium build -p ios --target device --dg "Upload by dg command !"
なお、 --dg
を使うにあたっては、事前にdgコマンドのインストールが必要です。
dgコマンドについて詳しくは、 DeployGateのサイト をご覧ください。
Appcelerator Titanium Smartphone App Development Cookbook - Second Edition
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る