Edited at

アプリ上でサーバの「メンテナンス中」をリアルタイムに伝える方法


TL;DR

アプリは何も悪くないのにサーバ障害のせいでレビューががた落ち、というのは30分ぐらいの開発で防げるので実装しておこう


アプリは悪くないのに評価が下がって辛いとき

どんなに完璧でバグがなく実装されたアプリでも、通信相手となるサーバ側で問題が起きてしまうとアプリは使えなくなってしまいます。

このとき、ずっと 読み込み中 のままだったり エラーが発生しました と表示され続けるだけになっていると、ユーザーにとっては アプリそのものが 壊れているのと変わりません。「せっかくインストールしたのに、壊れてて使えない」わけなので、必然的にレビュー欄が荒れてしまいます。

一方、障害の発生時でも起動したアプリ中で ごめん、いまなんとかしてるよ! ということが正しく伝えられれば、ユーザーもいきなり怒ったりはしません。

この記事では、


  • サービス提供側からのメッセージを

  • リアルタイムにユーザーに提示する

  • できる限り簡単な実装

について紹介します。

今回は Google が 2014 年に買収した Firebase というリアルタイムバックエンドサービスを使います。以下ではAndoroidで説明してますが、iOSでもCocoaPodsでサクッと導入して同じように作って共通化することができます。


どんな感じ

「やばい、サーバ止まってる……」

そんな切羽詰まった状況下でも、Web上でステータスを更新することで、ユーザー側に状況をリアルタイムに伝えることができます。


  • 稼働状態 (active) が false になると、アプリに指定したメッセージの Snackbar が表示され、問題が発生していることをアプリのユーザーに伝えます。

  • 回復状況に応じて書き換えていくと、リアルタイムに反映されます

この実装本体は 諸々含めて 61行 120 行 程度で書かれており、 Activity へこんな感じのコールバックイベント実装を追加する だけで実装できます。これだけでメンテ中にアプリ起動されたときや、通信が切れたり復活したりした場合も自動的に再接続が行わるなど、様々な状況下で正常に動作するようになっています。(Firebaseよくできてる)

リポジトリはこちら: https://github.com/tnj/FirebaseMaintenanceMode


作り方

まず Android Studio でいつもの Blank Activity プロジェクトを作ります。


Android Studio でプロジェクトに Firebase を追加する

Command + ; (Windows だと Ctrl + Alt + Shift + S ) で Project Structure を開き、 Cloud を開くと Firebase のチェックボックスがあるのでこれにチェックを入れて OK を押します。

以下の行が追加されます。


app/build.gradle

compile 'com.firebase:firebase-client-android:2.3.1'



AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />


あと、ライブラリのファイルが重複していてビルドがこけるので、手動で以下の設定を app/build.gradle へ追加します。


app/build.gradle

android {

// ...
// 諸々の設定のあと
// ...

packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE-FIREBASE.txt'
exclude 'META-INF/NOTICE'
}
}



Firebase のアカウントを作る

次に Firebase のサイト の右上の Sign Up からアカウントを作成します。 Google のアカウントを選択するだけです。

サインアップできると My First App という最初のアプリが既に作成されています。


データを作る

My First AppManage App を開くとデータの編集画面になります。

Firebase は基本的に JSON ライクな key-value のツリー型でデータを保存します。

今回は、 status という名前で、「サービスが生きているかどうか(active)」「死んでいるときに表示するメッセージ(message)」の2つをキーにデータを作ってみます。

親要素 (名前は自動生成) にカーソルを合わせると + マークが現れるのでこれをクリックします。

namestatus と入力して、即値でなくて子要素を追加するため value は空のまま、その右の + を押します。

子要素の1つ目は name: active, value: true と入力して Add を押します。

一旦ここまでのものがコミットされます。

status に要素を追加するために、マウスカーソルを合わせると右に出てくる + を押します。

name: message, value: "" と入力して Add を押します。

最終的にはこんな形になっているはずです。

JSON で表現するとこんなシンプルな感じのデータです。

{

"status": {
"active": true,
"message": ""
}
}

