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

実装
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()
は再度実行不可能なので、スタートボタンが押せる度に新しいインスタンスを作る。
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
は、ストップウォッチの状態を管理します。
sealed class TimerState {
object Start: TimerState()
object Stop: TimerState()
}
Layout
DataBindingを使用しており、ViewModelの状態を自動で変更してくれます。
android:clickable
は、trueだとボタンを押すことができます。これを設定しないと、スタートボタンを連打するとCoroutineが無限に生成されてストップウォッチのタイマーがものすごい速さで進みます。他の対処方法やそもそも実装方法が間違ってるかもしれません。
android:alpha
で、クリックできなくなったわかりやすくするためにボタンの色を薄くしています。
<?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の定義などを行う。
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
}
}