Hatena::ブログ(Diary)

toweroflの日記 RSSフィード Twitter

2012-04-26

モジュールからのイベントの通知

@k0sukeyさんの素晴らしすぎるモジュールTiPlatformConnectは既にご存知とは思いますが、各認証サービスの状態の通知に

  self.listeners = {};

//略

  Twitter.prototype.addEventListener = function(eventName, callback) {
    this.listeners = this.listeners || {};
    this.listeners[eventName] = this.listeners[eventName] || [];
    this.listeners[eventName].push(callback);
  };
  
  /*
   * Fire an event
   */
  Twitter.prototype.fireEvent = function(eventName, data) {
    var eventListeners = this.listeners[eventName] || [];
    for (var i = 0; i < eventListeners.length; i++) {
      eventListeners[i].call(this, data);
    }
  };

というコードが使用されています。

なるほど、JavascriptでaddEventListener/fireEventを実装するにはたったこれだけのコードを書くだけでよかったのですね。
というかこれ自体はそのまんまObserverパターンと言われるものだと思いますが、いくら本を読んで勉強しても実際に使いこなせなきゃね、、、。


例としてこのようなモジュールを書いてみました。

TiPopupSelector


モジュールはiPadのPopover風なウィンドウにテーブルを載せただけのものですが、実際にはこのようにViewオブジェクトを利用する場合は前回(といっても二ヶ月も前ですが)のように既存のViewにオレオレイベントを追加するやり方でも問題無いかと思います。

ただ、Viewを使用しないネットワークやデータの永続化のモジュールの場合で、コールバックで結果を処理するのではなくオブザーバーにしておきたいというケースもあります。

参考 CallbackとListenerとObserverの違い

いずれにしてもTiを使う上でみなさん知っておくべきコードかと思います。

2012-02-09

新・俺ルール誕生

自分なりのTitaniumの書き方の核となる部分だったwindow stackと最近お別れして別のやり方に切り替えたので今日はそのお話。
といってもなんのことやらわからないかと思いますが、最近まで自分は開いたWindowをグローバル変数配列にどんどん入れることでウィンドウ間のデータの受け渡しをしたり関数の呼び出しを自由に行えるような書き方をしていました(参考)。

しかしAppceleratorによるサンプルやテンプレートを見た後、CommonJSのモジュール間のデータやメッセージのやりとりについて別のやり方を思いついて、いくつかのプロジェクトで実際に試してみました。
名付けて「Bubble、Catch Bubble法」。

名前を聞いて想像がついたかもしれませんが元ネタはWebブラウザでのイベントです。ご存知のようにブラウザでは例えば
html→body→div→div→p→a
というようなDOMの構造で「a」にclickなどのイベントが発生した時、a→p→div→div→と伝播していきます。

そこでTitaniumのWindowやViewについても同じように
Window1→Window2→Window3→Table
というように開いていった先のテーブルでイベントが発生した場合、Table→Window3→Window2→Window1
というように伝播するようにしておけば良いのではないかと思って書いたのがこれ

// Window2
    var self = Ti.UI.createWindow();

    var _bubble = function(type, options, propagation, source) { // (A) バブルの発生
      self.fireEvent('bubble', { //self(ここではwindow)に対してを発生させ呼び出し元のウィンドウに送る
        btype: type,
        bsource: source || 'Window2',
        boptions: options || {}, //データはココに突っ込む
        bpropagation: propagation || true
      });
    };
    
    var _catchBubble = function(e) {// (B) バブルをキャッチするイベントハンドラの定義
        if (e.btype === 'foo') {//Window3などで発生したイベントをキャッチ
           doSomething();
        }
        if (e.bpropagation) {//伝播を止めたい場合はpropagationをfalseにしておく。
            _bubble(e.btype, e.boptions, e.bpropagation, e.bsource);
        }
    };

イベントを伝播したいWindowやViewにこれら2つの関数をそれぞれ定義していきます。
その使い方は

// Window2
    button.addEventListener('click', function(){
        _bubble('somthingHappen', {baz: 'hogehoge'}); //呼び出し元のWindowに伝えたいイベント
    }); 


    button2.addEventListener('click', function(){
        var window3 = Ti.UI.createWindow();//次のWindowを生成したときには

        window3. addEventListener('bubble', _catchBubble);// (C) Windowに対してbubbleイベントをリッスン

        window3.open();
    }); 

