Jetpack Compose 状态驱动:从零实现可暂停恢复的计时器
2026/9/2 1:56:40 网站建设 项目流程

在实际的 Android 声明式 UI 开发中,Jetpack Compose 带来的不只是“代码写界面”的体验变化,更是一套从状态管理、重组机制到组合项生命周期的全新思维方式。很多初学者跟着视频教程做计时器,代码能跑起来,但稍微改一个需求,比如增加暂停、恢复、重置,或者把计时器封装成可复用组件,就会卡住。原因往往不是 Kotlin 语法不熟,而是没有理解 Compose 的状态驱动模型:界面不是通过“调用函数主动修改控件”来更新的,而是通过“状态变化触发重组”自动反映到界面上。

这篇文章以一个计时器项目为主线,从 Compose 的核心概念讲起,再逐步完成一个具备开始、暂停、恢复、重置能力的计时器,并解释每一段代码背后的设计意图。整体内容既适合刚接触 Compose 的 Android 开发者,也适合已经写过基础界面、想加深对状态和生命周期理解的读者。

1. 先理解 Jetpack Compose 的声明式编程逻辑

学习 Compose 之前,需要先分清传统 View 体系和 Compose 之间的本质差异。很多人写 Compose 代码时仍习惯用“找控件、改控件”的思维方式,结果写出大量生硬代码,甚至出现界面不更新、状态不同步的问题。

1.1 命令式 UI 和声明式 UI 的区别

传统的 Android View 体系是命令式 UI。开发者通过findViewById拿到控件对象,再调用setTextsetVisibilitysetOnClickListener等方法手动修改界面状态。这种模式的问题在于,当界面状态变多、交互变复杂时,状态变更与 UI 更新之间的对应关系会变得难以维护。比如一个页面里有按钮、文本、列表、loading 状态,每次状态变化都要手动控制多个控件,很容易漏掉某一部分。

Jetpack Compose 是声明式 UI。开发者只需要描述“当前状态应该呈现什么界面”,至于状态变化后怎么更新,由 Compose 框架自动完成。用一句话概括:传统写法是“把文本改成 1”,Compose 写法是“当文本状态为 1 时,界面显示 1;当状态变为 2 时,界面自动显示 2”。

这种模式下的核心思路是:

  • 界面是状态的函数,给定一份状态,就得到一份界面。
  • 状态变化后,Compose 会重新执行相应组合函数,生成新的界面。
  • 开发者不需要手动比较新旧界面差异,也不需要逐控件更新。

1.2 组合函数与重组机制

在 Compose 中,用@Composable注解标记的函数称为组合函数。组合函数不能直接像普通函数那样理解成“执行一次就结束”,它具备参与重组的能力。

当一个函数内部读取了某个mutableStateOf状态值时,Compose 会记录这种读取关系。之后该状态值发生变化,Compose 会找到读取该状态的所有组合函数,并重新执行这些函数。这个重新执行的过程叫重组。

例如下面这段代码:

@Composable fun TimerScreen() { var seconds by remember { mutableStateOf(0) } Text(text = "当前秒数:$seconds") }

seconds从 0 变为 1 时,Compose 不会重建整个 Activity,也不会重新执行所有界面代码,而是只重组读取了seconds的部分。这种精确更新是 Compose 性能设计的关键。

这里有一个容易忽略的点:remember的作用是让状态在重组后仍然保留。如果没有remember,重组时mutableStateOf会重新创建,状态值就会被重置。remember的常见使用方式是配合mutableStateOf,但两者的职责不同:

工具职责
mutableStateOf创建可观察状态,状态变化能触发重组
remember在重组期间保存状态,避免状态丢失
rememberSaveable在 Activity 重建或进程被系统回收后保存状态

1.3 Compose 计时器项目需要掌握的最小概念集

做一个计时器,至少需要掌握以下概念:

  • @Composable组合函数。
  • remembermutableStateOf管理界面状态。
  • LaunchedEffect处理协程任务,比如每秒更新一次计时。
  • ButtonText等基础组件。
  • Modifier调整布局和样式。

把这些概念组合起来,才能写出一个状态正确、行为稳定的计时器。如果只照着视频敲代码,不弄清楚这些概念,后续做暂停、恢复、重置时就会无从下手。

