いや、単純に、バッドノウハウ的なやつです。最近気づいたことを色々と。
Alloy
Tab、Windowはファイルを分けるべし
<Alloy><!--悪い例-->
<TabGroup>
<Tab>
<Window>
<Label text="Hello" />
</Window>
</Tab>
</TabGroup>
</Alloy>
AlloyではこんなXMLでレイアウトを記述できますが、これは悪い書き方です。TabやWindowオブジェクトを再利用したい(別の場面でモーダル表示したい、他のタブでも表示させたいなどなど)場合、同じコードを2つ書かなければいけなくなります。しかもiOSはNavigationWindowを使うなんてことになるともっと面倒になります。
なので、TabとWindowはファイルを分けておくようにしましょう。
<Alloy>
<TabGroup>
<Require src="homeTab" />
<Require src="userTab" />
<Require src="settingsTab" />
</TabGroup>
</Alloy>
<Alloy>
<Tab>
<Require src="homeWindow" />
</Tab>
</Alloy>
<Alloy>
<Window>
<View class="container" />
</Window>
</Alloy>
せめてこんな感じで分割しましょう。本当は最後の部分は
<Alloy>
<Window>
<Require src="homeView" />
</Window>
</Alloy>
<Alloy><!--homeView.xml-->
<View class="container" />
</Alloy>
と、やはり分割してもいいかもしれません。なぜなら、AndroidのTablet対応でiOSのようにFragmentを使った画面分割を実現する場合、View単位でコンポーネントを使いまわせる方が楽になるからです。
##callbackよりtrigger
コントローラ間でコールバック関数を渡すのは異なるプログラムの結合度を高めてしまい、誤動作の原因になります。疎結合、疎結合とマントラを唱えながら、Alloyのtriggerを利用することで代用しましょう。
//悪い例
var controller = Alloy.createController('userList', {callback: cb}),
win = controller.getView();
win.open();
//userList.js
function doSomething(){
if (!e.error) {
$.args.callback();
}
}
//Trigger
var controller = Alloy.createController('userList'),
win = controller.getView();
controller.on('done', successCallback);
controller.on('undone', errorCallback);
//userList.js
function doSomething(e){
if (e.error) {
$.trigger('error');
} elsse {
$.trigger('done');
}
}
$.trigger
は簡単なメッセージのやり取りにも対応しています。$.trigger('done', {message: 'Good bye cruel world'})
みたいに書くこともできます。
Window
Windowにレイアウトを記述しない
Windowにlayoutをverticalやhorizontalなどと指定してはいけません。ウェブ用のHTMLで一般的な要領で、画面全体を1つのViewで囲ってしまいましょう。例えば、後からクライアントの要望でオーバーレイのチュートリアル画面をWindowに追加しなければいけなくなった時に、これがあるだけで悩む時間が減ります。
<Alloy><!--悪い例-->
<Window layout="vertical">
<Label>Hello</Label>
</Window>
</Alloy>
全体を覆うだけで使いやすくなります。
<Alloy>
<Window>
<View class="container">
<Label>Hello</Label>
</View>
</Window>
</Alloy>
'.container': {
top: 0,
left: 0,
right: 0,
height: Ti.UI.SIZE,
layout: 'vertical'
}
クリックイベントはまとめると便利、まとめすぎると地獄
イベントリスナーは各ページに1つしかないと時に非常にシンプルで見通しのいいプログラムが書けるのですが、10個程度のイベントリスナーで何か深刻な問題が起きることもないので、無理にまとめるとひどい目にあいます。TableViewやListViewとTextField、Button、Switchまで全部同じイベントリスナーにまとめると破綻します。
<ListView onItemclick="doClick"><!--悪い例-->
...
</ListView>
<Button onClick="doClick">Click!</Button>
<Switch onClick="doClick">Tap!</Switch>
<TextField onClick="doClick" />
特にこのように挙動が異なる機能のイベントを統一するのは却って不要な複雑化を招きます。イベントが1つから4つになったところで、同じ関数の内部で複雑な分岐をさせるよりはるかにましでしょう?
WebView
WebViewはWindowのopenイベントでaddする
AndroidでNexus 5を利用している場合、UserAgentをセットすることでWebViewのHTMLが壊れることがあります。これはNexus 5がバグのあるChromiumをフォークして使っているのと、それがUserAgentをセットすることにより呼び出されるという複合技です。
<Alloy><!--悪い例-->
<Window>
<WebView url="http://example.com/" />
</Window>
</Alloy>
これだとSelectメニューが動かなかったり色々と苦労することがあります。
<Alloy>
<Window onOpen="doRender">
<View class="container" id="container" />
</Window>
</Alloy>
function doRender(){
var controller = Alloy.createController('webview'),
view = controller.getView();
controller.url = 'http://example.com/';
$.container.add(view);
}
User Agentを指定する場合はアプリの起動時に
Nexus 5Xなどの端末では、User Agentを変更するだけでHTMLが正しく表示されないなど様々な不具合が噴出します。これはWebViewの初期化時に分岐してバグのあるコンポーネントが内部で呼び出されるのが原因なのですが、Titanium SDK 6以降はこれをうまく解決できるようになっています。alloy.js
など起動時に読み込まれる箇所で
if (OS_ANDROID) {
Ti.userAgent = "My Great User Agent 1.0";
}
と指定することでWebViewやその他のHTTP通信のUser Agentを変更することができます。この場合は上のようなWebViewの初期化時にUser Agentを指定するような挙動を回避することができるので、端末固有の不具合を回避することが可能になります。
ListView
ListItemからイベントは発火させない
大事なことなので一回しか言いません。
ListViewでは、各行のListItemの中のオブジェクトからイベントを発火させようとしてもうまくいきません。うまくいっているように見えるのは、iOSだからです。
<ListView>
<Templates>
<ItemTemplate name="template1">
<ImageView bindId="avatar" class="avatar" onClick="doClick" />
<Label bindId="username" class="username" />
</ItemTemplate>
</Templates>
<ListSection id="section"/>
</ListView>
このdoClick
はiOS/Android共に実行されますが、Androidでは少し違う実行のされ方をします。
$.section.appendItems([
{
avatar: {
image: '/images/avatar.png',
user_id: 1
}
}
]);
こんな風に一行追加してあるとします。このImageViewをタップすると
function doClick(e){
alert(e.source.user_id); // 1
alert(e.source.image); // /images/avatar.png
}
iOSではこのようにbindした値を取得することができます。しかし、Androidではuser_id
はe.source
オブジェクトにはbindされませんのでnullが返ります。
なので、ここでは正しい実装はappendItems
を利用するのではなく、Data Bindingを使うことです。
画面全体が縦にスクロールするならなるべくListViewを使う
アプリとウェブのデザインが兼任のプロジェクトでよくあるのですが、アプリ固有のコンポーネントはウェブのよく似たコンポーネントと比べると様々な制約があります。適当にまとめて最後に全体をスクロールできるようにするだけでは、確かに要件を満たすものの、あとで手痛いしっぺ返しをくらいます。
端末のサイズに関係なくレイアウトすることができるのでScrollViewは便利なのですが、スマートフォン向けのWebのデザインのような外観を採用するのにScrollViewを利用すると、どうしても動きがスムーズになりません。ListViewにはtemplateの機能があるので、それを駆使して一見普通のViewコンポーネントが並んでいるだけに見える画面でもListViewによるレイアウトにしておくだけで、後々の扱いが楽になります。
- いつでもステータスバーをタップしてトップに戻ることができる
- クリックイベントをListSectionで分岐すれば見通しのいいプログラムになる
- パフォーマンスがいい
Button
clickイベントが遅いからとsingletapイベントに頼ると痛い目にあう
Androidではsingletapはdblclickと併用した方がいいかもしれません。
<Button onSingletap="doClick">Tap me</Button>
iOSではこれでclickイベントよりも反応がいいボタンを作ることができます。が、Androidでは素早く続けてタップすると二度目のイベントが捨てられてしまいます。
<Button platform="android" onSingletap="doClick" onDblClick="doClick">Tap me</Button>
<Button platform="ios" onSingletap="doClick">Tap me</Button>
なんと、こうしておくと1回目のタップはsingletapイベントが、2回目はdblclickイベントが拾ってくれるので、うまく両方ともイベントを発火させることができます。
以上、徒然なるままに書きました。