Skip to content

SDK-5822: Flutter business logic — StyleResolver, VariableEvaluator, ColorParser, DimensionCalculator#39

Merged
CTLalit merged 22 commits into
feat/SDK-5821-flutter-modelsfrom
feat/SDK-5822-flutter-business-logic
May 28, 2026
Merged

SDK-5822: Flutter business logic — StyleResolver, VariableEvaluator, ColorParser, DimensionCalculator#39
CTLalit merged 22 commits into
feat/SDK-5821-flutter-modelsfrom
feat/SDK-5822-flutter-business-logic

Conversation

@CTLalit

@CTLalit CTLalit commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • StyleResolver: resolves all node styles once at construction time; handles inline > styleClass > theme priority, cascades text properties only, resolves named colors via theme
  • VariableEvaluator: evaluates {{variable}} templates with object.key paths, ternary expressions, and comparison operators
  • ColorParser: converts SDK RGBA #RRGGBBAA to Flutter ARGB 0xAARRGGBB with correct byte swap
  • DimensionCalculator: resolves Dimension values to logical pixels; returns null for wrap_content/match_parent

Test plan

  • flutter analyze — no issues
  • flutter test — all tests pass (78 total)
  • StyleResolver: cascading text props; visual props do not cascade; inline > class > theme priority
  • VariableEvaluator: variable substitution, object.key, ternary, comparisons
  • ColorParser: 6-char opaque, 8-char RGBA→ARGB swap, null handling
  • DimensionCalculator: percent, dp/sp/px, wrap_content/match_parent → null

🤖 Generated with Claude Code

