Retrofitを使った通信でjsonオブジェクトを取ってくる処理がよくわからなかったので、調べてまとめてみました
調べることになった経緯も書いてありますので、Retrofitの@Query
や@Path
、Jsonオブジェクトのパースに関してだけ知りたい方は 7, 番外編をご覧ください
お天気アプリを作ってみたい!という方はまず1.プロローグで紹介したサイトのソースコードをコピペしてから、このページを追っていっていただけるといいと思います
(追記)
api keyを抜いた状態で今回のプロジェクトをgithubにpushしました
1. プロローグ (お天気アプリの試作)
ことの発端は
http://dev.classmethod.jp/smartphone/android/okhttp-retrofit-rxandroid/
こちらのサイトのコードを動かしてみようとして、上手くいかなかったことです
AndroidStudioでお天気アプリテスト用のプロジェクトを作成して、ソースコードを移植して実行してみましたが、通信がうまくいっていない様子でした
ログを見たところ401エラーが来ていました
調べてみたらAPI keyが必要とのこと
03-09 21:57:47.841 2976-2992/? D/=NETWORK=: ---> HTTP GET http://api.openweathermap.org/data/2.5/weather?q=Tokyo%2Cjp
03-09 21:57:47.841 2976-2992/? D/=NETWORK=: ---> END HTTP (no body)
03-09 21:57:47.850 2976-2993/? D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
03-09 21:57:47.853 2976-2992/? D/=NETWORK=: ---- ERROR http://api.openweathermap.org/data/2.5/weather?q=Tokyo%2Cjp
03-09 21:57:47.853 2976-2992/? D/=NETWORK=: java.lang.SecurityException: Permission denied (missing INTERNET permission?)
パーミッションが拒否された、というログがあります
同じ現象を postman というアプリケーションを使って確認してみます
[postman] (https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop)で
http://api.openweathermap.org/data/2.5/weaher?q=Tokyo%2Cjp
こんな感じのリクエスト送ると...

{"cod":401, "message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."}
jsonオブジェクト、かと思いきや
2. Jsonオブジェクトを取得できるようにする
ひとまず情報を取得できない原因が判明したので、対応します
OpenWeatherMapでは、無料アカウントで個人用api keyを取得できるので取ってきます
OpwnWeatherMapのAPIに関するページにapi keyの取得方法だけでなくapi keyの使い方まで乗っけてくれていたのでそれを参考に
http://api.openweathermap.org/data/2.5/forecast/city?id=524901&APPID= [今取得したapi key]
このリクエストをpostmanで送ると
ちゃんと帰ってきました

ここに city id の対応表がありました
上のリクエストのcity id: 524901はモスクワらしいです(Jsonオブジェクトのnameフィールドにもありますね)
さて、では満を持してpostman で送ったようなリクエストをAndroidで送れるように準備していこう!
…というところでハマりました
api処理用インターフェースのメソッドの書き方がわからない…
3. AndroidでAPIリクエストを送ってみようとして失敗する
自分なりにWeatherApiインターフェースのメソッドを作ってみたりしましたが、
ログを見た感じどうも意図したURLになっていない様子でした
失敗したメソッドがこちら
@GET("/data/2.5/weather?q={cityId}&{key}")
public Observable<WeatherEntity> getWithErr( @Query("cityId") String cityId, @Query("key") String key);
クエリとして指定するものをcity nameやcity idなどいろいろ試してみましたが、ダメでした
getメソッドはURLの指定の仕方が違っていて、getWithNameメソッドはapi keyが設定されていないので401エラーが返ってくる
…というところまでわったところで、getメソッドをなんとか動く状態にするところを目標とします
4. Retrofitで動的にリクエストを設定する方法を調べる
「openweathermap api retrofit apikey」
などとGoogle検索をかけて、いろいろなサイトを巡って
最終的に
Retrofit 2.0 Android Example | Web Services using Retrofit 2.0
こちらのサイトにあったコードを参考に
@GET("/data/2.5/weather?q=London,uk&appid=" + API_KEY)
public Observable<_WeatherEntity> getWheatherReport();
というリクエストを作ってみたところ、ロンドンの天気をとることに成功

