Android
Kotlin
Retrofit

Android Retrofit2 with Kotlin

実用的なライブラリをKotlin交えて紹介しようプロジェクト4

今回はAPIコールを簡単に書くことができるRetrofit2を紹介します。
ちなみに今回はJsonを自動的にクラスへ変換してくれるライブラリとしてGsonを利用します。

また、今回の実装例はこちらにPRを作っているので、よろしければ参考にどうぞ
https://github.com/HoNKoT/KotlinAndroidDatabindingSample/pull/3

Retrofit2

"A type-safe HTTP client for Android and Java"
と公式サイトに謳われているとおり、簡単にAPIコールするためのライブラリです。
また、バックグラウンドで行なってコールバックを指定することも簡単にできます。

公式サイト

使い方

使い方は至ってシンプル

  1. gradel
  2. interface実装
  3. HttpClient実装

gradle

build.gradle
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'

interface

APIを表現、定義します。
例えば、こちらのフリーAPIを参考にさせてもらいます
https://randomuser.me/api

このとき、ここで定義するのは規定URL https://randomuser.me/ 以降の部分になります

IApiService.kt
interface IApiService {
    @GET("api")
    fun apiDemo(): Call<RandomUserDemo>
}

Call ... Retrofitに用意されているレシーバクラス
RandomUserDemo ... 後述する、自分で定義したModelクラス

このように、簡単にGETのAPIを定義することが可能です。
もちろん、@POST, @PUT も用意されており、簡単に定義可能です。

パラメータの渡し方、パスの作成方法など、
細かい使い方にも触れておきます

REQUEST METHOD

もっともシンプルな形

@GET("users/list")

このように直接Queryパラメータを書くことも可能です

@GET("users/list?sort=desc")

なお、Kotlinの場合、引数がなければvalのGetterとしても指定することができます

@get:GET("api")
val apiDemo: Call<RandomUserDemo>

URL MANIPULATION

例えばURL上にidが入っている場合など、
URL上に可変要素を入れたい場合などは以下のように定義します

これはGETに限らず、POST、PUTも同様です

PATH

URLのパス途中に何かを入れたい場合はこのように
{}で囲んで命名し、@Path をNameと一緒に定義することで
渡したパラメータがURLパスの中に代入されます

@GET("group/{id}/users")
fun groupList(@Path("id") groupId : Int) : Call<List<User>>

QUERY

URLの最後につく?以降の要素はこのように書きます

@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int, @Query("sort") sort: String): Call<List<User>>

これを実行すると、以下のようなURLになる

https://randomuser.me/group/3/users?sort=desc
注意:例なので、動くURLではないです

REQUEST BODY

あるインスタンスがもつ要素をkey, value変換して
APIで送りたい、というときには、そのインスタンスクラスを引数にするインターフェースを用意し、@Bodyをつけます。

@POST("users/new")
fun createUser(@Body user: User): Call<User>;

ここでのUserは、後述する自作Modelクラスであり、
どのKeyがどのインスタンス変数に該当するかというのを記載する必要がありますが、
そうすることで自動的に変換してJsonに変換して送ることができます

FORM ENCODED

変数をkey, valueで変換して送りたい場合(Form指定)は
以下のように@FormUrlEncodedをつけ、送りたいデータには@Fieldをつけます。

@FormUrlEncoded
@POST("user/edit")
fun updateUser(@Field("first_name") first: String, @Field("last_name") last: String): Call<User>

また、Multipartとの併用はできません

MULTIPART

Maltipart指定してデータを送りたい場合にも
以下のように@Multipartをつけ、送りたいデータには@Partあるいは@PartMapをつけます。

@Multipart
@PUT("user/photo")
fun updateUser(@Part("photo") photo: RequestBody, @Part("description") description: RequestBody): Call<User>

また、FormUrlEncodedとの併用はできません

HEADER MANIPULATION

ヘッダの指定もできます

静的指定

単体指定

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
fun widgetList(): Call<List<Widget>>;

複数指定

@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
fun getUser(@Path("username") username: String): Call<User>

動的指定

@GET("user")
fun getUser(@Header("Authorization") authorization: String): Call<User>

Model

受け取り(あるいは送信)するデータのModelクラスを作成します。
今回は例として、こちらのvalueがランダムになるフリーAPIをお借りします

https://randomuser.me/api

