今天我們來用Handler實作一個番茄鐘計時器,主要希望透過這個實作來理解Handler的功能。要說明Handler,就不可避免地得順便一起提到Thread和Runnable,他們三個與多執行緒相關的重要概念,它們各自有不同的角色並且可以互相搭配使用。
Thread代表一個執行緒,它允許你在背景中執行任務而不會阻塞主執行緒(通常是 UI 執行緒)。在Kotlin 中,可以通過擴展Thread類或直接創建並運行Runnable來使用執行緒。
Runnable是一個介面,通常作為執行緒要執行的任務。Runnable只包含一個方法run(),當你把它傳遞給 Thread或Handler時,該方法會在相應的執行緒上執行。
Handler主要用於執行緒之間的通訊,通常在Android應用中,Handler用來發送和處理消息或Runnable。它會將任務排隊,並在與Handler相關聯的Looper所運行的執行緒上執行這些任務。
簡單來說Thread負責在新執行緒中運行任務,Runnable是描述任務的介面,而Handler是用來在特定執行緒上排隊和調度任務的工具,特別是在Android中用於處理UI執行緒的更新。
下面的程式碼是使用Handler來實作一個番茄鐘計時器
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import java.util.concurrent.TimeUnit
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PomodoroTimer()
}
}
}
@Composable
fun PomodoroTimer() {
var timeLeftInMillis by remember { mutableStateOf(25 * 60 * 1000L) } // 25 分鐘 (單位為毫秒)
var isRunning by remember { mutableStateOf(false) }
var handler = remember { Handler(Looper.getMainLooper()) }
var lastUpdateTime by remember { mutableStateOf(0L) } // 上次更新的時間
// 每秒更新一次倒數時間
val updateRunnable = object : Runnable {
override fun run() {
if (isRunning) {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - lastUpdateTime // 計算已經經過的時間
timeLeftInMillis -= elapsedTime // 減去已經經過的時間
lastUpdateTime = currentTime // 更新上次 tick 的時間
if (timeLeftInMillis > 0) {
handler.postDelayed(this, 1000) // 每秒更新一次
} else {
timeLeftInMillis = 0
isRunning = false
}
}
}
}
// 將毫秒轉換為 mm:ss 格式
fun formatTime(timeInMillis: Long): String {
val minutes = TimeUnit.MILLISECONDS.toMinutes(timeInMillis) % 60
val seconds = TimeUnit.MILLISECONDS.toSeconds(timeInMillis) % 60
return String.format("%02d:%02d", minutes, seconds)
}
// 開始或繼續計時
fun startTimer() {
if (!isRunning) {
lastUpdateTime = System.currentTimeMillis() // 記錄當前時間
handler.post(updateRunnable) // 開始更新
isRunning = true
}
}
// 暫停計時
fun pauseTimer() {
handler.removeCallbacks(updateRunnable) // 停止更新
isRunning = false
}
// 重置計時
fun resetTimer() {
handler.removeCallbacks(updateRunnable) // 停止更新
timeLeftInMillis = 25 * 60 * 1000L // 重置為 25 分鐘
isRunning = false // 停止運行狀態
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "番茄鐘", style = androidx.compose.material3.MaterialTheme.typography.headlineLarge)
Spacer(modifier = Modifier.height(32.dp))
Text(text = formatTime(timeLeftInMillis), style = androidx.compose.material3.MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(32.dp))
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { if (isRunning) pauseTimer() else startTimer() }) {
Text(text = if (isRunning) "暫停" else "開始")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { resetTimer() }) {
Text(text = "重置")
}
}
}
}
@Preview(showBackground = true)
@Composable
fun PomodoroTimerPreview() {
PomodoroTimer()
}
Handler是Android用來處理線程之間消息傳遞的工具,我們利用它在主線程中每秒更新一次UI。Runnable是一個可以被Handler執行的任務,在這裡我們每秒執行一次,來更新剩餘的時間。
var handler = remember { Handler(Looper.getMainLooper()) }
這裡,我們初始化了一個Handler,並且將它關聯到Looper.getMainLooper()。MainLooper是Android 的主線程(UI 線程),這意味著這個Handler將會在主線程上運行它處理的任務,從而保證更新倒數時間的操作與界面顯示保持一致,避免異步操作造成的問題。
val updateRunnable = object : Runnable {
override fun run() {
if (isRunning) {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - lastUpdateTime // 計算已經經過的時間
timeLeftInMillis -= elapsedTime // 減去已經經過的時間
lastUpdateTime = currentTime // 更新上次 tick 的時間
if (timeLeftInMillis > 0) {
handler.postDelayed(this, 1000) // 每秒更新一次
} else {
timeLeftInMillis = 0
isRunning = false
}
}
}
}
上面這段程式碼創建了一個Runnable,它每秒會更新一次計時器的狀態。我們可以把它想像成一個不斷重複運行的「任務」,每次運行時,Runnable 都會更新剩餘時間,然後重新計劃自己在1秒後再次運行。
在這個計時器中,Handler扮演了核心的計時角色,它確保Runnable在正確的時間點執行,並且控制整個倒數過程,同時通過Runnable和postDelayed(),它讓我們可以控制倒數計時的開始、暫停和繼續,並且確保計時過程中的UI變化都能順利運行。
APP效果如下
今天我們透過一個簡單的實作來了解Handler的用法和作用,我們今天的內容就先到這邊,讓我們明天再見。