2024年已经过半了,我作为聋人独立开发者,我经常反思自己在这半年中的成长,自己这半年到底进步了多少?在这篇文章里,我分享一个用Jetpack Compose、Material 3和Kotlin协程开发NimTwoTrackApp的案例。如果你有一定开发经验,相信这篇文章对你会非常有所帮助。
NimTwoTrackApp模拟两位选手赛跑。应用界面中包含两个按钮:开始/停止和重置,两个用于显示赛跑者进度的进度条。选手 1 和 2 被设置为不同的速度“奔跑”。
这项目使用 Jetpack Compose 进行 UI 构建,结合 Material 3 设计元素实现了简洁美观UI。Kotlin 协程处理并发任务,两个者多个选手的进度同步更新而不阻塞主线程。
PS:适合已有编程基础的开发者,如果你是初学者,建议先看看我另一篇基础文章:安卓软件开发-手把教讲解Kotlin协程-腾讯云开发者社区-腾讯云 (tencent.com)
@Composable
fun NimTwoTrackScreen(
playerOne: RaceParticipant,
playerTwo: RaceParticipant,
isRunning: Boolean,
onRunStateChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.run_a_race),
style = MaterialTheme.typography.headlineSmall,
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
StatusIndicator(
participantName = playerTwo.name,
currentProgress = playerTwo.currentProgress,
maxProgress = stringResource(
R.string.progress_percentage,
playerTwo.maxProgress
),
progressFactor = playerTwo.progressFactor,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.size(dimensionResource(R.dimen.padding_large)))
StatusIndicator(
participantName = playerTwo.name,
currentProgress = playerTwo.currentProgress,
maxProgress = stringResource(
R.string.progress_percentage,
playerTwo.maxProgress
),
progressFactor = playerTwo.progressFactor,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.size(dimensionResource(R.dimen.padding_large)))
RaceControls(
isRunning = isRunning,
onRunStateChange = onRunStateChange,
onReset = {
playerOne.reset()
playerTwo.reset()
onRunStateChange(false)
},
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Preview(showBackground = true)
@Composable
fun NimTwoTrackAppPreview() {
AppTheme {
NimTwoTrackApp()
}
}
StatusIndicator
用于显示每个选手的当前进度,通过进度条和文本实时反映选手状态。
@Composable
private fun StatusIndicator(
participantName: String,
currentProgress: Int,
maxProgress: String,
progressFactor: Float,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
) {
Text(
text = participantName,
modifier = Modifier.padding(end = dimensionResource(R.dimen.padding_small))
)
Column(
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
) {
LinearProgressIndicator(
progress = progressFactor,
modifier = Modifier
.fillMaxWidth()
.height(dimensionResource(R.dimen.progress_indicator_height))
.clip(RoundedCornerShape(dimensionResource(R.dimen.progress_indicator_corner_radius)))
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.progress_percentage, currentProgress),
textAlign = TextAlign.Start,
modifier = Modifier.weight(1f)
)
Text(
text = maxProgress,
textAlign = TextAlign.End,
modifier = Modifier.weight(1f)
)
}
}
}
}
RaceControls
提供了比赛的控制功能,包括开始、暂停和重置。比赛的控制由 onRunStateChange
和 onReset
函数实现。
@Composable
private fun RaceControls(
onRunStateChange: (Boolean) -> Unit,
onReset: () -> Unit,
modifier: Modifier = Modifier,
isRunning: Boolean = true,
) {
Column(
modifier = modifier.padding(top = dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
Button(
onClick = { onRunStateChange(!isRunning) },
modifier = Modifier.fillMaxWidth(),
) {
Text(if (isRunning) stringResource(R.string.pause) else stringResource(R.string.start))
}
OutlinedButton(
onClick = onReset,
modifier = Modifier.fillMaxWidth(),
) {
Text(stringResource(R.string.reset))
}
}
}
RaceControls
提供按钮来控制比赛的状态(开始、暂停、重置),按钮点击事件通过回调函数控制状态变化。NimTwoTrackApp
负责初始化启动界面,包含两个选手的进度条和比赛控制按钮。
@Composable
fun NimTwoTrackApp() {
val playerOne = remember { RaceParticipant("NimPlayer1", 1) }
val playerTwo = remember { RaceParticipant("NimPlayer2", 2) }
var raceInProgress by remember { mutableStateOf(false) }
if (raceInProgress) {
LaunchedEffect(playerOne, playerTwo) {
coroutineScope {
launch { playerOne.run() }
launch { playerTwo.run() }
}
raceInProgress = false
}
}
NimTwoTrackScreen(playerOne, playerTwo, raceInProgress, onRunStateChange = { raceInProgress = it })
}
NimTwoTrackApp
初始化两个选手对象,通过 LaunchedEffect
启动协程同时更新选手的进度。比赛状态由 raceInProgress
控制,当状态变更时,界面会响应。(1)RaceParticipant
类是每个选手的状态持有者,通过协程更新每个选手的进度。
package com.nim.nimTwoTrack.ui
class RaceParticipant(
val name: String,
val maxProgress: Int = 100,
val progressDelayMillis: Long = 500L,
private val progressIncrement: Int = 1,
private val initialProgress: Int = 0
)
init {
require(maxProgress > 0) { "maxProgress=$maxProgress; 必须大于0" }
require(progressIncrement > 0) { "progressIncrement=$progressIncrement; 必须大于0" }
}
(2)init{}
require
语句:这是一个简单输入验证,确保 maxProgress
和 progressIncrement
都是正数。如果传递了非法值,代码会抛出 IllegalArgumentException
输出相应的错误信息。
maxProgress
:必须大于 0,否则比赛无法进行。progressIncrement
:也必须大于 0,否则进度无法前进。(3) 选手的当前进度 (currentProgress
)
var currentProgress by mutableStateOf(initialProgress)
private set
currentProgress
:用于跟踪选手的当前进度,通过 mutableStateOf
管理状态的变化。这是 Jetpack Compose 中常用的方式,通过 mutableStateOf
可以保证 UI 在状态改变时自动刷新。private set
:保证 currentProgress
只能在类的内部修改,外部不能直接更改它的值。(4)run()
方法
suspend fun run() {
while (currentProgress < maxProgress) {
delay(progressDelayMillis)
currentProgress += progressIncrement
}
}
delay
是一个挂起函数,会暂停当前协程但不会阻塞主线程。progressIncrement
增加。(5) reset()
方法
fun reset() {
currentProgress = 0
}
(6) progressFactor
属性
val RaceParticipant.progressFactor: Float
get() = currentProgress / maxProgress.toFloat()
通过 setContent
初始化 UI,调用 NimTwoTrackApp
启动App的主界面。
...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
//调用UI
NimTwoTrackApp()
}
}
}
}
}
JUnit
和 Kotlin Coroutines Test
进行 单元测试。
@Test
fun TestProgressUpdated() = runTest {
val expectedProgress = 1
launch { raceParticipant.run() }
advanceTimeBy(raceParticipant.progressDelayMillis)
runCurrent()
assertEquals(expectedProgress, raceParticipant.currentProgress)
}
RaceParticipant
启动后,进度是否按预期增加。advanceTimeBy
模拟时间流逝,协程更新进度,通过 assertEquals
检查当前进度是否等于预期值。@Test
fun TestFinishedProgressUpdated() = runTest {
launch { raceParticipant.run() }
advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
runCurrent()
assertEquals(100, raceParticipant.currentProgress)
}
100
。advanceTimeBy
快进整个赛跑过程,验证终点的 currentProgress
值。@Test
fun TestPausedProgressUpdated() = runTest {
val expectedProgress = 5
val racerJob = launch { raceParticipant.run() }
advanceTimeBy(expectedProgress * raceParticipant.progressDelayMillis)
runCurrent()
racerJob.cancelAndJoin()
assertEquals(expectedProgress, raceParticipant.currentProgress)
}
cancelAndJoin()
停止协程,验证暂停时的进度是不是正确。@Test
fun TestPausedAndResumedProgressUpdated() = runTest {
val expectedProgress = 5
repeat(2) {
val racerJob = launch { raceParticipant.run() }
advanceTimeBy(expectedProgress * raceParticipant.progressDelayMillis)
runCurrent()
racerJob.cancelAndJoin()
}
assertEquals(expectedProgress * 2, raceParticipant.currentProgress)
}
开发这个App时,我遇到了五个技术主要难点是
mutableStateOf
和remember保证
状态的一致性和内存效率。launch
和async
,可以帮助App启动和管理协程。通过这个项目,我对Jetpack Compose、Material 3和Kotlin协程的实用性有了更深的理解。这个UI框架让我快速构建了漂亮和高端UI,也处理了复杂的后台任务。虽然一开始有点复杂,真的可以做到了能让开发工作变得轻松很多,我意识到了测试非常重要,可以保证每个部分都能按预期工作。
有任何问题欢迎提问,感谢大家阅读 :)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。