perf: Add stable keys to Lazy list items for performance (#323)

Added stable `key` parameters to `items(...)` and `itemsIndexed(...)` calls across 13 screens. This prevents Compose from treating all list mutations as full item replacements and eliminates unnecessary full list recompositions during state updates.

Modifications applied to:
- KeysScreen.kt, SharedComponents.kt, AchievementsScreen.kt, PluginsScreen.kt
- SessionsScreen.kt, CronJobsScreen.kt, McpServersScreen.kt, PairingScreen.kt
- ToolsetsScreen.kt, ChatScreen.kt, ModelScreen.kt, SkillsScreen.kt
- KanbanScreen.kt, ProfilesScreen.kt, WebhooksScreen.kt

Note: LogsScreen.kt was left without a key because logs are string lines that can be duplicated, and using `.hashCode()` would cause a "Key was already used" crash. Positional indexing is safer there.

Fixes #279
This commit is contained in:
M57 authored and GitHub committed 2026-06-23 12:17:45 +03:00
1 parent d9c7471350
commit 4d47db263f
15 files changed
+17 -17

No files matched your search

@@ -101,7 +101,7 @@ fun AchievementsScreen(
placeholder = "Search achievements...",
)
}
items(filteredAchievements) { achievement ->
items(filteredAchievements, key = { it.id }) { achievement ->
val pct = achievement.progress_pct?.toFloat()?.div(100f) ?: 0f
Card(
@@ -715,7 +715,7 @@ private fun ChatInputBar(
LazyColumn(
modifier = Modifier.heightIn(max = 200.dp),
) {
items(filteredCommands) { cmd ->
items(filteredCommands, key = { it }) { cmd ->
val description =
when (cmd) {
"/help" -> stringResource(R.string.command_help_desc)
@@ -246,7 +246,7 @@ fun FilterChipRow(
),
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
) {
items(chips) { chip ->
items(chips, key = { it }) { chip ->
FilterChip(
selected = chip == selectedChip,
onClick = { onChipSelected(chip) },
@@ -89,7 +89,7 @@ fun CronJobsScreen(
contentPadding = listContentPadding,
verticalArrangement = listItemSpacing,
) {
items(state.jobs) { job ->
items(state.jobs, key = { it.id }) { job ->
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -149,7 +149,7 @@ fun KanbanScreen(
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
items(state.columns) { column ->
items(state.columns, key = { it.name }) { column ->
val colName = column.name
val colTasks =
filteredTasks.filter {
@@ -178,7 +178,7 @@ fun KanbanScreen(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(colTasks) { task ->
items(colTasks, key = { it.id }) { task ->
TaskCard(
task = task,
onMoveLeft =
@@ -125,7 +125,7 @@ fun KeysScreen(
placeholder = "Search keys...",
)
}
items(filteredEnvVars.toList()) { (key, config) ->
items(filteredEnvVars.toList(), key = { it.first }) { (key, config) ->
var isEditing by remember { mutableStateOf(false) }
val revealedVal = state.revealedValues[key]
val isRevealed = revealedVal != null
@@ -116,7 +116,7 @@ fun McpServersScreen(
placeholder = "Search MCP servers...",
)
}
items(filteredServers) { server ->
items(filteredServers, key = { it.name }) { server ->
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -134,7 +134,7 @@ fun ModelScreen(
placeholder = "Search models...",
)
}
items(filteredProviders) { provider ->
items(filteredProviders, key = { it.slug }) { provider ->
val isExpanded = expandedProviderSlug == provider.slug
val isCurrent = provider.is_current == true
@@ -121,7 +121,7 @@ fun PairingScreen(
}
}
items(pending) { item ->
items(pending, key = { "${it.platform}_${it.code ?: it.user_id}" }) { item ->
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -201,7 +201,7 @@ fun PairingScreen(
}
}
} else {
items(approved) { item ->
items(approved, key = { "${it.platform}_${it.user_id}" }) { item ->
Card(
modifier = Modifier.fillMaxWidth(),
) {
@@ -119,7 +119,7 @@ fun PluginsScreen(
placeholder = "Search plugins...",
)
}
items(filteredPlugins) { plugin ->
items(filteredPlugins, key = { it.name }) { plugin ->
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
@@ -118,7 +118,7 @@ fun ProfilesScreen(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(state.profiles) { profile ->
items(state.profiles, key = { it.name }) { profile ->
val isActive = profile.name == state.activeProfileName
Card(
modifier = Modifier.fillMaxWidth(),
@@ -105,7 +105,7 @@ fun SessionsScreen(
placeholder = "Search sessions...",
)
}
items(filteredSessions) { session ->
items(filteredSessions, key = { it.id }) { session ->
Card(
modifier =
Modifier
@@ -237,7 +237,7 @@ fun SkillsScreen(
}
}
} else {
items(filtered) { skill ->
items(filtered, key = { it.name }) { skill ->
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -133,7 +133,7 @@ fun ToolsetsScreen(
placeholder = "Search toolsets...",
)
}
items(filteredToolsets) { toolset ->
items(filteredToolsets, key = { it.name }) { toolset ->
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -192,7 +192,7 @@ fun WebhooksScreen(
}
}
} else {
items(filteredSubscriptions) { sub ->
items(filteredSubscriptions, key = { it.name }) { sub ->
Card(
modifier = Modifier.fillMaxWidth(),
) {