feat(native-list): apply the row style on iOS and Android - #110
huhuanming wants to merge 1 commit into
Conversation
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) { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
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
applyRowStyleon iOS and Android, followingdocs/STYLE_SPEC.md§8. It runs after the per-template binder so it is the last writer — on Android specifically afterapplySize, 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.
metricCarddraws its value through the viewidentityuses for its title; one status view carriesrail.status,activity.status,message.time, andmetricCard.trend.That table is
nativeListStyleSlotinNativeListModels.ktandstyleSlotinNativeListCell.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:
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:resetViews()restores visibility, gravity, maxLines, layout params, background and padding — nottextSize,typefaceorlineHeightreset()restores the shared label fonts — not the data, metric and media label fonts, nor any text alignmentWithout
resetRowStylea 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 assigninglabel.fontorlabel.textColorafterwards has no visible effect. The style pass therefore rebuilds the line box, centering font metrics inside an explicitlineHeightthe same way React Native and the Web engine do.Deliberately unchanged
Recorded in the spec's status table rather than guessed at:
height.usesSelectorSourceScaleis computed withitems.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.dataRowcolumns in thetablelayout, whose text lives insideNativeListTableColumnView/tableDataColumns. Thelinearlayout is covered.Verification
swiftc -parse ios/NativeListCell.swift— cleanjest— 5 suites, 78 tests, still green (JS untouched)NativeListStyleSlotTestunder Gradle, any on-device checkSuggested review path
NativeListModels.kt— the mapping table, againstSTYLE_SPEC.md§4NativeListStyleSlotTest.kt— especially the collision testresetRowStyleon both platforms — does it restore everythingapplyRowStylecan write?🤖 Generated with Claude Code