【Android】Routerクラスを導入して、通信クラスをまとめる（iOS風）GET / POST / DELETE / PUT

# 【Android】Routerクラスを導入して、通信クラスをまとめる（iOS風）

![kotlin_800x320.png](https://qiita-image-store.s3.amazonaws.com/0/62822/79c5423b-7866-5c82-aecf-84c14d1168b6.png)

## 前提条件
+ AndroidStudio
+ Kotlin
+ OKHTTP3

## はじめに
+ 私は主にiosアプリの方を中心に製作していますが、API通信には[**Alamofire**](https://github.com/Alamofire/Alamofire)というライブラリをしばしば使います。
+ そのライブラリではswiftの旨味を生かした、Routerクラスというものがenumで定義されており、kotlinでもenumがあるので、それを生かして同じようにenumで定義しました。
+ KotlinとSwiftで同じようなコードが書ければ、アプリの保守性も上がると思い製作しました。

+ ですので、iOSのAlamofireを触ったことがある人には馴染めるかと思います。
+ 根っからの、アンドロイダーの方はもしかするとキモイかもしれんです。
+ 今はiOSでも、今後Androidを作るかもしれない人は役に立つかも！？

### ようするに
+ **Kotlinの旨味を生かして、通信クラスを実装しよう！！**


## 機能定義
+ Kotlinで記述
+ https通信ができる
+ OKHttp3を使用する
+ apiのバージョニングができる
+ urlのpathを追加できる
+ get post delete put 等のhttpメソッドが叩ける
+ それぞれ、parameterをmapで渡すことができる
+ 通信は非同期でおこなう

 
## サンプル

- **Router.kt (APIの種類を列挙します)**
- APIService.kt (実際の処理を記述)
- TLSSocketFactory.kt (SSLで必要になります)

### Router.kt
+ 叩くAPIに応じて、postやdeleteメソッドかを明示できたり、pathを列挙しています
+ addPathSegmentでチェーンすれば長いpathも作れます
+ 例) DeleteGroupだと (DELETE)　https://xxx.xxxx.jp/api/v2/groups/delete (key=value,key2=value,...) というRequestが生成されます。

``` Router.kt
import okhttp3.*

enum class Router() {
    GetVoice,
    CreateUser,
    UpdateUser,
    CreateGroup,
    GroupCheck,
    DeleteGroup,
    JoinGroup,
    LeftGroup,
    FinishJoin,
    SelectUser,
    GetStatus,
    CallStandby,
    Complete;

    fun params(map: Map<String,String>?): Request {
        return when (this) {
            //httpメソッド
            GetVoice ->     get(map)
            CreateUser ->   post(map)
            UpdateUser ->   put(map)
            CreateGroup ->  post(map)
            GroupCheck ->   get(map)
            DeleteGroup ->  delete(map)
            JoinGroup ->    post(map)
            LeftGroup ->    put(map)
            FinishJoin ->   put(map)
            SelectUser ->   put(map)
            GetStatus ->    get(map)
            CallStandby ->  get(map)
            Complete ->     get(map)
        }
    }

    // xx.xx.jp/api/v2/oooo <-pathを追加できます
    private fun route(): HttpUrl.Builder {
        return HttpUrl.Builder().scheme("https").host("xxx.xxxx.jp").addPathSegment("api").addPathSegment("v2").addPathSegment(
                when (this) {
                    GetVoice -> "voice"
                    CreateUser, UpdateUser -> "users"
                    CreateGroup, GroupCheck, DeleteGroup, JoinGroup, LeftGroup, FinishJoin, SelectUser, GetStatus -> "groups"
                    CallStandby, Complete, TestPush -> "dials"
                }
        ).addPathSegment(
    // xx.xx.jp/api/v2/oooo/OOOO <-pathを追加できます
                when (this) {
                    GroupCheck -> "check_start"
                    DeleteGroup -> "delete"
                    JoinGroup -> "join"
                    LeftGroup -> "left"
                    FinishJoin -> "start"
                    SelectUser -> "select"
                    GetStatus -> "status"
                    CallStandby -> "matching"
                    Complete -> "complete"
                    else -> ""
                }
        )
    }
    private fun createUrl(route: HttpUrl.Builder, query: Map<String,String>?): Request.Builder {
        if (query == null) { return Request.Builder().url(route.build()) }
        with(route) {
            for ((key, value) in query) {
                this.addQueryParameter(key,value)
            }
        }
        return Request.Builder().url(route.build())
    }

    private fun createUrl(route: HttpUrl.Builder): Request.Builder {
        return Request.Builder().url(route.build())
    }

    private fun post(map: Map<String,String>?) :Request {
        return createUrl(route()).post(buildBody(map)).build()
    }

    private fun get(query: Map<String,String>?): Request {
        return createUrl(route(),query).get().build()
    }

    private fun delete(map: Map<String,String>?) :Request {
        return createUrl(route()).delete(buildBody(map)).build()
    }

    private fun put(map: Map<String,String>?): Request {
        return createUrl(route()).put(buildBody(map)).build()
    }

    private fun buildBody(map: Map<String,String>?): RequestBody {
        val body = with(MultipartBody.Builder()) {
            map?.let {
                for ((key, value) in map) {
                    addFormDataPart(key, value)
                }
            }
            build()
        }
        return body
    }
}
```

### APIservice.kt
+ API通信の実際の処理はここにまとめます


``` APIService.kt
class APIService{
    companion object {
        //https通信で必要
        val socketFactory: SSLSocketFactory?
            get() = TLSSocketFactory()

        fun createUser(token: String, name: String, sex: Sex, handler: (String) -> Unit) {
            val map = mapOf("device_token" to token,
                    "name" to name,
                    "sex" to sex.rawval().toString(),
                    "device" to "android")

            this.enqueueAccess(Router.CreateUser.params(map), { result ->
                val user_id = result.getString("user_id")
                handler(user_id)
            }, { code, message ->
             //error処理
            })
        }

        fun createGroup(name: String, handler: (String) -> Unit) {
            val map = mapOf("host_user_id" to user.userId.toString(),
                    "name" to name)
            this.enqueueAccess(Router.CreateGroup.params(map), { res ->
                val groupId = res.getString("group_id")
                handler(groupId)
            }, { code, message ->
             //error処理
            })
        }

        fun deleteGroup(success: () -> Unit) {
            val map = mapOf("host_id" to user.userId,
                    "group_id" to group.groupId)
            this.enqueueAccess(Router.DeleteGroup.params(map), { res ->
                success()
            }, { code, message ->
          　 //error処理

            })
        }

        private fun enqueueAccess(request: Request, successHandler: (JSONObject) -> (Unit), errorHandler: (Int, String?) -> (Unit)) {
            object : AsyncTask<Void, Void, String>() {
                protected override fun doInBackground(vararg params: Void): String? {
                    var result: String? = null
                    val client = object : OkHttpClient() {
                        override fun sslSocketFactory(): SSLSocketFactory? {
                            return socketFactory
                        }
                    }
                    try {
                        val response = client.newCall(request).execute()
                        val code = response.code()
                        if (code < 200 || code >= 300) {
                            return null
                        }
                        result = response.body().string()
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                    return result
                }

                protected override fun onPostExecute(result: String?) {
                    if (result == null) { errorHandler(999,"error!!"); return }
                    val json = JSONObject(result)
                    val code = json.getInt("code")
                    if (code >= 200 && code < 300) {
                        successHandler(json)
                    } else {
                        errorHandler(code,"error message")
                    }
                }
            }.execute()
        }
    }
}
```

### TLSSocketFactory.kt
+ https通信で必要（httpでいい人はいらない）

``` TLSSocketFactory.kt
class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
constructor() : SSLSocketFactory() {

    private val internalSSLSocketFactory: SSLSocketFactory

    init {
        val context = SSLContext.getInstance("TLS")
        context.init(null, null, null)
        internalSSLSocketFactory = context.socketFactory
    }

    override fun getDefaultCipherSuites(): Array<String> {
        return internalSSLSocketFactory.defaultCipherSuites
    }

    override fun getSupportedCipherSuites(): Array<String> {
        return internalSSLSocketFactory.supportedCipherSuites
    }

    @Throws(IOException::class)
    override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
        return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
    }

    @Throws(IOException::class, UnknownHostException::class)
    override fun createSocket(host: String, port: Int): Socket {
        return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
    }

    @Throws(IOException::class, UnknownHostException::class)
    override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
        return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
    }

    @Throws(IOException::class)
    override fun createSocket(host: InetAddress, port: Int): Socket {
        return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
    }

    @Throws(IOException::class)
    override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
        return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
    }

    private fun enableTLSOnSocket(socket: Socket?): Socket {
        if (socket != null && socket is SSLSocket) {
            socket.enabledProtocols = arrayOf("TLSv1.1", "TLSv1.2")
        }
        return socket!!
    }
}

```

## 使い方
### UserModel.kt
+ ModelクラスなどからAPIを呼び出すだけでよくなります。


``` UserModel.kt
class UserModel {
    companion object {
        fun createUer(name:String,sex: Sex) {
            val token = UserDefault.getPreferenceValue(PreferenceKey.KEY_DEVICE_TOKEN)
            //APIをよびだすYO!
            APIService.createUser(token.toString(), name, sex) { userId ->
                val user = User(userId, name, sex)
                UserViewModel.user.value = user
            }
        }
    }
}
```

## まとめ
+ KotlinとSwiftで同じようなコードが書ければ、アプリの保守性も上がるかと思います。
+ Androidに関しては初心者なので、指摘等あればおしらせください。

## 参考
https://github.com/Alamofire/Alamofire