2. 环境准备:Compose 项目的最小配置

编写 Compose 代码前,建议先确认开发环境。这里以 Android Studio 和 Gradle Kotlin DSL 为例,给出一个可运行的 Compose 项目配置。

2.1 开发环境要求

项目建议要求
IDEAndroid Studio Hedgehog 或更高版本
JDKJDK 17
Android Gradle Plugin8.2 或更高
Kotlin1.9 或更高
Compose Compiler与 Kotlin 版本匹配
最低 SDK24 左右即可,视实际兼容要求调整

如果原始教程使用的是旧版本,比如 Kotlin 1.8 和 AGP 7.x,落地前要确认版本匹配。Compose Compiler 需要和 Kotlin 版本严格对应,否则会出现编译错误,比如This version of the Compose Compiler requires Kotlin version X

2.2 模块级 build.gradle.kts 配置

创建一个空应用项目后,在app/build.gradle.kts中配置 Compose 相关选项:

android { namespace = "com.example.timer" compileSdk = 34 defaultConfig { applicationId = "com.example.timer" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } } dependencies { val composeBom = platform("androidx.compose:compose-bom:2024.06.00") implementation(composeBom) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") implementation("androidx.activity:activity-compose:1.9.0") debugImplementation("androidx.compose.ui:ui-tooling") }

关键点说明:

  • buildFeatures { compose = true }用于开启 Compose 支持。
  • Compose BOM 用于统一管理 Compose 相关库的版本,避免一个个指定版本。
  • activity-compose提供setContent入口,让 Activity 可以直接承载 Compose 界面。

2.3 AndroidManifest 和 MainActivity 入口

如果项目是新建的,AndroidManifest.xml中正常声明 MainActivity 即可。这里要关注的是 Activity 内部如何把 Compose 界面挂载到窗口。

package com.example.timer import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.example.timer.ui.theme.TimerTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { TimerTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { TimerScreen() } } } } }

在这个入口代码里,setContent是 Activity 和 Compose 世界的连接点。它接收一个组合函数,并把这个组合函数的内容作为界面展示出来。整个计时器项目的后续开发都围绕TimerScreen展开。

3. 用状态驱动完成计时器的核心逻辑

计时器的核心逻辑不复杂:一个变量记录已经过去的秒数,一个开关控制是否在计数,一个循环每秒增加一次计数。但放到 Compose 中,要处理的不只是逻辑本身,还包括状态如何保存、如何启动任务、如何停止任务。

3.1 定义一个保存计时状态的函数

先定义一个rememberTimerState函数,用mutableStateOf保存当前秒数和运行状态。

@Composable fun rememberTimerState(): TimerState { return remember { TimerState() } } class TimerState { var seconds by mutableStateOf(0) private set var isRunning by mutableStateOf(false) private set fun start() { isRunning = true } fun pause() { isRunning = false } fun reset() { seconds = 0 isRunning = false } fun tick() { seconds++ } }

这里把状态封装进TimerState类,有几个好处:

  • 界面层不需要关心计时逻辑具体怎么改状态。
  • 多个组合函数可以共享同一个TimerState实例。
  • 后续如果需要增加“记录圈数”“存储历史记录”等功能,只需要扩展TimerState

remember在这里的作用是保证进入重组后,TimerState实例仍然是同一个,不会因为重组而重新创建。

3.2 用 LaunchedEffect 处理每秒计数

Compose 中不应该直接用while(true)Thread.sleep去更新状态,因为组合函数可能因为重组被取消或重新执行,直接在组合函数里启动线程会造成重复启动或内存泄漏。

正确的做法是使用LaunchedEffect。它会在组合函数进入组合时启动一个协程,并在组合函数退出组合或被取消时自动取消协程。

@Composable fun TimerScreen(state: TimerState) { LaunchedEffect(state.isRunning) { while (state.isRunning) { delay(1000) state.tick() } } Text( text = formatTime(state.seconds), style = MaterialTheme.typography.displayLarge ) }

