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

#はじめに
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**を使ったやり方を使えばメインスレッドと同じ
これを基本とすれば今後作成するクラスがより一層カプセル化できる様になると思われます。