当前位置: 首页 > news >正文

用Jetpack Compose从零实现安卓计时器:状态驱动UI与协程实战

这次我们直接用 Jetpack Compose 写一个能跑起来的安卓计时器。项目目标很明确:不碰 XML 布局,全程用声明式 UI 完成界面、状态、定时刷新和控制逻辑,最后打包 APK 装到手机上验证。

如果你已经看过不少 Compose 教程,但对“状态驱动界面刷新”还没形成直觉,这个计时器项目就是最合适的练手案例。它比 To-Do 列表更贴近真实需求,涉及到协程、状态保存、重组、点击事件、动态文本格式化,做一遍之后,Compose 的核心机制基本就通了。

本文会从零开始,带你完成项目创建、Gradle 配置、计时器界面编写、状态管理、运行调试和 APK 导出。代码全部是可直接复制的完整示例,不搞半成品。

1. 核心能力速览

能力项说明
开发框架Jetpack Compose,声明式 UI
项目类型安卓原生应用,单 Activity
核心功能秒表计时、开始/暂停/重置、动态文本刷新
界面实现纯 Compose,无 XML 布局
状态管理remember、rememberSaveable、mutableLongStateOf
定时刷新Kotlin 协程 + LaunchedEffect + delay
最低运行环境Android 5.0(API 21)以上,Android Studio 模拟器或真机
构建产物Debug APK / Release APK
是否支持 API本地应用,无远程接口,可通过 Compose 封装本地服务
是否支持批量任务本身不支持,可结合 WorkManager 扩展周期任务
适合场景Compose 新手练习、状态管理理解、计时器类工具开发

这里要特别说明一点:Compose 不是“不用写逻辑”,而是把“界面如何根据状态变化”这件事交给了框架。你只需要定义状态,界面会自动重组。这也是声明式 UI 和传统 findViewById 写法最大的区别。

2. 适用场景与使用边界

这个计时器项目适合以下读者:

  • 刚接触 Compose,想知道一个完整应用怎么从零搭建的开发者。
  • 已经会用 XML + View 体系写安卓,想对比声明式 UI 和命令式 UI 差异的开发者。
  • 需要一个“小而完整”的实战项目,用来理解状态、重组、协程这几个概念的初学者。
  • 准备用 Compose 做毕设、课程设计或内部工具,需要先跑通最小原型的开发者。

能解决的问题:

  • 理解 Compose 中状态如何驱动界面刷新。
  • 掌握 LaunchedEffect 的生命周期绑定方式。
  • 掌握协程中 delay 执行定时任务的写法。
  • 掌握 Button、Text、Card 等基础组件的组合方式。
  • 掌握 APK 构建和真机安装流程。

不适合的场景:

  • 生产级秒表应用:缺少后台运行、锁屏计时、系统通知、精确到毫秒的计时精度。
  • 多模块大型项目:本文只做一个单模块示例,不涉及 Navigation、Hilt、Room。
  • 需要远程同步数据的场景:没有接后端,也没有数据库。

这里要强调合规边界:

  • 如果后续要把这个项目发布到应用商店,需要确认应用图标、名称、素材不侵犯第三方权益。
  • 如果扩展网络功能、上传用户数据,需要在隐私政策中明确说明,并取得用户授权。
  • 不要拿这个项目去套壳发布,更不要加入任何获取用户隐私的代码。
  • 项目中使用的时间、版本号等依赖,以你本机 Android Studio 实际解析结果为准。

3. 环境准备与前置条件

开始之前,先把环境确认一遍。下面给出一套通用检查清单,具体版本以你本机为准。

3.1 必需软件

软件用途版本建议
Android Studio开发 IDE建议使用较新的稳定版
JDK编译 Java/Kotlin 代码JDK 17 或更高
Android SDK编译安卓应用建议 API 34 或更高
Gradle构建工具由 Android Studio 自动管理

安装完成后,建议先创建一个 Empty Activity 项目确认环境没问题,再继续下面的步骤。

3.2 硬件要求

  • 开发机:8GB 内存以上。Compose 编译和模拟器运行同时进行时,内存占用会明显升高,16GB 更从容。
  • 真机或模拟器:Android 5.0 以上。如果电脑配置一般,可以直接用真机调试,避免模拟器卡顿。

3.3 磁盘空间

  • Android Studio 安装后默认占 3GB 以上。
  • Gradle 依赖下载,首次构建需要较大的网络传输量,建议预留 10GB 空间。
  • 构建缓存会随项目增多逐渐膨胀,建议定期清理。