这段代码里的LaunchedEffect(state.isRunning)很关键。当state.isRunning从 false 变为 true 时,LaunchedEffect会重新启动,协程开始循环计时。当state.isRunning变为 false 时,LaunchedEffect会取消上一个协程并重新启动,但因为while (state.isRunning)条件已经不满足,所以循环会直接结束。

为什么这里要用state.isRunning作为 key,而不是不传 key?如果不传 key,LaunchedEffect只在组合函数首次进入组合时启动一次,后续即使state.isRunning变化,协程仍然运行,无法通过暂停按钮停止计数。用state.isRunning作为 key,可以让协程的生命周期跟随运行状态变化。

formatTime函数用于把秒数格式化成00:0000:00:00

fun formatTime(totalSeconds: Int): String { val hours = totalSeconds / 3600 val minutes = (totalSeconds % 3600) / 60 val seconds = totalSeconds % 60 return if (hours > 0) { String.format("%02d:%02d:%02d", hours, minutes, seconds) } else { String.format("%02d:%02d", minutes, seconds) } }

3.3 完整界面:显示时间与操作按钮

接下来把显示时间、开始按钮、暂停按钮、重置按钮组合到一个界面上。

@Composable fun TimerScreen(state: TimerState) { LaunchedEffect(state.isRunning) { while (state.isRunning) { delay(1000) state.tick() } } Column( modifier = Modifier .fillMaxSize() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = formatTime(state.seconds), style = MaterialTheme.typography.displayLarge, modifier = Modifier.padding(bottom = 32.dp) ) Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { if (!state.isRunning) { Button(onClick = { state.start() }) { Text(if (state.seconds > 0) "继续" else "开始") } } else { Button(onClick = { state.pause() }) { Text("暂停") } } Button(onClick = { state.reset() }) { Text("重置") } } } }

这段代码中,界面会根据state.isRunning自动显示“开始/继续”或“暂停”。整个过程中,没有手动调用任何“更新控件”的方法,界面变化完全由状态驱动。

注意按钮文案的逻辑:当seconds大于 0 且当前未运行时,显示“继续”;否则显示“开始”。这个细节说明状态之间是有关联的,设计状态时不能只用一个isRunning布尔值,还要考虑开始和继续的场景。

4. 把状态保存到 ViewModel:避免旋转屏幕丢数据

上面的实现能在普通场景下运行,但有一个明显问题:旋转屏幕时,Activity 会重建,remember保存的状态会丢失。remember只能在组合过程中保留状态,无法跨 Activity 重建保留。

解决这个问题通常有两种方式:

  • 使用rememberSaveable保存可序列化的状态。
  • 使用ViewModel保存状态,让状态独立于界面生命周期。

对于计时器这种需要持续运行的场景,推荐使用ViewModel

4.1 创建 TimerViewModel

class TimerViewModel : ViewModel() { var seconds by mutableStateOf(0) private set var isRunning by mutableStateOf(false) private set fun start() { isRunning = true } fun pause() { isRunning = false } fun reset() { seconds = 0 isRunning = false } fun tick() { seconds++ } }

此时不需要remember,因为ViewModel实例的生命周期由 ViewModelStore 管理,Activity 重建后,同一个 ViewModel 实例仍然存活。

4.2 在 Activity 中获取 ViewModel

Compose 中可以使用viewModel()获取 ViewModel 实例:

import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun TimerRoute( viewModel: TimerViewModel = viewModel() ) { val state = viewModel TimerScreen(state) }

TimerScreen的逻辑可以保持不变,因为它只依赖state接口。这样做的收益是:界面和状态源解耦,测试时可以传入自定义 state,生产环境可以直接用 ViewModel。

4.3 进程被系统回收时的处理

ViewModel能解决 Activity 重建问题,但无法解决进程被系统回收的问题。如果应用进入后台后被系统杀掉,重新打开时 ViewModel 会重新创建,计时秒数仍会丢失。

如果业务要求“进程被杀后继续计时”,需要在onStoponSavedInstanceState中保存上次计时时间和运行状态,恢复时计算已经过去的时间。这个方案会增加复杂度,一般只在正式产品中需要。

学习阶段建议循序渐进:

  • 先掌握remember
  • 再理解ViewModel
  • 最后处理进程恢复。