{
    "info": {
        "page": 1, 
        "results": 1, 
        "seed": "7aed050e936ddbda", 
        "version": "1.1"
    }, 
    "results": [
        {
            "cell": "0486-036-722", 
            "dob": "1979-04-08 06:14:29", 
            "email": "roger.holt@example.com", 
            "gender": "male", 
            "id": {
                "name": "TFN", 
                "value": "023064228"
            }, 
            "location": {
                "city": "port macquarie", 
                "postcode": 1650, 
                "state": "western australia", 
                "street": "3769 dogwood ave"
            }, 
            "login": {
                "md5": "e7f8bb220a82a28ad9375081f022ff2f", 
                "password": "aaaaaaa", 
                "salt": "Q6J1Te1h", 
                "sha1": "e2ec839f88e8fdaddbb2a83877d9afd8342bf26c", 
                "sha256": "a602c4c64dc6fc1b94d6c98eedbf4383d79cd8894e21cc9f37f9fca3000cea05", 
                "username": "greenduck262"
            }, 
            "name": {
                "first": "roger", 
                "last": "holt", 
                "title": "mr"
            }, 
            "nat": "AU", 
            "phone": "09-2659-5113", 
            "picture": {
                "large": "https://randomuser.me/api/portraits/men/78.jpg", 
                "medium": "https://randomuser.me/api/portraits/med/men/78.jpg", 
                "thumbnail": "https://randomuser.me/api/portraits/thumb/men/78.jpg"
            }, 
            "registered": "2004-12-21 07:19:21"
        }
    ]
}

で、2階層目まで変換したものがこちら
それぞれのkey名が変数名に該当します

data class RandomUserDemo(var info: Info,
                          var results: List<Result>)

data class Info(var seed: String,
                var results: Int,
                var page: Int,
                var version: String)

data class Result(var gender: String,
                  var email: String,
                  var registered: String,
                  var dob: String,
                  var phone: String,
                  var cell: String)

CUSTOM KEY NAME

Key名と変数名を違うものにしたい、という場合は
@SerializedNameをつけることで変更可能です

data class Result(var gender: String,
                  var email: String,
                  var registered: Int,
                  var dob: Int,
                  var phone: Int,
                  var cell: Int,
                  @SerializedName("id") var idMap: IdMap)

data class IdMap(var name: String,
                 var value: String)

TRANSIENT

逆に、key,value に変換したくない要素もあると思います。
そんなときはTransientを使いましょう。

JavaとKotlinで書き方が少し違うのでどちらも載せます

public class IdMap {
    public long transient id = 0;
}
data class IdMap(@Transient var id: Long,
                 var name: String,
                 var value: String)

HttpClient

ログやヘッダー、基底URLの定義などを行い、
HttpClientを作成します

    val httpBuilder: OkHttpClient.Builder get() {
        // create http client
        val httpClient = OkHttpClient.Builder()
                .addInterceptor(Interceptor { chain ->
                    val original = chain.request()

                    //header
                    val request = original.newBuilder()
                            .header("Accept", "application/json")
                            .method(original.method(), original.body())
                            .build()

                    return@Interceptor chain.proceed(request)
                })
                .readTimeout(30, TimeUnit.SECONDS)

        // log interceptor
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        httpClient.addInterceptor(loggingInterceptor)

        return httpClient
    }

Retrofit instance

HttpClientを使い、Retrofitフレームワークを経由して
APIを定義したinterfaceをインスタンス化します

    // core for controller
    val service: IApiService = create(IApiService::class.java)

    lateinit var retrofit: Retrofit

    fun <S> create(serviceClass: Class<S>): S {
        val gson = GsonBuilder()
                .serializeNulls()
                .create()

        // create retrofit
        retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create(gson))
                .baseUrl("http://randomuser.me/") // Put your base URL
                .client(httpBuilder.build())
                .build()

        return retrofit.create(serviceClass)
    }

このserviceがinterfaceであり、実際にAPIコールを行う際に利用するものです

Singleton

これはアプリケーションで使い回すことをお勧めします。
Daggerを実装する場合はこのようにしましょう

AppModule.kt
    @Singleton
    @Provides
    internal fun providesApiService(): IApiService {
        return ApiService().service
    }

Execute

実行は以下のように行います

同期

try {
      val response = API.apiDemo().execute()
      if (response.isSuccessful()) {
          return response.body()
      } else {
          // failed
      }
} catch (e: IOException e) {
      e.printStackTrace()
}

非同期

参考ソースはJavaですが、こんな感じで非同期にします。
ただお勧めとしてはRxJavaを利用すると、スッキリかけます。

API.apiDemo().enqueue(new Callback<RandomUserDemo>() {
         @Override
         public void onResponse(Call<RandomUserDemo> call, retrofit2.Response<RandomUserDemo> response) {
            RandomUserDemo demo = response.body();
         }

         @Override
         public void onFailure(Call<RandomUserDemo> call, Throwable t) {
         }
});