3.4 网络环境

  • 首次同步 Gradle 依赖需要访问 Maven 仓库。
  • 如果依赖下载慢,可以在项目的settings.gradle.kts中配置国内镜像仓库。
  • 如果使用多个仓库,注意仓库顺序会影响解析速度,一般把国内镜像放在前面。

4. 项目创建与 Gradle 配置

这一节直接操作。先创建一个新项目,再改配置。

4.1 创建 Empty Activity 项目

打开 Android Studio,选择 New Project,模板选择 Empty Activity,填写项目信息:

  • Name:TimerCompose
  • Package name:例如com.example.timercompose
  • Language:Kotlin
  • Minimum SDK:API 21 或更高

创建完成后,Android Studio 会自动生成以下核心文件:

  • MainActivity.kt:应用入口。
  • ui/theme/目录:主题相关文件。
  • build.gradle.kts:模块构建文件。
  • AndroidManifest.xml:应用配置。

4.2 确认 Compose 依赖

打开app/build.gradle.kts,确认包含 Compose 相关配置。下面是一份通用模板,版本号需要按你项目的实际版本调整:

android { namespace = "com.example.timercompose" compileSdk = 34 defaultConfig { applicationId = "com.example.timercompose" minSdk = 21 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.core:core-ktx:1.13.1") implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") }

注意一个常见问题:Compose 插件的 Kotlin 版本和项目 Kotlin 版本必须匹配。如果打开项目后报错提示 Kotlin 版本冲突,优先检查libs.versions.toml或根目录build.gradle.kts中的 Kotlin 版本号。

4.3 同步项目

点击 Android Studio 右上角的 Sync Now,等待依赖下载完成。第一次同步会花几分钟,看到BUILD SUCCESSFUL再继续。

5. 计时器界面实现

接下来进入核心代码。我们要实现的效果:

  • 屏幕中央显示计时时间,格式为MM:SS
  • 下方三个按钮:开始、暂停、重置。
  • 点击“开始”后,计时器每秒递增。
  • 点击“暂停”后,时间停止变化。
  • 点击“重置”后,时间归零。

5.1 基础版:remember 管理状态

先写一个最直接的版本,用remember保存计时秒数,用LaunchedEffect执行循环更新。

package com.example.timercompose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { TimerApp() } } } } } @Composable fun TimerApp() { var elapsedSeconds by remember { mutableLongStateOf(0L) } var isRunning by remember { mutableStateOf(false) } LaunchedEffect(isRunning) { while (isRunning) { delay(1000) elapsedSeconds++ } } Column( modifier = Modifier .fillMaxSize() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = formatTime(elapsedSeconds), fontSize = 64.sp, fontFamily = FontFamily.Monospace ) Spacer(modifier = Modifier.height(32.dp)) Button( onClick = { isRunning = !isRunning } ) { Text(if (isRunning) "暂停" else "开始") } Spacer(modifier = Modifier.height(12.dp)) Button( onClick = { elapsedSeconds = 0L isRunning = false } ) { Text("重置") } } } fun formatTime(totalSeconds: Long): String { val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 return String.format("%02d:%02d", minutes, seconds) }

这段代码需要导入androidx.compose.runtime.mutableStateOf。运行后,点击“开始”按钮,计时器会从00:00开始每秒递增。

这里有一个关键机制,后面排查问题也会撞到:

  • LaunchedEffect(isRunning)会在isRunning变为true时启动协程。
  • while (isRunning)不是死循环卡死界面,因为delay(1000)会挂起协程,不阻塞主线程。
  • 每次elapsedSeconds++改变状态,就会触发Text重组,界面自动更新。

5.2 为什么用 mutableLongStateOf 而不是 mutableStateOf

对于 Int、Long 这类基本类型,Compose 提供了专用状态类型:

  • mutableIntStateOf
  • mutableLongStateOf
  • mutableFloatStateOf

如果用mutableStateOf<Long>也可以,但底层会做装箱,性能上多了无谓的对象分配。在计时器这种每秒更新一次的场景,差别不大;但如果你做高频动画,就应该用基本类型专用状态。

5.3 动态文本格式化

formatTime函数用了%02d补齐两位数。这样:

  • 0 秒显示为00:00
  • 65 秒显示为01:05
  • 600 秒显示为10:00

如果直接用elapsedSeconds.toString(),你会看到012这样的数字,看起来不像计时器。文本显示和业务逻辑分离,是 Compose 中比较推荐的做法。