CTLalit and others added 22 commits May 25, 2026 16:15
…ser, DimensionCalculator (Flutter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…utter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ERY) (Flutter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… opacity (Flutter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… support (Flutter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g (Flutter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ButtonElement: GestureDetector + StyleApplier wrapping Center(Text); fires actionListener on onClick action
- SpacerElement: match_parent → Spacer(); fixed dimensions → SizedBox
- DividerElement: horizontal/vertical SizedBox+ColoredBox; hides Flutter Orientation to resolve name conflict with ND Orientation enum
- NativeDisplayRenderer: dispatch BUTTON/SPACER/DIVIDER in _buildElement
- Tests: 9 tests covering all three elements

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uto-scroll (Flutter)

- GalleryRenderer converted from stub StatelessWidget to full StatefulWidget
- PageView.builder with viewportFraction from effectiveItemsPerView
- PageController + Timer for auto-scroll; both disposed in dispose()
- infiniteScroll: null itemCount + modulo index into children list
- Page spacing via symmetric Padding around each child
- Dot indicators rendered as Container rows/columns inside Stack when showIndicators is true
- Hides Flutter Orientation to resolve name conflict with ND Orientation enum
- container_test.dart: replaced stub test with 4 GalleryRenderer tests; upgraded wrap() with RootHeightScope + ResolvedStylesScope

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add video_player: ^2.9.2 dependency
- VideoElement: StatefulWidget initialising VideoPlayerController.networkUrl
- Reads autoPlay, loop, muted, showControls from node bindings (string values)
- Sets looping and volume after controller.initialize()
- Shows play/pause icon overlay when showControls is true; tap toggles playback
- Renders SizedBox.shrink when url is empty or controller not yet initialised
- NativeDisplayRenderer: dispatch ElementType.video to VideoElement
- Tests: 6 boolean-binding unit tests + 2 widget smoke tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add webview_flutter: ^4.9.0 dependency
- HtmlElement: StatefulWidget initialising WebViewController in initState()
- html binding loaded via loadHtmlString (with optional baseUrl); url binding via loadRequest
- html binding takes priority over url when both are present
- javascriptEnabled / transparentBackground read from HtmlConfig
- wrap_content height logs warning and falls back to 200dp; match_parent allows parent to constrain
- NativeDisplayRenderer: dispatch ElementType.html to HtmlElement; remove now-unreachable wildcard case (all 7 ElementType values are now handled)
- Tests: 6 configuration logic tests for bindings and HtmlConfig parsing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AnimationModifier: StatefulWidget with SingleTickerProviderStateMixin
- Resolves AnimationType to FadeTransition, SlideTransition, ScaleTransition combinations:
  fadeIn → Fade; slideIn* → Slide; scaleIn → Scale; fadeScaleIn → Fade+Scale; fadeSlideIn → Fade+Slide
- Slide begin offsets per direction: left=(-1,0), right=(1,0), top=(0,-1), bottom=(0,1)
- Easing resolved to Flutter Curves; spring → Curves.elasticOut
- Delay via Future.delayed before controller.forward()
- NativeDisplayRenderer: wraps every built node in AnimationModifier when node.animation is non-null and type != none
- Tests: 7 tests covering all major animation type combinations and completion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add url_launcher: ^6.3.0 dependency
- ActionHandler: const class with optional NativeDisplayActionListener
- OpenUrlAction: launches URL via launchUrl; openInBrowser=true uses externalApplication mode
- CustomAction: forwards key, value, metadata to listener as flat params map
- NavigateAction: forwards destination + params to listener
- TrackEventAction: forwards eventName + properties to listener
- CompositeAction: sequential iterates actions in order; parallel uses Future.wait
- Export ActionHandler from clevertap_native_display.dart public API
- Tests: 6 listener-dispatch tests (custom, navigate, event, composite, no-listener guards)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… plugin stubs (Flutter)

- NativeDisplayBridge.dart: static MethodChannel wrapper for com.clevertap.flutter.nativedisplay
  - fetchConfig(unitId): invokes fetchDisplayUnit, parses JSON into NativeDisplayConfig, returns null on PlatformException
  - pushViewedEvent(unitId): invokes pushViewedEvent
  - pushClickedEvent(unitId, elementId?): invokes pushClickedEvent; omits elementId key when null
- Android: CleverTapNativeDisplayPlugin.kt — FlutterPlugin + MethodCallHandler; NativeDisplayPluginBridge interface for host-app delegation
- iOS: CleverTapNativeDisplayPlugin.swift — FlutterPlugin; NativeDisplayPluginBridge protocol for host-app delegation
- Export NativeDisplayBridge from clevertap_native_display.dart public API
- Tests: 6 mock-MethodChannel tests covering fetchConfig (null, parse, PlatformException), pushViewedEvent, pushClickedEvent (with/without elementId)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SourceKit cannot resolve 'import Flutter' outside a full Xcode/Flutter
build context. The standard fix for Flutter plugin files is the
canImport guard. UIKit was unused and removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 4-tab bottom nav (Banners, Browser, Demos, More) mirroring Android/iOS apps
- BannerShowcaseScreen: scrollable list of 10 banner cards with detail navigation
- BannerDetailScreen: full-screen single banner display
- ArrangementDemoScreen: interactive chip strip switching all 7 strategies
- AnimationDemoScreen: 3 animation demos with info card, keyed for rebuild
- HomeScreenDemo: loads home_screen.json with action/component listeners
- OtherDemosScreen: tabbed view (Home, Gallery, E-commerce, Social, Dashboard)
- BridgeIntegrationScreen: loads mock product/notification configs, shows API snippet
- SlotDemoScreen: native display units interleaved with mock feed items
- TestBrowserScreen: prev/next navigation across 181 test-config filenames
- JsonViewerScreen: scrollable raw JSON with copy-to-clipboard
- MoreMenuScreen: list navigation to all demo screens
- Assets: 10 banner JSONs + 12 config JSONs copied from android-sample
- flutter analyze: no issues

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Flutter)

Android (flutter-sample/android/):
- SampleApplication: init CleverTap, bind NativeDisplayBridge, register listener
- MainActivity: EventChannel (native→Dart units_updated), MethodChannel (pushEvent)
- AndroidManifest: CT account ID/token/region meta-data, INTERNET permission
- build.gradle.kts: CT core SDK 8.0.0 + native-display-sdk dependencies

iOS (flutter-sample/ios/):
- AppDelegate: CleverTap.autoIntegrate(), NativeDisplayBridge bind + fetch
- Podfile: CleverTap-iOS-SDK ~>7.0, CleverTapNativeDisplay :path

Dart (flutter-sample/lib/):
- CleverTapIntegrationScreen: subscribe NativeDisplayBridge.eventStream,
  parse units_updated events, render via NativeDisplayView, Send Event → pushEvent
- app.dart: bottom nav Events/Slots/Browser/More matching Android structure
- SlotDemoScreen: 15 feed items + 4 dashed-border slot placeholders
- Lint fixes: prefer_const_constructors / prefer_const_literals

flutter/ plugin:
- android/build.gradle + AndroidManifest.xml: plugin Android library scaffold
- NativeDisplayBridge: pushEvent (sample MethodChannel) + eventStream (EventChannel)
- gallery_renderer.dart: LayoutBuilder fallback height for unbounded PageView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…op composite build

Problem: flutter-sample referenced clevertap-native-ui-kit (compiled Kotlin 2.1.0)
via includeBuild, forcing Kotlin 2.1.0 + AGP 8.9.1 + Gradle 8.11.1 on the sample.

Fix: remove includeBuild and the native-display-sdk dependency entirely. Subscribe
to display unit callbacks via CleverTapAPI.setDisplayUnitListener(DisplayUnitListener)
from clevertap-android-sdk:8.0.0 (Java interface — no Kotlin binary compat issue).
Forward CleverTapDisplayUnit.getJsonObject().toString() directly to Flutter via
EventChannel. Restores original Flutter scaffold versions: Kotlin 1.8.22, AGP 8.7.0,
Gradle 8.10.2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…play

Two bugs prevented display units from rendering in CleverTapIntegrationScreen:

1. Wrong JSON format: CleverTapDisplayUnit.getJsonObject() returns raw CT JSON
   (type:advanced-builder, slot_id, wzrk_id, etc.) not NativeDisplayConfig JSON.
   Flutter's NativeDisplayConfig.fromJson() cannot parse this format.
   Fix: extract NativeDisplayConfig from the raw CT JSON using the same 3-strategy
   detection as NativeDisplayConfigParser:
   - native_display_config top-level key (dashboard format)
   - custom_kv.nd_config string value
   - root top-level key (direct NativeDisplayConfig JSON)

2. Timing race: display units can arrive from CT server before Flutter subscribes
   to the EventChannel, causing them to be silently dropped (eventSink is null).
   Fix: cache the last received unit JSONs in SampleApplication.cachedUnitJsons
   and replay them immediately in EventChannel.StreamHandler.onListen().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utter sample platform targets

## Flutter SDK — renderer fixes

- native_display_view: aspectRatio now takes precedence over width.percent — when AR is
  set on a node the renderer uses full available parent width and derives height =
  parentWidth / aspectRatio, matching Android (Compose modifier ordering) and iOS (guard
  check in resolveRootWidth). _effectiveWidth returns availableWidth when AR > 0;
  _applyRootSizing short-circuits immediately when AR is present.
- native_display_view: fix _computeRootHeight for percent height roots — previously fell
  through to screenHeight, inflating percent-based fonts by up to 67 %.
- native_display_renderer: remove debugLabel field and all _wrapWithSizing debugPrint calls.
- box_container: remove _buildChild debugPrint.
- vertical_container / horizontal_container: promote from v1 stubs to full arrangement
  implementations — all ArrangementStrategy values (spaced, space_between, space_evenly,
  space_around, start, center, end) now produce correct Column/Row with spacers or
  MainAxisAlignment.
- button_element: propagate maxLines, overflow, and softWrap from resolved style.
- style_applier: fix border resolution — require width > 0 before rendering; skip zero-
  width borders rather than rendering a phantom 1 dp default.

## Flutter sample app

- clevertap_integration_screen: remove debug red border, outer LayoutBuilder, and
  [SAMPLE] log prints; clean up raw JSON logging from onUnitsLoaded.
- test_browser_screen: add test-parity-80pct-ar.json to browser list.
- Add 180+ test-config JSON assets for the in-app test browser.
- Add multi-platform scaffolding (linux, macos, web, windows).
- pubspec: add clevertap_plugin dependency; update flutter-sdk path reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…knowledge bases

Codifies the cross-platform rule discovered while debugging the Flutter renderer:
when aspectRatio is set on a node, it takes full available parent width and derives
height = parentWidth / aspectRatio. Any width.percent (or height.percent) value is
ignored. The rule is identical on all three platforms:

- Android: Compose applies aspectRatio() modifier before fillMaxWidth(fraction); the
  AR modifier locks minW=parentWidth, making fillMaxWidth ineffective.
- iOS: resolveRootWidth() guard returns parentWidth when aspectRatio > 0, regardless
  of percent. Documented in NativeDisplayRenderer.swift line 244.
- Flutter: _effectiveWidth returns availableWidth when layout.aspectRatio > 0;
  _applyRootSizing short-circuits when AR is set.

aspectRatio is skipped only when BOTH width AND height are simultaneously fixed (dp/sp/px).
Percent dimensions are never treated as "fixed" for this check.

Files updated:
- CLAUDE.md — layout system section
- .claude/reference/CLAUDE_CODE_REFERENCE_ACTUAL.md — layout model + priority table
- .claude/reference/JSON_STRUCTURE_REFERENCE.md — How it works + Priority Rules sections
- docs/JSON_STRUCTURE_REFERENCE.md — same (kept in sync)
- docs/BACKEND_PAYLOAD_SPEC.md — per-platform mechanism table
- .claude/agents/android-sdk/knowledge/rendering-pipeline.md — Stage 4 precedence rule
- .claude/agents/android-sdk/knowledge/gotchas.md — new gotcha entry
- .claude/agents/ios-sdk/knowledge/architecture.md — Swift code evidence
- .claude/agents/flutter-sdk/knowledge/rendering-pipeline.md — root sizing implementation
- .claude/agents/flutter-sdk/knowledge/architecture.md — NativeDisplayView description
- .claude/agents/testing/knowledge/json-generation-rules.md — Section 6 Aspect Ratio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add NativeDisplayUnit model (unitId, slotId, config, customExtras, resolvedStyles)
- Add NativeDisplayConfigParser with tryParse/parseAll running off main thread
  via Isolate.run(); falls back to synchronous on Dart < 3.4
- Mirrors 3-strategy extraction from Android NativeDisplayConfigParser.kt:
  native_display_config key → custom_kv.nd_config string → root key
- NativeDisplayView accepts optional pre-resolved resolvedStyles to skip
  redundant StyleResolver calls when using NativeDisplayConfigParser
- Export both new types from the public barrel
- Simplify clevertap_integration_screen.dart: remove _deepCast, _extractEntry,
  _UnitEntry; replace with NativeDisplayConfigParser.parseAll() (4 lines)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NativeDisplayView: StatelessWidget → StatefulWidget; StyleResolver.resolveAll()
  now runs once in initState/didUpdateWidget, not on every parent rebuild;
  added fixed-dimension shortcut that bypasses LayoutBuilder when root has
  only dp dimensions (no percent, no aspectRatio)
- style_applier.dart: for solid backgrounds, bake opacity into Color.withValues()
  instead of wrapping the whole subtree in Opacity widget (avoids saveLayer)
- image_element.dart: replace Image.network() with CachedNetworkImage for static
  images (GIFs keep Image.network for animation fidelity); wrap in RepaintBoundary
- video_element.dart: wrap in RepaintBoundary to isolate frame repaints
- gallery_renderer.dart: wrap each page item in RepaintBoundary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CTLalit CTLalit merged commit 507575c into feat/SDK-5821-flutter-models May 28, 2026
1 check passed
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 575247f3-7ed5-40ef-bf22-6e66a677c02d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/SDK-5822-flutter-business-logic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CTLalit

CTLalit commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of the consolidated Flutter PR #38, which contains all Flutter SDK code in one place for easier review. Please review and provide feedback on PR #38 instead.

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