Android + Kotlin 1.4のcoroutines(Async, Await)でHTTP通信を非同期処理


はじめに

Kotlin 1.3がリリースされ、coroutinesが正式版になりました。
Kotlin 1.3にバージョンを上げるとKotlin 1.2系で動いていたcoroutinesの処理が動かなくなってしまったため、少しコードの修正が必要になりました。

2020年8月17日にKotlin 1.4がリリースされました

過去に投稿した
AndroidでKotlinのcoroutine(Async, Await)を使ってサクッとHTTP通信(非同期処理)を行う
をKotlin 1.3 1.4でも動くように修正しました。
(前回の記事と内容はそこまで変わらないです。このスニペットの背景などは上記ページを参照ください)

2019年2月16日更新

ライブラリのバージョンを更新しました。
サンプルコードを公開しました。
https://github.com/jonghyo/android-http-async-await

2019年3月27日更新

Okhttp3のバージョンが3.13以降の場合
Javaの compileOptionsでJava 8機能を有効にしないとエラーが発生するので修正しました。
@kk2170さん、ありがとうございます!

2019年4月18日更新

Kotlin1.3.30がリリースされたので、Kotlinのバージョンを修正しました。

2019年6月2日更新

GlobalScope.launch(Dispatchers.Main)をむやみに利用することはアンチパターンとされています。Kotlin Coroutineを使う際のスコープに関して触れたセクションを追加しました。
@tetsu0831 さん、ありがとうござます!

2019年7月14日更新

Kotlin1.3.40がリリースされたので、Kotlinのバージョンを修正しました。
また、kotlinx-coroutinesのバージョンを上げました。

2019年8月24日更新

Kotlin1.3.50がリリースされたので、Kotlinのバージョンを修正しました。
また、kotlinx-coroutines,OKHttp3のバージョンを上げました。

Kotlin 1.3.50の内容をまとめたのでもしよろしければ読んでください!
Kotlin 1.3.50がリリースされました!!

2020年3月7日更新
Kotlin1.3.70がリリースされたので、Kotlinのバージョンを修正しました。
また、kotlinx-coroutines,OKHttp3のバージョンを上げました。

2020年8月7日更新
livedoor Weather APIが2020年7月31日をもって提供終了となったため本記事のLGTM!を表示するサンプルに変更しました。GitHubのサンプルコードも修正しました。

@yuki-kamikita さん、ご指摘ありがとうございます!

2020年8月17日更新
Kotlin1.4がリリースされたので、Kotlinのバージョンを修正しました。
また、kotlinx-coroutinesのバージョンを上げました。

事前準備

Gradle設定

使用するライブラリの依存解決のため、以下の記述をappのbuild.gradleに追加します。
Kotlin coroutine用ライブラリは2020年8月17日時点で最新の1.3.9を利用します。

app/build.gradle
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.4.0'  //http通信ライブラリ
    implementation 'com.eclipsesource.minimal-json:minimal-json:0.9.5' //jsonパースライブラリ
    def coroutines_version = '1.3.9' //Kotlin coroutines用ライブラリ(async, await)のバージョン
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" //Kotlin coroutines用ライブラリ(async, await)
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" //Kotlin coroutines用ライブラリ(async, await)
}

Okhttp3のバージョンが3.13以降の場合、 compileOptionsでJava 8の機能を有効化する必要があります。

詳細は、 @kk2170 さんの記事にて解説されています。
ありがとうございます!

OkHttp3をAndroidで使おうとしたときにjava.lang.ClassCastException: Bootstrap methaod returned nullが発生して初期化に失敗する

app/build.gradle
android {
//compileOptions以前の記述は省略
    compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
    }
}

使用するKotlinのバージョン変更

プロジェクトのbuild.gradleで、Kotlin coroutineを使うためにKotlin1.4にバージョンを変更します。

build.gradle
ext.kotlin_version = '1.4.0'

HTTP通信処理を書く

今回はHTTP GETを行うメソッドを定義しました。
関数の呼び出し元でasyncなどを使うことが推奨されているようなので少し修正しました。

