JavaScript
Titanium
Alloy

アプリケーション起動時、復帰時にいろいろやりたい。

More than 1 year has passed since last update.

17:30 頃になると、お隣の庭園で鳥たちが一斉に囀り始めます。
10 分程囀りまくった後、ピタッと止みます。:x::bird:
あれくらいの体内時計が私にも欲しいです。

Titanium Mobile でアプリケーションを作っています。

やりたいこと

「アプリケーションを起動した時に処理をしたい」という要件がありました。ここでのアプリケーションを起動とは以下を示します。

  • アプリケーション起動
  • スリープ状態からの復帰

iOS の場合

Ti.Appresumed イベントが使えます。
起動時は発火しないので、一度だけ手動で実行しています。

index.js
function doResume() {
    console.log('ti:resume');
}

if (OS_IOS) {
    Ti.App.addEventListener('resumed', doResume);
    doResume();
}

これだけです。かんたん。

Android の場合

アクティビティへのイベント登録

Android では Ti.Appresumed イベントは使用できません。アプリケーションとしての resumed イベントは持っていないのです。
Android はアクティビティという概念(ライフサイクル)の上でアプリケーションが動いています。イベントはアクティビティの状態遷移毎に発火されます。

なので、アクティビティにイベントを登録します。

index.js
$.index.activity.addEventListener('resume', doResume);

はい、ランタイムエラーです。

[ERROR] TiExceptionHandler: (main) [149,149] ----- Titanium Javascript Runtime Error -----
[ERROR] TiExceptionHandler: (main) [0,149] - In alloy/controllers/index.js:1,69
[ERROR] TiExceptionHandler: (main) [0,149] - Message: Uncaught TypeError: Object #<Object> has no method 'addEventListener'
[ERROR] TiExceptionHandler: (main) [1,150] - Source: "]=!0,a.destroy=function(){},_.extend(o,o.__views),o.index.activity.addEventLi
[ERROR] V8Exception: Exception occurred at alloy/controllers/index.js:1: Uncaught TypeError: Object #<Object> has no method 'addEventListener'

addEventListener なぞ知らんと言われています。そう、アクティビティが無いんです。神様は言いました「WindowOpen するまで Activity がないんじゃよ。」ありがたきお言葉、頂戴いたしました。ですので、以下のようにしてみます。

index.js
function doOpen() {
    if (OS_ANDROID) {
        var activity = $.index.activity;
        if (!activity._events) {
            activity.addEventListener('resume', doResume);
        }
    }
}

$.index.addEventListener('open', doOpen);

おk、アプリケーションを復帰時に doResume するようになりました。ただ、これだとアプリケーションを起動時に doResume が実行されたりされなかったりします。タイミング次第ですが、イベント登録前に resume イベントが発火してしまう可能性があるからです。だったらもうアプリケーションを起動時は手動で発火する、ってことでイベント登録のタイミングを遅延させます。

index.js
function doOpen() {
    console.log('ti:open');
    if (OS_ANDROID) {
        var activity = $.index.activity;
        if (!activity._events) {
            setTimeout(function() {
                activity.addEventListener('resume', doResume);
            }, 500);
        }
    }
}

$.index.addEventListener('open', doOpen);
doResume();

ふぅ:sweat_drops:
なんとかそれっぽく動くようになりましたが、まだまだ問題はあるのです。

バックボタン対応

問題点

Android にはバックボタンが付いています。:back:
アプリケーション起動画面で戻るボタンを押下すると、アクティビティが Destroy されてしまいアプリケーションは終了扱いになってしまいます。次回起動時に、またスプラッシュスクリーンが表示されてしまうのが嫌です。

解決方法

バックボタンを押した時はホームボタンを押したことにしてしまいます。

function doAndroidback() {
    console.log('ti:androidback');
    var intent = Ti.Android.createIntent({
        action: Ti.Android.ACTION_MAIN
    });
    intent.addCategory(Ti.Android.CATEGORY_HOME);
    Ti.Android.currentActivity.startActivity(intent);
};
$.index.addEventListener('android:back', doAndroidback);

