はじめに
以前、Coroutinesが実験段階の時にAsyncTaskと同等の機能をCoroutinesで実装する記事を上げました。(記事はこちら)
Coroutinesが正式にリリースされて約1年が経とうとしているので、正式版のCoroutinesでAsyncTaskと同等の機能を実装方法を記事にまとめて見ようと考えました。
今回は、キャンセル処理もしっかり載せております。
記事の構成は、以前の内容を同じようにしております。
見比べながらこの時期を読むと、実験段階との差がはっきりします。
動作環境
この記事の動作環境は以下のとおりです。
Android Studio:3.5
Kotln:1.3.50
Open JDK:1.8
compileSdkVersion:29
targetSdkVersion:29
minSdkVersion:23
Coroutines:1.3.2
AsyncTaskとは
AsyncTaskは、Androidで非同期処理の実装に便利なユーティリティクラスの1つです。
詳細は、下記のサイトを参照してください。
(https://developer.android.com/reference/android/os/AsyncTask.html)
Coroutinesとは
Coroutinesとは、Kotlin 1.3から正規にに導入された非同期処理を簡単に実装できる機能です。
async/await/launchなどの関数利用して、非同期処理を実現します。
関数の詳細については、公式サイトを参照してください。
AsyncTaskと同等の機能をCoroutinesで実装した時の差を確認するため、本記事でCoroutine詳細については省略します。
#サンプルアプリ
完成イメージ
この記事で作成するサンプルアプリの完成イメージは、下図のとおりです。
サンプルアプリの詳細
スタートボタンを押下すると、TextViewの表示が「始めます」に変わります。その後、TextViewに1〜10の値が0.8秒間隔でカウントアップします。カウントアップが終わると、TextViewの表示は「終わります」に変わります。
ソースコードと解説
サンプルアプリは、以下のファイルで構成されています。
(プロジェクト生成後から変更がないマニフェストファイルなどは、省略しています。)
ファイル一覧
レイアウト
- activity_main.xml
Activity
- MainActivity.kt(AsyncTaskで実装)
- MainActivity.kt(Coroutinesで実装)
Gradle
- build.gradle(app)
Gradle
AndroidでCoroutineを利用するため、build.gradle(app)のdependenciesに下記の2行を追記します。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
レイアウト
最終的なレイアウトのイメージは、下図のとおりです。
activity_main.xml
画面全体のレイアウトファイルの内容は、以下のとおりです。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="jp.co.casareal.afterasync.MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="30sp" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="スタート"
android:textSize="30sp" />
</LinearLayout>
Activity
MainActivity.kt(AsyncTaskで実装)
AsyncTaskでサンプルアプリを実装した時のアクティビティのソースコードは、以下のとおりです。
package jp.co.casareal.asyncbefor
import android.os.AsyncTask
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
MyAsyncTask().execute()
}
}
inner class MyAsyncTask : AsyncTask<Void, Int, Void>() {
override fun onPreExecute() {
text.setText("始めます")
Thread.sleep(800)
}
override fun doInBackground(vararg param: Void?): Void? {
for (i in 1..10) {
publishProgress(i)
Thread.sleep(800)
}
return null
}
override fun onProgressUpdate(vararg values: Int?) {
text.setText(values[0].toString())
}
override fun onPostExecute(result: Void?) {
text.setText("終わります")
}
}
}
実装の解説
AsyncTaskを継承したクラスを、インナークラスとして定義しています。
onPreExecuteメソッド、onProgressUpdateメソッド、onPostExecuteメソッドは、UIの変更できるmainスレッドで実行されます。その為、サンプルコードでは、TextViewのプロパティを変更しています。
MainActivity.kt(Coroutineで実装)
package jp.co.casareal.kotlinasynccoroutine
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
val scope = CoroutineScope(Dispatchers.Default)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
button.setOnClickListener {
scope.launch {
myTask()
}
}
}
override fun onPause() {
super.onPause()
scope.coroutineContext.cancelChildren()
}
private suspend fun myTask() {
try {
// onPreExecuteと同等の処理
withContext(Dispatchers.Main) {
text.text = "始めます"
}
// doInBackgroundメソッドと同等の処理
Thread.sleep(800)
for (i in 1..10) {
//onProgressUpdateメソッドと同等の処理
withContext(Dispatchers.Main) {
text.text = i.toString()
}
Thread.sleep(800)
}
// onPostExecuteメソッドと同等の処理
withContext(Dispatchers.Main) {
text.text = "終わります"
}
} catch (e: Exception) {
// onCancelledメソッドと同等の処理
Log.e(localClassName, "ここにキャンセル時の処理を記述", e)
}
}
}
実装の解説
非同期処理を実装するために、launch関数を利用しています。launch関数の定義は公式サイトを参照してください。
サンプルコードでは、下記の2種類の関数を使用します。
- launch
- withContext
asyncを使用しない理由としては、UIの更新を行うためにmainスレッドでの実行が必要になります。
その際、Coroutineのスレッドを変更して、UI更新処理が完了するまで、親コルーチンを中断する必要があります。
もし、asyncでやるとなるとawaitで中断する方法をとります。しかし、この様な場合は、withContext関数を利用して、Contextを切り替える方が適切です。
そのため、本記事ではwithContextを利用しています。
Scopeについて
Coroutinesを実行させるには、どのスコープで実行させるかというのが必要になります。
Coroutinesを理解する上で、多少なりとも「Scope」と「Context」と「Job」について理解しておか泣かないとこのサンプルコードを理解出来ません。また、キャンセルさせるためにも、Contextが必要となります。
この3つがどの様な位置にあるかを図にしてみました。
図の通りですが、Scopeの中にContextが存在し、そのContextの中にJobが存在します。
キャンセルする場合は、Contextをキャンセルするとその配下にあるJob全てをキャンセル出来ます。
また、CoroutineScopeは複数種類あります。
- CoroutineScope
- GlobalScope
GlobalScopeはAndroidで推奨さていないので、CoroutineScopeを利用します。
GlobalScopreが推奨されていない理由はご自身でお調べください。
さらにCoroutineScopeの実行するスレッドを引数で選択します。
val scope = CoroutineScope(Dispatchers.Default)
実装例では、上記の部分になります。
4つの種類がKotlinでは用意されています。
種類 | 説明 |
---|---|
Dispatchers.Main | Mainスレッド(UIスレッド)で実行 |
Dispatchers.Default | バックグラウンドの共有プールを利用する |
Dispatchers.IO | 追加でスレッドを立てる。 |
Dispatchers.Unconfined | 基本的には使用しないように説明がKotlinで書かれています。 |
今回はCoroutineScopeで実行させます。
launch関数
scope.launch {
// 処理
}
上記の方法で記述すると、「mainスレッドではないスレッド」で処理が実行されます。その為、UIの変更は出来ません。
withContext(Dispatchers.Main)関数
withContext関数はCorouineのContextを切り替える事ができます。
UIを変更するのであればMainスレッドに切り替える必要があります。引数に切り替えたいスレッドを指定します。
以下の様な実装になります。
withContext(Dispatchers.Main) {
// UIの更新
}
suspend修飾子
コルーチンから呼び出すメソッドや関数は、「suspend」修飾子を必ず付与しなければなりません。
private suspend fun myTask() {・・・処理・・・}
キャンセル処理
Coroutineをキャンセルするには、安全性を確保するために全てのJobをキャンセルする必要があります。
全てのJobをキャンセルするにはContextを使ってキャンセルします。ContextをキャンセルするとそのContextに保持されているJobは全てキャンセルされます。また、Jobの中で生成した子ルーチンも全てキャンセルされます。
override fun onPause() {
super.onPause()
scope.coroutineContext.cancel()
}
キャンセルするとコルーチンないでJobCancellationExceptionが発生します。もし、キャンセルの際に何かしらの処理を行いたい場合は、catch句で終了処理を記述します。
private suspend fun myTask() {
try {
〜〜〜省略〜〜〜
} catch (e: Exception) {
// onCancelledメソッドと同等の処理
Log.e(localClassName, "ここにキャンセル時の処理を記述", e)
}
まとめ
実験段階の実装方法と大きく異る部分もありますが、基本的な思想は変わっていないように感じます。
AndroidでCoroutineを使用する時はThreadやScope、Contextに注意が必要です。