最近「白猫プロジェクト」をやりまくってる「僧職系エンジニア」です。
ミラLv100、アンナLv98、クライブLv90のPTで頑張ってます!
今回は実際の業務でiOS/Androidの非同期通信のロジックを書いてみて
感じたことと、実際にどうやったらその問題を解決出来るかを記事にしました。 同じ事に悩んでるネイティブエンジニアの役に立てたらいいなぁ。
特に「非同期処理」の移行作業が一番ストレス感じます。禿げるぐらいに。
「iOSだとアレだったけど、Androidはコレだったな」とインタフェースの実装の差に毎回少しずつ精神が削られていきます。
そんなときもiOSとAndroidで実装方法違う+可読性低下により、段々泣きそうになります。
思わず心の中で(Node.jsで実装してるんじゃねーんだよ...)と呟くぐらいに。
そこらへんも兼ねて、何とかうまい事できないものかなぁと模索してたところ
「そういえば前にそれっぽいライブラリをどこかで見たなぁ」と思い出し、記憶を遡ったところ
今回のお題であるBolts(iOS/Android)に当たったわけです。
Facebookに買収されたParseチームが作ったそうな。
最新のFacebookiOSSDKなどでも使用されています。
公式ページには既に「Swiftだとこう書くよ!」と暖かく新言語サポートもしてくれており期待出来ます!
ではこれで何が出来るのか?
一度BoltsのTaskにラップしてしまえば、
それ以外にも機能はあるんですが、今回は使用しないので一旦無視します。
BFTask、Task<TResult>には処理の実行ステータスや処理結果、エラーなどを設定できるAPIがあるので、非同期処理の終了時にこれらを呼んでやります。
結果やエラーのどちらかを設定することにより呼び出し元のcontinueWithXXX内部の処理がトリガーされる仕組みになっています。
用意したTaskオブジェクトは、以下のように実行します。
内容は「都市名、国を引数で渡し、成功時には天気の結果をJSONで返し、失敗の場合はエラー情報を返す」です。
天気情報に関してはOpenWeatherMapのAPIを使用させていただいており、
通信部分に関してはiOSはAFNetworking
AndroidはVolleyを使用しています。
移行するときに疲れます。では続いてBoltsを使用したケースを見てみましょう。
とまぁ、導入前に比べると色々考えることがなくなり、移行作業が楽になりますね。
もう少しルール決めをしっかりすれば、軽めのコンバーターを作って、
コンバーターを実行するともう片方のソースに変換するぐらいのことは出来そうな感じしませんか?しますよね?
そうすると移行コストが結構減る!->他の事に時間が使えるという幸せ連鎖が発動します。
個人的には導入後の方が移行コストが格段に下がった感じがしたので
現状のプロジェクトでも途中から導入し始めました。
快く協力してくれた同じチームの何でも屋に感謝でございます。 「continueWithXXX」って書くのが長いと感じており、iOSアプリだけでいい人は
PromiseKitの方が記述量が減ると思います。
ミラLv100、アンナLv98、クライブLv90のPTで頑張ってます!
今回は実際の業務でiOS/Androidの非同期通信のロジックを書いてみて
感じたことと、実際にどうやったらその問題を解決出来るかを記事にしました。 同じ事に悩んでるネイティブエンジニアの役に立てたらいいなぁ。
iOS/Android開発の移行作業について
そもそも異なる言語を使っているため、移行作業はどうしても発生しますがその中でも特に「非同期処理」の移行作業が一番ストレス感じます。禿げるぐらいに。
非同期処理のインタフェース設計
非同期処理なので処理終了後に呼び出してもらいたい処理をiOSだとBlock、AndroidだとInterfaceでメソッドに渡しますが、そのパターンも- 成功時と失敗時は別々のコールバックで処理する
- 成功時と失敗時は同一のコールバックで処理する
- 失敗時は無視して、成功時のみ処理する
移行する際の思考の切り替え
インタフェースが決まっても、実際にどちらかからロジックを移行する際に「iOSだとアレだったけど、Androidはコレだったな」とインタフェースの実装の差に毎回少しずつ精神が削られていきます。
コールバックがネストした場合の制御
単一のコールバックだったらまだしも、Aが終わったらBみたいな流れを実装するケースは存在します。そんなときもiOSとAndroidで実装方法違う+可読性低下により、段々泣きそうになります。
思わず心の中で(Node.jsで実装してるんじゃねーんだよ...)と呟くぐらいに。
何とかならないもんか?
「全部1人で出来るもん」の人は全然苦にならないと思いますが、大体の開発はチームで行うのでそこらへんも兼ねて、何とかうまい事できないものかなぁと模索してたところ
「そういえば前にそれっぽいライブラリをどこかで見たなぁ」と思い出し、記憶を遡ったところ
今回のお題であるBolts(iOS/Android)に当たったわけです。
What is Bolts?
iOS/Android両対応のライブラリです。同じようなAPIで扱うことができます。Facebookに買収されたParseチームが作ったそうな。
最新のFacebookiOSSDKなどでも使用されています。
公式ページには既に「Swiftだとこう書くよ!」と暖かく新言語サポートもしてくれており期待出来ます!
ではこれで何が出来るのか?
jsのpromiseのような機能が使えるよ!
アプリを構築する上で、様々な非同期処理を各々専用のライブラリを用いて実装すると思いますが、それらを全て、Boltsの提供するTaskというオブジェクトでラップします。一度BoltsのTaskにラップしてしまえば、
- 並列処理
- 直列処理
- インタフェースの統一
それ以外にも機能はあるんですが、今回は使用しないので一旦無視します。
Boltsを使用した処理フロー
非同期処理をBoltsを使用して整理するには、先に述べた通り、まず個別の非同期処理をTaskでラップします。 具体的には、iOSではBFTask、AndroidではTask<TResult>をcreateし、実際の非同期処理(例えばHTTP通信など)を開始してから、Taskオブジェクトを返します。BFTask、Task<TResult>には処理の実行ステータスや処理結果、エラーなどを設定できるAPIがあるので、非同期処理の終了時にこれらを呼んでやります。
結果やエラーのどちらかを設定することにより呼び出し元のcontinueWithXXX内部の処理がトリガーされる仕組みになっています。
用意したTaskオブジェクトは、以下のように実行します。
Taskを返すメソッド.continueWithXXX( // ここで非同期処理後の内容を制御する. // nullを返す事によりこのスコープの処理を終わらせる. return null; );Aが終わった後にBを実行するなどの制御が必要な場合は
// A Taskを返すメソッド.continueWithXXX( return Taskを返すメソッド; // B ).continueWithXXXX( // タスク return null; );の様に処理をつなげていきます。 では実際にiOSとAndroidのロジックをBotls対応にしてみます。
Bolts導入前のiOS/Androidの非同期ロジック
まずは、よくある非同期通信のロジックを、Boltsを使わずに書いてみます。内容は「都市名、国を引数で渡し、成功時には天気の結果をJSONで返し、失敗の場合はエラー情報を返す」です。
天気情報に関してはOpenWeatherMapのAPIを使用させていただいており、
通信部分に関してはiOSはAFNetworking
AndroidはVolleyを使用しています。
iOS
OpenWeatherMapManager.h#import <Foundation/Foundation.h> @interface OpenWeatherMapManager : NSObject /** 天気情報取得(Bolts非対応) */ -(void) getWeatherInfo:(NSString*)city country:(NSString*)country success:(void(^)(NSDictionary* json))success failure:(void(^)(NSError* error))failure; @endOpenWeatherMapManager.m
#import "OpenWeatherMapManager.h" #import "AFNetworking.h" @implementation OpenWeatherMapManager -(void) getWeatherInfo:(NSString*)city country:(NSString*)country success:(void(^)(NSDictionary* json))success failure:(void(^)(NSError* error))failure { AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.responseSerializer = [AFJSONResponseSerializer serializer]; [manager GET:@"http://api.openweathermap.org/data/2.5/weather" parameters:@{@"q":[NSString stringWithFormat:@"%@,%@", city, country]} success:^(AFHTTPRequestOperation *operation, id responseObject) { if(success) { success(responseObject); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { if(failure) { failure(error); } }]; }呼び出し元
[[OpenWeatherMapManager new] getWeatherInfo:@"Tokyo" country:@"jp" success:^(NSDictionary *result) { NSLog(@"お天気情報:%@", result); } failure:^(NSError *error) { NSLog(@"エラーになりました。%@", error.description); }];
Android
OpenWeatherMapManager.javapublic class OpenWeatherMapManager { private Context context; public OpenWeatherMapManager(final Context context) { this.context = context; } /** * コールバック用 */ public interface WeatherMapResult { public void success(JSONObject json); public void failure(Exception e); } /** * 天気情報取得(Bolts非対応) * OMVollyRequestはRequestを継承した独自クラスです。 * OMApplicationにはApplicationを継承した独自クラスで、メンバー変数にVolleyのRequestQueueを持っています。 */ public void getWeatherInfo(String city, String country, final WeatherMapResult callback) { String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "," + country; // リクエストを作成し OMVollyRequest request = new OMVollyRequest(Request.Method.GET, url, null, new Response.Listener() { @Override public void onResponse(JSONObject response) { if(callback != null) { callback.success(response); } } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { if(callback != null) { callback.failure(error); } } }); // Queueに追加する ((OMApplication)context).getRequestQueue().add(request); } }呼び出し元
// MainActivityからの呼び出し例 OpenWeatherMapManager manager = new OpenWeatherMapManager(getApplication()); manager.getWeatherInfo("Tokyo", "jp", new OpenWeatherMapManager.WeatherMapResult() { @Override public void success(JSONObject json) { Log.i("OWM", "お天気情報:" + json.toString()); } @Override public void failure(Exception e) { Log.i("OWM", "エラーになりました。:" + e.toString()); } });まぁこれでも悪くはないんですが、先に書いた通りインタフェースが違うので
移行するときに疲れます。では続いてBoltsを使用したケースを見てみましょう。
Bolts導入後のiOS/Androidの非同期ロジック
OpenWeatherMapManager.h#import <Foundation/Foundation.h> @class BFTask; @interface OpenWeatherMapManager : NSObject /** 天気情報取得(Bolts対応) コールバックが消えてるだと... */ -(BFTask*) getWeatherInfo:(NSString*)city country:(NSString*)country; @endOpenWeatherMapManager.m
#import "OpenWeatherMapManager.h" #import "AFNetworking.h" #import "BFTask.h" #import "BFTaskCompletionSource.h" @implementation OpenWeatherMapManager -(BFTask*) getWeatherInfo:(NSString*)city country:(NSString*)country { BFTaskCompletionSource *task = [BFTaskCompletionSource taskCompletionSource]; AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.responseSerializer = [AFJSONResponseSerializer serializer]; [manager GET:@"http://api.openweathermap.org/data/2.5/weather" parameters:@{@"q":[NSString stringWithFormat:@"%@,%@", city, country]} success:^(AFHTTPRequestOperation *operation, id responseObject) { // 成功時にはresultに値を設定し [task setResult:responseObject]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // 失敗時にはerrorに値を設定 [task setError:error]; }]; return task.task; } @end呼び出し元
[[[OpenWeatherMapManager new] getWeatherInfo:@"Tokyo" country:@"jp"] continueWithBlock:^id(BFTask *task) { // task.isCancelledでtaskのキャンセル判定も出来ます。 if(task.error){ NSLog(@"エラーになりました。%@", error.description); }else{ NSLog(@"お天気情報:%@", result); } return nil; }];
Android
OpenWeatherMapManager.javapublic class OpenWeatherMapManager { private Context context; public OpenWeatherMapManager(final Context context) { this.context = context; } public Task getWeatherInfo(String city, String country) { // iOSと同じくタスクを生成し final Task.TaskCompletionSource task = Task. create(); String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "," + country; OMVollyRequest request = new OMVollyRequest(Request.Method.GET, url, null, new Response.Listener() { @Override public void onResponse(JSONObject response) { // 結果を設定 task.setResult(response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // エラーを設定 task.setError(error); } }); ((OMApplication)context).getRequestQueue().add(request); return task.getTask(); } }呼び出し元
// MainActivityからの呼び出し例 OpenWeatherMapManager manager = new OpenWeatherMapManager(((MainActivity) getActivity()).getLDApplication()); manager.getWeatherInfo("Tokyo", "jp").continueWith(new Continuation<JSONObject, Object>() { @Override public Object then(Task jsonObjectTask) throws Exception { if(jsonObjectTask.getError() != null) { Log.i("OWM", "エラーになりました。:" + jsonObjectTask.getError().toString()); }else{ Log.i("OWM", "お天気情報:" + jsonObjectTask.getResult().toString()); } return null; } });なんということでしょう、Boltsのお力添えによりインタフェースが統一され、可読性がアップしております。
とまぁ、導入前に比べると色々考えることがなくなり、移行作業が楽になりますね。
もう少しルール決めをしっかりすれば、軽めのコンバーターを作って、
コンバーターを実行するともう片方のソースに変換するぐらいのことは出来そうな感じしませんか?しますよね?
そうすると移行コストが結構減る!->他の事に時間が使えるという幸せ連鎖が発動します。
最後に
ということで、Boltsを触ってみましたがいかがでしたでしょうか?個人的には導入後の方が移行コストが格段に下がった感じがしたので
現状のプロジェクトでも途中から導入し始めました。
快く協力してくれた同じチームの何でも屋に感謝でございます。 「continueWithXXX」って書くのが長いと感じており、iOSアプリだけでいい人は
PromiseKitの方が記述量が減ると思います。