これでデータの準備ができました。


セキュリティ&ルールの設定

Firebase では読み書き権限やバリデーションのルールを Security & Rules から JSON 形式で設定します。

今回の status は読み取りは誰でもできてよく、書き込みは認証された人しか書けてはいけないので、以下のように設定します。

{

"rules": {
"status": {
".read": true,
".write": "auth != null"
}
}
}

詳しくはセキュリティのドキュメントをご参照ください。


アプリ側の実装

まず、 Firebase を Application#onCreate() で初期化します。

Firebase.setAndroidContext(this);

次に、先ほど定義した status を受け取る POJO なオブジェクトを用意します。オプションですが、 @IgnoreJsonProperties アノテーションを付けておくと後々キーを追加したときにエラーにならずに済みます。

@JsonIgnoreProperties(ignoreUnknown = true)

public class Status {
private boolean active;
private String message;

public Status() {
}

public boolean isActive() {
return active;
}

public String getMessage() {
return message;
}
}

次に、実際に受け取る部分を書きます。 Firebase ではデータの受け取りは常に非同期でコールバック経由で行われます。

Firebase statusRef = new Firebase(FIREBASE_ROOT);

statusRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Status status = dataSnapshot.getValue(Status.class);
// ここで通知のブロードキャストを投げる
}

@Override
public void onCancelled(FirebaseError firebaseError) {
// エラー時の処理、今回は特に何もしない
}
});

FIREBASE_ROOT には、 Firebase のデータ URL を書きます。これは先のデータ編集画面で、 status をクリックして開いた (status がルートになった) 状態のブラウザのアドレスバーに表示される URL そのものです。今回の場合は以下のアドレスになります。

https://fiery-inferno-8489.firebaseio.com/status

今回のアプリのメンテナンス情報は、アプリケーション全体で反映されるべき情報なので、この処理も Application#onCreate() のタイミングで呼ばれるようにします。 Activity などではブロードキャストイベントを受信して画面に反映するようにします。サンプルではブロードキャストにはイベント通知に実装が手軽な otto を使っていますが、 LocalBroadcastManager でも何でも問題ありません。

あとは受け取る側の Activity 側の実装を書きます。

@Override

protected void onResume() {
super.onResume();
// イベントリスナを登録
MaintenanceMode.bus().register(this);
}

@Override
protected void onPause() {
super.onPause();
// イベントリスナの登録解除
MaintenanceMode.bus().unregister(this);
}

@Produce
public MaintenanceMode.Status getCurrentMaintenanceMode() {
// 起動直後に呼び出される処理。現在知りうる最新のデータを返す。
return MaintenanceMode.current();
}

@Subscribe
public void maintenanceModeChanged(MaintenanceMode.Status status) {
// statusに変更があった場合に呼び出されるのでSnackbarを表示/非表示にする
if (!status.isActive()) {
bar = Snackbar.make(findViewById(R.id.coordinatorLayout), status.getMessage(), Snackbar.LENGTH_INDEFINITE);
bar.show();
} else if (bar != null && bar.isShown()) {
bar.dismiss();
}
}

できあがった最終的な実装はこちら


まとめ

ちょっとした仕組みですが、いざというときにあるととても役立ちます。機能追加の例としては、


  • URL を付けられるようにしてお知らせページへ誘導する

  • アプリケーションのバージョンやOSのバージョンにあわせて出し分けられる

  • FirebaseのREST APIを叩いてSlackから更新する仕組みを作る

などなどが考えられます。

ちなみに Firebase は同時 100 接続までは無料ですが、それを超える接続数を捌くためには月額 $25 からの利用料が掛かります。ただそれで接続数が無制限になるので、お仕事としてアプリを運用しているならサーバ代と考えれば全然元は取れると思います。もちろん普通にバックエンドとして使っても便利です。


追記

実機で試せるようにしてみました:

いま上のgifのように10秒に1回メンテになったり問題解消したりするbotを動かしてます。実際に手元で起動直後の動作を見たり、マナーモードにして実際に通信切ってみたりしたときの動作が試せます。