Timerでストップウォッチ

個人開発でタイマー的なものを作ろうと思って、実装できたので備忘録として書きます。

# 目標
<img width= 300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289190/bbbc908d-ee5e-485f-d46c-e3014deaf687.gif">


# 実装
## Gradle

```build.gradle
android {
    ...
    dataBinding {
        enabled = true
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
dependencies {
    ...
    // coroutine
    def coroutinesVersion = '1.3.7'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"

    // ktx
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.fragment:fragment-ktx:1.2.4"
}
```

## ViewModel
`Timer().scheduleAtFixedRate(delay, period, TimerTask)`は、delay(ms)後にTimerTaskをperiod(ms)間隔で実行します。
今回は、スタートボタンが押されたら、`startTimer()`を実行し、`Timer()`を起動します。`Timer()` で実行するタスクは、100(ms)毎にストップウォッチを100(ms)進めます。ストップボタンが押された場合は、`Timer()`を破棄(cancel())します。一度、破棄された`Timer()`は再度実行不可能なので、スタートボタンが押せる度に新しいインスタンスを作る。

```MainViewModel.kt
class MainViewModel : ViewModel() {
    private var timer = Timer()
    private val timerTask: TimerTask.() -> Unit = {
        viewModelScope.launch {
            _time.value = time.value?.plus(1)
        }
    }

    private val _time = MutableLiveData<Int>(0)
    val time: LiveData<Int>
        get() = _time

    private val _timerState = MutableLiveData<TimerState>(TimerState.Stop)
    val timerState: LiveData<TimerState>
        get() = _timerState

    val timerText: LiveData<String> = Transformations.map(time) {
        val hour = it / 36000
        val minute = (it % 36000) / 600
        val second = ((it % 36000) % 600) / 10

        "%02d:%02d:%02d".format(hour, minute, second)
    }

    fun startTimer() {
        _timerState.value = TimerState.Start
        timer = Timer()
        timer.scheduleAtFixedRate(0, 100, timerTask)
    }

    fun stopTimer() {
        _timerState.value = TimerState.Stop
        timer.cancel()
    }

    fun resetTimer() {
        _time.value = 0
    }
}
```

## State
`TimerState`は、ストップウォッチの状態を管理します。

```TimerState.kt
sealed class TimerState {
    object Start: TimerState()
    object Stop: TimerState()
}
```

## Layout
DataBindingを使用しており、ViewModelの状態を自動で変更してくれます。
`android:clickable`は、trueだとボタンを押すことができます。これを設定しないと、スタートボタンを連打するとCoroutineが無限に生成されてストップウォッチのタイマーがものすごい速さで進みます。他の対処方法やそもそも実装方法が間違ってるかもしれません。
`android:alpha`で、クリックできなくなったわかりやすくするためにボタンの色を薄くしています。


```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    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"
    >

    <data>

        <import type="com.example.myapplication.TimerState" />

        <variable
            name="viewmodel"
            type="com.example.myapplication.MainViewModel"
            />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        >

        <TextView
            android:id="@+id/timer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="140dp"
            android:text="@{viewmodel.timerText}"
            android:textSize="30sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="@string/timer"
            />

        <Button
            android:id="@+id/start_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="220dp"
            android:onClick="@{() -> viewmodel.startTimer()}"
            android:text="@string/start"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:clickable="@{viewmodel.timerState instanceof TimerState.Start ? false : true}"
            android:alpha="@{viewmodel.timerState instanceof TimerState.Start ? 0.2f : 1f}"
            />

        <Button
            android:id="@+id/stop_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="220dp"
            android:onClick="@{() -> viewmodel.stopTimer()}"
            android:text="@string/stop"
            app:layout_constraintEnd_toStartOf="@+id/reset_button"
            app:layout_constraintStart_toEndOf="@+id/start_button"
            app:layout_constraintTop_toTopOf="parent"
            android:clickable="@{viewmodel.timerState instanceof TimerState.Stop? false : true}"
            android:alpha="@{viewmodel.timerState instanceof TimerState.Stop ? 0.2f : 1f}"
            />

        <Button
            android:id="@+id/reset_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginTop="220dp"
            android:onClick="@{() -> viewmodel.resetTimer()}"
            android:text="@string/reset"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:clickable="@{viewmodel.timerState instanceof TimerState.Start ? false : true}"
            android:alpha="@{viewmodel.timerState instanceof TimerState.Start ? 0.2f : 1f}"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

## Activity
bindingの定義などを行う。

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val mainViewModel: MainViewModel by viewModels()


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewmodel = mainViewModel
        binding.lifecycleOwner = this
    }
}
```