Skip to content

fix: ui state stability to reduce render cycles#839

Open
ovitrif wants to merge 7 commits intomasterfrom
refactor/immutable-collections
Open

fix: ui state stability to reduce render cycles#839
ovitrif wants to merge 7 commits intomasterfrom
refactor/immutable-collections

Conversation

@ovitrif
Copy link
Collaborator

@ovitrif ovitrif commented Mar 10, 2026

Closes #837

Use kotlinx-collections-immutable types across UI state, composable parameters, and repository state to fix Compose stability and reduce unnecessary recompositions, thus improving rendering performance.

Note

Visible user-facing performance of Compose UI is only measurable in release builds.

Usually Compose is very fast but in extreme conditions on low-end devices loaded with resources-consuming it might be possible to notice a relevant improvement in refresh-rate. The real value is in updating the UI code to conform to the current best practices, which should influence AI agents to generate better code from now on.

Description

  • Add kotlinx-collections-immutable dependency and Compose compiler stability config for external types (com.synonym.bitkitcore.*, org.lightningdevkit.ldknode.*)
  • Replace List/Map/Set with ImmutableList/ImmutableMap/ImmutableSet in all UiState data classes and composable function parameters
  • Add @Immutable annotations to state data classes
  • Migrate remaining collectAsState() calls to collectAsStateWithLifecycle()
  • Update CLAUDE.md with immutable collection rules

Preview

Changes might be visible on low-end devices when scrolling down long lists like the one on the all activity view.
But due to the fact that Compose is much less performant in debug builds, only release builds are relevant for benchmarking.

Tl;DR: The changes of this refactoring is simply raising the codebase to the standards of best practices recommended in modern android development nowadays. (one could say: "trust me bro" :trollface: )

QA Notes

  • rely on e2e suite
  • sanity check random screens and 1-2 main flows
  • generate test activities via dev settings > verify activity list scrolling is smooth over huge swipes

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ovitrif ovitrif added this to the 2.2.0 milestone Mar 10, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ovitrif ovitrif changed the title refactor: use immutable collections in ui state fix(ui): unnecessary re-rendering Mar 11, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

This comment has been minimized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ovitrif ovitrif changed the title fix(ui): unnecessary re-rendering fix: ui state stability to avoid unnecessary re-rendering Mar 11, 2026
@ovitrif ovitrif changed the title fix: ui state stability to avoid unnecessary re-rendering fix: ui state stability to reduce render cycles Mar 11, 2026
@ovitrif ovitrif requested review from jvsena42 and piotr-iohk March 11, 2026 03:15
@ovitrif

This comment was marked as resolved.

@claude

This comment has been minimized.

ovitrif and others added 2 commits March 18, 2026 08:33
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ovitrif ovitrif marked this pull request as ready for review March 18, 2026 08:16
@jvsena42
Copy link
Member

@claude What are the advantages of using kotlinx-collections-immutable instead of @Stable and @Inmutable annotations?

@claude
Copy link

claude bot commented Mar 20, 2026

Claude finished @jvsena42's task in 1m 2s —— View job


kotlinx-collections-immutable vs. @Stable/@Immutable annotations

Great question — these are complementary tools, not alternatives. Here's why the library types are needed even when annotations exist:


The core problem: Kotlin List is not truly immutable

List<T> in Kotlin is just a read-only interface, not an immutable type. A MutableList implements List, so any List<T> reference could be backed by a mutable collection:

val list: List<String> = mutableListOf("a", "b")  // compiles fine

The Compose compiler knows this. When it sees a plain List<T> field, it marks the containing class as unstable and will recompose it on every state emission — even if the data hasn't changed.


What @Immutable/@Stable do (and don't do)

These annotations are promises to the compiler, not enforcement:

@Immutable  // "trust me, all fields are deeply immutable"
data class MyState(val items: List<String>)  // ⚠️ lie — List isn't immutable
  • The compiler skips recompositions it should perform → subtle UI update bugs
  • No compile-time enforcement — if a field is actually mutable, nothing catches it
  • Still required for external types you can't modify (Rust FFI, third-party libs)

What ImmutableList/ImmutableMap/ImmutableSet do differently

These are structurally immutable types backed by persistent data structures. The Compose compiler can verify their stability without trusting an annotation:

// Compiler can infer stability — no annotation tricks needed
data class MyState(val items: ImmutableList<String>)
@Immutable + List<T> ImmutableList<T>
Compile-time enforcement
Compose compiler can verify ❌ (trusts annotation) ✅ (knows the type)
Actually immutable at runtime
Works for external types ❌ (can't change external types)

Why this PR uses both

They solve different problems and are used together:

  1. ImmutableList/ImmutableMap/ImmutableSet — for collection fields in your own UiState classes. Gives the compiler real guarantees.

  2. @Immutable/@Stable — for UiState data classes themselves (to cover non-collection fields), and via the stability config file for external Rust/LDK types (com.synonym.bitkitcore.*, org.lightningdevkit.ldknode.*) that can't be modified.

  3. @Stable instead of @Immutable — when a class contains external types or Throwable (weaker contract: "equal inputs → equal outputs" rather than "deeply immutable").


TL;DR: @Immutable is a promise; ImmutableList is a guarantee. The library types let the compiler independently verify stability for collections, while annotations remain necessary for external types and whole-class stability contracts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: fix composables stability

2 participants