先日「JXUGC #13 東京 緊急開催 Xamarin のすべて!」に参加してきました。
「DroidKaigi2016 アプリを Xamarin に移植した話」 というセッションで、元のアプリで使用している Java 製のライブラリを .NET 製のライブラリで置き換えたという話があり、その中で Refitというライブラリが出てきました(Java 製 Retrofit2 → .NET 製 Refit に置き換え) 。
Refit は RESTful API を呼び出すクラスを自動生成するライブラリとの事です。
今まで使った事がなかったので、これを機に使ってみました。
実行環境
確認は WPF および Xamarin.Forms(Android、UWP)で行いました。
手順に大きな違いはないので、まとめて説明します。
呼び出し API
実際に API を呼び出して見ないと確認できませんので、今回は GitHub の API を呼び出す事とします。
単純な API としてユーザ名を渡して対応するユーザ情報を取得できる API がありますのでこれを使用します。
呼び出し方は、以下の URL に GET でアクセスするだけです({username} にユーザ名を入れる)。
https://api.github.com/users/{username}
レスポンスとして JSON 形式でユーザ情報が返ってきます。
作業手順
基本的な呼び出しを行うまでに行った作業手順を記載します。
ライブラリ取得
「ソリューションの NuGet パッケージの管理」から Refit をインストールします。
"refit" で検索すると見つかるので、プロジェクトを選択してインストールするだけです。作業を行った時は最新の安定版は 2.4.1 でした。
Xamarin.Forms の場合は、PCL プロジェクトだけではなく .Droid や .UWP のプロジェクトにもインストールします(PCL プロジェクトだけにインストールすると、実行時に例外が発生してしまいます)。
レスポンス情報クラス作成
API 呼び出しのレスポンスで得られる情報を格納するクラスを作成します。
ユーザ情報として得られる情報は多数ありますが、今回は id と name のみ取得してみます。
作成したクラスは以下のような物です。
public class GitHubUser
{
public int Id { get; set; }
public string Name { get; set; }
}
単純に取得する情報のプロパティだけを作成します。
クラス生成用インタフェース作成
API 呼び出しクラスを自動生成するためのインタフェースを作成します。
[Headers("User-Agent: RefitCallSample")]
interface IGitHubApi
{
[Get("/users/{username}")]
Task<GitHubUser> GetUserAsync(string userName);
}
API 呼び出し用オブジェクトを取得するためのメソッドを定義します。
引数には可変パラメータ(この場合はユーザ名)を、戻り値には Task<取得する情報の型> を指定します。
メソッドの属性で以下の情報を指定します。
- HTTP メソッド(この場合は GET)
- URL の可変部分(API 毎に違う部分。ベース URL は別途指定)。
{} で囲まれている箇所は、メソッドの引数で置き換える部分。
なお、インタフェース自体に Headers 属性を付けていますが、これは HTTP のヘッダに User-Agent を追加するための物です。
User-Agent が未指定だと GitHub の API 呼び出しが失敗するために追加しています(指定してあれば何でも問題ないようです)。
API 呼び出し
クラス生成および API 呼び出しは以下のようになります。
var api = RestService.For<IGitHubApi>("https://api.github.com");
var result = await api.GetUserAsync("xamarin");
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
- RestService.For で API 呼び出しクラスのオブジェクトを取得します。
IGitHubApi は作成したインタフェース名、メソッドの引数は API のベース URL です。 - インタフェースで定義したメソッドを使って API を呼び出します。引数はユーザ名です。
- 最後に取得した情報をデバッグ出力しています。
実行するとユーザ情報が取得できました。
エラー処理
ここまでで、API を呼び出して結果を取得するところまでできました。
次に API でエラーが発生した場合の動作を見てみます。
自動生成されたクラスを使った API 呼び出しでエラーが発生した場合は、ApiException がスローされます。
try
{
var api = RestService.For<IGitHubApi>("https://api.github.com");
var result = await api.GetUserAsync("xamarin111");
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
}
catch (ApiException ex)
{
Debug.WriteLine($"StatusCode = {ex.StatusCode}, Content = {ex.Content}");
Debug.WriteLine($"ApiException : {ex.Message}");
}
上記のようにユーザ名として存在しない名前を指定すると API がエラー終了し ApiException がスローされます。
ApiException のプロパティで StatusCode や Content が取得できますのでそれらを見て処理を行います。
今回呼び出した API の場合は、StatusCode は 404、Content は JSON 形式でメッセージ等が取得できます。
なお、ApiException がスローされるのは通信が行えてレスポンスが返ってきている場合です。
通信異常等の場合は別の例外がスローされます(例えばネットワークに接続されていない場合は HttpRequestException がスローされます)。
通信タイムアウト
通信を行っているので、サーバから応答がない場合に備えてタイムアウト時間を指定したいと思います。
Refit のソースを見ると内部で HttpClient を生成していますが、特に Timeout の設定を行っていないようです。
Refit のインタフェースとしてタイムアウト時間を指定する物が見当たらなかったので、外部から設定する方法を何パターンか検討しました。
自動生成クラスの型を使用する
RestService.For を呼び出した時に自動生成されたクラスのオブジェクトが返されます(IGitHubApi インタフェースを渡した場合 AutoGeneratedIGitHubApi というクラスが生成されていました)。
このクラスは、Client という内部で生成した HttpClient を保持するプロパティを持っています。
一度ビルドすれば、このクラスの定義も参照できるようになるので、取得したオブジェクトを as 演算子で変換して使用します。
try
{
var api = RestService.For<IGitHubApi>("https://api.github.com");
(api as AutoGeneratedIGitHubApi).Client.Timeout = TimeSpan.FromMilliseconds(5000);
var result = await api.GetUserAsync(userName);
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
}
catch (ApiException ex)
{
Debug.WriteLine($"StatusCode = {ex.StatusCode}, Content = {ex.Content}");
Debug.WriteLine($"ApiException : {ex.Message}");
}
catch (TaskCanceledException ex)
{
Debug.WriteLine($"TaskCanceledException : {ex.Message}");
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
RestService.For で取得したオブジェクトを AutoGeneratedIGitHubApi 型に変換し、Client.Timeout プロパティにタイムアウト時間を設定します。
この状態で API 呼び出しを行うと、指定したタイムアウト時間を過ぎればタイムアウトします。
この方法でタイムアウトした場合は、TaskCanceledException がスローされます。
リフレクションでプロパティにアクセスする
自動生成されたクラスに Client プロパティがあるのはわかっているので、リフレクションでプロパティにアクセスする事でも同様の指定が行えます。
try
{
var api = RestService.For<IGitHubApi>("https://api.github.com");
var property = api.GetType().GetTypeInfo().GetDeclaredProperty("Client");
var client = property.GetValue(api, null) as HttpClient;
client.Timeout = TimeSpan.FromMilliseconds(5000);
var result = await api.GetUserAsync(userName);
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
}
catch (ApiException ex)
{
Debug.WriteLine($"StatusCode = {ex.StatusCode}, Content = {ex.Content}");
Debug.WriteLine($"ApiException : {ex.Message}");
}
catch (TaskCanceledException ex)
{
Debug.WriteLine($"TaskCanceledException : {ex.Message}");
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
リフレクションを使って "Client" とい名前のプロパティを取得し、Timeout プロパティにタイムアウト時間を設定します。
インタフェースにプロパティを追加する
自動生成されたクラスに Client プロパティが追加されるのはわかっているので、最初に用意するインタフェースに Client プロパティを追加して、インタフェース経由でアクセスできるようにします。
[Headers("User-Agent: RefitCallSample")]
interface IGitHubApi
{
HttpClient Client { get; }
[Get("/users/{username}")]
Task<GitHubUser> GetUserAsync(string userName);
}
try
{
var api = RestService.For<IGitHubApi>("https://api.github.com");
api.Client.Timeout = TimeSpan.FromMilliseconds(5000);
var result = await api.GetUserAsync(userName);
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
}
catch (ApiException ex)
{
Debug.WriteLine($"StatusCode = {ex.StatusCode}, Content = {ex.Content}");
Debug.WriteLine($"ApiException : {ex.Message}");
}
catch (TaskCanceledException ex)
{
Debug.WriteLine($"TaskCanceledException : {ex.Message}");
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
インタフェースに追加してあるので、RestService.For で取得した状態でそのまま Client プロパティにアクセスできます。
Reactive Extensions の Timeout メソッドを使用する
Refit の単体テストソースを見ると、Task<...> ではなく IObservable<...> の形で取得し、Reactive Extensions の Timeout メソッドでタイムアウトするようになっていましたので、この方法も試してみます。
まず、Reactive Extensions が必要になるので NuGet から取得します("Rx-Main" で検索すれば見つかります)。
さらに、インタフェースの戻り値の型を IObservable<...> に変更します。
[Headers("User-Agent: RefitCallSample")]
interface IGitHubApi
{
[Get("/users/{username}")]
IObservable<GitHubUser> GetUserObservableAsync(string userName);
}
そして、API 呼び出しにメソッドチェーンで Timeout メソッドを追加します。
try
{
var api = RestService.For<IGitHubApi>("https://api.github.com");
var result = await api.GetUserObservableAsync(userName).Timeout(TimeSpan.FromMilliseconds(5000));
Debug.WriteLine($"Id = {result.Id}, Name = {result.Name}");
}
catch (ApiException ex)
{
Debug.WriteLine($"StatusCode = {ex.StatusCode}, Content = {ex.Content}");
Debug.WriteLine($"ApiException : {ex.Message}");
}
catch (TimeoutException ex)
{
Debug.WriteLine($"TimeoutException : {ex.Message}");
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
この方法でタイムアウトした場合は、TimeoutException がスローされます。
自動生成されたクラスのプロパティを参照する方法は内部実装に依存していて設計変更があると使用できなくなる可能性がありますので、この方法が一番確実な方法だと思います。
まとめ
非常に簡単な API だけですが Refit を使った呼び出しを試してみました。
今回は試していませんが Refit では POST でパラメータを渡すといった事も可能ですので、一般的な RESTful API であれば対応できそうです。
RESTful API 呼び出しは通信や JSON 解析等同じような処理を作成する事になりますので、その部分を自動生成できる Refit は便利だと思います。
今回試したソースは GitHub にあげてあります。
https://github.com/norimakiXLVI/RefitCallSample