Android Retrofit2 with Kotlin


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

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

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

# Retrofit2

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

[公式サイト](http://square.github.io/retrofit/)

# 使い方

使い方は至ってシンプル

1. gradle
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

もっともシンプルな形

```kotlin
@GET("users/list")
```

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

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

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

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

### URL MANIPULATION

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

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

#### PATH

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

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

#### QUERY

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

```kotlin
@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`をつけます。

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

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

#### FORM ENCODED

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

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

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

#### MULTIPART

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

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

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

### HEADER MANIPULATION

ヘッダの指定もできます

#### 静的指定

単体指定

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

複数指定

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

#### 動的指定

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


## Model

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

`https://randomuser.me/api`

```json
{
    "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"
        }
    ]
}
```

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

```kotlin
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`をつけることで変更可能です

```kotlin
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で書き方が少し違うのでどちらも載せます

```Java
public class IdMap {
    public long transient id = 0;
}
```

```Kotlin
data class IdMap(@Transient var id: Long,
                 var name: String,
                 var value: String)
```

## HttpClient

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

```Kotlin
    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をインスタンス化します

```Kotlin
    // 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

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

### 同期

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

### 非同期

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

```java
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) {
         }
});
```