不要一上来就把所有状态都塞进持久化方案,那样会模糊 Compose 学习的重点。

5. 从学习示例到生产级计时器的差距

视频教程中的计时器通常是演示LaunchedEffectmutableStateOf的最小案例。如果按真实产品标准看,还需要补齐不少细节。

5.1 计时精度问题

上面示例使用delay(1000)每秒加 1。真实运行中,delay不是严格精确的,它受系统调度、主线程负载影响。长时间运行后,计时器可能偏差几秒甚至几十秒。

改进方式是不依赖累加次数,而是记录开始时间和累计时间:

class TimerViewModel : ViewModel() { private var startTime: Long = 0L private var accumulatedTime: Long = 0L var elapsedMillis by mutableStateOf(0L) private set fun start() { startTime = SystemClock.elapsedRealtime() isRunning = true } fun pause() { accumulatedTime += SystemClock.elapsedRealtime() - startTime isRunning = false elapsedMillis = accumulatedTime } fun tick() { elapsedMillis = accumulatedTime + (SystemClock.elapsedRealtime() - startTime) } }

这种方式下,哪怕delay因为主线程繁忙被推迟执行,最终计算出的时间仍然是准确的时间间隔。

5.2 界面刷新频率

delay(1000)每秒刷新一次界面,对于秒级显示足够。如果显示毫秒,比如“00:00.5”,就需要把刷新间隔改成 50 或 100 毫秒。刷新频率越高,重组次数越多,对性能和耗电影响越大。

生产项目建议:

  • 秒级显示时,每秒刷新一次。
  • 毫秒级显示时,每 100 毫秒刷新一次,并通过derivedStateOf或字符串格式化避免不必要的重组。
  • 不需要显示时间时,暂停或停止计时刷新。

5.3 生命周期与资源释放

LaunchedEffect会把协程绑定到组合生命周期。如果 Activity 进入后台,组合没有退出,协程会继续运行并每秒更新状态,这会造成不必要的计算和电量消耗。

LifecycleResumeEffectrepeatOnLifecycle可以按生命周期控制计时行为。简单做法是:

  • onStart恢复计时。
  • onStop暂停计时并保存累计时间。

实际项目中,通常还需要考虑:

  • 屏幕息屏时是否暂停。
  • 用户切到后台再回来时,界面显示的时间是否准确。
  • 是否需要在通知栏显示计时状态。

这些属于产品需求,需要和普通的 Compose 状态管理逻辑配合处理。

6. 常见问题排查:计时器为什么不更新、停不下来

学习 Compose 计时器时,最常见的问题集中在状态丢失、协程重复启动、界面不更新和按钮状态不正确这四类。下面按现象、原因、检查方式、解决方案整理。

6.1 界面不更新,Text 一直显示 0

可能原因:

  • 没有使用mutableStateOf,只是普通 Kotlin 变量。
  • 使用了remember { mutableStateOf(0) },但读取时没有用by委托。
  • LaunchedEffect没有正确触发,协程没有运行。

检查方式:

  • 查看状态属性是否由mutableStateOf构造。
  • 检查是否使用了val seconds by remember { mutableStateOf(0) }
  • tick()中输出日志,确认每秒是否调用。

解决方案:

var seconds by remember { mutableStateOf(0) }

不要这样写:

var seconds = remember { mutableStateOf(0) }

第一种写法通过by委托读取seconds的当前值,可以直接seconds++;第二种写法读取到的是MutableState对象本身,界面显示时也要写seconds.value,而且seconds++无法作用到MutableState上。

6.2 按钮点了开始,计时器没有反应

可能原因:

  • LaunchedEffect的 key 没有使用isRunning,协程启动后没有感知到状态变化。
  • 点击按钮修改了状态,但协程中的条件没有变。

常见错误写法:

LaunchedEffect(Unit) { while (true) { delay(1000) state.tick() } }

这种写法的问题非常明显:不管isRunning是 true 还是 false,协程都会一直运行。点击暂停按钮后,协程仍然继续计时。

推荐写法:

LaunchedEffect(state.isRunning) { while (state.isRunning) { delay(1000) state.tick() } }

6.3 旋转屏幕后计时归零

