retrofitでAPIを楽に使う
androidから何かサーバ側のAPIを叩く場合、色々と用意しなければならない。OkHttpなどのHTTP通信を行うインスタンス、通信は基本非同期処理なのでコールバックを渡すためのラッパー、それにAPIのURLとパラメータのチェック、APIから返ってきた値をコンバートするためのものなど…それらの面倒事を一手に引き受けてくれるのがretrofitだ。
概要
retrofitはHTTPクライアント。javaのインターフェース形式でAPIを定義出来る。こんな風に。
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
/users/{user}/reposというパスに対してGETアクセスをするというメソッドがこれだけで定義できる。{user}は可変のユーザー名。userに入る引数とともにこのlistReposというメソッドを呼んであげればそれでHTTPGETが出来る。返り値はRepoクラスのインスタンスのListとなる。あとはbuilderを使ってこのインターフェースを実装したインスタンスを作ればオールOK。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .build(); GitHubService service = retrofit.create(GitHubService.class); Call<List<Repo>> repos = service.listRepos("octocat");
当然GETだけでなくPOSTやPUTやDELETEも可能。formEncordedなものや画像データ、マルチパートも投げることができる。 内部的にはOkHttpやVolleyなどをクライアントとして利用することができる。なのでOkHttpに噛ませたインターセプターなどともバッティングすることなく利用することができる。 また、レスポンスはjacksonやgsonなどのコンバータがサポートされているので、サポートされているもののうち好きなコンバータを使えばレスポンスjson=>javaオブジェクトへの変換も勝手にやってくれる。
導入
android studioでgradleを使っているなら以下をいれてsyncすればOK。
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
バージョンは2をしていしたほうがいいだろう。それまではバージョン1.9がメインだったものの、夏ごろにメジャーバージョンがあがった。で、メジャーバージョンがあがったのにつれてインターフェースの宣言の仕方などがかなり変わったようだし、今のところは2.0に関しての情報も十分にあるので1.9を選ぶ必要はない。以降はバージョン2.0での使い方を説明する。
jsonのコンバーターを指定する場合はそれ用のライブラリも必要となることに注意。自分はjacksonを使っているので、以下も一緒にbuild.gradleに書き加えてある。
compile 'com.squareup.retrofit:converter-jackson:2.0.0-beta2'
使ってみよう
使う前にはまずレスポンスを格納するEntityクラスが必要なのでそれを作っておく。jacksonをつかっているので、jackson形式でのEntityを定義している。ここは適宜gsonなどに変えればいい。
public class Repo { public int id; public String name; public String url; public boolean private; }
そして重複するがインターフェースの宣言。
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
あとはこれを呼ぶ。
public class GithubActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_github); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .build(); GitHubService service = retrofit.create(GitHubService.class); Call<List<Repo>> call = service.listRepos("shim0mura"); call.enqueue(new Callback<List<Repo>>() { @Override public void onResponse(Response<List<Repo>> response) { if (response.isSuccess()) { List<Repo> result = response.body(); } } @Override public void onFailure(Throwable t) { } }); } }
serviceという変数にretrofitのインターフェースを実装したインスタンスをいれる。そしてそれから定義したメソッドを呼ぶ。そのメソッドの返り値にenqueueしたところで通信を始める。enqueueメソッドの引数はコールバック。通信が終了したところでコールバック内のメソッドが実行される。基本的に20x系のレスポンスでも50xや40xなどのエラー系のレスポンスでも全部onResponseが呼び出される。なので20x系の成功時のレスポンスを判定したいときはisSuccessメソッドで判定を行う。あとはonResponseの仮引数、この場合はresponseというインスタンスのbodyメソッドを呼べば、jsonなどの生のレスポンスは先ほど定義したEntityクラスインスタンスに勝手にコンバートしてくれる。楽ちん。
enqueueを使わずにcall.execute()とすれば非同期でない同期的な通信もできるらしい。使ったこと無いけど…
POSTも出来る
GET以外については以下のようにメソッドを定義する。
@POST("/user") Call<User> createUser( @Body HashMap<String, User>user );
@Bodyアノテーションでpostしたいオブジェクトを指定すればいい。呼び出し時も
new HashMap<String, User> postUser = new HashMap<>(); User user = new User(); user.name = "shim0mura"; user.job ="unemployed"; postUser.put("user", user); Call<User> call = service.createUser(postUser);
とGETとほとんど変わることなく呼び出せる。enqueueなどのコールバック処理も全く同じ。インターフェース側だけ変えればいいだけなのである。 ちなみにわざわざHashMapを渡さず
@POST("/user") Call<User> createUser( @Body User user );
と直にEntityを渡す事もできる。というかそっちの方が普通な使い方。じゃあ何故HashMapなんか使ったのかというと、サーバ側でparams[:user][:name]というrailsっぽい形式で取得したかったため。後者のような普通なpostだとparams[:CreateUser]の中にnameやjobが入っているという形式になってしまう。そのままだとどうやらメソッド名(しかもアッパーキャメル)で内容がpostされてしまう。HashMapを使えば、hashのキーがpost内容のキーになるのでrails使いには楽。
OkHttpを使う
更に、retrofitの内部のHTTPクライアントとしてOkHttpやVolleyなどを使うこともできる。 使い方というか指定の仕方は簡単。
OkHttpClient client = new OkHttpClient(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .client(client) .build();
これだけ。buildの際にclientメソッドを呼び出し、その引数としてOkHttpのインスタンスを入れればいいだけ。なのでそのインスタンスをclientに入れる前にインターセプターを噛ましていても問題ない。
OkHttpClient client = new OkHttpClient(); client.interceptors().add(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(chain.request()); // Do anything with response here return response; } }); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .client(client) .build();
これでちゃんとインターセプターは動作する。
コンバータの指定
更に更にコンバータもHTTPクライアントと同じようにして指定することができる。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://api.nuuneoi.com/base/") .addConverterFactory(JacksonConverterFactory.create()) .build();
これだけ。コンバータをgsonに変えたくなったら、GsonConverterFactory.create()に変えてやればいい。(もちろんbuilde.gradleでgsonコンバータを加えている前提)
という感じで自前の面倒なコンバータやAPIラッパーを作らなくてもretrofitさえあれば大体のことはまかなえる。素敵。
OkHttpでオレオレ認証を通過する
railsなどにAPIサーバとしての機能をもたせたならば、そのAPIを叩く側、つまりandroid側でもオレオレ認証を通過できるようにしたい。デフォルトのandroidのHTTP通信をするDefaultHttpClientなどは分からないけど、今主流のHTTPクライアントなOkHttpなら割と楽にできる。
OkHttpはそれ単体としてコールバック機能をもったHTTPクライアントとしても利用できるし、また最近話題になっているRESTAPIを楽に叩けるインターフェースを備えたretrofitの中でもHTTPクライアントとして利用できる。もちろん以下に書くコードを追加してもretrofitはそれまでどおりに使える。オレオレ認証を通過するという機能を加えて。
実装
public class Customtrust { private static final OkHttpClient client = new OkHttpClient(); public Customtrust(Context context){ client.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); // 証明書の追加 // サーバ側の開発用オレオレ証明書のcrtを直接文字列に書きだしてそれを追加 SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); client.setSslSocketFactory(sslContext.getSocketFactory()); } public void execute(final Request request, final Callback callback){ client.newCall(request).enqueue(callback); } public OkHttpClient getClient(){ return client; } public static OkHttpClient createNewClient(){ OkHttpClient c = new OkHttpClient(); c.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); c.setSslSocketFactory(sslContext.getSocketFactory()); return c; } private static InputStream trustedCertificatesInputStream() { // PEM files for root certificates of Comodo and Entrust. These two CAs are sufficient to view // https://publicobject.com (Comodo) and https://squareup.com (Entrust). But they aren't // sufficient to connect to most HTTPS sites including https://godaddy.com and https://visa.com. // Typically developers will need to get a PEM file from their organization's TLS administrator. String developmentCertificationAuthority = "" + "-----BEGIN CERTIFICATE-----\n" + "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n" + "hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n" + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\n" + "BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\n" + "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\n" + "EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\n" + "Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\n" + "dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n" + "6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\n" + "pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n" + "9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n" + "/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\n" + "Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n" + "+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\n" + "qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\n" + "SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\n" + "u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\n" + "Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\n" + "crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\n" + "FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n" + "/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\n" + "wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n" + "4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n" + "2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\n" + "FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\n" + "CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\n" + "boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\n" + "jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\n" + "S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\n" + "QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n" + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n" + "NVOFBkpdn627G190\n" + "-----END CERTIFICATE-----\n"; return new Buffer() .writeUtf8(developmentCertificationAuthority) .inputStream(); } private static SSLContext sslContextForTrustedCertificates(InputStream in) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in); if (certificates.isEmpty()) { throw new IllegalArgumentException("expected non-empty set of trusted certificates"); } // Put the certificates a key store. char[] password = "password".toCharArray(); // Any password will work. KeyStore keyStore = newEmptyKeyStore(password); int index = 0; for (Certificate certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificate); } // Wrap it up in an SSL context. KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); return sslContext; } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream in = null; // By convention, 'null' creates an empty key store. keyStore.load(in, password); return keyStore; } catch (IOException e) { throw new AssertionError(e); } } }
trustedCertificatesInputStreamというprivateメソッドの中で宣言している長い文字列はSSL通信に必要なサーバ証明書。以下の記事などに書いてあるコマンドによって生成されたサーバ証明書の内容をそのまま貼り付ければOK。
あとはこのOkHttpインスタンスをget出来るようなラッパーを作ればいい。そのラッパーもdaggerなどのDIを使って環境ごとに変更すればより良い。
OkHttpでオレオレ認証通過できるようにする日本語情報が無いなぁ…と色々探した末stackoverflowでこれに関する記事を見つけて喜んだものの、実はOkHttpのgithub内にあるレシピ集にそれが含まれてた。灯台下暗し。
android studioのエミュレータ(ADB)でSDカードを挿す
その昔、eclipseなどのプラグインとしてandroid開発するときについてきたAVD(Android Virtual Device、仮想デバイス、エミュレータ)は死ぬほど起動が遅かった。とんでもなく遅かった。エミュレータの起動が遅すぎて、1回エミュレータを起動している間にアプリを作り上げることができちゃったよHAHAHAなどというジョークも出てきたり出てこなかったりするほどだった。いやまぁ僕がたった今考えたジョークだから出てこなかったんだろうけど。それからGenyMotionというエミュレータが出てきた。virtualboxを利用してエミュレータを起動するのである。それまでのエミュレータに比べると爆速というレベルだった。
そして今、android studioが普及して、AVD周りのサポートがよくなった。GenyMotionを入れなくとも、そこそこ速いエミュレータを利用することが出来る。今後android studio側でスピードアップを含めたエミュレータ周りの改善がなされていくだろう、と考えているので、今はGenyMotionはつかわずandroid studioのAVDマネージャーを通して純正のエミュレータを利用している。
環境: mac OSX10.9
android studio: 1.4.1
で、その中で出会ったエラーというか変な現象。ギャラリーから画像を読み込みたいときは外部ストレージのパーミッションが必要になることがある。アプリ側でパーミッションの許可ダイアログを出して、設定からアプリのパーミッションをいじっても外部ストレージ、つまりはSDカードが読み込まれない。どういうわけか、SDカード自体がAVD側で認識されていないっぽいAVDマネージャーでSDカードを読み込むように設定しても、うまくいかない。 後述のstackoverflowの質問者はこういう感じで挿さってるかどうか確認してる。
//This prints: External: removed Log.d(TAG, "External: " + Environment.getExternalStorageState() );
で、色々探してたらstackoverflowでそれに関することが出てきた。どうやら手動でSDカードを挿さないといけないらしい。手動で、と言ってもAVDマネージャ経由でなくコンフィグを自分で書き換えるという意味。
以下のディレクトリにADVのコンフィグファイルが配置されている。
~/.android/avd/Nexus_5_API_23_x86.avd/config.ini
Nexus5...の部分のディレクトリ名は自分でつくったAVDの名前になるので変わることがある。で、とにかくこのconfig.iniの中にsdカードに関する以下の行が含まれているので、それをyesに書き換える。
hw.sdCard=yes
で、これでADVを再起動すればOK。無事にSDカードが認識された状態になる。 参考:
OkHttpで生のjsonを出力する
OkHttpというか、retrofitの話。retrofitとそのHTTPクライアントとしてOkHttpを使っているという前提。OkHttp単体でもいけるけど。
retrofitを使ってサーバ側とjsonでやり取りをしていると、retrofitで生のjsonレスポンスを見たくても見れない。基本retrofitではコールバック内で取得できるレスポンスはjsonコンバータによってjavaのオブジェクトに変換されているために見れない。見る方法もあるのかもしれないけど探した限りはそれっぽい情報は見つからなかったし、あったとしても今のところはそう簡単に見れないっぽい。
開発中やデバッグ時は生のjsonがどう出てるのか知りたいことが結構あると思うんだけど、それが簡易に出来ないのですごく不便。なのでそこをOkHttpのInterceptorを使って解決する。retrofit側で生のresponsebodyを出すよりもretrofitの中のHTTPクライアントの方で解決するほうが早かった。というかそれしか方法がわからなかった。
実装
public class RawJsonInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); String rawJson = response.body().string(); Log.d("raw json", String.format("raw JSON response is: %s", rawJson)); return response.newBuilder().body(ResponseBody.create(response.body().contentType(), rawJson)).build(); } }
こういう感じのInterceptorを作る。特に難しいことはしていない。ただ、最後returnをしている部分でresponseを再作成しているのは一度しかbodyを読み込めないというOkHttpの仕様があるため。
あとはこれをOkHttpに噛ませればいい。
OkHttpClient client = new OkHttpClient(); client.interceptors().add(new RawJsonInterceptor());