今回のサンプルでは、ステータス関連のリスナーとして「OnStateLoadedListener」「OnStateDeletedListener」を実装します。
OnStateLoadedListenerは、クラウドから状態を読み込んだ際、または読み込んだ状態がコンフリクト(競合)した際に呼び出されるコールバックが定義されています。
OnStateDeletedListenerは、クラウドから状態を削除した際に呼び出されるコールバックが定義されています。
まずはOnStateLoadedListenerの実装から見てみます。実装するメソッドは2つです。
@Override public void onStateLoaded(int statusCode, int stateKey, byte[] data) { Log.d(TAG, "onStateLoaded"); if (statusCode == AppStateClient.STATUS_OK) { // successfully loaded data displayMessage("Loaded: " + toLong(data)); } else if (statusCode == AppStateClient.STATUS_NETWORK_ERROR_STALE_DATA) { // could not connect to get fresh data, // but loaded (possibly out-of-sync) cached data instead displayMessage("Stale data: " + toLong(data)); } else { // handle error displayMessage("Data load error: " + statusCode); } }
onStateLoaded()は状態が読み込まれた際に呼び出されます。statusCodeは読み込みに成功したかどうかを値として持ち、stateKeyはどの状態が読み込まれたかを判別するのに使用します。状態はバイト配列として読み込まれます。サンプルアプリでは、簡単な判定を行い、TextViewに内容を表示しています。
判定内で「STATUS_NETWORK_ERROR_STALE_DATA」を特別扱いしているのは、状態がクラウドから取得はできたものの、それが同期が取られた最新のデータではなく、キャッシュから取得したデータである場合を考慮するためです。クラウドへの保存と取得の間隔が短い場合や、それが高頻度で発生する場合に起こりやすいようです。
サンプルでは、上記のように簡単に済ませていますが、間に通信が挟まり、サービスが自身の管理下にないため、エラーハンドリングは発生する可能性のあるものごとに、きめ細かく行うべきです。ぜひ、Javadocでメソッドの詳細を確認してみてください。
“状態”はバイト配列なので、アプリの状態に限らず、何でもクラウドに保存可能です。
ただハイスコアのような、ゲームに関するデータは、GamesClientでそれ専用のメソッドが提供されているので、ここでは、最終更新日時のようなものを保存することにします。toLong(byte[])がバイト配列をlongに変換するメソッドです。
もう1つのonStateConflict()は、状態がコンフリクトした場合に呼び出されます。
@Override public void onStateConflict(int stateKey, String resolvedVersion, byte[] localData, byte[] serverData) { Log.d(TAG, "onStateConflict"); long localVal = toLong(localData); long serverVal = toLong(serverData); long resolvedVal = Math.max(localVal, serverVal); byte[] resolvedData = toBytes(resolvedVal); getAppStateClient().resolveState(this, STATE_LAST_UPDATE, resolvedVersion, resolvedData); displayMessage("Conflicted: " + resolvedVal); }
このメソッド内では、何らかの方法でコンフリクトを解決しなければなりません。サンプルでは、より大きな値、つまり常に新しいタイムスタンプを採用しています。
コンフリクトは複数の端末でアプリを操作した場合に発生しますし、単一端末でもアプリの作り次第では発生します。クラウドにデータを保存するのも、クラウドからデータを取得するのも、非同期で通信が行われるためです。
分かりやすい例として、上図のようなシーケンスが考えられます。ネットワークが遅延するような環境で発生する可能性があります。
複数端末で操作すると、狙ってコンフリクトを発生できます。ユーザーの利便性を考慮して、コンフリクト時の対応をきちんと設計しておくことが重要です。
必要がなくなった“状態”はクラウドから削除できます。OnStateDeleteListenerを実装しておくことで、削除の際、それが成功したかどうかをコールバックで知ることができます。
@Override public void onStateDeleted(int statusCode, int stateKey) { Log.d(TAG, "onStateDeleted"); if (statusCode == AppStateClient.STATUS_OK) { displayMessage("Deleted"); } else { displayMessage("Delete error: " + statusCode); } }
ここでは失敗時に何もしていませんが、アプリ内にフラグを設けて失敗を管理したり、時間を置いてリトライしたりするのが、より良い実装でしょう。
リスナーを実装し終えたところで、クラウドとの状態の同期方法を説明します。
今回のサンプルでは、クラウドとの状態同期はすべてボタンがトリガーになっています。switch-case文で実装してあるので、以下にまとめて引用します。
case R.id.button_save_to_cloud: // save to cloud long val = System.currentTimeMillis(); getAppStateClient().updateState(STATE_LAST_UPDATE, toBytes(val)); displayMessage("Save data: " + val); break; case R.id.button_load_from_cloud: // load from cloud getAppStateClient().loadState(this, STATE_LAST_UPDATE); break; case R.id.button_delete_from_cloud: // delete from cloud getAppStateClient().deleteState(this, STATE_LAST_UPDATE); break;
上から保存、取得、削除です。
保存は、現在時刻をlongからbyte[]に変換し、AppStateClient#updateState(int, byte[])に渡しています。第1引数のキーがデータを識別する値で、今回は定数で0を使用しています。複数の“状態”を管理する場合、キーがユニークになるようにアプリで管理する必要があります。loadState()メソッドには、リスナーを渡して保存が完了したかどうかをコールバックで受け取れるものも用意されています。必要に応じて使い分けてください。
取得は、AppStateClient#loadState(OnStateLoadedListener, int)で行います。前述のとおり、非同期で取得したデータが第1引数のリスナーにコールバックされます。
削除も取得と同様、AppStateClient#deleteState(OnStateDeleteListener, int)に渡した第1引数のリスナーに、結果が非同期でコールバックされます。
Copyright © ITmedia, Inc. All Rights Reserved.