Umi Uyuraのブログ

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

R-BROS. FEST 2016でGRIM SPANKYとLIFE IS GROOVEを観てきました

3/18(木)、ニコ生で放送している「ロック兄弟」という番組のイベント「R-BROS. FEST 2016」に行ってきました。

一昨年くらいに車のCMで流れていたジャニス・ジョプリンの「MOVE OVER」、てっきり外人さんのカバーなのかなーと思っていたんですけど、歌っているのはGRIM SPANKYという日本人のバンドということを知りまして。

それで聴いてみた彼らのファースト・アルバム「SUNRISE JOURNEY」がカッコよく、さらに先日出たミニアルバム「ワイルド・サイドを行け」が5曲ながら名曲ばかりな感じで、最近のお気に入りアーティストのひとつでした。

これはライブで観てみたいな―とウォッチしていたところ、今回のイベントが行けそうだったので、チケットを申し込むことに。

で、そのイベントは対バン形式で、もう一組アーティストが出演するというので名前を見てみたら、なんとLIFE IS GROOVEではないですか!

実はこちらも以前から気になっていて、かつてTBSで放送されていた「さんまのスーパーからくりテレビ」で天才少年ギタリストとして紹介されていた山岸竜之介君が参加しているバンド。しかもメンバーがRISEのベーシストKenKenに、レジェンド・ムッシュかまやつ氏!年齢差60歳、どんな音を出すのか、非常に気になる!


当日は本編開演時間ギリギリに到着したら、オープニングアクトを務めていたarko lemmingというバンドが終わる頃でした。残念。

ステージ準備中は、OKAMOTO'Sのドラマー・オカモトレイジがDJとして会場を盛り上げていました。

「ロック兄弟」の番組MCの方々の諸々が終わって、いよいよGRIM SPANKY登場!

GRIM SPANKYの魅力のひとつは、やっぱりヴォーカル・松尾レミの声だと思いますが、ライブでのシャウトもかっこ良い。あの声にあの音、ドンピシャです。

疾走感のある「ワイルド・サイドを行け」から始まって、コール&レスポンスが盛り上がる「NEXT ONE」、バラード「大人になったら」などなど、少ない曲数でしたが、堪能しました。

それにしても、ほとんど直立で無表情な感じなのに、指がめっちゃ動いてブイブイやっていたベースの人はどなたなんだろう。

でもって、後半はLIFE IS GROOVE!

ドラム、キーボードに2名のホーン・セクションにお三方という編成。

のっけからKenKenのベースがブリブリ鳴って超ファンキー!ボリュームメーターが2、3個あがったような音圧。

山岸竜之介君は、まだ16歳ですかー。なんだかさわやか好青年になっていてビックリ。

そして、そんな2人を見守るというより見守られながら、飄々とマイペースにステージを彷徨うお茶目なムッシュ。

体を動かさずにはいられなくなる感じのバンドサウンドと、ゆる〜い感じのKenKenのMCとの落差もまた面白かった。

さらにアンコール。

GRIM SPANKYとLIFE IS GROOVEがステージにあがると、KenKenが「GRIM SPANKYとぜひこの曲をやってみたかった」といって始まったのは、あのドラム!そう、MOVE OVER!イベントだし、対バン形式で曲数少ないし、まさか演奏するとは思っていなかった!

あの声でMOVE OVER、やっぱりカッコよい。

しかも何がスゴいって、下北沢のジャニスの息子と同じステージにいるんですよね。

その後もスパイダースの「バン・バン・バン」を全員で演奏!そのどさくさに紛れてムッシュにサインをもらうオカモトズ・レイジ(笑)

いやー、とっても濃厚なイベントでした。

GRIM SPANKYは4月に「“ワイルド・サイドを行け”ツアー」をはじめ、イベントにも多数出演予定のようなので、詳しくは 公式サイト をどうぞ。

LIFE IS GROOVEは4/27にシングルが出るそうで、発売記念的なイベントもあるようなので、こっちも LIFE IS GROOVE OFFICIAL SITE をチェック。

どちらも改めて単独公演で観てみたいなー。

f:id:umi-uyura:20160317221019j:plain

ロック兄弟、番組自体は観たことがないけど、最高なイベントをありがとうございました。

ロック兄弟

ワイルド・サイドを行け(初回限定盤)(DVD付)

ワイルド・サイドを行け(初回限定盤)(DVD付)

