Google Play Musicにアップロードした楽曲を再ダウンロードする
定額の音楽配信サービスは、Google Play Musicを使っています。
特に邦楽は聴きたいアーティストの作品がないことも多いわけですが、Google Play Musicには、手元の音源をアップロードしておくことで、登録しているデバイスに同期して聴くことができる機能があります。
Google Play ミュージック マネージャで音楽を追加する - Google Play ヘルプ
アップロードは50,000曲までできるということなので、ざっくりアルバム4,000〜5,000枚分くらいと考えても、自分の持っているCD全て入ってしまうくらいアップできます。(ちゃんと数えたことないけど、たぶん500枚くらい?多くても1,000枚はない程度)
それならGoogle Play Music上に自分のライブラリを集約させてしまうことも可能なわけですが、もしそうした場合に気になるのが、アップロードした楽曲を再び手元にダウンロードすることはできるのか?ということ。
これができるのであれば、バックアップとしても使えるので、自分的に使い勝手があがるかもと思い調べてみたところ、ちゃんと?その手段が用意されていました。
設定画面の ライブラリのダウンロード の ダウンロード ボタンを押すと…
おもむろにダウンロードが始まりました。
途中でブラウザを閉じるなどしても、またログインするとダウンロードが再開されるようで、進捗状況は左下に表示されています。
ダウンロードされる場所は、設定画面(スクリーンショット一枚目)にある ダウンロードフォルダ で指定できます。
個別ダウンロードはできない
残念ながら、今のところアップロードした楽曲を個別にダウンロードする手段はなさそうです。
もし50,000曲をアップロードしていたと仮定すると、1曲5MBとしても250GBにもなってしまうので、これを一括ダウンロードしかないとなると不便。
とは言っても、それだけの曲数をアップロードするような頃には何かしら改善されていそうな気もしますし、それまで使い続けているかもわからないので、今のところは問題なし。
【国内正規品】Beats by Dr.Dre Mixr 密閉型オンイヤーヘッドホン ホワイト BT ON MIXR WHT
- 出版社/メーカー: beats by dr.dre
- メディア: エレクトロニクス
- 購入: 1人 クリック: 1回
- この商品を含むブログを見る
Alloy Modelの学習・その4(REST API)の続き
前回、REST APIのData Bindingを使ってみたのですが、
そのときに課題として残していたエラーハンドリングなどレスポンスに対する処理に関してと、開発中にAPIエンドポイントを分離したい場合の方法についてメモ。
レスポンスに対する処理
napp.alloy.adapter.restapi を使う際の、エラー処理を含むレスポンスの処理方法を調べてみました。
napp.alloy.adapter.restapiのリポジトリに掲載されている例では引数を受け取っていませんでしたが、各メソッドでは、以下のように success
と error
のコールバックを受け取ることができます。
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); } }); }
各 success
と error
コールバックは、サーバーからのレスポンスの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ステータスコードが必要な場合は、あらかじめレスポンスボディに含めるなどする必要がありそうです。
iOSとAndroidでエンドポイントを分ける
特にPCローカルのサーバーと、iOSとAndroidのシミュレータ/エミュレータで通信確認をする場合のメモです。
iOSのシミュレータでは 127.0.0.1
で開発しているMac側で立ち上げているAlloyModelStudyRestStubにアクセスできるのですが、AndroidのエミュレータやGenymotionの場合、 127.0.0.1
は端末自体を示すらしく、 10.0.2.2
か 10.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:android
や os: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
- 作者: Jason Kneen
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/11/30
- メディア: Kindle版
- この商品を含むブログを見る
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版
- この商品を含むブログを見る