Skip to content

fix(chart): render in-app meteogram as image, not a PlatformView#41

Merged
timbortnik merged 2 commits into
mainfrom
fix/in-app-chart-drop-platformview
Jun 30, 2026
Merged

fix(chart): render in-app meteogram as image, not a PlatformView#41
timbortnik merged 2 commits into
mainfrom
fix/in-app-chart-drop-platformview

Conversation

@timbortnik

@timbortnik timbortnik commented Jun 30, 2026

Copy link
Copy Markdown
Owner

The in-app chart was an AndroidView PlatformView. On Flutter 3.4x that composites via Texture Layer Hybrid Composition: the native ImageView's surface is rendered into an ImageReader and handed to Impeller as an external texture. On the Vulkan backend (e.g. Adreno / Android 12), the raster thread's Image.getHardwareBuffer() JNI call can throw on a closed/expired image, and the engine turns any pending JNI exception into a fatal abort (platform_view_android_jni_impl.cc CheckException) -- flutter/flutter#175267. Crash seen on v1.2.3 (Flutter 3.44.1); still latent on main (3.44.4), as the engine fix does not cover this case.

The chart is a static, non-interactive bitmap, so the PlatformView is overkill. Rasterize the SVG to PNG natively (reuse the already-present MainActivity.renderSvgToPng, previously dead code) and display it with a plain Flutter Image.memory. This removes the only TextureLayer in the app, eliminating the Impeller external-texture path entirely. Rendering is unchanged (same AndroidSVG rasterizer as the widget).

  • Drop NativeSvgChartView, SvgChartPlatformView, SvgChartViewFactory and the registerViewFactory call.
  • Add NativeSvgService.renderSvgToPng; cache PNG bytes (stable instance so MemoryImage hits the image cache; gaplessPlayback avoids flicker).
  • Move the renderSvg handler off the platform thread.
  • Real Flutter Semantics now reach the charts: they carry resource-ids (home_hourly_chart / home_weekly_chart) and a localized content-desc, replacing the native content-desc hack. e2e/docs updated to match.

Verified: make analyze clean; 123 Dart tests; Kotlin tests + debug build (JDK 17); runs on emulator with both charts rendering and the a11y ids

  • labels surfacing. Field confirmation needs a Vulkan device (x86_64 emulator cannot reproduce it).

Summary by CodeRabbit

  • New Features

    • Chart rendering now uses Flutter images with accessible labels and stable identifiers, improving how charts are displayed and found in the app and tests.
    • Added support for converting chart graphics into PNG bytes for faster, more reliable rendering.
  • Bug Fixes

    • Improved chart generation so it runs off the main thread and returns results safely.
    • Updated automated tests to work with the new chart image format.

The in-app chart was an AndroidView PlatformView. On Flutter 3.4x that
composites via Texture Layer Hybrid Composition: the native ImageView's
surface is rendered into an ImageReader and handed to Impeller as an
external texture. On the Vulkan backend (e.g. Adreno / Android 12), the
raster thread's Image.getHardwareBuffer() JNI call can throw on a
closed/expired image, and the engine turns any pending JNI exception
into a fatal abort (platform_view_android_jni_impl.cc CheckException) --
flutter/flutter#175267. Crash seen on v1.2.3 (Flutter 3.44.1); still
latent on main (3.44.4), as the engine fix does not cover this case.

The chart is a static, non-interactive bitmap, so the PlatformView is
overkill. Rasterize the SVG to PNG natively (reuse the already-present
MainActivity.renderSvgToPng, previously dead code) and display it with a
plain Flutter Image.memory. This removes the only TextureLayer in the
app, eliminating the Impeller external-texture path entirely. Rendering
is unchanged (same AndroidSVG rasterizer as the widget).

- Drop NativeSvgChartView, SvgChartPlatformView, SvgChartViewFactory and
  the registerViewFactory call.
- Add NativeSvgService.renderSvgToPng; cache PNG bytes (stable instance
  so MemoryImage hits the image cache; gaplessPlayback avoids flicker).
- Move the renderSvg handler off the platform thread.
- Real Flutter Semantics now reach the charts: they carry resource-ids
  (home_hourly_chart / home_weekly_chart) and a localized content-desc,
  replacing the native content-desc hack. e2e/docs updated to match.