Generations

Generations

Titaniumもくもく会に参加しました

先週のことですが、久々にTitanium仲間のゆる〜い集まりがありました。

ひさびさ〜と思ったら、去年の Twilio & Titaniumミートアップに行ってきました - Umi Uyuraのブログ 以来ぶりだったんですね。

1月には新年会があったようですが、私は都合があわず参加できなかったので、半年以上ぶりの集まりでした。

Titaniumもくもく会 - Titanium | Doorkeeper

今回の会場は大日本印刷株式会社(DNP)様でした。

以下、発表された方のメモです。

Titanium周りの最新動向 - @kaz_konnoさん

まずは @kaz_konnoさん からTitaniumの最新動向について・・・ということだったのですが、特にないとのこと(笑)

とは言え、やはり年初に飛び込んできたAxway社によるAppceleratorの買収のニュースは驚きでした。

Axway Acquires Appcelerator—And Why This is Great News for All

Axwayという会社についてはよく知らないのですが、エンタープライズ向けの製品をいろいろと持っているところのようです。

まさかTitanium終了か!?という懸念もありましたが、今のところ製品・サービスラインナップはこれまでと変わりなく提供されるようなので、一安心。

Co-FounderのJeffもそういった相手を選んでの決断だったのではないか、ということでした。

Indie foreverプランについて

昨年、Appcelerator Platformという基本有料のサービス体系に変わってから、それ以前にTitaniumアカウントを持っていれば、Indieを永久無料で使えるというプランが期間限定で提供されていました。

このプランへの切り替えは、本来は昨年の6/30までに以降手続きが必要で、もう終了しています。

ただ、この切替方法が少々わかりにくく、実は新しいアカウントに切り替わった時点では Developer プランという状態で、アカウントの設定から Indie に手動でプランを変更しないといけないのです。

これに気づかずに Indie へ切り替えそこねてしまった人も多いようで、期限を過ぎてしばらくは直接サポートに連絡するなどしてIndieに切り替えてもらったようなケースもあるようですが、その救済措置も終了ということのようです。

Developer Portal

Developer向けのポータルページが新しくなりました。

Appcelerator Developer

最新情報は Appcelerator Developer Updates というページに集約されているようなので、Titanium/Appcelerator Platformに関して情報が欲しい人は、定期的にこちらをチェックすると良いでしょうとのこと。

公式のDeveloper Blogや、Webinarやチュートリアル動画を掲載しているAppcelerator University、そしてMediumに投稿されたTitanium関連の記事をピックアップしているAll Titaniumというコーナーがあります。

以前あったQ&Aフォーラムについては、Stack Overflow上でサポートに移行したそうです。

Newest 'appcelerator' Questions - Stack Overflow

投稿する際は appcelerator タグを付けておくと、Appceleratorの人も見てくれているようです。

ただ上記は英語のサイトになってしまうので、日本語でサポートが必要な方は Titaniumユーザー会のサポートBBS へ投稿するか、Twitterハッシュタグ #TitaniumJP を付けて投稿すると、どなたか拾ってくれるかもしれません。

最新のSDK情報

最新のSDK 5.2.0がリリースされていて、3/10の時点で5.2.1のRCも出ています。

SDK 5.2.0で提供される新機能などは、以下のラップアップ記事にある動画が非常にわかりやすいので、サンプルと合わせて見てみると良いと思います。

Titanium Mobile DNPの取り組みについて - DNP山下さん

続いて、会場を提供していただいた大日本印刷株式会社の山下さんから、DNPのTitanium Mobileへの取り組みについて紹介していただきました。

DNPでは、特に2011〜2012年ころ、Titanium meetup TokyoやTitanium mobile 2.0ローンチ記念イベントなどで会場提供などでTitaniumコミュニティ活動を支えてくれていました。

DNP自体もネイティブ以外の開発プラットフォームも幅広く取り入れているということで、そのひとつとしてTitaniumも実践活用されているとのこと。

法人向け業務支援やカスタマイズアプリなど、特にタブレット向けアプリのUI構築などで使っていて、分野としてもサイネージやアンケート、コンシェルジュ、教育など多岐に渡るそう。

最近の事例としては、京都のギャラリーなどで鑑賞補助用のアプリに使っていたり、DNPオリジナルタブレット向けにAndroidホームアプリなどに使っているそうです。