6. 状态保存与配置变更

上面代码有一个隐藏问题:如果旋转屏幕,Activity 重建,remember状态会丢失,计时器归零。

解决方式是使用rememberSaveable

var elapsedSeconds by rememberSaveable { mutableLongStateOf(0L) } var isRunning by rememberSaveable { mutableStateOf(false) }

把这两行替换到TimerApp()中。这样旋转屏幕后,时间和运行状态会被系统保存并恢复。

需要注意:

  • rememberSaveable只能保存可以放入 Bundle 的数据。Long、Boolean、String 都可以。
  • 如果以后要保存自定义数据类,需要自定义 Saver。
  • 进程被系统杀死后,rememberSaveable不一定能恢复,持久化需要 ViewModel + SavedStateHandle 或 DataStore。

对计时器项目,rememberSaveable已经够用。

7. 增加“加时间”按钮,理解重组细节

我们在界面上再加一个按钮,每次点击给计时器增加 10 秒。这个按钮的意义在于:验证“状态变化一定会触发重组”。

Button( onClick = { elapsedSeconds += 10L } ) { Text("加 10 秒") }

放在“重置”按钮下方即可。点击后,中央文本会立刻从00:05跳到00:15,即使计时器没有运行。这说明:界面显示完全由状态决定,和“是否在跑”无关。

这里有一个值得测试的小实验:把elapsedSeconds改成局部普通变量,不交给remember,看看点击按钮后界面是否会变化。结果是不会。这就是命令式 UI 和声明式 UI 的核心差异:

  • 命令式 UI:你需要手动调用textView.text = ...
  • 声明式 UI:你只需要改变状态,框架负责重组。

8. 协程定时任务与生命周期

Compose 中有多个执行协程的入口,它们的区别直接决定代码会不会出问题。

API绑定范围适合场景
LaunchedEffectComposable 生命周期进入组合时启动,离开组合时取消
rememberCoroutineScope当前 Composable在事件回调中启动协程
DisposableEffectComposable 生命周期需要手动清理资源

计时器用LaunchedEffect最合适,因为:

  • 进入界面自动开始。
  • 离开界面自动取消,不会泄漏。
  • 依赖isRunning变化时,旧协程取消,新协程启动。

如果是点击按钮后执行一次性协程,比如请求网络、写入文件,就要用rememberCoroutineScope

