AndroidアプリでKtorとCoroutine使ってasync/awaitしてみるサンプル書いてみた

今日12月16日は[夜9:00からシン・ゴジラ](https://sakekurage.com/godzilla-tv/)ですね！楽しみです！

今年はオールアバウトを退職したので、もう社員ではありませんが、ありがたいことに一日分枠をいただきましたので、社外から参加させていただきます。

# Ktorについて
先日Ktor1.0が正式リリースされましたね！
[Ktor](https://ktor.io/)は非同期サーバ/クライアントシステムをKotlinで構築するためのフレームワークらしいです。
サーバサイドフレームワークは二年連続でアドベントカレンダーのネタにしちゃってるので、今回はクライアントサイドフレームワークをネタにしたいと思います。

> Ktor is a framework for building asynchronous servers and clients in connected systems using the powerful Kotlin programming language

ちなみにKtorは「ことぅあー」じゃなくて、「くとぉあー」って読むっぽい？

https://translate.google.com/?hl=ja&q=ktor#view=home&op=translate&sl=auto&tl=ja&text=ktor

# 実装してみる
試しにQiitaのAPIから投稿を取得するクライアントアプリを作ってみます。

## 準備
KtorをAndroidプロジェクトへインストールします。

```app/build.gradle

buildscript {
    repositories {
        google()
        jcenter()
        maven { url "https://kotlin.bintray.com/kotlinx" } ←追加
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://kotlin.bintray.com/kotlinx" } ←追加
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

```

```build.gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "test.com.example"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    // ↓↓↓↓ここから
    implementation "io.ktor:ktor-client-android:$ktor_version"
    implementation "io.ktor:ktor-client-gson:$ktor_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
    // ↑↑↑↑ここまで追加
}
```

```gradle.properties
kotlin_version = 1.3.10
ktor_version = 1.0.1
coroutines_version = 1.0.1

kotlin.coroutines = enable
```

```app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="test.com.example">

    <uses-permission android:name="android.permission.INTERNET" /> ← 追加
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

## UI部分の実装
UI部分を実装します。TextViewに通信した結果を突っ込んでるだけの超手抜きですが。

```MainActivity.kt
package test.com.example

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    val url = "https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3Ayaotti"
    val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        Log.d(TAG, "Begin onCreate") // Async/Awaitの挙動に感動するためのログ
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {
            Log.d(TAG, "Begin Load Qiita")
            val response = AsyncQiitaRequest().get<Array<QiitaItemEntity>>(url)
            GlobalScope.apply {
                launch(Dispatchers.Main) {
                    main_text_view.text = response[0].body
                }
            }
            Log.d(TAG, "Finish Load Qiita")
        }
        Log.d(TAG, "Finish onCreate")
    }
}
```

## 通信部分を実装する
Ktor ClientとCoroutineで非同期処理APIクライアントを実装します。

```AsyncRequest.kt
package test.com.example

import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async

class AsyncRequest {
    suspend inline fun<reified T> get(url: String, params: Map<String, Any> = mapOf()): T {
        val client = HttpClient(Android) {
            install(JsonFeature) {
                serializer = GsonSerializer()
            }
            engine {
                connectTimeout = 100_000
                socketTimeout = 100_000
            }
        }
        val request = GlobalScope.async {
            Thread.sleep(2000) // 通信しててもUIに影響がないことを確認するためのテスト
            val response = client.get<T>(url)
            client.close()
            return@async response
        }
        return request.await()
    }
}
```

```QiitaItemEntity.kt
package test.com.example

data class QiitaItemEntity(
        val rendered_body: String,
        val body: String,
        val coediting: Boolean,
        val comments_count: Int,
        val created_at: String,
        // val group:
        val id: String,
        val likes_count: Int,
        // tags:
        val private: Boolean,
        val reactions_count: Int,
        val title: String,
        val updated_at: String,
        val url: String,
        // user:
        val page_views_count: Int?
)
```

## 処理流れまとめ
基本的な処理の流れは下記のような感じでしょうか。
1. MainActivityのGlobalScope.launchで、UIスレッドとは別のスレッド（以下スレッドA）を作成
2. スレッドA内でQiitaの記事データ読み込みを開始。（この時点ではUIスレッドとスレッドAが同時に実行される。体感したければ、activity_main.xmlにButtonを配置し、GlobalScope.async内で、Thread.sleepしてみると良いかも）
3. データを読み込み終えたタイミングでUIスレッドを中断、スレッドA内のapplyで取得したQiitaの投稿データをTextViewへセットする処理をUIスレッドへ渡す。
4. UIスレッドでTextViewにデータをセット

```
        GlobalScope.launch {
            Log.d(TAG, "Begin Load Qiita")
            val response = AsyncQiitaRequest().get<Array<QiitaItemEntity>>(url)
            GlobalScope.apply {
                launch(Dispatchers.Main) {
                    main_text_view.text = response[0].body
                }
            }
            Log.d(TAG, "Finish Load Qiita")
        }
```

下記の`client.get<T>(url)` の部分がKtor Clientの機能ですね。
asyncで非同期でデータを取得し、データを取得し終えるタイミング（await）でデータを返しています。

```
        val request = GlobalScope.async {
            val response = client.get<T>(url)
            client.close()
            return@async response
        }
        return request.await()
```

# おわりに
以上、Ktorを用いたasync/awaitのサンプルでした。
Ktorを使うことでクライアントサイドの通信周りを、すごくサクッと実装できた感があります。
特に、レスポンスのJsonマッピング部分が楽だった印象です。Retrofitなどを使うと、Gsonを利用するための設定をごちゃごちゃ書いた記憶がありますが、KtorはJsonシリアライザーを[いくつかサポート（GsonとかJackson）](https://ktor.io/clients/http-client/features/json-feature.html#kotlinx-serialization)しており、JsonシリアライザーにGsonを使うよーっていう宣言をするだけでGsonを使えたので、超楽でしたね。

```
        val client = HttpClient(Android) {
            install(JsonFeature) {
                serializer = GsonSerializer()
            }
        }
```

あ、そういえばエラーハンドリングしてないや。

# 今後
僕がKtorの存在を知ったのは、1.0リリース後なので超にわかですが、Ktor使いやすかったので今後も何かで使ってみたいですね。
あと、[マルチプラットフォームで利用するためのドキュメント](https://ktor.io/clients/http-client/multiplatform.html)なんてものもあるので、マルチプラットフォームでの実装も割と楽にできそうな予感...！
（実際Ktorのマルチプラットフォームのサンプルプロジェクトは結構凄かった。）
試してみたいですね〜
では、皆さま良いお年を！