Skip to content

feat(native-list): apply the row style on iOS and Android - #110

Open
huhuanming wants to merge 1 commit into
codex/native-list-row-stylefrom
codex/native-list-row-style-native
Open

huhuanming wants to merge 1 commit into
codex/native-list-row-stylefrom
codex/native-list-row-style-native

Conversation

@huhuanming

Copy link
Copy Markdown
Contributor

Stacked on #109, which is stacked on #107. Review in order; this PR's diff is P2 only.

Important

The native code here is not compiled or run by this change. I verified Swift syntax (swiftc -parse, clean) and compiled and ran the Kotlin mapping table standalone, but neither platform was built against its real SDK from this branch. Please build both apps and check the example pages before merging.

What

applyRowStyle on iOS and Android, following docs/STYLE_SPEC.md §8. It runs after the per-template binder so it is the last writer — on Android specifically after applySize, which re-dispatches font size and typeface by row type and would otherwise overwrite the style.

The mapping is the risky part, so it is a pure function

A style key names a model field, and the view pool is shared, so the same field lands in a different view per template. metricCard draws its value through the view identity uses for its title; one status view carries rail.status, activity.status, message.time, and metricCard.trend.

That table is nativeListStyleSlot in NativeListModels.kt and styleSlot in NativeListCell.swift. The Kotlin copy is unit-tested (NativeListStyleSlotTest), including a check that no template maps two style keys onto one view — a collision would make one of them silently win.

I also extracted the shipped Kotlin function verbatim and compiled and ran it here:

ALL PASS: 19 mapping cases, 10 collision checks

The table exists twice because the two languages cannot share it; §4 is the source of truth for both, and the spec now says so.

Both platforms needed a reset first

STYLE_SPEC.md §7 rule 2. Neither reset path is complete:

Platform What its reset misses
Android resetViews() restores visibility, gravity, maxLines, layout params, background and padding — not textSize, typeface or lineHeight
iOS reset() restores the shared label fonts — not the data, metric and media label fonts, nor any text alignment

Without resetRowStyle a style would leak into the next row that reuses the view — the exact recycling bug the spec warns about. Both are guarded by a dirty flag, so an unstyled list pays nothing.

iOS: the attributed-string trap

The binders install an attributed string through setLineHeight, so assigning label.font or label.textColor afterwards has no visible effect. The style pass therefore rebuilds the line box, centering font metrics inside an explicit lineHeight the same way React Native and the Web engine do.

Deliberately unchanged

Recorded in the spec's status table rather than guessed at:

  • Row heights. A styled row that grows still needs an explicit height.
  • Android's list-wide source scale (§6.3). usesSelectorSourceScale is computed with items.any { … }, so one selector row switches the sub-400dp metric system for the whole list. Making it per-row would move every selector list's metrics and needs device verification. The new code adds no list-wide decision of its own, so §7 rule 3 holds for it.
  • dataRow columns in the table layout, whose text lives inside NativeListTableColumnView / tableDataColumns. The linear layout is covered.

Verification

  • swiftc -parse ios/NativeListCell.swift — clean
  • Kotlin mapping table compiled and run standalone — 19 mapping cases + 10 collision checks pass
  • jest — 5 suites, 78 tests, still green (JS untouched)
  • Not done: iOS build, Android build, NativeListStyleSlotTest under Gradle, any on-device check

Suggested review path

  1. NativeListModels.kt — the mapping table, against STYLE_SPEC.md §4
  2. NativeListStyleSlotTest.kt — especially the collision test
  3. resetRowStyle on both platforms — does it restore everything applyRowStyle can write?
  4. Build both apps, open the example Native List pages, confirm unstyled rows are pixel-identical

🤖 Generated with Claude Code

Both platforms gain applyRowStyle, running after the per-template binder so it is
the last writer. Android must run after applySize, which re-dispatches font size
and typeface by row type and would otherwise overwrite the style.

A style key names a model field, and the view pool is shared, so the same field
lands in a different view per template: metricCard draws its value through the
view identity uses for its title, and one status view carries rail.status,
activity.status, message.time and metricCard.trend. The mapping is
nativeListStyleSlot in NativeListModels.kt and styleSlot in NativeListCell.swift,
both following docs/STYLE_SPEC.md section 4. The Kotlin copy is unit-tested,
including a check that no template maps two style keys onto one view - a
collision would make one of them silently win.

Both platforms also gain resetRowStyle, running before the binder, because
neither reset path is complete (STYLE_SPEC section 7 rule 2). Android's
resetViews restores visibility, gravity, maxLines, layout params, background and
padding but not textSize, typeface or lineHeight. iOS's reset restores the fonts
of the shared labels but not those of the data, metric and media labels, nor any
text alignment. Without the reset a style would leak into the next row that
reuses the view. Both resets are guarded by a dirty flag, so an unstyled list
pays nothing.

On iOS the pass rebuilds the attributed string rather than assigning font and
textColor: the binders install an attributed string through setLineHeight, so a
plain font assignment would not take effect. The rebuilt line box centers font
metrics inside an explicit lineHeight, matching React Native and the Web engine.

Deliberately unchanged, and recorded in the spec's status table:

- Row heights. A styled row that grows still needs an explicit height.
- The list-wide source scale on Android. Making it per-row would move every
  selector list's metrics and needs device verification.
- dataRow columns in the table layout, whose text lives inside
  NativeListTableColumnView; the linear layout is covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"time" -> "status"
else -> null
}
"dataRow" -> when (field) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Data-row secondary styles are silently dropped on native]

When a dataRow provides the valid style.columnSecondary field, this mapping returns null, so Android ignores it; the new iOS mapping has the same omission. Web renders this slot, and table rows also use separate column-label views that the columns path never reaches.

Implement columnSecondary in the native linear and table data-row renderers (secondary spans/labels respectively) and cover both layouts with a parity test.

* one of those would therefore leak into the next row reusing the view, so put them
* back to the constructor baseline before the binder runs.
*/
private fun resetRowStyle() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Android style alignment leaks across recycled rows]

A styled row with alignment: "center" or "end" changes the shared label's gravity. On the next bind, resetRowStyle() restores only typeface and line spacing for labels such as subtitle and status; their gravity is not restored by resetViews() either. An unstyled row reusing that view can therefore keep the previous row's alignment after scrolling or a direct rebind.

Reset the baseline horizontal gravity for every styleable shared label and add a styled-to-unstyled reuse regression test.

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.

2 participants