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