0
3

More than 3 years have passed since last update.

【Kotlin】スレッドを使って一定間隔でイベントを発生

Last updated at Posted at 2020-06-03

はじめに

Threadを使って一定時間毎にイベントを発生させるアプリを作ります。
独自イベントは、別途独自イベントを紹介するページを参考にして下さい。

アプリの仕様

ボタンを押すとカウンターが自動的にカウントアップするアプリを作ります。
100ms 1/10秒ごとに1カウントアップします。
開始ボタンでカウントアップ開始、停止ボタンがカウントアップを停止します。
※中断は未使用

launchを使ったやり方

準備

スレッドを使うのですがそのまま使うと画面View(UI)にアクセス出来ないのでcoroutines(コルーチン)を使用します。コルーチンを使うにはGradleスクリプトのbuild.gradle(app)に下記を追加する必要があります。

build.gradle(app)

build.gradle(app)
dependencies {
   省略
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
}

strings.xml

strings.xml
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
    <string name="app_name">ThreadTimerSample</string>
    <string name="btStart">開始</string>
    <string name="btStop">停止</string>
    <string name="btPause">中断</string>
</resources>

activity_main.xml

ボタンを3つとタイマーカウント用のTextViewを1つ配置します。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btStart"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="onButtonStart"
                android:text="@string/btStart" />

            <Button
                android:id="@+id/btPause"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="onButtonPause"
                android:text="@string/btPause" />

            <Button
                android:id="@+id/btStop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="onButtonStop"
                android:text="@string/btStop" />

        </LinearLayout>

        <TextView
            android:id="@+id/tvTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

MainActivity.kt
class MainActivity : AppCompatActivity()  {

    private val timer = ThreadTimer()

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

    private val mainListener = object : ThreadTimerInterface{
        override fun onSuccess(result: Int) {
            super.onSuccess(result)
            val vtTime = findViewById<TextView>(R.id.tvTime)
            vtTime.setText(result.toString())
        }
    }

    fun onButtonStart(view : View){
        timer.start()
    }
    fun onButtonStop(view : View){
        timer.stop()
    }
    fun onButtonPause(view : View){}
}

abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onDestroy() {
        super.onDestroy()
        //cancel()
    }
}

interface  ThreadTimerInterface : ThreadTimer.Listener {
    fun onSuccess(result: Int){}
}

class ThreadTimer() : ScopedAppActivity(){
    private var listener: ThreadTimerInterface? = null
    interface Listener {}
    var thread : Thread? = null
    var counter:Int = 0
    var enable : Boolean = false

    fun setListener(listener: ThreadTimer.Listener?) {
       if (listener is ThreadTimerInterface) {
           this.listener = listener as ThreadTimerInterface
       }
    }

    fun start(){
        if (enable == true) return
        enable = true

        thread = Thread {
            while (enable) {
                try {
                    Thread.sleep(100)
                    launch {
                        counter++
                        listener?.onSuccess(counter);
                    }

                } catch (ex: InterruptedException) {
                    Thread.currentThread().interrupt()
                }
            }
        }
        thread?.start()
    }
    fun stop(){
        if (enable == false) return
        thread?.interrupt()
        enable = false
    }
}

説明

イベントは別で説明していますのでスレッドに限定します。

start.kt
    fun start(){
        if (enable == true) return
        enable = true

        thread = Thread {
            while (enable) {
                try {
                    Thread.sleep(100)
                    launch {
                        counter++
                        listener?.onSuccess(counter);
                    }

                } catch (ex: InterruptedException) {
                    Thread.currentThread().interrupt()
                }
            }
        }
        thread?.start()
    }
    fun stop(){
        if (enable == false) return
        thread?.interrupt()
        enable = false
    }

クラスのstartメソッドが呼ばれると thread = Threadとしてスレッドを生成して同時にスレッドで行う処理も書いています。

スレッドの処理はwhile で永久ループとしてその中でThread.sleep(100)として100ms 1/10秒休んだ後、カウントアップを行い、イベントを発生させています。
launch {} で囲んだ部分はスレッドセーフとなっており画面表示 ViewのUIにアクセス出来ます。launch を外すとアプリが落ちます。
永久ループの中でスレッドの停止命令が出ても良いようにtry{}catch{}でスレッドを中断させています。

stopメソッドではスレッドを停止させています。
startとstopそれぞれ処理が連続で2回実行されないようにenable値を利用して防止しています。

なお
abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {で本当はcancel()を呼ぶらしいのですがエラーのためコメントにしてます。

Handlerを使ったやり方

スレッドセーフにするためにわざわざlaunch を使うのは煩わしいので回避方法を考えました。
要はHandlerを利用してメインと同じプロセスで処理が出来れば良いのでHandlerを使えば良いのです。

ところがHandlerをメインで生成してスレッド側で一時的にメッセージを受け取り、それをイベントとして発生させることが出来ません。独自Handlerを作っても同じです。

ですが先ほどの例で作ったThreadTimerはlaunch さえ不要であれば 継承していないAnyクラスなのでHandlerを継承してその内部でThreadを作って処理すれば解決できます。

AndroidStudio 4の環境で動作するように改良しました。

MainActivity.kt

MainActivity.kt
class MainActivity : AppCompatActivity()  {

    private val timer = ThreadTimer()

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

    private val mainListener = object : ThreadTimerInterface{
        override fun onSuccess(result: Int) {
            super.onSuccess(result)
            val vtTime = findViewById<TextView>(R.id.tvTime)
            vtTime.setText(result.toString())
        }
    }

    fun onButtonStart(view : View){
        timer.start()
    }
    fun onButtonStop(view : View){
        timer.stop()
    }
    fun onButtonPause(view : View){}
}

interface  ThreadTimerInterface : ThreadTimer.Listener {
    fun onSuccess(){}
}

class ThreadTimer() : android.os.Handler() {
    private var listener: ThreadTimerInterface? = null
    interface Listener {}

    var thread : Thread? = null
    var counter:Int = 0
    var enable : Boolean = false

    override fun handleMessage(msg: Message) {
        listener?.onSuccess()
    }


    fun setListener(listener: ThreadTimer.Listener?) {
        if (listener is ThreadTimerInterface) {
            this.listener = listener as ThreadTimerInterface
        }
    }

    fun start(){
        if (enable == true) return
        enable = true

        thread = Thread {
            while (enable) {
                try {
                    Thread.sleep(100)
                    counter++
                    super.sendEmptyMessage(counter)

                } catch (ex: InterruptedException) {
                    Thread.currentThread().interrupt()
                }
            }
        }
        thread?.start()
    }
    fun stop(){
        if (enable == false) return
        thread?.interrupt()
        enable = false
    }
}

これでも同じ動作になることがわかります。

handleMessage.kt
    override fun handleMessage(msg: Message) {
        listener?.onSuccess(msg.what)
    }

メインと同じプロセスとしていったん処理を受け取るここが要です。
イベントが1つなので非常に簡単な書き方になりましたが msgの内容で処理を分岐させることもできます。
メインのプロセスなのでここで扱う変数は msgに限定した方がよさそうです。

今回使用した値は数値のため書き換わることはありませんが、オブジェクトを使用する場合はこの中で変数を生成して値をコピーして使う必要があります。

最後に

スレッドセーフ部分と独自イベント部分を除けばごく普通のプログラムとなりました。
特にHandlerを使ったやり方を使えばメインスレッドと同じ
これを基本とすれば今後作成するクラスがより一層カプセル化できる様になると思われます。

0
3
0

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
0
3