LoginSignup
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-12-15

今日12月16日は夜9:00からシン・ゴジラですね!楽しみです!

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

Ktorについて

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

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

ちなみに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)しており、JsonシリアライザーにGsonを使うよーっていう宣言をするだけでGsonを使えたので、超楽でしたね。

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

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

今後

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6