Mobile Services の Android SDK で Future のサポートと大幅な変更を実施
このポストは、8 月 4 日に投稿した Futures support and breaking changes in the Mobile Services Android SDK の翻訳です。
このたび、Azure Mobile Services の Android SDK においてメジャー アップデートがリリースされました。Android での非同期呼び出しの利用方法に関して皆様からお寄せいただいたご意見に基づいて、すべての非同期処理で Future (英語) がサポートされるようになりました。これにより、いくつものコールバックをネストすることなく、複数の非同期処理を (バックグラウンドのスレッドで) 簡単に実行できるようになります。今回の変更は、既存の機能に追加的に提供される (つまり、コールバック パラメーターを取るメソッドと共存する形で、Future を返す新しいメソッドが提供される) ケースがほとんどですが、より高度なシナリオで使用される一部の基本的なインターフェイス (ServiceFilter (英語) など) については大幅な変更が行われました。そのため、Mobile Services の Android SDK で非同期処理を実行する際に、Future が既定のモデルとなりました。今回のリリースではオフラインのサポートも追加されていますが、ここでは大幅な変更点に絞ってご説明したいため、オフラインについては今後のブログでご説明することにします。
このブログの以降のセクションでは、サポートされる新機能と一連の大幅な変更点についてご説明します。新しい SDK をお試しになりたい方は、https://aka.ms/Iajk6q (英語) で入手いただけます。ただし、あくまでもアルファ版であり変更される可能性があることをご了承ください。
Future
ここでは新しくサポートされた Future を使用してコードをよりシンプルに記述する例をご紹介します。複数の処理を実行する必要がある場合、これまではコールバックのネストによって作業に支障をきたすことがありました。たとえば、クエリ結果によってアイテムを更新するという処理を繰り返し実行する場合、コールバック ベースだと次のようにコードが複雑になる可能性があります。
final MobileServiceTable<TodoItem> table = mClient.getTable(TodoItem.class);
table.where().field("complete").eq(false).execute(new TableQueryCallback<TodoItem>() {
@Override
public void onCompleted(final List<TodoItem> items, int unused,
Exception error, ServiceFilterResponse response) {
TableOperationCallback<TodoItem> updateCallback = new TableOperationCallback<TodoItem>() {
private int mIndex;
@Override
public void onCompleted(TodoItem updated,
Exception error, ServiceFilterResponse response) {
mIndex++;
if (mIndex == items.size()) {
tv.setText("Marked all items as complete");
} else {
TodoItem item = items.get(mIndex);
item.setComplete(true);
table.update(item, this);
}
}
};
if (items.size() > 0) {
TodoItem first = items.get(0);
first.setComplete(true);
table.update(first, updateCallback);
}
}
});
一方、Future がサポートされている場合、コードをバックグラウンドのスレッドで実行すれば、よりわかりやすい形で呼び出しが可能になります。以下のコードは上記のコードと同じ処理を実行するものですが、6 行に集約される形となっています (ここでは、一部の例外処理とスレッドのスキップが分散されています。これらは、コードが UI スレッド外で実行される場合、あるいは UI コンポーネントを変更する必要がない場合は不要となります)。
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
final MobileServiceTable<TodoItem> table = mClient.getTable(TodoItem.class);
try {
MobileServiceList<TodoItem> results = table.where().field("complete").eq(false).execute().get();
for (TodoItem todoItem : results) {
todoItem.setComplete(true);
table.update(todoItem).get();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText("Marked all items as complete");
}
});
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}.execute();
上記のコードを小さくできたのは一部の例外処理とスレッドのスキップを分散させたためですが、さらにコードのロジックも以下の 6 行に集約されています。
final MobileServiceTable<TodoItem> table = mClient.getTable(TodoItem.class);
MobileServiceList<TodoItem> results = table.where().field("complete").eq(false).execute().get();
for (TodoItem todoItem : results) {
todoItem.setComplete(true);
table.update(todoItem).get();
}
このシンプルなプログラミング モデルこそが (特に Future の順次実行を念頭に置いた場合に)、Future を使用する主な利点です。またこのほかにも、キャンセルや複数の Future を簡単に結合 (join) するなどの機能が提供されています。
新しい API
Future のメリットについてはおわかりいただけたかと思いますが、実際にはどのように使えばよいのでしょうか? メインの処理、つまりテーブルやカスタム API に関しては、これまでコールバック パラメーターを取っていた個々のメソッドに新たなオーバーロードが追加されています。これらは、コールバック パラメーターを取るのではなく、Future インターフェイスを戻します。既存のアプリケーションの変更を最小限に抑えるために現在のコールバック ベースのメソッドは残されていますが、これらは @deprecated とマーク付けされて、将来のリリース (メジャー バージョンの更新) で廃止される可能性があります。これが該当するのは、カスタム API (たとえば、MobileServiceClient クラス (英語) の invokeApi メソッドにはこれまで 9 種類のオーバーロードがありましたが、これが 18 種類になりました)、プッシュ登録/登録解除、型が指定されたテーブルと型が指定されていないテーブル (すべての CRUD 処理)、それにログイン (すべてのオーバーロード) です。例として以下に示したのが、invokeApi メソッドの古いオーバーロードです。
public <E> void invokeApi(String apiName, Class<E> clazz, ApiOperationCallback<E> callback)
public <E> void invokeApi(String apiName, Object body, Class<E> clazz, ApiOperationCallback<E> callback)
public <E> void invokeApi(String apiName, String httpMethod, List<Pair<String, String>> parameters, Class<E> clazz, ApiOperationCallback<E> callback)
public <E> void invokeApi(String apiName, Object body, String httpMethod, List<Pair<String, String>> parameters, final Class<E> clazz, final ApiOperationCallback<E> callback)
public void invokeApi(String apiName, ApiJsonOperationCallback callback)
public void invokeApi(String apiName, JsonElement body, ApiJsonOperationCallback callback)
public void invokeApi(String apiName, String httpMethod, List<Pair<String, String>> parameters, ApiJsonOperationCallback callback)
public void invokeApi(String apiName, JsonElement body, String httpMethod, List<Pair<String, String>> parameters, final ApiJsonOperationCallback callback)
public void invokeApi(String apiName, byte[] content, String httpMethod, List<Pair<String, String>> requestHeaders, List<Pair<String, String>> parameters, final ServiceFilterResponseCallback callback)
また、以下に示したものが、既存のメソッドと共存する形で提供された新しいバージョンのメソッドです。
public <E> ListenableFuture<E> invokeApi(String apiName, Class<E> clazz)
public <E> ListenableFuture<E> invokeApi(String apiName, Object body, Class<E> clazz)
public <E> ListenableFuture<E> invokeApi(String apiName, String httpMethod, List<Pair<String, String>> parameters, Class<E> clazz)
public <E> ListenableFuture<E> invokeApi(String apiName, Object body, String httpMethod, List<Pair<String, String>> parameters, final Class<E> clazz)
public ListenableFuture<JsonElement> invokeApi(String apiName)
public ListenableFuture<JsonElement> invokeApi(String apiName, JsonElement body)
public ListenableFuture<JsonElement> invokeApi(String apiName, String httpMethod, List<Pair<String, String>> parameters)
public ListenableFuture<JsonElement> invokeApi(String apiName, JsonElement body, String httpMethod, List<Pair<String, String>> parameters)
public ListenableFuture<ServiceFilterResponse> invokeApi(String apiName, byte[] content, String httpMethod, List<Pair<String, String>> requestHeaders, List<Pair<String, String>> parameters)
大きな変更点
Future のみ/コールバックなしのモデルに移行する場合、既存のメソッドを非推奨とし、Future (英語) の結果を返す新しいメソッドを追加することで主なクラスの下位互換性を維持することができます。今回私たちが採用したのがこの方法です。コールバック ベースでありながら Future メソッドを追加できなかったインターフェイスも一部あります (当該メソッドを実装している何らかのクラスに支障が起きる恐れがあるためです)。すべてのインターフェイスを重複させてクラスで新旧のいずれも実装可能とすることもできましたが、それではパッケージが無用に大きくなる恐れがありました。そこで私たちは、Future ベースのコードに移行する中で、これらのインターフェイスを大幅に変更することに決めました。これが、今回のリリースでメジャー バージョン番号を上げることにした理由です。さらに大幅な変更を鋭意進める中で、一部コードのクリーンアップを実施しました。たとえば、(やや大きめの) com.microsoft.windowsazure.mobileservices パッケージ (英語) を複数の「サブパッケージ」に分割することで、クラスをわかりやすく整理できるようになりました。また、コードの記述が少なくて済むように、クエリ結果の変更も行いました。それではこうした大幅な変更点についてご説明しながら、コードを記述する際にどう対処すればよいかを見ていくことにしましょう。
パッケージの変更
com.microsoft.windowsazure.mobileservices パッケージ (英語) に含まれる一連のパブリック クラスが、新しいパッケージにどのように分割されているかを以下に示します。「(*)」が付いているものは追加の変更がなされており、後ほどご説明します。
- com.microsoft.windowsazure.mobileservices.authentication
- MobileServiceAuthenticationProvider
- MobileServiceUser
- com.microsoft.windowsazure.mobileservices.http
- AndroidHttpClientFactory
- AndroidHttpClientFactoryImpl
- NextServiceFilterCallback (*)
- ServiceFilter (*)
- ServiceFilterRequest
- ServiceFilterResponse
- com.microsoft.windowsazure.mobileservices.notifications
- GcmNativeRegistration
- GcmTemplateRegistration
- MobileServicePush
- Registration
- RegistrationCallback
- RegistrationGoneException
- TemplateRegistration
- TemplateRegistrationCallback
- UnregisterCallback
- com.microsoft.windowsazure.mobileservices.table
- MobileServiceJsonTable
- MobileServicePreconditionFailedException
- MobileServicePreconditionFailedExceptionBase
- MobileServiceSystemProperty
- MobileServiceTable
- TableDeleteCallback
- TableJsonOperationCallback
- TableJsonQueryCallback
- TableOperationCallback
- TableQueryCallback
- com.microsoft.windowsazure.mobileservices.table.query
- MobileServiceQuery (*)
- Query に名称変更されました。ExecutableQuery<E> および ExecutableJsonQuery という 2 つの新しいサブクラスが提供されます。
- MobileServiceQuery (*)
- MobileServiceQueryOperations
- タイプを指定したクエリの作成に使用する静的動作を実行するクラス。QueryOperations に名称変更されました。
- QueryOrder
数多くのパッケージ変更がなされていますが、Eclipse で自動的に対処できます。[Source] メニューで [Organize Imports] (または Ctrl+Shift+O) を選択してください。
MobileServiceQuery
MobileServiceQuery インターフェイスは、テーブルのクエリを表すために使用するもので、「E」型のパラメーターを持つジェネリック クラスでした。しかし、型を指定する方法 (シリアル化) と型を指定しない方法 (JSON) のいずれでも使用でき、SDK の他の使用パターンとは異なるものとなっていました。そこで、クエリ オブジェクトの型として、型を指定する場合は ExecutableQuery<E> を、型を指定しない場合は ExecutableJsonQuery を使用できるようにしました。変数にクエリ オブジェクトを格納しない場合は、コードを更新する必要はありません。一例として、SDK の既存のバージョン (1.1.6) で動作する以下のコードをご覧ください。
public class QueryChangesDemo {
private MobileServiceTable<TodoItem> typedTable;
private TableQueryCallback<TodoItem> typedCallback;
private MobileServiceJsonTable jsonTable;
public QueryChangesDemo(MobileServiceClient client) {
typedTable = client.getTable(TodoItem.class);
jsonTable = client.getTable("TodoItem");
typedCallback = typedCallback = new TableQueryCallback<TodoItem>() {
@Override
public void onCompleted(List<TodoItem> results, int count, Exception error, ServiceFilterResponse response) {
}
};
}
public void before() {
// クエリ オブジェクトへの割り当てを行わない - 変更は不要
typedTable.where().field("complete").eq(false).execute(typedCallback);
// 'query' の型が変更
MobileServiceQuery<TableQueryCallback<Todoitem>> query = typedTable.where().field("complete").eq(false);
query.execute(typedCallback);
// TableJsonQueryCallback.onCompleted の 'count' パラメーターは廃止
TableJsonQueryCallback jsonCallback = new TableJsonQueryCallback() {
@Override
public void onCompleted(JsonElement result, int count, Exception error,
ServiceFilterResponse response) {
}
};
// クエリ オブジェクトへの割り当てを行わない - 変更は不要
jsonTable.where().field("complete").eq(false).execute(jsonCallback);
// 'jsonQuery' 変数の型が変更
MobileServiceQuery<TableJsonQueryCallback> jsonQuery = jsonTable.where().field("complete").eq(false);
jsonQuery.execute(jsonCallback);
}
}
ここで新しい SDK (2.x) を使用する際、実行前にクエリを変数へ割り当てる必要がある場合は変数の型を変更しなければなりませんが、そうでなければ変更の必要はありません。
public void after() {
// クエリ オブジェクトへの割り当てを行わない - 変更は不要
typedTable.where().field("complete").eq(false).execute(typedCallback);
// 'query' の型が ExecutableQuery<TodoItem> に変更
ExecutableQuery<Todoitem> query = typedTable.where().field("complete").eq(false);
query.execute(typedCallback);
// TableJsonQueryCallback.onCompleted の 'count' パラメーターは廃止
TableJsonQueryCallback jsonCallback = new TableJsonQueryCallback() {
@Override
public void onCompleted(JsonElement result, Exception error,
ServiceFilterResponse response) {
}
};
// クエリ オブジェクトへの割り当てを行わない - 変更は不要
jsonTable.where().field("complete").eq(false).execute(jsonCallback);
// 'jsonQuery' 変数の型が変更
ExecutableJsonQuery jsonQuery = jsonTable.where().field("complete").eq(false);
jsonQuery.execute(jsonCallback);
}
もう一つ変更された点としては、TableJsonQueryCallback インターフェイスの onCompleted メソッドで「count」パラメーターが指定されていましたが、この値が入力されることはありませんでした (これが使用されたとしたらエラーです)。今回、Android API とマネージ コードの連携を図るために、このパラメーターは廃止されました。
サービス フィルター
前述したように、サービス フィルターは内部的に高度なシナリオで使用されます。こうしたフィルターの定義をコールバック ベースから Future ベースに変更しました。これにより、ServiceFilter.handleRequest メソッドと NextServiceFilterCallback.onNext メソッドはコールバック パラメーターを取るのではなく、ListableFuture (英語) を返すように変更されました。これ以外については動作の変更はありません。例として、以下に「アイデンティティ」用のサービス フィルターを示します。このフィルターはパイプラインの次のフィルターにメッセージを渡します。
ServiceFilter identityFilter = new ServiceFilter() {
@Override
public ListenableFuture<ServiceFilterResponse> handleRequest(
ServiceFilterRequest request, NextServiceFilterCallback next) {
return next.onNext(request);
}
};
先に述べたように、こうしたフィルターは高度なユーザーのみが必要とするものなので、多くのユーザーの皆様はこの変更について気にされる必要はないかもしれません。
まとめ
ここでは SDK に関する皆様からのご意見に基づいて Android SDK に追加した Future サポートについてご紹介しました。また、一部大幅な変更を実施した点についてもご説明しました。今回の SDK はアルファ版としてリリースされたものであり、今後、皆様からお寄せいただくコメント、ご提案、ご意見を参考にしながら、より広範囲へ向けたリリースを目指したいと考えています。今後、同じく追加されたオフライン サポートについての記事も発信するつもりです。今や Android SDK は管理された iOS 版の SDK に引けを取るものではありません。この SDK は https://aka.ms/Iajk6q (英語) でダウンロードできますので、ぜひお試しいただき貴重なご意見をこのブログのコメント欄、あるいは MSDN フォーラムまでお寄せください。