TitaniumでAndroidのホームアプリも作れるというのは知らなかったので、ちょっとやってみたい。

今後は、Titanium×Windowsも視野に入れて研究中ということで、特にWindowsタブレットをターゲットに活用していきたいということでした。

ただ、このあとのWindows開発の発表にもありましたが、SDK 5.2.1時点ではまだまだ使い物にならない状態ということで、前途多難な道のりにはなりそうな感じ。

最近は、特に企業から改まってTitaniumの事例などを耳にすることは少ないので、適材適所を見極めて活用されているところがあるというのを聞けたのは心強いですね。

faster-titanium - @shinoutさん

続いて CureApp@shinoutさん の発表は、TiShadow/LiveViewを凌ぐ新たな爆速開発ツール「faster-titanium」の紹介。

どういうものかは、GitHubリポジトリのアニメーションGIFを見ていただくとイメージしやすいかと思います。

CureApp/faster-titanium: Accelerate Titanium development

TiShadowやLiveViewは便利ですが、落ちやすかったり、なんだか動かないということが多かったので、LiveViewをベースにしつつ、中身をES6ベースにして徹底的にリファクタリングしたそうで、安定性とReact Native並のリロード速度を実現されたそうです。

現状はiOSでしか動いていませんが、ベースとなるLiveViewがAndroidについてはできているので、同様にできるであろうということと、のちのちはWindowsにも対応していきたいという話でした。

Titaniumの特徴として、各プラットフォーム向けのUIをJavaScriptAlloyフレームワークなど簡略化された構文で構築できる点にあると思いますが、どうしてもビルドして動作確認という部分に時間がかかってしまいます。この部分がfaster-titaniumで短縮できると、さらに開発効率があがるので、Titaniumの強みがより活かせるツールだと思います。

まだ出来たてということもあり、絶賛フィードバック募集中ということなので、ぜひ使って行きたいと思います。

Titanium x Windows開発 - @flat_8_kikiさん

最後は @flat_8_kikiさん による、TitaniumによるWindowsアプリ開発についての発表。

もはや涙無くしては語れない物語で、メモを取る手も止まってしまうほどでした。

この過酷な闘いについてはQiitaに記録されていますので、ぜひご覧ください。

そもそもユニバーサルWindowsアプリ開発自体の情報もまだまだ少ない気がするので、そういった点でも貴重な情報だと思います。

SDK 5.4.xおよび5.5.xあたりで改修されている見込みのようなので、最低限の開発ができるようになるのは、その頃からという感じでしょうか。

まだまだ闘いは始まったばかりということで、ガンガン地雷を踏み抜いてくださる覚悟ということなので、今後のWindows×Titaniumの進展も期待できそうです。

※2016/03/25追記

資料公開していただいたので、追記しました。

おわり

終わってみると、非常に濃い話が多くてもくもくどころじゃなかった会でした。

最近は同じマルチプラットフォーム開発でも、React NativeやMSに買収されたXamarinなどに話題を持って行かれている感がありますが、Titanium自体も着実に進化していて、実績という点では他に負けていない製品だと思います。

そして表に出てきてはいないけど、実は使っているという人もけっこういるようなので、そういった人にもぜひもくもく会などに遊びに来ていただいて、情報交換させてもらえると嬉しいですね。

Google Play Musicにアップロードした楽曲を再ダウンロードする

定額の音楽配信サービスは、Google Play Musicを使っています。

umi-uyura.hatenablog.com

特に邦楽は聴きたいアーティストの作品がないことも多いわけですが、Google Play Musicには、手元の音源をアップロードしておくことで、登録しているデバイスに同期して聴くことができる機能があります。

Google Play ミュージック マネージャで音楽を追加する - Google Play ヘルプ

アップロードは50,000曲までできるということなので、ざっくりアルバム4,000〜5,000枚分くらいと考えても、自分の持っているCD全て入ってしまうくらいアップできます。(ちゃんと数えたことないけど、たぶん500枚くらい?多くても1,000枚はない程度)

それならGoogle Play Music上に自分のライブラリを集約させてしまうことも可能なわけですが、もしそうした場合に気になるのが、アップロードした楽曲を再び手元にダウンロードすることはできるのか?ということ。

これができるのであれば、バックアップとしても使えるので、自分的に使い勝手があがるかもと思い調べてみたところ、ちゃんと?その手段が用意されていました。

