code-abyss 1.6.16 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/SKILL.md +24 -16
- package/skills/domains/ai/SKILL.md +2 -2
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/architecture/SKILL.md +2 -3
- package/skills/domains/architecture/security-arch.md +87 -0
- package/skills/domains/data-engineering/SKILL.md +188 -26
- package/skills/domains/development/SKILL.md +1 -4
- package/skills/domains/devops/SKILL.md +3 -5
- package/skills/domains/devops/performance.md +63 -0
- package/skills/domains/devops/testing.md +97 -0
- package/skills/domains/frontend-design/SKILL.md +12 -3
- package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/infrastructure/SKILL.md +174 -34
- package/skills/domains/mobile/SKILL.md +211 -21
- package/skills/domains/orchestration/SKILL.md +1 -0
- package/skills/domains/security/SKILL.md +4 -6
- package/skills/domains/security/blue-team.md +57 -0
- package/skills/domains/security/red-team.md +54 -0
- package/skills/domains/security/threat-intel.md +50 -0
- package/skills/orchestration/multi-agent/SKILL.md +195 -46
- package/skills/run_skill.js +134 -0
- package/skills/tools/gen-docs/SKILL.md +6 -4
- package/skills/tools/gen-docs/scripts/doc_generator.js +349 -0
- package/skills/tools/verify-change/SKILL.md +8 -6
- package/skills/tools/verify-change/scripts/change_analyzer.js +270 -0
- package/skills/tools/verify-module/SKILL.md +6 -4
- package/skills/tools/verify-module/scripts/module_scanner.js +145 -0
- package/skills/tools/verify-quality/SKILL.md +5 -3
- package/skills/tools/verify-quality/scripts/quality_checker.js +276 -0
- package/skills/tools/verify-security/SKILL.md +7 -5
- package/skills/tools/verify-security/scripts/security_scanner.js +133 -0
- package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
- package/skills/domains/COVERAGE_PLAN.md +0 -232
- package/skills/domains/ai/model-evaluation.md +0 -790
- package/skills/domains/ai/prompt-engineering.md +0 -703
- package/skills/domains/architecture/compliance.md +0 -299
- package/skills/domains/architecture/data-security.md +0 -184
- package/skills/domains/data-engineering/data-pipeline.md +0 -762
- package/skills/domains/data-engineering/data-quality.md +0 -894
- package/skills/domains/data-engineering/stream-processing.md +0 -791
- package/skills/domains/development/dart.md +0 -963
- package/skills/domains/development/kotlin.md +0 -834
- package/skills/domains/development/php.md +0 -659
- package/skills/domains/development/swift.md +0 -755
- package/skills/domains/devops/e2e-testing.md +0 -914
- package/skills/domains/devops/performance-testing.md +0 -734
- package/skills/domains/devops/testing-strategy.md +0 -667
- package/skills/domains/frontend-design/build-tools.md +0 -743
- package/skills/domains/frontend-design/performance.md +0 -734
- package/skills/domains/frontend-design/testing.md +0 -699
- package/skills/domains/infrastructure/gitops.md +0 -735
- package/skills/domains/infrastructure/iac.md +0 -855
- package/skills/domains/infrastructure/kubernetes.md +0 -1018
- package/skills/domains/mobile/android-dev.md +0 -979
- package/skills/domains/mobile/cross-platform.md +0 -795
- package/skills/domains/mobile/ios-dev.md +0 -931
- package/skills/domains/security/secrets-management.md +0 -834
- package/skills/domains/security/supply-chain.md +0 -931
- package/skills/domains/security/threat-modeling.md +0 -828
- package/skills/run_skill.py +0 -153
- package/skills/tests/README.md +0 -225
- package/skills/tests/SUMMARY.md +0 -362
- package/skills/tests/__init__.py +0 -3
- package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
- package/skills/tests/test_change_analyzer.py +0 -558
- package/skills/tests/test_doc_generator.py +0 -538
- package/skills/tests/test_module_scanner.py +0 -376
- package/skills/tests/test_quality_checker.py +0 -516
- package/skills/tests/test_security_scanner.py +0 -426
- package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
- package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
- package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
- package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
- package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
- package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
- package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
- package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
|
@@ -1,979 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: android-dev
|
|
3
|
-
description: Android 开发。Jetpack Compose、Kotlin、ViewModel、LiveData、Coroutines、Flow、MVVM、Android架构。当用户提到 Android 开发、Jetpack Compose、Kotlin 时使用。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 🤖 Android 开发 · Android Development
|
|
7
|
-
|
|
8
|
-
## Jetpack Compose 基础
|
|
9
|
-
|
|
10
|
-
### Composable 函数
|
|
11
|
-
```kotlin
|
|
12
|
-
import androidx.compose.material3.*
|
|
13
|
-
import androidx.compose.runtime.*
|
|
14
|
-
import androidx.compose.ui.Modifier
|
|
15
|
-
import androidx.compose.ui.unit.dp
|
|
16
|
-
|
|
17
|
-
@Composable
|
|
18
|
-
fun Greeting(name: String) {
|
|
19
|
-
Column(
|
|
20
|
-
modifier = Modifier.padding(16.dp)
|
|
21
|
-
) {
|
|
22
|
-
Text(
|
|
23
|
-
text = "Hello, $name!",
|
|
24
|
-
style = MaterialTheme.typography.headlineMedium
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
Spacer(modifier = Modifier.height(8.dp))
|
|
28
|
-
|
|
29
|
-
Button(onClick = { /* TODO */ }) {
|
|
30
|
-
Text("Click Me")
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### State 管理
|
|
37
|
-
```kotlin
|
|
38
|
-
@Composable
|
|
39
|
-
fun CounterScreen() {
|
|
40
|
-
var count by remember { mutableStateOf(0) }
|
|
41
|
-
var isEnabled by remember { mutableStateOf(true) }
|
|
42
|
-
|
|
43
|
-
Column(
|
|
44
|
-
modifier = Modifier.padding(16.dp),
|
|
45
|
-
horizontalAlignment = Alignment.CenterHorizontally
|
|
46
|
-
) {
|
|
47
|
-
Text(
|
|
48
|
-
text = "Count: $count",
|
|
49
|
-
style = MaterialTheme.typography.headlineLarge
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
Row(
|
|
53
|
-
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
54
|
-
) {
|
|
55
|
-
Button(onClick = { count-- }) {
|
|
56
|
-
Text("-")
|
|
57
|
-
}
|
|
58
|
-
Button(onClick = { count++ }) {
|
|
59
|
-
Text("+")
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
Switch(
|
|
64
|
-
checked = isEnabled,
|
|
65
|
-
onCheckedChange = { isEnabled = it }
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### LazyColumn 列表
|
|
72
|
-
```kotlin
|
|
73
|
-
@Composable
|
|
74
|
-
fun UserList(users: List<User>) {
|
|
75
|
-
LazyColumn(
|
|
76
|
-
contentPadding = PaddingValues(16.dp),
|
|
77
|
-
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
78
|
-
) {
|
|
79
|
-
items(users) { user ->
|
|
80
|
-
UserItem(user)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Composable
|
|
86
|
-
fun UserItem(user: User) {
|
|
87
|
-
Card(
|
|
88
|
-
modifier = Modifier.fillMaxWidth(),
|
|
89
|
-
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
90
|
-
) {
|
|
91
|
-
Row(
|
|
92
|
-
modifier = Modifier.padding(16.dp),
|
|
93
|
-
verticalAlignment = Alignment.CenterVertically
|
|
94
|
-
) {
|
|
95
|
-
AsyncImage(
|
|
96
|
-
model = user.avatarUrl,
|
|
97
|
-
contentDescription = null,
|
|
98
|
-
modifier = Modifier.size(48.dp)
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
Spacer(modifier = Modifier.width(16.dp))
|
|
102
|
-
|
|
103
|
-
Column {
|
|
104
|
-
Text(
|
|
105
|
-
text = user.name,
|
|
106
|
-
style = MaterialTheme.typography.titleMedium
|
|
107
|
-
)
|
|
108
|
-
Text(
|
|
109
|
-
text = user.email,
|
|
110
|
-
style = MaterialTheme.typography.bodySmall
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Compose 高级
|
|
119
|
-
|
|
120
|
-
### Side Effects
|
|
121
|
-
```kotlin
|
|
122
|
-
@Composable
|
|
123
|
-
fun TimerScreen() {
|
|
124
|
-
var seconds by remember { mutableStateOf(0) }
|
|
125
|
-
|
|
126
|
-
// LaunchedEffect: 启动协程
|
|
127
|
-
LaunchedEffect(Unit) {
|
|
128
|
-
while (true) {
|
|
129
|
-
delay(1000)
|
|
130
|
-
seconds++
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
Text("Elapsed: $seconds seconds")
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@Composable
|
|
138
|
-
fun AnalyticsScreen(screenName: String) {
|
|
139
|
-
// DisposableEffect: 清理资源
|
|
140
|
-
DisposableEffect(screenName) {
|
|
141
|
-
analytics.logScreenView(screenName)
|
|
142
|
-
onDispose {
|
|
143
|
-
analytics.logScreenExit(screenName)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// SideEffect: 同步状态到外部
|
|
148
|
-
SideEffect {
|
|
149
|
-
externalState.update(screenName)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
@Composable
|
|
154
|
-
fun SearchScreen() {
|
|
155
|
-
var query by remember { mutableStateOf("") }
|
|
156
|
-
var results by remember { mutableStateOf<List<Item>>(emptyList()) }
|
|
157
|
-
|
|
158
|
-
// snapshotFlow: 监听状态变化
|
|
159
|
-
LaunchedEffect(Unit) {
|
|
160
|
-
snapshotFlow { query }
|
|
161
|
-
.debounce(300)
|
|
162
|
-
.filter { it.isNotEmpty() }
|
|
163
|
-
.collectLatest { q ->
|
|
164
|
-
results = repository.search(q)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
Column {
|
|
169
|
-
TextField(
|
|
170
|
-
value = query,
|
|
171
|
-
onValueChange = { query = it }
|
|
172
|
-
)
|
|
173
|
-
LazyColumn {
|
|
174
|
-
items(results) { item ->
|
|
175
|
-
Text(item.name)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### Custom Modifier
|
|
183
|
-
```kotlin
|
|
184
|
-
fun Modifier.shimmer(): Modifier = composed {
|
|
185
|
-
var offsetX by remember { mutableStateOf(0f) }
|
|
186
|
-
val infiniteTransition = rememberInfiniteTransition()
|
|
187
|
-
|
|
188
|
-
val shimmerOffset by infiniteTransition.animateFloat(
|
|
189
|
-
initialValue = 0f,
|
|
190
|
-
targetValue = 1000f,
|
|
191
|
-
animationSpec = infiniteRepeatable(
|
|
192
|
-
animation = tween(1000, easing = LinearEasing)
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
this.drawWithContent {
|
|
197
|
-
drawContent()
|
|
198
|
-
drawRect(
|
|
199
|
-
brush = Brush.linearGradient(
|
|
200
|
-
colors = listOf(
|
|
201
|
-
Color.Transparent,
|
|
202
|
-
Color.White.copy(alpha = 0.3f),
|
|
203
|
-
Color.Transparent
|
|
204
|
-
),
|
|
205
|
-
start = Offset(shimmerOffset, 0f),
|
|
206
|
-
end = Offset(shimmerOffset + 200f, 0f)
|
|
207
|
-
)
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 使用
|
|
213
|
-
Box(
|
|
214
|
-
modifier = Modifier
|
|
215
|
-
.size(200.dp)
|
|
216
|
-
.shimmer()
|
|
217
|
-
)
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Navigation
|
|
221
|
-
```kotlin
|
|
222
|
-
@Composable
|
|
223
|
-
fun AppNavigation() {
|
|
224
|
-
val navController = rememberNavController()
|
|
225
|
-
|
|
226
|
-
NavHost(
|
|
227
|
-
navController = navController,
|
|
228
|
-
startDestination = "home"
|
|
229
|
-
) {
|
|
230
|
-
composable("home") {
|
|
231
|
-
HomeScreen(
|
|
232
|
-
onNavigateToDetail = { id ->
|
|
233
|
-
navController.navigate("detail/$id")
|
|
234
|
-
}
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
composable(
|
|
239
|
-
route = "detail/{itemId}",
|
|
240
|
-
arguments = listOf(navArgument("itemId") { type = NavType.IntType })
|
|
241
|
-
) { backStackEntry ->
|
|
242
|
-
val itemId = backStackEntry.arguments?.getInt("itemId")
|
|
243
|
-
DetailScreen(itemId = itemId)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## ViewModel & LiveData
|
|
250
|
-
|
|
251
|
-
### ViewModel
|
|
252
|
-
```kotlin
|
|
253
|
-
class UserViewModel(
|
|
254
|
-
private val repository: UserRepository
|
|
255
|
-
) : ViewModel() {
|
|
256
|
-
|
|
257
|
-
private val _users = MutableLiveData<List<User>>()
|
|
258
|
-
val users: LiveData<List<User>> = _users
|
|
259
|
-
|
|
260
|
-
private val _isLoading = MutableLiveData(false)
|
|
261
|
-
val isLoading: LiveData<Boolean> = _isLoading
|
|
262
|
-
|
|
263
|
-
private val _error = MutableLiveData<String?>()
|
|
264
|
-
val error: LiveData<String?> = _error
|
|
265
|
-
|
|
266
|
-
fun loadUsers() {
|
|
267
|
-
viewModelScope.launch {
|
|
268
|
-
_isLoading.value = true
|
|
269
|
-
_error.value = null
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
val result = repository.getUsers()
|
|
273
|
-
_users.value = result
|
|
274
|
-
} catch (e: Exception) {
|
|
275
|
-
_error.value = e.message
|
|
276
|
-
} finally {
|
|
277
|
-
_isLoading.value = false
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
fun refresh() {
|
|
283
|
-
loadUsers()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### StateFlow (推荐)
|
|
289
|
-
```kotlin
|
|
290
|
-
class UserViewModel(
|
|
291
|
-
private val repository: UserRepository
|
|
292
|
-
) : ViewModel() {
|
|
293
|
-
|
|
294
|
-
private val _uiState = MutableStateFlow(UserUiState())
|
|
295
|
-
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
296
|
-
|
|
297
|
-
init {
|
|
298
|
-
loadUsers()
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
fun loadUsers() {
|
|
302
|
-
viewModelScope.launch {
|
|
303
|
-
_uiState.update { it.copy(isLoading = true) }
|
|
304
|
-
|
|
305
|
-
repository.getUsers()
|
|
306
|
-
.onSuccess { users ->
|
|
307
|
-
_uiState.update {
|
|
308
|
-
it.copy(users = users, isLoading = false)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
.onFailure { error ->
|
|
312
|
-
_uiState.update {
|
|
313
|
-
it.copy(error = error.message, isLoading = false)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
data class UserUiState(
|
|
321
|
-
val users: List<User> = emptyList(),
|
|
322
|
-
val isLoading: Boolean = false,
|
|
323
|
-
val error: String? = null
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
// Compose 中使用
|
|
327
|
-
@Composable
|
|
328
|
-
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
|
|
329
|
-
val uiState by viewModel.uiState.collectAsState()
|
|
330
|
-
|
|
331
|
-
when {
|
|
332
|
-
uiState.isLoading -> LoadingView()
|
|
333
|
-
uiState.error != null -> ErrorView(uiState.error!!)
|
|
334
|
-
else -> UserList(uiState.users)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
## Kotlin Coroutines
|
|
340
|
-
|
|
341
|
-
### 基础用法
|
|
342
|
-
```kotlin
|
|
343
|
-
// 启动协程
|
|
344
|
-
viewModelScope.launch {
|
|
345
|
-
val result = withContext(Dispatchers.IO) {
|
|
346
|
-
repository.fetchData()
|
|
347
|
-
}
|
|
348
|
-
updateUI(result)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 并发执行
|
|
352
|
-
suspend fun loadData() = coroutineScope {
|
|
353
|
-
val users = async { repository.getUsers() }
|
|
354
|
-
val posts = async { repository.getPosts() }
|
|
355
|
-
|
|
356
|
-
CombinedData(
|
|
357
|
-
users = users.await(),
|
|
358
|
-
posts = posts.await()
|
|
359
|
-
)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 超时控制
|
|
363
|
-
try {
|
|
364
|
-
withTimeout(5000) {
|
|
365
|
-
repository.fetchData()
|
|
366
|
-
}
|
|
367
|
-
} catch (e: TimeoutCancellationException) {
|
|
368
|
-
// 处理超时
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### Flow
|
|
373
|
-
```kotlin
|
|
374
|
-
class UserRepository {
|
|
375
|
-
fun observeUsers(): Flow<List<User>> = flow {
|
|
376
|
-
while (true) {
|
|
377
|
-
val users = api.getUsers()
|
|
378
|
-
emit(users)
|
|
379
|
-
delay(30000) // 每30秒刷新
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
fun searchUsers(query: String): Flow<List<User>> = flow {
|
|
384
|
-
emit(emptyList()) // 初始状态
|
|
385
|
-
delay(300) // 防抖
|
|
386
|
-
val results = api.search(query)
|
|
387
|
-
emit(results)
|
|
388
|
-
}.flowOn(Dispatchers.IO)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ViewModel 中使用
|
|
392
|
-
class UserViewModel(
|
|
393
|
-
private val repository: UserRepository
|
|
394
|
-
) : ViewModel() {
|
|
395
|
-
|
|
396
|
-
val users: StateFlow<List<User>> = repository.observeUsers()
|
|
397
|
-
.stateIn(
|
|
398
|
-
scope = viewModelScope,
|
|
399
|
-
started = SharingStarted.WhileSubscribed(5000),
|
|
400
|
-
initialValue = emptyList()
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
private val searchQuery = MutableStateFlow("")
|
|
404
|
-
|
|
405
|
-
val searchResults: StateFlow<List<User>> = searchQuery
|
|
406
|
-
.debounce(300)
|
|
407
|
-
.filter { it.isNotEmpty() }
|
|
408
|
-
.flatMapLatest { query ->
|
|
409
|
-
repository.searchUsers(query)
|
|
410
|
-
}
|
|
411
|
-
.stateIn(
|
|
412
|
-
scope = viewModelScope,
|
|
413
|
-
started = SharingStarted.WhileSubscribed(),
|
|
414
|
-
initialValue = emptyList()
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
fun search(query: String) {
|
|
418
|
-
searchQuery.value = query
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### Channel
|
|
424
|
-
```kotlin
|
|
425
|
-
class EventBus {
|
|
426
|
-
private val _events = Channel<Event>(Channel.BUFFERED)
|
|
427
|
-
val events: Flow<Event> = _events.receiveAsFlow()
|
|
428
|
-
|
|
429
|
-
suspend fun send(event: Event) {
|
|
430
|
-
_events.send(event)
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// 使用
|
|
435
|
-
viewModelScope.launch {
|
|
436
|
-
eventBus.events.collect { event ->
|
|
437
|
-
when (event) {
|
|
438
|
-
is Event.UserLoggedIn -> handleLogin(event.user)
|
|
439
|
-
is Event.DataUpdated -> refresh()
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
## MVVM 架构
|
|
446
|
-
|
|
447
|
-
### Model
|
|
448
|
-
```kotlin
|
|
449
|
-
data class User(
|
|
450
|
-
val id: Int,
|
|
451
|
-
val name: String,
|
|
452
|
-
val email: String,
|
|
453
|
-
val avatarUrl: String
|
|
454
|
-
)
|
|
455
|
-
|
|
456
|
-
data class LoginRequest(
|
|
457
|
-
val username: String,
|
|
458
|
-
val password: String
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
data class LoginResponse(
|
|
462
|
-
val token: String,
|
|
463
|
-
val user: User
|
|
464
|
-
)
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Repository
|
|
468
|
-
```kotlin
|
|
469
|
-
interface UserRepository {
|
|
470
|
-
suspend fun login(username: String, password: String): Result<LoginResponse>
|
|
471
|
-
suspend fun getProfile(): Result<User>
|
|
472
|
-
fun observeUser(): Flow<User?>
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
class UserRepositoryImpl(
|
|
476
|
-
private val api: ApiService,
|
|
477
|
-
private val userDao: UserDao,
|
|
478
|
-
private val tokenManager: TokenManager
|
|
479
|
-
) : UserRepository {
|
|
480
|
-
|
|
481
|
-
override suspend fun login(username: String, password: String): Result<LoginResponse> {
|
|
482
|
-
return try {
|
|
483
|
-
val request = LoginRequest(username, password)
|
|
484
|
-
val response = api.login(request)
|
|
485
|
-
tokenManager.saveToken(response.token)
|
|
486
|
-
userDao.insert(response.user)
|
|
487
|
-
Result.success(response)
|
|
488
|
-
} catch (e: Exception) {
|
|
489
|
-
Result.failure(e)
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
override suspend fun getProfile(): Result<User> {
|
|
494
|
-
return try {
|
|
495
|
-
val user = api.getProfile()
|
|
496
|
-
userDao.insert(user)
|
|
497
|
-
Result.success(user)
|
|
498
|
-
} catch (e: Exception) {
|
|
499
|
-
Result.failure(e)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
override fun observeUser(): Flow<User?> {
|
|
504
|
-
return userDao.observeUser()
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
### ViewModel
|
|
510
|
-
```kotlin
|
|
511
|
-
@HiltViewModel
|
|
512
|
-
class LoginViewModel @Inject constructor(
|
|
513
|
-
private val repository: UserRepository
|
|
514
|
-
) : ViewModel() {
|
|
515
|
-
|
|
516
|
-
private val _uiState = MutableStateFlow(LoginUiState())
|
|
517
|
-
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
|
|
518
|
-
|
|
519
|
-
fun updateUsername(username: String) {
|
|
520
|
-
_uiState.update { it.copy(username = username) }
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
fun updatePassword(password: String) {
|
|
524
|
-
_uiState.update { it.copy(password = password) }
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
fun login() {
|
|
528
|
-
val state = _uiState.value
|
|
529
|
-
if (!state.isValid) return
|
|
530
|
-
|
|
531
|
-
viewModelScope.launch {
|
|
532
|
-
_uiState.update { it.copy(isLoading = true, error = null) }
|
|
533
|
-
|
|
534
|
-
repository.login(state.username, state.password)
|
|
535
|
-
.onSuccess {
|
|
536
|
-
_uiState.update { it.copy(isLoading = false, isLoggedIn = true) }
|
|
537
|
-
}
|
|
538
|
-
.onFailure { error ->
|
|
539
|
-
_uiState.update {
|
|
540
|
-
it.copy(isLoading = false, error = error.message)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
data class LoginUiState(
|
|
548
|
-
val username: String = "",
|
|
549
|
-
val password: String = "",
|
|
550
|
-
val isLoading: Boolean = false,
|
|
551
|
-
val error: String? = null,
|
|
552
|
-
val isLoggedIn: Boolean = false
|
|
553
|
-
) {
|
|
554
|
-
val isValid: Boolean
|
|
555
|
-
get() = username.isNotEmpty() && password.length >= 6
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
### View (Compose)
|
|
560
|
-
```kotlin
|
|
561
|
-
@Composable
|
|
562
|
-
fun LoginScreen(
|
|
563
|
-
viewModel: LoginViewModel = hiltViewModel(),
|
|
564
|
-
onLoginSuccess: () -> Unit
|
|
565
|
-
) {
|
|
566
|
-
val uiState by viewModel.uiState.collectAsState()
|
|
567
|
-
|
|
568
|
-
LaunchedEffect(uiState.isLoggedIn) {
|
|
569
|
-
if (uiState.isLoggedIn) {
|
|
570
|
-
onLoginSuccess()
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
Column(
|
|
575
|
-
modifier = Modifier
|
|
576
|
-
.fillMaxSize()
|
|
577
|
-
.padding(16.dp),
|
|
578
|
-
verticalArrangement = Arrangement.Center
|
|
579
|
-
) {
|
|
580
|
-
OutlinedTextField(
|
|
581
|
-
value = uiState.username,
|
|
582
|
-
onValueChange = viewModel::updateUsername,
|
|
583
|
-
label = { Text("Username") },
|
|
584
|
-
modifier = Modifier.fillMaxWidth()
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
Spacer(modifier = Modifier.height(8.dp))
|
|
588
|
-
|
|
589
|
-
OutlinedTextField(
|
|
590
|
-
value = uiState.password,
|
|
591
|
-
onValueChange = viewModel::updatePassword,
|
|
592
|
-
label = { Text("Password") },
|
|
593
|
-
visualTransformation = PasswordVisualTransformation(),
|
|
594
|
-
modifier = Modifier.fillMaxWidth()
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
if (uiState.error != null) {
|
|
598
|
-
Text(
|
|
599
|
-
text = uiState.error!!,
|
|
600
|
-
color = MaterialTheme.colorScheme.error,
|
|
601
|
-
style = MaterialTheme.typography.bodySmall,
|
|
602
|
-
modifier = Modifier.padding(top = 8.dp)
|
|
603
|
-
)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
Spacer(modifier = Modifier.height(16.dp))
|
|
607
|
-
|
|
608
|
-
Button(
|
|
609
|
-
onClick = viewModel::login,
|
|
610
|
-
enabled = uiState.isValid && !uiState.isLoading,
|
|
611
|
-
modifier = Modifier.fillMaxWidth()
|
|
612
|
-
) {
|
|
613
|
-
if (uiState.isLoading) {
|
|
614
|
-
CircularProgressIndicator(
|
|
615
|
-
modifier = Modifier.size(24.dp),
|
|
616
|
-
color = MaterialTheme.colorScheme.onPrimary
|
|
617
|
-
)
|
|
618
|
-
} else {
|
|
619
|
-
Text("Login")
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
## 依赖注入 (Hilt)
|
|
627
|
-
|
|
628
|
-
### 配置
|
|
629
|
-
```kotlin
|
|
630
|
-
@HiltAndroidApp
|
|
631
|
-
class MyApplication : Application()
|
|
632
|
-
|
|
633
|
-
@AndroidEntryPoint
|
|
634
|
-
class MainActivity : ComponentActivity() {
|
|
635
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
636
|
-
super.onCreate(savedInstanceState)
|
|
637
|
-
setContent {
|
|
638
|
-
MyApp()
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### Module
|
|
645
|
-
```kotlin
|
|
646
|
-
@Module
|
|
647
|
-
@InstallIn(SingletonComponent::class)
|
|
648
|
-
object NetworkModule {
|
|
649
|
-
|
|
650
|
-
@Provides
|
|
651
|
-
@Singleton
|
|
652
|
-
fun provideOkHttpClient(): OkHttpClient {
|
|
653
|
-
return OkHttpClient.Builder()
|
|
654
|
-
.addInterceptor(AuthInterceptor())
|
|
655
|
-
.connectTimeout(30, TimeUnit.SECONDS)
|
|
656
|
-
.build()
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
@Provides
|
|
660
|
-
@Singleton
|
|
661
|
-
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
|
662
|
-
return Retrofit.Builder()
|
|
663
|
-
.baseUrl("https://api.example.com")
|
|
664
|
-
.client(okHttpClient)
|
|
665
|
-
.addConverterFactory(GsonConverterFactory.create())
|
|
666
|
-
.build()
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
@Provides
|
|
670
|
-
@Singleton
|
|
671
|
-
fun provideApiService(retrofit: Retrofit): ApiService {
|
|
672
|
-
return retrofit.create(ApiService::class.java)
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
@Module
|
|
677
|
-
@InstallIn(SingletonComponent::class)
|
|
678
|
-
abstract class RepositoryModule {
|
|
679
|
-
|
|
680
|
-
@Binds
|
|
681
|
-
@Singleton
|
|
682
|
-
abstract fun bindUserRepository(
|
|
683
|
-
impl: UserRepositoryImpl
|
|
684
|
-
): UserRepository
|
|
685
|
-
}
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
## Room 数据库
|
|
689
|
-
|
|
690
|
-
### Entity
|
|
691
|
-
```kotlin
|
|
692
|
-
@Entity(tableName = "users")
|
|
693
|
-
data class UserEntity(
|
|
694
|
-
@PrimaryKey val id: Int,
|
|
695
|
-
val name: String,
|
|
696
|
-
val email: String,
|
|
697
|
-
@ColumnInfo(name = "avatar_url") val avatarUrl: String,
|
|
698
|
-
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
|
|
699
|
-
)
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### DAO
|
|
703
|
-
```kotlin
|
|
704
|
-
@Dao
|
|
705
|
-
interface UserDao {
|
|
706
|
-
@Query("SELECT * FROM users")
|
|
707
|
-
fun observeAll(): Flow<List<UserEntity>>
|
|
708
|
-
|
|
709
|
-
@Query("SELECT * FROM users WHERE id = :id")
|
|
710
|
-
suspend fun getById(id: Int): UserEntity?
|
|
711
|
-
|
|
712
|
-
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
713
|
-
suspend fun insert(user: UserEntity)
|
|
714
|
-
|
|
715
|
-
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
716
|
-
suspend fun insertAll(users: List<UserEntity>)
|
|
717
|
-
|
|
718
|
-
@Delete
|
|
719
|
-
suspend fun delete(user: UserEntity)
|
|
720
|
-
|
|
721
|
-
@Query("DELETE FROM users")
|
|
722
|
-
suspend fun deleteAll()
|
|
723
|
-
}
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
### Database
|
|
727
|
-
```kotlin
|
|
728
|
-
@Database(
|
|
729
|
-
entities = [UserEntity::class],
|
|
730
|
-
version = 1,
|
|
731
|
-
exportSchema = false
|
|
732
|
-
)
|
|
733
|
-
abstract class AppDatabase : RoomDatabase() {
|
|
734
|
-
abstract fun userDao(): UserDao
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
@Module
|
|
738
|
-
@InstallIn(SingletonComponent::class)
|
|
739
|
-
object DatabaseModule {
|
|
740
|
-
|
|
741
|
-
@Provides
|
|
742
|
-
@Singleton
|
|
743
|
-
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
|
744
|
-
return Room.databaseBuilder(
|
|
745
|
-
context,
|
|
746
|
-
AppDatabase::class.java,
|
|
747
|
-
"app_database"
|
|
748
|
-
)
|
|
749
|
-
.fallbackToDestructiveMigration()
|
|
750
|
-
.build()
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
@Provides
|
|
754
|
-
fun provideUserDao(database: AppDatabase): UserDao {
|
|
755
|
-
return database.userDao()
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
## 网络层
|
|
761
|
-
|
|
762
|
-
### Retrofit
|
|
763
|
-
```kotlin
|
|
764
|
-
interface ApiService {
|
|
765
|
-
@POST("auth/login")
|
|
766
|
-
suspend fun login(@Body request: LoginRequest): LoginResponse
|
|
767
|
-
|
|
768
|
-
@GET("users/{id}")
|
|
769
|
-
suspend fun getUser(@Path("id") id: Int): User
|
|
770
|
-
|
|
771
|
-
@GET("users")
|
|
772
|
-
suspend fun getUsers(
|
|
773
|
-
@Query("page") page: Int,
|
|
774
|
-
@Query("limit") limit: Int = 20
|
|
775
|
-
): List<User>
|
|
776
|
-
|
|
777
|
-
@Multipart
|
|
778
|
-
@POST("upload")
|
|
779
|
-
suspend fun uploadImage(
|
|
780
|
-
@Part file: MultipartBody.Part
|
|
781
|
-
): UploadResponse
|
|
782
|
-
}
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Interceptor
|
|
786
|
-
```kotlin
|
|
787
|
-
class AuthInterceptor(
|
|
788
|
-
private val tokenManager: TokenManager
|
|
789
|
-
) : Interceptor {
|
|
790
|
-
override fun intercept(chain: Interceptor.Chain): Response {
|
|
791
|
-
val original = chain.request()
|
|
792
|
-
val token = tokenManager.getToken()
|
|
793
|
-
|
|
794
|
-
val request = if (token != null) {
|
|
795
|
-
original.newBuilder()
|
|
796
|
-
.header("Authorization", "Bearer $token")
|
|
797
|
-
.build()
|
|
798
|
-
} else {
|
|
799
|
-
original
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return chain.proceed(request)
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
class LoggingInterceptor : Interceptor {
|
|
807
|
-
override fun intercept(chain: Interceptor.Chain): Response {
|
|
808
|
-
val request = chain.request()
|
|
809
|
-
Log.d("API", "Request: ${request.method} ${request.url}")
|
|
810
|
-
|
|
811
|
-
val response = chain.proceed(request)
|
|
812
|
-
Log.d("API", "Response: ${response.code}")
|
|
813
|
-
|
|
814
|
-
return response
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
## 性能优化
|
|
820
|
-
|
|
821
|
-
### LazyColumn 优化
|
|
822
|
-
```kotlin
|
|
823
|
-
@Composable
|
|
824
|
-
fun OptimizedList(items: List<Item>) {
|
|
825
|
-
LazyColumn(
|
|
826
|
-
contentPadding = PaddingValues(16.dp),
|
|
827
|
-
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
828
|
-
) {
|
|
829
|
-
items(
|
|
830
|
-
items = items,
|
|
831
|
-
key = { it.id } // 关键:提供稳定的 key
|
|
832
|
-
) { item ->
|
|
833
|
-
ItemCard(item)
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
@Composable
|
|
839
|
-
fun ItemCard(item: Item) {
|
|
840
|
-
// 使用 remember 避免重组时重新计算
|
|
841
|
-
val formattedDate = remember(item.timestamp) {
|
|
842
|
-
formatDate(item.timestamp)
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
Card {
|
|
846
|
-
Text(formattedDate)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
### 图片加载 (Coil)
|
|
852
|
-
```kotlin
|
|
853
|
-
@Composable
|
|
854
|
-
fun AsyncImage(url: String) {
|
|
855
|
-
AsyncImage(
|
|
856
|
-
model = ImageRequest.Builder(LocalContext.current)
|
|
857
|
-
.data(url)
|
|
858
|
-
.crossfade(true)
|
|
859
|
-
.memoryCachePolicy(CachePolicy.ENABLED)
|
|
860
|
-
.diskCachePolicy(CachePolicy.ENABLED)
|
|
861
|
-
.build(),
|
|
862
|
-
contentDescription = null,
|
|
863
|
-
modifier = Modifier.size(200.dp)
|
|
864
|
-
)
|
|
865
|
-
}
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
### 避免过度重组
|
|
869
|
-
```kotlin
|
|
870
|
-
@Composable
|
|
871
|
-
fun ExpensiveScreen(data: Data) {
|
|
872
|
-
// ❌ 错误:每次重组都会创建新实例
|
|
873
|
-
val processor = DataProcessor()
|
|
874
|
-
|
|
875
|
-
// ✅ 正确:使用 remember
|
|
876
|
-
val processor = remember { DataProcessor() }
|
|
877
|
-
|
|
878
|
-
// ✅ 使用 derivedStateOf 避免不必要的重组
|
|
879
|
-
val filteredData by remember {
|
|
880
|
-
derivedStateOf {
|
|
881
|
-
data.items.filter { it.isActive }
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
## 测试
|
|
888
|
-
|
|
889
|
-
### Unit Test
|
|
890
|
-
```kotlin
|
|
891
|
-
@Test
|
|
892
|
-
fun `login success updates state correctly`() = runTest {
|
|
893
|
-
val repository = FakeUserRepository()
|
|
894
|
-
val viewModel = LoginViewModel(repository)
|
|
895
|
-
|
|
896
|
-
viewModel.updateUsername("test")
|
|
897
|
-
viewModel.updatePassword("password")
|
|
898
|
-
viewModel.login()
|
|
899
|
-
|
|
900
|
-
advanceUntilIdle()
|
|
901
|
-
|
|
902
|
-
val state = viewModel.uiState.value
|
|
903
|
-
assertTrue(state.isLoggedIn)
|
|
904
|
-
assertNull(state.error)
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
class FakeUserRepository : UserRepository {
|
|
908
|
-
var loginResult: Result<LoginResponse>? = null
|
|
909
|
-
|
|
910
|
-
override suspend fun login(username: String, password: String): Result<LoginResponse> {
|
|
911
|
-
return loginResult ?: Result.success(
|
|
912
|
-
LoginResponse("token", User(1, "Test", "test@example.com", ""))
|
|
913
|
-
)
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
override suspend fun getProfile(): Result<User> {
|
|
917
|
-
return Result.success(User(1, "Test", "test@example.com", ""))
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
override fun observeUser(): Flow<User?> = flowOf(null)
|
|
921
|
-
}
|
|
922
|
-
```
|
|
923
|
-
|
|
924
|
-
### UI Test
|
|
925
|
-
```kotlin
|
|
926
|
-
@get:Rule
|
|
927
|
-
val composeTestRule = createComposeRule()
|
|
928
|
-
|
|
929
|
-
@Test
|
|
930
|
-
fun loginFlow() {
|
|
931
|
-
composeTestRule.setContent {
|
|
932
|
-
LoginScreen(onLoginSuccess = {})
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
composeTestRule
|
|
936
|
-
.onNodeWithText("Username")
|
|
937
|
-
.performTextInput("testuser")
|
|
938
|
-
|
|
939
|
-
composeTestRule
|
|
940
|
-
.onNodeWithText("Password")
|
|
941
|
-
.performTextInput("password123")
|
|
942
|
-
|
|
943
|
-
composeTestRule
|
|
944
|
-
.onNodeWithText("Login")
|
|
945
|
-
.performClick()
|
|
946
|
-
|
|
947
|
-
composeTestRule
|
|
948
|
-
.onNodeWithText("Welcome")
|
|
949
|
-
.assertIsDisplayed()
|
|
950
|
-
}
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
## 工具清单
|
|
954
|
-
|
|
955
|
-
| 工具 | 用途 |
|
|
956
|
-
|------|------|
|
|
957
|
-
| Android Studio | IDE |
|
|
958
|
-
| Gradle | 构建工具 |
|
|
959
|
-
| Hilt | 依赖注入 |
|
|
960
|
-
| Retrofit | 网络请求 |
|
|
961
|
-
| Room | 数据库 |
|
|
962
|
-
| Coil | 图片加载 |
|
|
963
|
-
| LeakCanary | 内存泄漏检测 |
|
|
964
|
-
| Detekt | 代码规范 |
|
|
965
|
-
|
|
966
|
-
## 最佳实践
|
|
967
|
-
|
|
968
|
-
- ✅ Jetpack Compose 优先,View 系统按需使用
|
|
969
|
-
- ✅ MVVM 架构 + Repository 模式
|
|
970
|
-
- ✅ StateFlow 替代 LiveData
|
|
971
|
-
- ✅ Kotlin Coroutines 处理异步
|
|
972
|
-
- ✅ Hilt 依赖注入
|
|
973
|
-
- ✅ Room 本地持久化
|
|
974
|
-
- ✅ 使用 key 优化 LazyColumn
|
|
975
|
-
- ✅ remember/derivedStateOf 避免重组
|
|
976
|
-
- ✅ 单元测试覆盖 ViewModel
|
|
977
|
-
- ✅ UI 测试验证关键流程
|
|
978
|
-
|
|
979
|
-
---
|