今天我們來用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的用法和作用,我們今天的內容就先到這邊,讓我們明天再見。