実際これを使っていくつかサンプルを書いているのでよければ御覧ください。

ofl / Sample.RSS
ofl / Template.Tabbed
ofl / TiMyTemplate

2012-01-27

CFBundleShortVersionStringが反映されない。

iOSアプリアップデートする際にvalidationでバージョン情報を書き換えるのを忘れていたためオーガナイザーに怒られることよくありますよね。

ただ、今回自分が出くわしたトラブルはtiapp.xmlでバージョンをいくら変更してもinfo.plistに反映されないという現象。
初回のリリース後にアプリの雛形を変更したり(これ)アプリに日本語タイトルを追加したりと原因として思い当たるフシはあるのですが解決策が見つかりません。

そこで本家の掲示板を覗いてみるとやはり同じようなトラブルに会っている人がいるようです。
最終的にはこちらを参考にbuildで作成されたinfo.plistのバージョンを変更してプロジェクトフォルダ直下に配置してなんとかvalidationが通りましたやれやれ。
CFBundleShortVersionString does not update

2012-01-21

self.fireEvent()にはしびれたが

ひきつづきAppcelerator発のサンプルやテンプレートについての話。

前回のサンプルの他にいくつかのテンプレートなども公開されています。
Titanium Week WebCastのQ&Aチャットログより抜粋(毎度id:donayamaさんに感謝)

前回のサンプルですが


TodoList側で発生したイベントをApplicationWindowに伝える手段としてself.fireEvent()というのは一瞬なんだそりゃと思いましたがすぐにその賢さにしびれました。

しかしそれに比べてApplicationWindow側からTodoListを操作する場合にtodoList.fireEvent()というのはどうでしょう。個人的には疑問に思います。

たとえば通常のviewオブジェクトの場合は

    //buttonの参照があってそれを直接操作できるのなら
    button.setBackgroundColor('red');//または
    button.backgroundColor = 'red';    

というようなやり方が一般的なのに。

    button.fireEvent('changeBackgroundColor', {color: 'red'});

というようなやり方をわざわざさせるような必要があるのか。
もちろんこのやり方をするためには

    //TodoList.js
    self.changeBackgroundColor = funciton(color){...};// オレオレ関数をviewオブジェクトにひもづけ。
    //ApplicationWindow.js
    self.changeBackgroundColor('red');

というように定義した関数を外部に公開する必要があります。

またイベントリスナーの欠点として引数として渡せるものがシリアライズできるデータに限られるという点があります(逆に参照の循環がおこらないので利点でもありますが)。自分で定義したオブジェクトや画像、viewなどを渡したい場合は別の経路が必要になります。

発生したイベントをモジュール間で伝達するやり方としてイベントリスナー以外は一切使うべきではないというのであればそれに従うべきかなとは思います。しかし、今回公開されているサンプルの中にもこうしたオレオレ関数をTiのviewオブジェクトに束縛して、外部に公開している箇所もあります(sample)。
つまり「基本的にはイベントリスナーを使うべき。でも例外もゴニョゴニョ」というような歯切れの悪さが感じられます。

他にも気になった点を上げると、

バケツリレー

たとえばこの例だとtabGroupのtabを次のウィンドウに渡すためにそのウィンドウ自身にtabをヒモ付ています。
Sample.RSS / Resources / ui / common / MasterView.js

	var win1 = new AppWindow(L('home'))
	var tab1 = Ti.UI.createTab({
		window: win1
	});
	win1.containingTab = tab1;

これだとさらに次のウィンドウもtabで開くときに

	var nextWin = new FooWindow(L('foo'))
	nextWin.containingTab = win1.containingTab;

とバケツリレーが必要になりそうです。
tabがアプリの開始から終了まで破棄されることがないのであれば、グローバル変数(的に使える何か)に定義しておく方がすっきりすると思うのですが。

なんでnew

モジュールで読み込んだコンストラクタからオブジェクトを生成する際にnew XXX()としていますが、UIのオブジェクトだとprototype継承などしそうもないのにnewを使う必要があるのか疑問に思います。
通常のviewオブジェクトがTi.UI.createButton()などで生成するのでそれに合わせたほうがわかりやすい気がします。