API_KEYはとりあえず インターフェース内で
final String API_KEY = “your api key";
こんな感じに宣言したものを使っています
ただ、このままだとせっかく切り分けた処理の自由度が低い (getWeatherReportメソッドの仕様では都市ごとにメソッドを書く必要がある) ので、URLの情報をメソッドの引数として渡せるようにしたいです
しかし、相変わらずRetrofitの@Query
や@Path
に関してわからないことばかりなので調査続行
5. @Query
について
試行錯誤を繰り返しているうちになんとなくつかめてきたので、まとめます
まず、@Query
から
@Query
をつけた引数はクエリとして渡されるので、?q=
は勝手にRetrofitが補ってくれてるみたいです
(複数のクエリが渡される場合は&
も補ってくれる模様)
@GET("/data/2.5/weather")
public Observable<WeatherEntity> get(@Query("q") String name, @Query("appid") String key);
これでうまく動きました
weather以降のクエリを、メソッドの引数として渡すことができた様です
実際にこのメソッドを使っているところが下の部分
(省略)
// 非同期処理の実行
adapter.create(WeatherApi.class).get("London,uk", API_KEY)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
(省略)
}
メソッドを呼び出すときに引数としてクエリの値である “London,uk” と API_KEY を渡しています
API_KEYは取得したapi keyの文字列をString型のグローバル変数で宣言しています
public class MainActivity extends AppCompatActivity {
final String API_KEY = "your api key";
(省略)
}
実行した時の通信ログです
London,uk の部分が、ログのURLでは London%2Cuk となっていること以外は getメソッドに書いた通りのURLになっています
%2Cというのは , をURLエンコードした後の形式ですので、意図した通りのURLになっているといえるでしょう
参考:URLエンコード対応表メモ 2015-01-26
ここで少しURLに関して突っ込んでおきます
青くなっている部分がリクエストを送るためのURLで、
http://api.openweathermap.org
がエンドポイントと言われる部分です
エンドポイント以降はどんなデータが返ってくるのかを決めています
これはOpenWeatherMapが定義したapiの形式です (RESTfulなapiというらしいです)
@GET("/data/2.5/weather")
public Observable<WeatherEntity> get(@Path("path") String path, @Query("q") String name, @Query("appid") String key);
エンドポイント以降のパスを決めているのが get メソッドの上についている@GET
という部分
(GETというのはhttpでのGETメソッドのことですから、POSTメソッドを始めとする他のhttpメソッドもあるのでしょうが、ここでは省略します)
London,uk と書いた部分、United KingdomのLondonということでしょうが、UK以外のLondonはどこなのでしょう…
閑話休題
6. @Path
について
@Query
がどんな働きをするのかわかったところで、@Path
について調べてみました(ただ、Pathに関してはQueryを抑えてからだと簡単でした)
@Path
とした引数の値はURLに
…/{path}/
という感じに続くのかな、と思ったので実験してみます
@GET("/data/2.5/{path}")
public Observable<WeatherEntity> get(@Path("path") String path, @Query("q") String name, @Query("appid") String key);
(省略)
// 非同期処理の実行
adapter.create(WeatherApi.class).get("weather", "London,uk", API_KEY)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
(省略)
}
こちらはPathの動作確認をした時のログ
一つ前のログと同じですね
@Path
に関してはこれで問題なく動作するみたいです
ここまででパスとクエリを動的に指定することができるようになりました
7. RetrofitのQueryとPathについてまとめ
@Query
をつけた引数はQueryとして渡されるので、?q= や & はRetrofitが補ってくれます
この時Queryがつくのは@GET(…)
に書かれた…の部分以降ということになるようです
@Path
をつけた引数は@GET{…}
で書かれたパス(の一部)を引数として指定します
つまり、
@GET("/data/2.5/{path}")
public Observable<WeatherEntity> get(@Path("path") String path, @Query("q") String name, @Query("appid") String key);
上のように書かれたgetメソッドを
get("weather", "London,uk", API_KEY)
というように使った場合は
@GET{"/data/2.5/{path}”}
の部分に引数として指定されたPathやQueryの情報が追加されて、
@GET{"/data/2.5/weather?q=London,uk&appid=<api key>”}
と書いたのと同じ効果を発揮することになります
ここで注意が必要なのは
@Path(“…”)
に書く文字列は変数のような扱いで、任意に決めても大丈夫ですが
@Query(“…”)
に書く文字列はapiで指定された文字をそのまま書かなければいけないという点です
このように書き換えても問題なく動作しますが、
@GET("/data/2.5/{changed_value}")
public Observable<WeatherEntity> get(@Path("changed_value") String path, @Query("q") String name, @Query("appid") String key);
こうすると動きません
@GET("/data/2.5/{path}")
public Observable<WeatherEntity> get(@Path("path") String path, @Query("q") String name, @Query("changed_value") String key);
@Query
のところに入る文字はそのままURLで送られる文字列になってしまうから、といったところでしょうか
ここまでで、ひとまず@Query
と@Path
の役割はわかったので調査終了です
RESTfulなapiを Retrofitで活用することがだいぶやりやすくなった気がします
間違っている部分もあるかと思いますので、ぜひご指摘お願いいたします
追記
ちゃんと公式?リファレンスがありました
自分なりに理解した後で見るからなんとなくわかりますが
こちらがQueryの動的な追加の方法を説明した部分
Query parameters can also be added.
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
ざっくりしすぎていませんか…泣
番外編: Jsonオブジェクト <=> Javaオブジェクト
余談ですが、今回 Jsonオブジェクトを変換する先のjavaオブジェクトとして、下のようなentityクラスを作ったのですが、Retrofitがどんな感じにパースしているのかよくわからない…
public class WeatherData {
@SerializedName("coord")
public Coord coord;
@SerializedName("weather")
public List<Weather> weathers;
@SerializedName("base")
public String base;
@SerializedName("main")
public Main main;
@SerializedName("visibility")
public int visibility;
@SerializedName("wind")
public Wind wind;
@SerializedName("clouds")
public Cloud cloud;
@SerializedName("dt")
public int dt;
@SerializedName("sys")
public Sys sys;
@SerializedName("id")
public int id;
@SerializedName("name")
public String name;
@SerializedName("cod")
public int cod;
public class Coord {
public double lon;
public double lat;
}
public class Weather {
public int id;
public String main;
public String description;
public String icon;
}
public class Main {
public double temp;
public int pressure;
public int humidity;
public double tempMin;
public double tempMax;
}
public class Wind {
public double lon;
public double lat;
}
public class Cloud {
public int all;
}
public class Sys {
public int type;
public int id;
public double message;
public String country;
public int sunrise;
public int sunset;
}
}
謎ポイントとしてあげられるのが、こっちで定義していないのに(おそらくRetrofitが) よしなに扱ってくれること
第29回 JavaオブジェクトとJSONオブジェクトの変換に便利な「Google Gson」2012-03-23
こういうことをしなくて済んでいる理由が知りたい…
…と思っていたら、教えてもらえました
上のentityは
public class WeatherEntity {
public Coord coord;
@SerializedName("weather")
public List<Weather> weathers;
public String base;
public Main main;
public int visibility;
public Wind wind;
@SerializedName("clouds")
public Cloud cloud;
public int dt;
public Sys sys;
public int id;
public String name;
public int cod;
public class Coord {
public double lon;
public double lat;
}
public class Weather {
public int id;
public String main;
public String description;
public String icon;
}
public class Main {
public double temp;
public int pressure;
public int humidity;
public double tempMin;
public double tempMax;
}
public class Wind {
public double lon;
public double lat;
}
public class Cloud {
public int all;
}
public class Sys {
public int type;
public int id;
public double message;
public String country;
public int sunrise;
public int sunset;
}
}
こんな感じに書けばいいらしいです
Retrofitが変数名とjsonオブジェクトのkeyと見比べてくれているようで、
変数名とkeyが一致しているものは @SerializedName
は不要とのこと
jsonオブジェクトの一部のデータだけ、entityにフィールドを用意してもうまく動いていた理由がわかって幸せ