API処理するhttpクライアントを作る
実際のAPI処理をする場合に、このように毎回get関数やpost関数などを使う場合、同じようなコードを多く記述する必要があります。そこで、共通する処理を行う場合のクラス実装例がリスト6です。
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:chat_app/data/Response.dart';
// (1) http.BaseClientを継承する
class ApiClient extends http.BaseClient{
final String apiKey;
final String baseUrl;
final http.Client _client;
ApiClient(this.apiKey,this.baseUrl,this._client);
// (2) 共通するリクエストの前処理
@override
Future send(http.BaseRequest request) {
request.headers['Content-Type'] = 'application/json';
request.headers['X-Chat-App-Key'] = apiKey;
return _client.send(request);
}
/*
* 初期設定の取得
*/
Future config() async{
var url = Uri.parse(baseUrl + "/login/config.php");
var res = get(url);
// (3) レスポンスを処理する共通化処理
var response = await _parseResponse(res);
if(response.ok){
return Future.value(true);
}
else{
return Future.value(false);
}
}
// : (省略)
// (4) 共通するレスポンス処理
Future _parseResponse(Future response){
// (5) Future処理ができるようにする
var c = Completer();
// (6) httpレスポンス結果の処理
response.then((value){
if(value.statusCode == 200){
var json = jsonDecode(utf8.decode(value.bodyBytes)) as Map;
if(json['ok']){
// (7) データの設定
c.complete(Future.value(Response.ok(json['code'], json['message'],json['body'])));
}
else{
c.complete(Response.error(json['code'], json['message']));
}
}
else{
c.complete(Response.error(value.statusCode, value.body));
}
});
// (8) Futureインスタンス
return c.future;
}
}
リクエスト処理を共通化するためには、(1)のようにhttp.BaseClientクラスを継承したクラスを作成します。このクラスを実装する場合には、(2)のsendメソッドを実装する必要があります。このメソッドでは、リクエストに関する共通処理を実装できます。
今回は、Content-TypeとX-Chat-App-Keyというヘッダを共通処理として設定しています。また、レスポンス処理も共通部分が多いため、(3)のように共通メソッドを作って処理します。その実装部分が(4)です。
共通処理では、httpパッケージでのFutureインスタンスを引数で設定し、アプリケーションでの独自クラスのFutureインスタンスを返すようにします。つまり、Futureで扱うデータクラスの変換を行う処理をしています。
また、その際に、async/awaitキーワードを使わないようにしました。そのためには、(5)の用にCompleterクラスを使います。このクラスは前回紹介したFutureクラスのメソッドでは作れないようなFutureを作ることができます。
例えば、今回のようにデータ準備が他の非同期処理の結果を待って、新たなFutureインスタンスを作成したい場合に利用します。JavaScriptではPromiseのような役割と言えば伝わる人も多いかも知れません。httpレスポンスを受け取れる状態になったら(6)のようにthenメソッドを使って処理をします。
そして、返却するFutureのデータの準備ができたら、(7)のようにcompleteメソッドを使って結果を設定します。最後に、(8)のようにfutureプロパティを返せば、Futureで扱うデータを変換して返すメソッドが作成できます。
作成したAPIクライアントを利用する
先ほど作成したAPIクライアントを利用するようにしたILoginControllerを実装したものがリスト7です。これまでよりもかなりすっきりした実装が可能になります。
import 'package:chat_app/client/ApiClient.dart';
import 'package:chat_app/controller/ILoginController.dart';
import 'package:chat_app/data/Response.dart';
import 'package:http/http.dart' as http;
class ClientLoginController extends ILoginController{
String apiKey;
String baseUrl;
ClientLoginController(this.apiKey,this.baseUrl);
// : (省略)
@override
Future login(String loginId, String password){
// (1) httpクライアントの作成
var client = http.Client();
try{
var api = ApiClient(apiKey, baseUrl, client);
return api.login(loginId, password);
}
finally{
// (2) 切断
client.close();
}
}
}
(1)ではhttpのクライアントを作成します。このクラスはhttp接続を管理するためのクラスです。このインスタンスを共有すれば、一度の接続で複数のリクエストが行えます。ただし、(2)のように切断処理も自分で行う必要があります。
このように必ず、一回の接続で、一回のリクエストしかしない場合には、このインスタンスを先ほどのApiClientクラス内で作成すればより処理が簡略化できるはずです。
最後に
http通信自体は、httpパッケージを使えばそれほど難しくなく行えます。また、async/awaitキーワードを使えば、あまり非同期処理についてあまり意識する必要はないかもしれません。しかし、少々複雑なケースになってくると、非同期処理についてもう少し深いレベルでの理解が求められます。
例えば、複数のhttpリクエストが必要な場合にasync/awaitキーワードだけで実装をしてしまうと、せっかくの非同期で待ち時間を節約できるメリットが活かせない場合があります。場合によっては、新たな非同期結果を作る必要があるため、今回のサンプル程度では少々、大げさですがCompleterを使って新たな非同期結果を作成する例などを紹介しました。
また、サーバから取得したデータは端末側に保存するケースが多々あります。次回は、データを端末に保存する方法について紹介します。