設定画面の ライブラリのダウンロードダウンロード ボタンを押すと…

f:id:umi-uyura:20160314235849p:plain

おもむろにダウンロードが始まりました。

f:id:umi-uyura:20160314235900p:plain

途中でブラウザを閉じるなどしても、またログインするとダウンロードが再開されるようで、進捗状況は左下に表示されています。

f:id:umi-uyura:20160314235912p:plain

ダウンロードされる場所は、設定画面(スクリーンショット一枚目)にある ダウンロードフォルダ で指定できます。

個別ダウンロードはできない

残念ながら、今のところアップロードした楽曲を個別にダウンロードする手段はなさそうです。

もし50,000曲をアップロードしていたと仮定すると、1曲5MBとしても250GBにもなってしまうので、これを一括ダウンロードしかないとなると不便。

とは言っても、それだけの曲数をアップロードするような頃には何かしら改善されていそうな気もしますし、それまで使い続けているかもわからないので、今のところは問題なし。

Alloy Modelの学習・その4(REST API)の続き

前回、REST APIのData Bindingを使ってみたのですが、

umi-uyura.hatenablog.com

そのときに課題として残していたエラーハンドリングなどレスポンスに対する処理に関してと、開発中にAPIエンドポイントを分離したい場合の方法についてメモ。

レスポンスに対する処理

napp.alloy.adapter.restapi を使う際の、エラー処理を含むレスポンスの処理方法を調べてみました。

napp.alloy.adapter.restapiのリポジトリに掲載されている例では引数を受け取っていませんでしたが、各メソッドでは、以下のように successerror のコールバックを受け取ることができます。

function doOpen() {
  memos.fetch({
    success : function(models, response){
      Ti.API.debug('memo fetch() - success = ' + models + ' / ' + response);
    },
    error : function(models, response){
      Ti.API.debug('memo fetch() - error = ' + models + ' / ' + response);
    }
  });
}

successerror コールバックは、サーバーからのレスポンスのHTTPステータスコードによって判断されて呼びだされるため、基本的に意識する必要はありません。

受け取ったモデルの加工だったりエラーに対する処理のほか、通信中のローディング画面の制御などもここでハンドリングできますね。

コールバックの形

操作対象がCollectionなのかModelなのかによって、コールバックには大きく2種類の形があるようです。

メソッド コールバック コールバックの形
Collection.fetch() success
error
function(models, response)
Model.save()
Model.destroy()
success
error
function(model, response)

各コールバックの第1引数は、主に操作したモデルデータを受け取るようです。

次に、共通している response には、Titanium.Network.HTTPClientの responseText が入ってくるようです。通信をJSONでやりとりする場合は、必要に応じて JSON.parse() するなどして扱うことになると思います。

コールバックの形を見るに、 responseText 以外の情報は受け取っていないようなので、例えばもしアプリ側でもHTTPステータスコードが必要な場合は、あらかじめレスポンスボディに含めるなどする必要がありそうです。

iOSAndroidでエンドポイントを分ける

特にPCローカルのサーバーと、iOSAndroidのシミュレータ/エミュレータで通信確認をする場合のメモです。

iOSのシミュレータでは 127.0.0.1 で開発しているMac側で立ち上げているAlloyModelStudyRestStubにアクセスできるのですが、AndroidエミュレータやGenymotionの場合、 127.0.0.1 は端末自体を示すらしく、 10.0.2.210.0.3.2 といったIPになるようです。

モデルの URL を書き換えれば対応できますが、その都度実施するのも面倒なので、あらかじめ config.json に設定しておき、そのURLを使うようにしました。

app/config.json

{
  ...
  "os:android env:development": {
    "API_URL": "http://10.0.3.2:4321/memos"
  },
  "os:ios env:development": {
    "API_URL": "http://127.0.0.1:4321/memos"
  },
  ...
}

config.json で設定できる値は、 os:androidos:ios といったプラットフォームをあらわすオブジェクトに、動作環境をあらわす env:development などを組み合わせることで、「Androidエミュレータの場合」「iOSエミュレータの場合」に使う値、というようにバリエーションをつけることができます。

それをモデルの中で参照することで、環境ごとに異なるURLを使えるようになりました。

app/models/memo.js

