はじめに
Threadを使って一定時間毎にイベントを発生させるアプリを作ります。
独自イベントは、別途独自イベントを紹介するページを参考にして下さい。
アプリの仕様
ボタンを押すとカウンターが自動的にカウントアップするアプリを作ります。
100ms 1/10秒ごとに1カウントアップします。
開始ボタンでカウントアップ開始、停止ボタンがカウントアップを停止します。
※中断は未使用
launchを使ったやり方
準備
スレッドを使うのですがそのまま使うと画面View(UI)にアクセス出来ないのでcoroutines(コルーチン)を使用します。コルーチンを使うにはGradleスクリプトのbuild.gradle(app)に下記を追加する必要があります。
build.gradle(app)
dependencies {
省略
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
}
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つ配置します。
<?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
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
}
}
説明
イベントは別で説明していますのでスレッドに限定します。
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
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
}
}
これでも同じ動作になることがわかります。
override fun handleMessage(msg: Message) {
listener?.onSuccess(msg.what)
}
メインと同じプロセスとしていったん処理を受け取るここが要です。
イベントが1つなので非常に簡単な書き方になりましたが msgの内容で処理を分岐させることもできます。
メインのプロセスなのでここで扱う変数は msgに限定した方がよさそうです。
今回使用した値は数値のため書き換わることはありませんが、オブジェクトを使用する場合はこの中で変数を生成して値をコピーして使う必要があります。
最後に
スレッドセーフ部分と独自イベント部分を除けばごく普通のプログラムとなりました。
特にHandlerを使ったやり方を使えばメインスレッドと同じ
これを基本とすれば今後作成するクラスがより一層カプセル化できる様になると思われます。