场景:点击开始后旋转屏幕,计时从 0 开始。

这种情况说明状态保存得不够。如果使用的是remember,旋转屏幕后组合函数重新创建,remember无法跨 Activity 重建保留状态,需要改成rememberSaveableViewModel

rememberSaveable的示例:

var seconds by rememberSaveable { mutableStateOf(0) }

rememberSaveable只能保存可放入 Bundle 的类型,如果状态比较复杂,比如包含 List、自定义对象,需要注意序列化问题。推荐在真实项目中使用ViewModel

6.4 暂停后继续计时,时间跳跃

现象:暂停时显示 00:10,继续后几秒内突然跳到 00:20。

原因:LaunchedEffectisRunning变为 false 时会取消协程,但如果旧的协程没有被真正取消,或者多个协程同时运行,就会出现重复tick()

检查方式:

  • tick()中加入日志,观察每秒是否调用了多次。
  • 检查是否在多个位置创建了LaunchedEffect

解决方案:

  • 确保LaunchedEffect的 key 是isRunning
  • 避免同一个组合函数中多个LaunchedEffect同时操作同一个状态。
  • 状态更新使用+= 1,而不是依赖外部传入的累计值。

6.5 常见问题速查表

问题现象常见原因检查方式处理建议
Text 一直显示 0状态不是mutableStateOf,或读取方式错误检查状态声明和读取代码使用by委托读取mutableStateOf
点击开始无反应LaunchedEffectkey 错误或缺少 key检查 key 是否设置为isRunningLaunchedEffect(state.isRunning)
旋转屏幕归零仅使用remember尝试旋转屏幕复现改用rememberSaveableViewModel
暂停后时间乱跳多个协程同时更新状态tick()中打印日志用 key 控制协程生命周期,避免重复启动
时间越来越慢使用delay(1000)累加秒数长时间运行对比系统时间改为基于SystemClock.elapsedRealtime()计算耗时

7. 把计时器扩展成可复用组件

当计时器逻辑稳定后,可以把它封装成独立组件,供多个页面或项目复用。封装时要注意状态与界面分离。

7.1 定义可复用状态接口

interface TimerState { val seconds: Int val isRunning: Boolean fun start() fun pause() fun reset() }

TimerViewModel实现这个接口。组合函数只依赖TimerState接口,不直接依赖 ViewModel,这样测试时可以传入 fake 实现,也方便替换为其他状态源。

7.2 封装通用计时器组件

@Composable fun TimerDisplay( state: TimerState, modifier: Modifier = Modifier ) { LaunchedEffect(state.isRunning) { while (state.isRunning) { delay(1000) if (state is TimerViewModel) { state.tick() } } } Column(modifier = modifier) { Text(text = formatTime(state.seconds)) if (!state.isRunning) { Button(onClick = { state.start() }) { Text(if (state.seconds > 0) "继续" else "开始") } } else { Button(onClick = { state.pause() }) { Text("暂停") } } Button(onClick = { state.reset() }) { Text("重置") } } }

这里的写法有一个取舍:直接判断state is TimerViewModel会让组件依赖具体实现,不够抽象。更合理的方案是在TimerState接口中增加tick()方法,但那样会暴露内部实现细节。封装时可以根据项目实际需要决定:

  • 如果组件只服务一个页面,可以直接使用 ViewModel。
  • 如果组件要跨项目复用,可以抽象tick(),但要在接口文档里说明它仅供内部协程调用。

7.3 在业务页面中使用组件

业务页面只需要创建 ViewModel,并传入计时器组件:

@Composable fun WorkoutScreen( viewModel: TimerViewModel = viewModel() ) { Column { Text("训练计时") TimerDisplay(state = viewModel) } }

这样设计后,计时逻辑可以复用到倒计时、正计时、番茄钟、健身旁路计时等场景,业务页面不需要重复实现。

8. 从计时器项目延伸到 Compose 学习路径

计时器项目虽然简单,但覆盖了 Compose 开发中最重要的几个核心点:状态、重组、协程、生命周期。完成这个项目后,建议按以下顺序继续深入。

8.1 解锁状态管理进阶