HttpUtil.kt
object HttpClient {
    //OKHttp3はシングルトンで使う
    val instance = OkHttpClient()
}

参考
KotlinでRetrofit2/OkHttp3を使ってXMLを取得する

HttpUtil.kt
class HttpUtil {

    fun httpGet(url : String): String? {
        val request = Request.Builder()
                .url(url)
                .build()

        val response = HttpClient.instance.newCall(request).execute()
        val body = response.body?.string()
        return body
    }
}

OkHttp 4.0.0以降はresponse.bodyの書き方が変わったみたいです。
Getting an error Using 'body(): ResponseBody?' is an error. moved to val with okhttp

ライブラリのimport文の修正

coroutines関連がKotlin1.3よりexperimentalではなくなったので修正します。
※こちらはKotlin 1.2系から1.3系以降にバージョンアップする場合に必要です。

MainActivity.kt(修正前)
import kotlin.coroutines.experimental,Dispatchers
import kkotlin.coroutines.experimental.GlobalScope
import kotlin.coroutines.experimental.async
import kotlin.coroutines.experimental.launch
MainActivity.kt(修正後)
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

HTTP GETの結果を用いてUIを更新する

今回はボタンをクリックすると、HTTP GETの処理が走り、結果をTextViewに反映する処理を書きました。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    val URL = "https://qiita.com/api/v2/items/bf3e4e06022eebe8e3eb" //サンプルとしてQiitaのAPIサービスを利用します
    var result = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val getButton = findViewById(R.id.button) as Button
        getButton.setOnClickListener(object : View.OnClickListener {
            override
            fun onClick(view: View) {
                onParallelGetButtonClick()
            }
        })
    }

    //非同期処理でHTTP GETを実行します。
    fun onParallelGetButtonClick() = GlobalScope.launch(Dispatchers.Main) {
        val http = HttpUtil()
    //Mainスレッドでネットワーク関連処理を実行するとエラーになるためBackgroundで実行
        async(Dispatchers.Default) { http.httpGet(URL) }.await().let {
    //minimal-jsonを使って jsonをパース
            val result = Json.parse(it).asObject()
            val textView = findViewById(R.id.text) as TextView
            textView.setText(result.get("likes_count").asInt().toString() + "LGTM!")
        }
    }

Kotlin Coroutineスコープについて(2019年6月2日追記)

KotlinでCoroutineを利用する場合、Coroutineのスコープについてケアする必要があります。
GlobalScope.launch(Dispatchers.Main)のようにGlobalScopeを目的もなく使うのはKotlin Coroutinesのアンチパターンとされています。

Kotlin Coroutines patterns & anti-patterns

上記を和訳、解説した @ikemura23さんの記事
Kotlin Coroutinesパターン&アンチパターン

GlobalScopeはアプリケーションの有効期間全体にわたって動作するため
GlobalScopeを同一アプリ内で乱用すると不都合が発生したり
Androidのアプリケーションライフサイクルとの間で問題が発生する可能性もあります。

単に、REST APIを叩く検証するだけであればrunBlockingなどの利用も検討すると良いでしょう。

【Kotlin】Coroutineを理解する

また、プロダクションコードではアプリケーション定義の
CoroutineScopeを利用することがベターだとされています。

図で理解する Kotlin Coroutine

@tetsu0831 さん、ご指摘ありがとうござます!

結果

HTTP GETを非同期処理で実行し、取得したjsonをTextViewに反映できました。

所感

Kotlin1.2系のcoroutines処理のままでは、Kotlin1.3系では動きませんでしたがそこまで大幅な修正は必要なさそうでした。

Kotlin 1.3系で正式採用されたcoroutinesもこなれてきた感がありますね。
Kotlin 1.4ではCoroutine Debuggerも実装されより便利になったみたいです!
Kotlin 1.4についての記事も書きたいな。


最後までお読み頂きありがとうございました!
Twitterでも技術ネタやデザインについてなどもツイートしているので、よかったらフォローしてもらえると嬉しいです
→ Twitter@fashioncrazy66