これで、バックボタンを押下してもアプリケーションはサスペンド状態なので、次回起動時にスプラッシュが表示されたり初期処理を再実行せずにすみますね。
また、A -> B -> C と画面遷移するアプリケーションだが、A は起動時にだけ表示するチュートリアルだから B でバックボタン押したら非表示にしたい、なんて場合に使い勝手いいかもしれません。

複数のウィンドウを持つアプリケーション

問題点

アプリケーションが1つのウィンドウしかない場合はいいのですが、複数のウィンドウを持つ場合は別途対処が必要です。アクティビティについては前述しましたが、別ウィンドウをオープンした時には、別アクティビティが作成されます。(以下、アプリケーション起動直後の画面をメイン画面、メインから開く画面をサブ画面と呼びます。)
メイン画面からサブ画面をオープンする時は、以下の順番でアクティビティのイベントが呼ばれます(たぶん)。

  1. メイン画面.pause
  2. サブ画面.create
  3. サブ画面.start
  4. サブ画面.resume
  5. メイン画面.stop

サブ画面を表示している時は、メイン画面のアクティビティはすでに stop 状態です。サブ画面を開いている時にホームボタンや切り替えを行い、再度アプリケーションを開いた時に発火するのは、サブ画面の resume イベントです。そして、サブ画面からバックボタンで戻るとメイン画面の resume イベントが発火してしまう、と。
こうなると「アプリケーションを起動した時に処理をしたい」という要件からだいぶ遠ざかってしまいます。:astonished:

ちなみに、サブ画面からバックボタンで戻った時のイベント発火順です。

  1. サブ画面.pause
  2. メイン画面.restart
  3. メイン画面.start
  4. メイン画面.resume
  5. サブ画面.stop
  6. サブ画面.destroy

問題点をまとめると

  • 全画面のアクティビティに resume イベントを登録するのか?
  • resume イベントが画面遷移により発火されたのか、アプリケーション復帰により発火されたのか、判断できない

解決方法

メイン画面からサブ画面を開く時に、モーダルで開いたらどうなるか試している時に気づきました。

index.js
function doClick(e) {
    var controller = Alloy.createController('hoge'),
        view = controller.getView();

    view.open({
        modal: true
    });
}
  1. メイン画面.pause
  2. サブ画面.create
  3. サブ画面.start
  4. サブ画面.resume

・・・あ、メイン画面 stop しないんだ。
バックボタンで戻ってきても restartstart しない、resume するだけだ。

:exclamation::exclamation::question::bulb:

メイン画面配下の画面をすべて modal: true で開くことにしました。メイン画面.start に doResume を登録しました。
んで、、

index.js
function doClick(e) {
    var controller = Alloy.createController('hoge'),
        view = controller.getView();

    view.open({
        modal: true
    });
}

function doResume() {
    console.log('ti:resume');
}

function doAndroidback() {
    var intent = Ti.Android.createIntent({
        action: Ti.Android.ACTION_MAIN
    });

    intent.addCategory(Ti.Android.CATEGORY_HOME);
    Ti.Android.currentActivity.startActivity(intent);
};

function doOpen() {
    if (OS_ANDROID) {
        var activity = $.index.activity;

        if (!activity._events) {
            setTimeout(function() {
                activity.addEventListener('start', doResume);
            }, 500);
        }
    }
}

if (OS_IOS) {
    Ti.App.addEventListener('resumed', doResume);
} else if (OS_ANDROID) {
    $.index.addEventListener('open', doOpen);
    $.index.addEventListener('android:back', doAndroidback);
}
doResume();

$.index.open();

これでなんとか要件通りの動きにはなりました:joy:
注意しなければならないのは、サブ画面以下の画面もすべてモーダルでオープンしないといけないことです。

あとがき

複数のウィンドウを持つアプリケーションの対応については、ちょっと泥臭すぎたかと思っています。画面を何個も開くアプリケーションではなかったので、この解決方法で対処しましたが、、、「似たようなことあって俺はこうしたよ」とか「こうすりゃいいんじゃね?」的なアドバイスがあれば是非コメントください。
:bow: