【Android】Routerクラスを導入して、通信クラスをまとめる(iOS風)
前提条件
- AndroidStudio
- Kotlin
- OKHTTP3
はじめに
- 私は主にiosアプリの方を中心に製作していますが、API通信には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に関しては初心者なので、指摘等あればおしらせください。