计时器里只用到单个状态对象,真实项目中通常会有多个状态源。后续可以学习:

  • StateFlowFlow在 Compose 中的使用。
  • collectAsStateWithLifecycle收集异步状态。
  • derivedStateOf根据其他状态计算派生状态,减少不必要的重组。
  • rememberCoroutineScope在非组合协程作用域中启动任务。

8.2 掌握列表和复杂布局

计时器界面比较简单,只有 Column 和 Row。真实项目会遇到 LazyColumn、LazyRow、嵌套滚动、动态高度等布局问题。建议在计时器基础上增加“历史记录”列表,学习LazyColumnitems的使用。

8.3 理解 Compose 性能优化

计时器每秒触发一次重组,性能压力不大,但已经能观察到重组的行为。可以继续学习:

  • 如何通过@Stable@Immutable标记减少重组范围。
  • rememberUpdatedState解决长时间协程读取旧值的问题。
  • Modifier.animateContentSizeCrossfade等动画 API。

8.4 学习路径清单

阶段学习目标练习方式
第一阶段掌握状态和重组mutableStateOf做一个计数器
第二阶段掌握协程生命周期LaunchedEffect做计时器
第三阶段掌握状态提升把计时器状态提升到父组件
第四阶段掌握 ViewModel用 ViewModel 保存计时器状态
第五阶段掌握列表给计时器增加历史记录列表
第六阶段掌握持久化保存最后计时时间并恢复

9. 最佳实践:做好一个 Compose 计时器的关键原则

计时器虽然小,但能反映一个人对 Compose 基础的理解程度。下面几条原则值得在实际项目中长期坚持。

9.1 状态放在最合理的层级

计时器的状态如果放在TimerScreen内部,页面退出后状态就丢失。如果放在 ViewModel,Activity 重建后仍能保留。如果多个页面共享同一个计时器,状态还应继续提升到 Activity 或应用级共享层。状态层级越高,生命周期越长,但也越难管理。

推荐顺序:

  • 单个页面使用rememberSaveable或页面级 ViewModel。
  • 多个页面共享使用 Activity 级 ViewModel。
  • 应用级共享使用单例或依赖注入容器。

9.2 不要在高频组合函数中做耗时操作

计时器每秒重组一次,如果组合函数里直接做文件读写、网络请求、大量计算,会在主线程产生明显卡顿。正确做法是把耗时操作放在协程或后台线程中,只在组合函数中读取最终状态。

9.3 用日志辅助理解重组

刚开始学习 Compose 时,可以打印日志观察重组次数:

@Composable fun TimerDisplay(state: TimerState) { LaunchedEffect(Unit) { println("TimerDisplay recomposed") } // ... }

要说明的是,LaunchedEffect(Unit)只在首次进入组合时执行,不能用来精确统计重组次数。更准确的方式是调用println("recompose")直接放在组合函数体中,但生产环境不要保留这种代码。

9.4 状态变量命名要体现业务含义

secondstime更清晰,isRunningrunning更准确,accumulatedTimetotal更容易理解。计时器项目规模小,命名影响不明显,但到大型项目中,含糊的命名会让状态流变混乱。

9.5 持续运行的任务要关注电量

计时器如果常驻后台,每秒唤醒一次 CPU 和更新 UI,对电量影响不可忽略。生产项目通常需要:

  • 后台时不刷新界面。
  • 需要提醒时使用前台服务。
  • 时间计算基于系统时间戳,而不是依赖循环次数。

10. 结语

Jetpack Compose 的声明式 UI 模式改变了 Android 界面的开发方式。计时器项目小,却完整覆盖了状态、重组、协程、生命周期这些核心概念。从remembermutableStateOf开始,到LaunchedEffect控制每秒计时,再到用 ViewModel 解决旋转屏幕丢状态,这一步一步走下来的过程,比单纯抄代码更有价值。

实际项目中,计时器通常会遇到更复杂的需求:倒计时、暂停恢复、后台保活、多计时器切换、通知栏提醒。建议先把这个基础计时器做稳定,再把状态管理、生命周期、持久化方案逐步加入。对新手而言,把这一条技术链路理解清楚,后续学习 Compose 的列表、动画、MVVM 架构都会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询