val scope = rememberCoroutineScope() Button(onClick = { scope.launch { // 模拟耗时任务 delay(2000) } }) { Text("执行任务") }

不要把LaunchedEffect当作万能入口。如果写成:

LaunchedEffect(Unit) { while (true) { delay(1000) elapsedSeconds++ } }

这个循环永远不停止,即使界面已经不可见,协程也不会自动取消,直到 Composable 离开组合。

9. 真机运行与 APK 导出

代码写完后,验证有两种方式。

9.1 模拟器运行

Android Studio 自带模拟器。选择设备后点击 Run 按钮,等待构建完成。

如果模拟器启动缓慢,常见原因:

  • 电脑未开启硬件加速。
  • 系统镜像版本过高。
  • 内存不足。

建议第一次用较低 API 级别的镜像,比如 API 30,启动速度会更快。

9.2 真机调试

  • 手机开启开发者选项和 USB 调试。
  • USB 连接电脑。
  • 手机弹窗选择“允许 USB 调试”。
  • Android Studio 识别到设备后,点击 Run。

运行后会直接安装 Debug 版 APK。

9.3 导出 APK

需要安装到其他手机,或者发给别人测试时,可以构建 APK 文件。

使用 Android Studio 菜单:

  • 选择 Build > Build Bundle(s) / APK(s) > Build APK(s)。
  • 构建完成后,右下角会弹出通知。
  • 点击通知中的 locate,打开 APK 目录。

如果使用命令行构建,在项目根目录执行:

./gradlew assembleDebug

生成的 APK 路径为:

app/build/outputs/apk/debug/app-debug.apk

把一个 Debug APK 发给别人安装时,建议说明:这是测试包,可能存在日志输出和调试信息。

10. 功能测试与效果验证

代码写完只是第一步。下面给出一套验证流程,可以作为项目自测清单。

10.1 基础计时测试

步骤操作预期结果
1启动应用界面显示 00:00,按钮显示“开始”
2点击“开始”文本每秒递增,按钮变为“暂停”
3等待 65 秒文本显示 01:05
4点击“暂停”文本停止变化
5点击“重置”文本变为 00:00,按钮变为“开始”

10.2 状态恢复测试

步骤操作预期结果
1点击“开始”,计时到 00:10时间持续递增
2旋转屏幕或切换深色模式时间和运行状态保留
3点击“暂停”状态正常切换

如果你没有使用rememberSaveable,旋转屏幕后时间会归零。这是第 6 节内容的主要验证点。

10.3 手动加时测试

步骤操作预期结果
1点击“加 10 秒”5 次时间显示 00:50
2点击“开始”从 00:50 继续递增
3点击“重置”时间归零

10.4 失败排查清单

问题现象可能原因排查方式
点击开始无反应isRunning 状态没生效确认按钮 onClick 是否更新了 isRunning
文本不刷新变量没有用 remember 包装检查是否写成了普通变量
每秒跳 2 次多个 LaunchedEffect 同时运行检查代码中是否重复添加 LaunchedEffect
旋转屏幕时间归零没有使用 rememberSaveable替换状态声明
按钮点击闪退导入错误或依赖缺失查看 Logcat 堆栈

11. 接口能力与批量任务扩展

很多教程讲到这里就结束了,但如果你想把计时器做成真实工具,还需要考虑“接口”和“批量任务”。Compose 界面本身没有远程 API,但你可以给计时器封装一个本地状态服务,方便后续扩展。

11.1 本地服务封装

把计时逻辑从 Composable 中抽出来,改成可测试的类:

class StopwatchState { var elapsedSeconds by mutableLongStateOf(0L) private set var isRunning by mutableStateOf(false) private set fun start() { isRunning = true } fun pause() { isRunning = false } fun reset() { isRunning = false elapsedSeconds = 0L } fun addSeconds(seconds: Long) { elapsedSeconds += seconds } }

在 Composable 中创建:

val stopwatch = remember { StopwatchState() } LaunchedEffect(stopwatch.isRunning) { while (stopwatch.isRunning) { delay(1000) stopwatch.elapsedSeconds++ } }

这样做的好处:

  • 逻辑脱离 UI,方便写单元测试。
  • 后续接 Room 数据库、通知、小组件时,不用改界面代码。
  • 多个界面共享同一个计时状态时,可以把StopwatchState放进 ViewModel。

11.2 API 接口接入思路

如果你的计时器要和后端同步,例如“把计时结果上传到服务器”,可以用 Retrofit 或 Ktor Client。接口路径不是固定的,这里只给一个通用示例:

data class TimerRecord( val taskName: String, val elapsedSeconds: Long, val finishedAt: Long ) interface TimerApi { @POST("api/timer/records") suspend fun uploadRecord(@Body record: TimerRecord): Response<Unit> }

调用端放在rememberCoroutineScope中:

val scope = rememberCoroutineScope() Button(onClick = { scope.launch { val record = TimerRecord("测试任务", elapsedSeconds, System.currentTimeMillis()) // 调用上传接口,这里需要替换为实际后端地址 // val response = timerApi.uploadRecord(record) } }) { Text("上传记录") }

这里必须提醒:真实接入网络接口前,要确认后端地址、鉴权方式、数据加密方式,不能在代码中硬编码生产环境密钥。

11.3 批量任务扩展

“批量任务”在计时器场景中可以理解为:多个计时任务按队列依次执行。比如:

  • 任务 A 计时 25 分钟。
  • 休息 5 分钟。
  • 任务 B 计时 25 分钟。

此时需要引入队列状态:

data class TimerTask( val name: String, val durationSeconds: Long ) class TaskQueue { val tasks: List<TimerTask> = listOf( TimerTask("任务A", 25 * 60), TimerTask("休息", 5 * 60), TimerTask("任务B", 25 * 60) ) }

每个任务结束后自动切换到下一个。实际开发中,建议把队列逻辑放进 ViewModel + StateFlow,而不是写在 Composable 里。

如果需要后台周期任务,比如“每天定时提醒”,可以用 WorkManager:

val workRequest = PeriodicWorkRequestBuilder<ReminderWorker>(1, TimeUnit.DAYS).build() WorkManager.getInstance(context).enqueue(workRequest)

WorkManager 适合系统级定时调度,但注意最小周期是 15 分钟,不能像普通协程那样精确到秒。

12. 资源占用与性能观察

Compose 应用在调试阶段,可以通过 Android Studio 自带的 Profiler 观察性能和资源占用。

12.1 显存和内存

安卓应用没有“显存”概念,主要是内存和 GPU 渲染资源:

  • Compose 会使用 GPU 加速绘制。
  • 文字、Button 等组件在大多数设备上都能流畅运行。
  • 计时器每秒刷新一次文本,对现代手机压力很小。
  • 如果你开了大量动画、模糊、阴影,才会出现明显的 GPU 负载。

观察方式:

  • 打开 Android Studio,选择 View > Tool Windows > Profiler。
  • 运行计时器应用。
  • 在 Profiler 面板中查看 Memory 和 Energy 曲线。

12.2 重组性能

Compose 的核心优化点是“避免不必要的重组”。你可以通过日志验证界面重组次数。

TimerApp中加一行:

@Composable fun TimerApp() { var elapsedSeconds by remember { mutableLongStateOf(0L) } var isRunning by remember { mutableStateOf(false) } println("TimerApp recompose, elapsed=$elapsedSeconds") ... }

运行后 Logcat 会输出大量日志。每次状态变化都会打印一次,这属于正常现象。如果发现没有状态变化也频繁打印,就要检查是否有父级 Composable 被强制重组。

12.3 避免在主线程做耗时操作

协程的delay不会阻塞主线程。如果你换成Thread.sleep(1000),界面会直接卡死。

这个坑在 Compose 项目中很常见。计时器项目里,只用delay,不要用Thread.sleep

12.4 降低资源占用的建议

  • 不要用全局变量或object保存大对象。
  • 计时器的状态放在remember或 ViewModel 中,不要放在静态单例里。
  • 如果需要长时间计时,建议在onStop中暂停,在onStart中恢复。
  • 不要创建重复的LaunchedEffect,避免多个协程同时累加状态。

13. 常见问题与排查方法

问题现象可能原因排查方式解决方案
项目同步失败,依赖下载不了网络问题或仓库被墙查看 Gradle 日志配置国内镜像仓库
Kotlin 版本与 Compose 插件不匹配版本号冲突检查根目录 build.gradle.kts统一 Kotlin 和 Compose 编译器版本
模拟器启动后黑屏系统镜像或硬件加速问题查看模拟器设置换 API 更低的镜像或使用真机
点击按钮应用崩溃空指针或依赖缺失查看 Logcat检查代码中引用是否完整导入
文本一直显示 00:00状态没有正确更新检查是否用了 remember确认 onClicks 更新的是同一个状态
启动后无法安装到手机手机未开启 USB 调试检查手机设置打开开发者选项,允许 USB 调试
构建时提示 namespace 错误AGP 版本和项目配置不一致检查 app/build.gradle.kts设置正确的 namespace
旋转屏幕后计时器重置没有使用 rememberSaveable检查状态声明替换为 rememberSaveable
按钮点击后界面卡住主线程执行了耗时操作查看 Profiler改用协程

14. 最佳实践与项目扩展

14.1 工程化组织建议

对于继续深入学习 Compose 的同学,建议把项目按以下目录组织:

app/src/main/java/com/example/timercompose/ ├── MainActivity.kt ├── data/ │ └── StopwatchState.kt ├── ui/ │ ├── theme/ │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── TimerScreen.kt └── viewmodel/ └── TimerViewModel.kt

早期练习可以全部写在 MainActivity 里,但项目变大后一定要拆分。

14.2 代码可维护性建议

  • formatTime抽成工具函数,加单元测试。
  • StopwatchState抽成类,加单元测试。
  • 把界面组件拆分成TimerScreenTimerControlsTimerDisplay
  • 使用 Material3 主题,不要手动写死颜色。

14.3 扩展方向

这个计时器项目可以继续扩展的方向很多:

  • 添加时间选择器,预设 5 分钟、10 分钟、25 分钟。
  • 添加通知功能,计时结束时发送系统通知。
  • 添加桌面小组件,不用打开应用就能看到计时状态。
  • 添加录音功能,记录任务名称和用时。
  • 添加数据持久化,保存历史记录。
  • 接后端接口,实现多设备同步。
  • 使用 Navigation 实现多页面跳转。

扩展时优先做的事:

  1. 把计时逻辑从 Composable 中抽到 ViewModel,用StateFlow暴露状态。
  2. 引入ViewModel+SavedStateHandle解决进程重建问题。
  3. 使用 Room 保存历史记录。

14.4 合规提醒

最后再说一次合规边界:

  • 计时器本身不涉及敏感权限,如果后续加通知功能,需要在AndroidManifest.xml中声明通知权限,并在代码中动态申请。
  • 如果加录音、定位、网络功能,必须明确告知用户,并且只在必要场景下使用。
  • 不要在你的项目里集成来路不明的 SDK,尤其是涉及账号、支付、隐私信息的。
  • 发布应用前,确认所有字体、图标、素材都有合法授权。
  • 如果复用开源代码,查看开源协议,按协议要求保留版权声明。

15. 总结与下一步

这个计时器项目做下来,你真正掌握的不是“计时器”本身,而是 Compose 的整套开发闭环:创建项目、配置依赖、定义状态、编写界面、协程定时、状态保存、构建 APK、真机调试。这一套流程,比单纯看 API 文档有用得多。

最值得先验证的是第 5 节的remember状态驱动机制:先跑通开始/暂停/重置,再改成rememberSaveable,理解两者差异。最容易踩的坑就是协程重复启动和状态没保存,遇到问题先看 Logcat,再看自己的状态声明。

接下来的学习路径,建议按以下顺序:

  1. 用 ViewModel 重构计时器,把状态管理从 Composable 中抽离。
  2. 用 StateFlow 替代 Compose 内部状态,观察 UI 与逻辑解耦后的变化。
  3. 接 Room 保存历史计时记录。
  4. 加 Navigation,做一个多页面应用。
  5. 试试 Material3 的自定义主题,让界面更接近生产项目。

把这几个步骤走完,Compose 的入门阶段就算真正结束了。建议把本文的关键代码片段收藏备用,尤其是LaunchedEffectrememberSaveable这两段,后面写其他应用大概率还会用到。

http://www.cnnetsun.cn/news/4350007.html

相关文章:

  • AI搜索,你的企业还在“隐形”?北京GEO优化公司推荐,这三家助你提升可见度
  • CCR 配置备份与恢复的完整实战笔记
  • 如何为pm-skills贡献一个新技能?完整开发流程与验证脚本使用指南
  • 手术场景视觉-轨迹联合预测模型:从原理到工程部署
  • iFixAi新手完整教程:从干净机器到可引用审计报告只需4步
  • 基于Java+SpringBoot的船舶物料供应商交易平台的设计与实现(毕业设计项目源码+文档)
  • FreeRTOS 中优先级反转的解决方案-互斥量
  • Monorepo中管理多个DESIGN.md:多设计系统并行的完整指南
  • AI视频转场不靠运气:用Skill固化创作流程
  • 轮腿机器人离板面加速:5cm技术鸿沟的动力学原理与仿真实现
  • Abaqus热力耦合断裂仿真:UMAT/VUMAT子程序开发与工程实践
  • 如何用Feynman的rank命令给论文排优先级:PaperRank基于引用与复现证据的科学评分完整指南
  • Humanizer-zh 去 AI 痕迹实战 4 场景:营销文案、学术摘要、博客文章改写前后完整对比
  • 区块链智能合约详解:从原理到可运行Solidity源码实战
  • Hallmark Hero 标题长度与字号钳制关系:4 档自动降档规则完整指南
  • Java校招面试复盘:从基础八股到工程化实战
  • 给AI装上长期记忆:ZeroClaw记忆系统(SQLite+嵌入向量)完整配置攻略
  • AI Agent从入门到落地:概念、工具调用与日志分析实战
  • 内容泄露如何溯源?从权限控制到水印取证的完整技术指南
  • 香橙派5安装Windows ARM全流程:UEFI与ACPI配置必备指南
  • PDFMathTranslate 完整使用指南:如何在本地快速完成 PDF 科学文档翻译
  • next-ai-draw-io:一句话画出专业架构图,自然语言 draw.io 完整上手指南
  • OpenVoice 语音克隆:3秒参考音频如何做到跨语言音色迁移与风格自由控制
  • 如何从零搭建PDF翻译网页服务:PDFMathTranslate部署与公网访问配置指南
  • 用双色球历史数据练手:Excel与MySQL数据处理全流程实战
  • MediaPipe ARM aarch64 构建实战:两条路径把 mediapipe 装进你的设备
  • 基于C#的FANUC FOCAS数据采集方案:从环境搭建到设备监控实现
  • 基于DeepSeek Harness构建Obsidian智能助手:从零开发AI知识管理Agent
  • 大模型多轮训练实战:从SFT到强化学习的迭代优化方法
  • Marin Pulumi基础设施即代码:一个Stack管理全部云资源的终极方案