LoginSignup
35
26

More than 3 years have passed since last update.

KotlinとCoroutinesでAyncTask

Posted at

はじめに

以前、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詳細については省略します。

サンプルアプリ

完成イメージ

この記事で作成するサンプルアプリの完成イメージは、下図のとおりです。
01_アプリイメージ.png

サンプルアプリの詳細

スタートボタンを押下すると、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"

レイアウト

最終的なレイアウトのイメージは、下図のとおりです。

02_レイアウト.png

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でサンプルアプリを実装した時のアクティビティのソースコードは、以下のとおりです。

MainActivity
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つがどの様な位置にあるかを図にしてみました。

Untitled.png

図の通りですが、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に注意が必要です。

35
26
1

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
  3. You can use dark theme
What you can do with signing up
35
26