exports.definition = {
  config: {
    columns: {
      "id": "integer",
      "contents": "text",
      "priority": "text"
    },
    adapter: {
      type: "restapi",
      collection_name: "memo",
      "idAttribute": "id"
    },
    "URL": Alloy.CFG.API_URL,           // <== config.jsonに定義したURL
    "debug": 1
  },
  ...
};

env:production などと組み合わせれば、本番環境向けビルドで使うURLも設定できます。

なお、以前はモデルの中でグローバルな Alloy オブジェクトを参照できなかったので、こちらの記事( AlloyのModelでREST APIからデータを取得する - Qiita )のように一手間必要だったようですが、Alloy 1.4.0?あたりから参照できるようになったようです。

Backbone.js的な対処方法

モデル側のURLの指定を変更する方法とは別に、Backbone.js的に、モデル操作時にオプションとしてURLを渡すという対処方法もあります。

その場合は、 Collection.fetch({url: <url>})Model.save(<params>, {url: <url>})Model.destroy({url: <url>}) といったあたりのメソッドを使えば良いようです。

エンドポイントを動的に変更するようなケースがあれば、使いみちがあるかもしれません。

まとめ

APIのレスポンスに関しては、自前で用意するAPIであれば設計段階で調整できますが、既成サービスのものなどを使う場合はいじりようがないので、実戦使用するときに、また色々と検証しようと思います。

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Alloy Modelの学習・その4(REST API)

引き続き、Alloy Modelを勉強中。

umi-uyura.hatenablog.com

クロスプラットフォームな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のレスポンスを得られる感じです。

f:id:umi-uyura:20160227114353p:plain

データベースを使っていないので、サーバーを再起動するとデータは初期状態に戻ります。

ソースは umi-uyura/AlloyModelStudyRestStub にありますので、こちらをご覧ください。

Qiitaにあった mocky を使ってちょっと賢いスタブAPIサーバを作る - QiitaNode.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.typerestapi に変更
  • URL を追加し、スタブAPIのURLを設定
  • debug を追加、これでAPIを使う際のリクエスト/レスポンスがデバッグ出力されてわかりやすい

他に headersparentNode をといった設定もありますが、今回は未使用。

以下はデバッグ出力の例。

f:id:umi-uyura:20160227114411p:plain

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を使ってきちんと実装する場合は、この点も考慮する必要があると思います。

参考

モックサーバー

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Alloy Modelの学習・その3(Properties Adapter)

引き続き、Alloy Modelを深ぼってます。

umi-uyura.hatenablog.com

標準で用意されている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を使って選択式にしています。

Android

f:id:umi-uyura:20160224234453p:plain f:id:umi-uyura:20160224234503p:plain

iOS

f:id:umi-uyura:20160224234513p:plain f:id:umi-uyura:20160224234524p:plain

ソースは 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

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Alloy Modelの学習・その2(マイグレーション)

前回の学習内容をもとに、せっかくなのでマイグレーション機能も試してみることにしました。

umi-uyura.hatenablog.com

今回のサンプルは前回と同じプロジェクトの 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を発行して処理します。

今回の場合、

  1. 優先度カラムを追加
  2. 既存データから優先度を判定してデータを更新

という処理が必要になるので、以下のような感じに実装してみました。

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();
  }
};

SQLiteDROP COLUMN が使えないので、直接テーブルからカラムを削除することができません。そこで、カラムが減った新しいテーブルを作成して、そこにデータを入れなおす必要があるようです。

最初に migrator.createTable() をしていますが、これはこのマイグレーションを実施するバージョンのアプリを新規でインストールする場合、まだテーブルが作成されていない可能性があるからです。 migrator.createTable() は内部で IF NOT EXISTS を使っているので、すでにテーブルがある状態で呼び出しても問題ありません。

その後、 ALTER TABLEpriority カラムを追加してから、 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 + ';');
};

で、ここまで実装して、ふと気付いたのですが、

・・・ロールバックってどうやって確認するんだろ?

声(ツイート)に出てた。

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の順序が画面に反映されました。

f:id:umi-uyura:20160222233635p:plain f:id:umi-uyura:20160222233650p:plain

見た目的には前回と変わっていないのですが、お気に入り( * )の数が多いものが、より上位に表示されるようになりました。また、お気に入りの数を増やした場合、より上位に移動します。

感想

今のところマイグレーションを使うような予定はないのですが、そういうのもあるのか、ということで。

参考

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition

Appcelerator Titanium Smartphone App Development Cookbook - Second Edition