Skip to content

Extract node list display settings to dedicated screen#5580

Merged
jamesarich merged 5 commits into
mainfrom
jamesarich/feature-node-list-settings-screen
May 23, 2026
Merged

Extract node list display settings to dedicated screen#5580
jamesarich merged 5 commits into
mainfrom
jamesarich/feature-node-list-settings-screen

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Motivation

The node list density and field visibility toggles were previously inlined in both the Android and Desktop settings screens. This made them harder to find, cluttered the main settings surface, and duplicated logic across platform source sets.

Approach

Extracts node list settings into a dedicated NodeListScreen that follows the state-holder/UI split pattern used elsewhere in the app:

  • NodeListScreen (state-holder): collects an aggregated NodeListSettingsState from the ViewModel via a single collectAsStateWithLifecycle() call, wires callbacks
  • NodeLayoutSettings (stateless UI): density picker (segmented button), animated live preview of both Complete and Compact node cards, and field visibility toggles grouped inside a Card

Key design decisions:

  • Aggregates 10 separate preference flows into a single StateFlow<NodeListSettingsState> using nested combine() (kotlinx.coroutines typed overloads cap at 5 params)
  • Compact-field toggles are wrapped in an M3 Card (surfaceContainerLow) to visually group them as a cohesive settings section
  • Individual toggle rows use M3 Expressive togglable ListItem with RectangleShape -- the checked-state rounded shape is intentional Expressive design
  • No additional spacing between toggle rows; M3 ListItem internal padding provides appropriate density

Screenshots

Complete mode Compact mode

Files changed

  • New: NodeListScreen.kt, NodeLayoutSettings.kt, NodeLayoutSettingsPreviews.kt
  • Modified: Routes.kt (new route), SettingsNavigation.kt (new entry), SettingsViewModel.kt (state aggregation), SettingsScreen.kt and DesktopSettingsScreen.kt (navigation links replace inline toggles)
  • New: Screenshot references for 3 preview variants (light + dark)

jamesarich and others added 4 commits May 22, 2026 16:30
Fixes two blocking issues identified in code review:

1. **State Management Anti-Pattern (10→1 listener)**
   - Created NodeListSettingsState data class to aggregate all node list prefs
   - Combined 10 individual state flows using flow.combine() operator
   - Reduces recomposition listeners from 10 to 1, eliminating unnecessary re-renders
   - NodeListScreen now uses single collectAsStateWithLifecycle() call

2. **Material 3 Divider Misuse (8 removed)**
   - Removed 8 HorizontalDividers between toggle rows (violated M3 guidelines)
   - Dividers now only used for major section breaks, not every item
   - Card/ExpressiveSection container already provides visual grouping
   - Updated padding spacing from 4.dp to 8.dp to compensate

Implementation:
- Added combine import to SettingsViewModel
- Created NodeListSettingsState data class with all 10 preference fields
- Exposed as nodeListSettings StateFlow with stateInWhileSubscribed()
- Updated NodeListScreen to use aggregated state
- Wrapped compact toggles in Column for better structure
- All Material 3 compliance maintained, visual experience improved

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three improvements:

1. **State model parameter**: Replace 10 individual state parameters with
   single NodeListSettingsState data class. Reduces parameter count from
   19 to 11 (1 state + 9 callbacks + modifier). Follows M3 best practice
   of passing dedicated UiState objects for cohesive state groups.

2. **Fix combine() overload bug**: kotlinx.coroutines only provides typed
   combine() overloads for up to 5 flows. The prior 10-arg call with
   mixed types (Flow<String> + 9×Flow<Boolean>) would not compile.
   Fixed with nested combines (5+5→2).

3. **Cleanup**: Remove unused imports (NodeListDensity in NodeListScreen,
   Arrangement in NodeLayoutSettings), remove no-op spacedBy(0.dp).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add proper import and replace 3 fully-qualified references with short
names for consistency with project conventions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap compact-mode field toggles in a Card with surfaceContainerLow
  background to visually group them as a cohesive settings section
- Add missing List icon import to Android SettingsScreen
- Add missing node_layout_section_title import to Desktop SettingsScreen
- Update screenshot references to reflect Card grouping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

📄 Docs staleness check — advisory

This PR modifies user-facing UI source files but does not update any page under docs/en/user/ or docs/en/developer/.

⚠️ Doc changes propagate to 3 consumers: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in docs/en/ automatically flows to all three.

Changed source files:

feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt

What to check:

Changed area Likely doc page
feature/messaging/ docs/en/user/messages-and-channels.md
feature/node/ docs/en/user/nodes.md or docs/en/user/node-metrics.md
feature/map/ docs/en/user/map-and-waypoints.md
feature/connections/ docs/en/user/connections.md
feature/settings/ docs/en/user/settings-radio-user.md or docs/en/user/settings-module-admin.md
feature/firmware/ docs/en/user/firmware.md
feature/intro/ docs/en/user/onboarding.md
feature/discovery/ docs/en/user/discovery.md
feature/docs/ Internal docs infrastructure
core/ui/ docs/en/developer/codebase.md or component-specific user pages

New page checklist (if adding a new doc page):

  1. Create the .md file in docs/en/user/ or docs/en/developer/ with last_updated frontmatter
  2. Register in DocBundleLoader.kt with string resources (in-app browser)
  3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this check.

Cross-platform note: This check is advisory while doc coverage matures. Both Android and Apple repos use the same skip-docs-check label and advisory severity. See meshtastic/design standards for shared conventions.

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2515 1 2514 0
View the top 1 failed test(s) by shortest run time
org.meshtastic.core.database.DatabaseManagerWithDbRetryTest::withDb retries against current database when previous pool closes during switch
Stack Traces | 18.4s run time
java.lang.AssertionError: expected:<42424242> but was:<null>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:120)
	at kotlin.test.junit.JUnitAsserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.core.database.DatabaseManagerWithDbRetryTest$withDb retries against current database when previous pool closes during switch$1.invokeSuspend(DatabaseManagerWithDbRetryTest.kt:99)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:24)
	at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:98)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$workRunner$1.invokeSuspend(TestBuilders.kt:326)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:256)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlockingImpl(Builders.kt:30)
	at kotlinx.coroutines.BuildersKt.runBlockingImpl(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK(Builders.concurrent.kt:172)
	at kotlinx.coroutines.BuildersKt.runBlockingK(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK$default(Builders.concurrent.kt:157)
	at kotlinx.coroutines.BuildersKt.runBlockingK$default(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:10)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:309)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:167)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.core.database.DatabaseManagerWithDbRetryTest.withDb retries against current database when previous pool closes during switch(DatabaseManagerWithDbRetryTest.kt:69)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

- Apply spotless formatting to all feature:settings source files
- Add modifier parameter to NodeListScreen (detekt composable rule)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich added this pull request to the merge queue May 23, 2026
Merged via the queue into main with commit a679278 May 23, 2026
18 checks passed
@jamesarich jamesarich deleted the jamesarich/feature-node-list-settings-screen branch May 23, 2026 00:09
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.

1 participant