まとめ

今後のTitaniumコーディングの基準が示されたことはチームでの開発やTitaniumの使い方を教えたりブログでコードの例示したりする人たちにとって福音です。
またこれによってモジュールの開発や多人数が参加するようなプロジェクトがどんどん活性化するんじゃないかと期待できるでしょう。

ただ今回の書き方が最終的な結論かといえば、もっとわかりやすくCode Strongな書き方への改善の余地はありそうです。現にどうやらアーリーアダプター達の間では今回のサンプルよりこれの内容の方が話題のようですし、個人的にもアイディアがあるのでうまくまとめることができればまた書きたいと思います。

2012-01-17

本命登場。さて

Appceleratorが新しくプロジェクトのサンプルなどいくつかのコードを公開しています。

Sample.Todo

どうやら今回提示されたコード例がテンプレートやKitchenSinkにも使われるようで今後の基準となることは間違いはなさそうです。

すでにご覧になってる方も多いと思いますが、ようやく出てきた本命といえるコーディングの方法を自分なりに解釈すると、

  • app.js以外すべてCommonJSのモジュール
  • 意味的に分割できる複雑なView(といってもサンプルなのでわりと単純ですが)をモジュールに閉じ込める。
  • UIについてはモジュールをrequireしておいてnewでオブジェクトを生成する。
  • 生成した返り値としてJavascriptインスタンスではなくTiのオブジェクトを返す(えーっ、それ前のベストプラクティスでやめといた方がいいって言ってたやん)。

このあたりはあまり驚きはなかったのですが面白いというか自分では思いつかなかった点が

  • モジュール間のメッセージのやり取りをイベントリスナーで行う

です。サンプルでは、

//ApplicationWindow.js(ベースとなるウィンドウ)
function ApplicationWindow() {
	var todoForm = new TodoFormView();//フォームを作成
	todoForm.addEventListener('todoSaved', function() {// (1)フォームのオレオレイベントをリッスン
		todoList.fireEvent('todosUpdated');// (3)別のviewにオレオレイベントで転送
	});
}
//TodoFormView.js(フォーム部分)
function TodoFormView() {
	var self = Ti.UI.createView({});
//...	
	button.addEventListener('click', function(e) {//ボタンがクリックされたら
		self.fireEvent('todoSaved');// (2)オレオレイベントを発生させる
	});
//...	
	return self;
}

  1. オレオレイベントリスナーを定義して呼び出し元でリッスンする。
  2. Viewから呼び出し元にメッセージを送信したい場合はself.fireEvent()する。
  3. さらにウィンドウからView側にメッセージを送る場合もオレオレイベントを使う。

ただlisten〜fireなどとまどろっこしいことをしなくてもviewの生成時に

var todoForm = new TodoFormView({
        onClick: function(){...},
        onError: function(){...},
//...
});

などとコールバックを渡してやることでもよさそうです。
そうではなく何故わざわざイベントリスナーでやり取りする方が良いのか考えてみると(このあたりはとても怪しい解説なので眉につばを、、、)

1,汚染を防ぐ。

関数やデータなどを引数などでオブジェクトや関数を渡した場合、モジュール側でどのようにでも変更されそれが呼び出し元にも影響するため危険である。

2,モジュール間の結びつきを疎にできる。

モジュール側でオブジェクトの参照が解放されないと呼び出元でオブジェクトを削除できないためメモリーリークを引き起こす原因となる。

さらに重要な要素として

3,Titaniumのコーディングスタイルとしての一貫性

があげられるかと思います。
これはTitaniumのUIにおいてはmediaなど一部で上記のコールバックの登録スタイルが見られるものの、ほとんどが

var button = Ti.UI.createButton({...});
button.addEventListener('click',function(){});

というやり方をしています。
つまり複雑なViewやさらにはWindowもすべてTi.UIオブジェクトの延長と捉えて、生成したオブジェクトに対してイベントリスナーを設定することがTiatniumのコーディングとして一貫しているのだとAppceleratorは考えているのではないでしょうか。

つづくはず