大学生活にも慣れてきました。 18 歳の夏を満喫している take です。こんにちは!
会社では、 JavaScript を書くお仕事をしています。楽しいです!
さて、 前回 は Titanium Mobile で Android アプリを書く際にハマるポイントをいくつか紹介しました。
最近、Titanium Mobile なのだから、1つのコードで Android, iOS 両対応するアプリを書こう!ということで、コードを書きながら、予め知っておくと開発が捗るポイントを何点か見つけましたので、ここで紹介したいと思います。
※これから紹介する方法 (コード) は私個人が勝手に考えて実践しているものなので、ベストプラクティスであると保証することはできません。間違っている点や、さらに良い方法があればコメント等お待ちしています。
方針は、 Android も iOS も共通のコードで同じ UI のアプリを作る事です。
Menu を実装する
Android と iOS の両対応でまずはじめに問題になるのは、iOS にはメニューを表示する機能が無い事です。
Android も ICS 以降では、基本的にメニューキーがありません。(古いアプリのためにメニューを表示する事は可能)
「メニューが無いなら実装すれば良いじゃない」
無いものは実装してしまいましょう。最近の Android のアプリには「≡」みたいなマークの付いたメニューが実装されているので、それを参考にします。
iOS ではアプリ側からアプリを終了することができないため、 Exit の項目を設置していません。
Android では、 Ti.UI.createWindow した Window を全て close() するとアプリが終了します。 iOS で同じ事を試すと、真っ白な画面が表示されるだけでアプリは終了されません。
[追記] Menu と同様に、 Back ボタンも iOS にはありませんので、実装する必要があります。
方法としては、今表示している Window を close するだけです。 (iOS のみ、最後の Window は close しないように注意してください)
シングルコンテキスト
Mobile Best Practices にも書かれている通り、公式でシングルコンテキストを用いることが推奨されています。
シングルコンテキストで書く利点は、
- パフォーマンスが良い
- JavaScript らしい書き方ができる
- CommonJS モジュールが活用できる (Ti.include は使わない)
などがあるそうです。
var win = Ti.UI.createWindow({url: "source.js"});
Ti.include("source.js");
という書き方はもう古いです。
これからは
var obj = require("view");
と書きましょう。
CommonJS の require を使う利点は、
- コードが分離される
- 変数スコープが変わるので変数名が衝突しない
- オブジェクト指向ができる
注意点ですが、 require するときのパスは、どこから呼び出す場合でも Resources ディレクトリからの相対パスで指定します。(Android, iOS 共にこの方法で動きます)
Resources ディレクトリ直下に置くファイルは app.js のみにして、他のコードは、android, iphone, ui, lib に分けます。
View 毎にコードを分離する
先程の view.js に書かれるのはこのようなコードです
module.exports = function () { var view = Ti.UI.createView(); var label = Ti.UI.createLabel({ text: "Hello" }); view.add(label); return view; };
view.js を呼び出すコード
var win = Ti.UI.createWindow(); var view = require("view")(); win.add(view); win.open();
こうすることで、View 毎に コードを分離する事ができます。
View を分離するときは、画面遷移を考えると簡単に分離する事ができます。
(1つの Window には、1つの View しか add しないと考えると分かりやすくなります)
View という分かりやすい単位毎にコードを分離することで、コードの見通しが良くなりメンテナンスがしやすくなります。
ここで、分離された View 毎に同じデータを共有する問題が出てきます。
View を require するときに、変数として g (グローバルオブジェクト) と 呼び出す View で必要になる引数を与えます。
グローバルオブジェクトは app.js で定義し、 View を呼び出す際に必ず 第一引数に渡すことで共有します。
Menu と Window はそれぞれ分離する
Menu を実装する、 View 毎に分離すると言いましたが、 Menu と Window のコードはどこに書くでしょうか。
Menu は1つのかたまりで CreateMenu.js として分離し、Window は生成部分だけ抜き出してまとめ、 CreateWindow.js として分離します。
CreateWindow.js に全ての Window が集まり、見通しが良くなります。
複数の View から利用するロジックは UI から分離する
View 毎に分離したコードの中に ロジックを書く事になりますが、その View 以外でも同じ処理をする場合は UI から分離します。
Resources/lib 辺りに入れて、必要な View から require しましょう。
View 等のサイズ指定は dip で行う
Android でマルチディスプレイ対応する場合の問題ですが、 android:anyDensity=”false” に設定して、 “dip” という単位でサイズを指定します。
anyDensity=”true” で数値を直接指定した場合と同じサイズになりますが、 anyDensity=”true” にすると Titanium のモジュールを使う場合等に問題になることがあります。
グローバルオブジェクトに
g.dip = function (size) {return String(size + "dip")};
を追加して、 g.dip(20) のように指定する事をお勧めします。
すると、万が一 px 指定に戻したくなった場合でも、 “dip” を “px” に直すだけの書き換えで済むため、かなり手間が省けます。
iOS で動くが Android で動かない コードや UI に注意する (逆もあるかも)
UI では、 TableView で TableViewRow に View を重ねた場合に、 View をタップしたときの click event が TableViewRow に伝播しないという問題があります。
コードでは、
String.prototype.hoge = function () {return this + "hoge"};
を app.js で定義したとして、require される MainView.js で “hoge”.hoge すると、
Uncaught TypeError: Object hoge has no method ‘hoge’
と言われてしまいます。
このどちらの例も、 iOS では動きます。
要点をまとめると以下の通りです。
- Menu を実装する
- シングルコンテキストで書く
- require するパスは Resources からの相対パス (.js は不要)
- グローバルオブジェクトを定義して全ての View に引数として渡す
- 画面遷移を意識して View 毎に分離する (1Window 1View)
- Menu と Window はそれぞれ分離する
- 複数の View から利用するロジックは UI から分離する
- サイズ指定は dip で行う
- iOS で動くが Android で動かないコード, UI に注意する
最後まで読んでいただきありがとうございました。