Verified: make analyze clean; 123 Dart tests; Kotlin tests + debug build
(JDK 17); runs on emulator with both charts rendering and the a11y ids
+ labels surfacing. Field confirmation needs a Vulkan device (x86_64
emulator cannot reproduce it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@timbortnik, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ce4eccdb-214b-434a-8990-8ec0783f691d

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4bb2d and 1ab0fcb.

📒 Files selected for processing (7)
  • CLAUDE.md
  • android/app/proguard-rules.pro
  • docs/ai/architecture.md
  • docs/ai/widget.md
  • e2e/specs/home_happy_path.e2e.js
  • lib/screens/home_screen.dart
  • test/home_screen_test.dart
📝 Walkthrough

Walkthrough

This PR replaces the Android PlatformView-based SVG chart rendering with native PNG rasterization. SvgChartPlatformView and SvgChartViewFactory are removed; MainActivity dispatches renderSvg on a background thread. HomeScreen now caches and renders PNG bytes via Image.memory. Accessibility identifiers and e2e documentation are updated accordingly.

Changes

SVG-to-PNG rasterization migration

Layer / File(s) Summary
Android native rasterization and threading
android/app/.../MainActivity.kt, android/app/.../SvgChartPlatformView.kt, android/app/.../SvgChartViewFactory.kt
Removes the PlatformView factory registration and the SvgChartPlatformView/SvgChartViewFactory classes; renderSvg rasterization is moved to a background thread with results posted via runOnUiThread.
Dart rasterization service API
lib/services/native_svg_service.dart, test/native_svg_service_test.dart
Adds renderSvgToPng to call the native renderSvg method and return PNG bytes or null on error, with unit tests covering success, argument mapping, and PlatformException handling.
HomeScreen PNG caching and rendering
lib/screens/home_screen.dart, lib/widgets/native_svg_chart_view.dart, test/home_screen_test.dart
Replaces cached SVG strings with cached PNG bytes (_ChartCacheEntry.png), reworks async chart generation to rasterize via renderSvgToPng, renders Image.memory wrapped in Semantics instead of NativeSvgChartView, removes the now-unused widget file, and updates widget tests with a transparent-pixel PNG fixture.
Accessibility identifiers and e2e/docs updates
lib/a11y_ids.dart, e2e/a11y_ids.js, e2e/README.md, e2e/specs/home_happy_path.e2e.js
Adds homeHourlyChart/homeWeeklyChart accessibility IDs and updates e2e documentation/comments to reflect that charts now expose both resource-id and content-desc.

Sequence Diagram(s)

sequenceDiagram
  participant HomeScreen
  participant NativeSvgService
  participant MainActivity
  participant BackgroundThread

  HomeScreen->>HomeScreen: generate SVG string
  HomeScreen->>NativeSvgService: renderSvgToPng(svg, width, height)
  NativeSvgService->>MainActivity: invoke renderSvg via MethodChannel
  MainActivity->>BackgroundThread: start Thread for rasterization
  BackgroundThread-->>MainActivity: PNG bytes or error
  MainActivity-->>NativeSvgService: result via runOnUiThread
  NativeSvgService-->>HomeScreen: Uint8List png or null
  HomeScreen->>HomeScreen: cache png and render Image.memory
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • timbortnik/widget#18: Both PRs modify MainActivity.configureFlutterEngine's MethodChannel handling on the same channel, overlapping at the renderSvg/method dispatch code.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: switching the meteogram from a PlatformView to an in-app image rendering path.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/in-app-chart-drop-platformview

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
e2e/specs/home_happy_path.e2e.js (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the new chart resource IDs in the assertions below.

These checks are still keyed off localized content-desc text, so they remain brittle to emulator locale changes even though this PR introduces stable homeHourlyChart / homeWeeklyChart ids. Switching them to byId(ids.homeHourlyChart) and byId(ids.homeWeeklyChart) would make the spec exercise the new contract directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/specs/home_happy_path.e2e.js` around lines 20 - 21, The home happy path
spec is still asserting the meteogram charts via localized content-desc text
instead of the new stable resource IDs. Update the assertions in the
home_happy_path.e2e.js flow to use the chart ids exposed by ids.homeHourlyChart
and ids.homeWeeklyChart, keeping the checks in the same test steps but switching
the selectors to byId so the spec validates the new contract directly.
test/home_screen_test.dart (1)

76-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add one assertion that the PNG charts actually render.

This mock now makes Image.memory succeed, but none of the tests shown here prove that the hourly/weekly chart widgets or their new semantics actually appear. A regression back to the placeholder SizedBox would still pass. As per path instructions, "Ensure tests assert real behavior; WidgetStore and SharedPreferences mocks should follow the existing patterns in test/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/home_screen_test.dart` around lines 76 - 79, The current test setup only
makes Image.memory decode successfully, but it does not verify that the PNG
chart widgets are actually rendered instead of falling back to a placeholder.
Update the home screen widget test to add an assertion that exercises the
hourly/weekly chart path and checks for the real chart widget or its semantics
using the existing test patterns in home_screen_test.dart, so a regression to
SizedBox would fail. Use the relevant widget entry points around the
hourly/weekly chart rendering and the existing mock setup to confirm actual
behavior rather than only mocking renderSvg.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/screens/home_screen.dart`:
- Around line 444-468: The async chart update path in the chart rendering method
should be guarded so stale renders cannot overwrite newer ones and `setState`
cannot run after dispose. Add a per-request generation/token check around the
full render flow in the chart commit logic, including the extra
`NativeSvgService.getCurrentTemperatureCelsius()` await, and verify both the
token and `mounted` immediately before the `setState` that updates `_chartCache`
and `_currentTemperatureCelsius`. Use the existing chart-rendering method and
`_chartCache` update block as the place to thread and validate the token.
- Around line 88-91: The chart cache invalidation logic in
_invalidateChartCaches is clearing entry.png too early, causing _buildChart to
render an empty fallback before the replacement frame is ready. Update the cache
model so stale/in-flight state is tracked separately from the existing bytes,
and keep the previous image data intact until native rasterization completes and
can atomically replace it. Make the change around _chartCache,
_invalidateChartCaches, and the chart-building path so weather, half-hour, and
theme refreshes remain gapless.

---

Nitpick comments:
In `@e2e/specs/home_happy_path.e2e.js`:
- Around line 20-21: The home happy path spec is still asserting the meteogram
charts via localized content-desc text instead of the new stable resource IDs.
Update the assertions in the home_happy_path.e2e.js flow to use the chart ids
exposed by ids.homeHourlyChart and ids.homeWeeklyChart, keeping the checks in
the same test steps but switching the selectors to byId so the spec validates
the new contract directly.

In `@test/home_screen_test.dart`:
- Around line 76-79: The current test setup only makes Image.memory decode
successfully, but it does not verify that the PNG chart widgets are actually
rendered instead of falling back to a placeholder. Update the home screen widget
test to add an assertion that exercises the hourly/weekly chart path and checks
for the real chart widget or its semantics using the existing test patterns in
home_screen_test.dart, so a regression to SizedBox would fail. Use the relevant
widget entry points around the hourly/weekly chart rendering and the existing
mock setup to confirm actual behavior rather than only mocking renderSvg.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: adc0fa52-be98-4dc5-93f2-60d1d878f65a

📥 Commits

Reviewing files that changed from the base of the PR and between fb6afc7 and 4e4bb2d.

📒 Files selected for processing (12)
  • android/app/src/main/kotlin/org/bortnik/meteogram/MainActivity.kt
  • android/app/src/main/kotlin/org/bortnik/meteogram/SvgChartPlatformView.kt
  • android/app/src/main/kotlin/org/bortnik/meteogram/SvgChartViewFactory.kt
  • e2e/README.md
  • e2e/a11y_ids.js
  • e2e/specs/home_happy_path.e2e.js
  • lib/a11y_ids.dart
  • lib/screens/home_screen.dart
  • lib/services/native_svg_service.dart
  • lib/widgets/native_svg_chart_view.dart
  • test/home_screen_test.dart
  • test/native_svg_service_test.dart
💤 Files with no reviewable changes (3)
  • android/app/src/main/kotlin/org/bortnik/meteogram/SvgChartViewFactory.kt
  • android/app/src/main/kotlin/org/bortnik/meteogram/SvgChartPlatformView.kt
  • lib/widgets/native_svg_chart_view.dart

Comment thread lib/screens/home_screen.dart
Comment thread lib/screens/home_screen.dart Outdated
Address PR review on the render-to-image change:

- Gapless refresh: _invalidateChartCaches now marks entries stale instead
  of nulling the PNG, so the current chart stays on screen until its
  replacement is rasterized. Previously it fell back to a blank SizedBox
  on weather/half-hour/theme refreshes, defeating gaplessPlayback.
- Async safety: _generateSvgAsync is single-flight (generating guard),
  claims the dirty flag up front so mid-render updates re-kick, and
  re-checks mounted after the final await before setState. A slow older
  render can no longer overwrite a newer one or setState after dispose.
- e2e: home happy-path locates charts by the new resource-ids
  (homeHourlyChart / homeWeeklyChart) instead of localized content-desc.
- test: assert both charts render as Images with their semantics ids;
  a regression to the placeholder SizedBox would now fail.
- docs: update CLAUDE.md + docs/ai/{architecture,widget}.md and drop the
  stale ProGuard keep-rules for the deleted PlatformView classes.

make analyze clean; 124 Dart tests; Kotlin tests + debug build (JDK 17).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timbortnik timbortnik merged commit 9af09fb into main Jun 30, 2026
6 checks passed
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