Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,11 @@ lib/
│ ├── location_service.dart # Native location (LocationBridge) with fallback
│ ├── widget_service.dart # Triggers native widget refresh + resize flag
│ ├── widget_store.dart # Method-channel KV bridge to HomeWidgetPreferences (replaces home_widget)
│ └── native_svg_service.dart # Method channel to native (weather fetch, SVG gen, cache)
│ └── native_svg_service.dart # Method channel to native (weather fetch, SVG gen + rasterize-to-PNG, cache)
├── theme/
│ └── app_theme.dart # MeteogramColors, WeatherGradients
├── widgets/
│ └── native_svg_chart_view.dart # Native SVG PlatformView
└── screens/
└── home_screen.dart # Main UI with SVG chart
└── home_screen.dart # Main UI; chart via Image.memory over native PNG

android/app/src/main/
├── kotlin/.../
Expand All @@ -73,9 +71,7 @@ android/app/src/main/
│ ├── WeatherFetcher.kt # Native HTTP client for Open-Meteo API
│ ├── WeatherDataParser.kt # Parse cached weather JSON
│ ├── SvgChartGenerator.kt # Native SVG generation (single source)
│ ├── MaterialYouColorExtractor.kt # Native Material You color extraction
│ ├── SvgChartPlatformView.kt # Native SVG rendering for in-app
│ └── SvgChartViewFactory.kt # PlatformView factory
│ └── MaterialYouColorExtractor.kt # Native Material You color extraction
└── res/
├── layout/meteogram_widget.xml # RemoteViews layout
├── xml/meteogram_widget_info.xml # Widget config
Expand Down Expand Up @@ -131,7 +127,7 @@ Android widgets use RemoteViews which only support:
**NOT supported:** View, Space, custom views, most Material widgets

### Data Flow
1. **In-app**: `home_screen.dart` gets location → calls `NativeSvgService.fetchWeather()` → Kotlin fetches from Open-Meteo → caches to SharedPreferences → Dart reads cache → Kotlin generates SVG → rendered via `NativeSvgChartView`
1. **In-app**: `home_screen.dart` gets location → calls `NativeSvgService.fetchWeather()` → Kotlin fetches from Open-Meteo → caches to SharedPreferences → Dart reads cache → Kotlin generates SVG → Kotlin rasterizes SVG to PNG (`renderSvgToPng`) → Dart displays bytes with `Image.memory` (no PlatformView — avoids the Impeller Vulkan external-texture crash)
2. **Widget**: Native code reads cached weather from SharedPreferences → `SvgChartGenerator.kt` generates SVG → AndroidSVG renders to bitmap → ImageView

### Background Refresh (fully native)
Expand Down
4 changes: 0 additions & 4 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
-keep class org.bortnik.meteogram.WeatherUpdateWorker { *; }
-keep class org.bortnik.meteogram.MeteogramApplication { *; }

# Keep platform views
-keep class org.bortnik.meteogram.SvgChartViewFactory { *; }
-keep class org.bortnik.meteogram.SvgChartPlatformView { *; }

# Kotlin serialization (if used)
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
Expand Down
23 changes: 11 additions & 12 deletions android/app/src/main/kotlin/org/bortnik/meteogram/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ class MainActivity : FlutterActivity() {

super.configureFlutterEngine(flutterEngine)

// Register PlatformView for native SVG chart rendering
flutterEngine.platformViewsController.registry.registerViewFactory(
"svg_chart_view",
SvgChartViewFactory(flutterEngine.dartExecutor.binaryMessenger)
)

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"generateSvg" -> {
Expand Down Expand Up @@ -76,12 +70,17 @@ class MainActivity : FlutterActivity() {
return@setMethodCallHandler
}

try {
val pngBytes = renderSvgToPng(svgString, width, height)
result.success(pngBytes)
} catch (e: Exception) {
result.error("RENDER_ERROR", e.message, null)
}
// Rasterize off the platform thread — PNG encode of a
// full-resolution chart bitmap is too heavy for the UI thread.
Thread {
try {
val pngBytes = renderSvgToPng(svgString, width, height)
runOnUiThread { result.success(pngBytes) }
} catch (e: Exception) {
Log.e(TAG, "Error rasterizing SVG", e)
runOnUiThread { result.error("RENDER_ERROR", e.message, null) }
}
}.start()
}
"fetchWeather" -> {
val latitude = call.argument<Double>("latitude")
Expand Down

This file was deleted.

This file was deleted.

43 changes: 22 additions & 21 deletions docs/ai/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The app follows a standard Flutter architecture with clear separation of concern
2. Weather service fetches data from Open-Meteo
3. Data is parsed into `WeatherData` model
4. SVG chart generated via `SvgChartGenerator`
5. In-app: SVG rendered via PlatformView (AndroidView → native ImageView)
5. In-app: SVG rasterized natively to PNG (`MainActivity.renderSvgToPng`), displayed with a plain Flutter `Image.memory` (no PlatformView)
6. Widget: SVG saved to file, native provider renders via AndroidSVG

### Widget Updates
Expand Down Expand Up @@ -93,11 +93,16 @@ Native Kotlin SVG generation - single source of truth for both widget and in-app

**Key benefit:** Works in background without Dart/Flutter engine

### NativeSvgChartView (`lib/widgets/native_svg_chart_view.dart`)
PlatformView wrapper for in-app SVG display. Responsibilities:
- Embed native Android ImageView via AndroidView
- Pass SVG string to native side via MethodChannel
- Bypass Flutter's image compositor for 1:1 pixel rendering
### In-app chart display (`lib/screens/home_screen.dart`)
The chart is a plain Flutter `Image.memory`, NOT a PlatformView. Flow:
- `NativeSvgService.renderSvgToPng()` sends the generated SVG + target pixel
size to native; `MainActivity.renderSvgToPng` rasterizes it (AndroidSVG →
Bitmap → PNG) off the platform thread and returns the bytes.
- `_buildChart` caches the bytes and renders them with `Image.memory`
(`gaplessPlayback`, stable instance so the bitmap isn't re-decoded).
- This keeps the chart out of Impeller's external-texture path (the old
`AndroidView` PlatformView composited as a `TextureLayer`, which fatally
aborted on some Vulkan devices — see `widget.md`).

### WeatherFetcher (`android/.../WeatherFetcher.kt`)
Native HTTP client for Open-Meteo API. Responsibilities:
Expand Down Expand Up @@ -173,18 +178,16 @@ lib/
│ ├── app_ar.arb # Arabic
│ └── ... # 30+ locales
├── screens/
│ └── home_screen.dart # Main screen with NativeSvgChartView
│ └── home_screen.dart # Main screen; chart via Image.memory over native PNG
├── services/
│ ├── location_service.dart # GPS/fallback location
│ ├── widget_service.dart # Home widget integration
│ └── native_svg_service.dart # Method channel to native
├── theme/
│ └── app_theme.dart # Colors, light/dark themes
└── widgets/
└── native_svg_chart_view.dart # PlatformView SVG display
└── theme/
└── app_theme.dart # Colors, light/dark themes

android/app/src/main/kotlin/.../
├── MainActivity.kt # PlatformView factory, Material You colors
├── MainActivity.kt # SVG generate + rasterize-to-PNG channel, Material You colors
├── MeteogramApplication.kt # Registers receivers, schedules alarm
├── MeteogramWidgetProvider.kt # Home screen widget provider
├── WidgetEventReceiver.kt # Handles locale/timezone changes
Expand All @@ -196,9 +199,7 @@ android/app/src/main/kotlin/.../
├── WeatherFetcher.kt # Native HTTP client for Open-Meteo
├── WeatherDataParser.kt # Parse cached weather JSON
├── SvgChartGenerator.kt # Native SVG generation
├── MaterialYouColorExtractor.kt # Native Material You color extraction
├── SvgChartViewFactory.kt # Creates PlatformView instances
└── SvgChartPlatformView.kt # Native ImageView + AndroidSVG rendering
└── MaterialYouColorExtractor.kt # Native Material You color extraction

scripts/
└── generate_version.sh # Generates version.dart from git
Expand All @@ -213,12 +214,12 @@ scripts/
- Background in `res/drawable/widget_background.xml` (gradient + rounded corners)
- Chart: reads SVG file → AndroidSVG → Bitmap → ImageView

### Android In-App (PlatformView)
- `SvgChartViewFactory` registered in MainActivity
- `SvgChartPlatformView` embeds native ImageView
- Receives SVG string via MethodChannel
- Renders via AndroidSVG → Bitmap → ImageView
- Bypasses Flutter's image compositor for 1:1 pixel rendering
### Android In-App (render-to-image)
- `MainActivity` exposes a `renderSvg` method channel
- Receives SVG string + pixel size, rasterizes via AndroidSVG → Bitmap → PNG
- Returns PNG bytes to Dart, which displays them with `Image.memory`
- No PlatformView / `TextureLayer`, so it avoids Impeller's external-texture
crash path on Vulkan devices (the reason this replaced the old `AndroidView`)

### iOS Widget
Not yet implemented. Would require:
Expand Down
21 changes: 15 additions & 6 deletions docs/ai/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,20 @@ await WidgetStore.updateWidget(androidName: 'MeteogramWidgetProvider');
`WidgetService.triggerWidgetUpdate()` refreshes **both** providers
(`MeteogramWidgetProvider` and `MeteogramWeeklyWidgetProvider`).

### In-app chart display (`lib/widgets/native_svg_chart_view.dart`)
### In-app chart display (`lib/screens/home_screen.dart`)

A PlatformView (`AndroidView`, viewType `svg_chart_view`) embeds a native
Android view that renders the SVG via AndroidSVG — bypassing Flutter's
compositor for 1:1 pixel rendering. The factory is registered in
`MainActivity` (`SvgChartViewFactory` → `SvgChartPlatformView`).
The chart is a plain Flutter `Image.memory`, **not** a PlatformView.
`NativeSvgService.renderSvgToPng()` sends the generated SVG + target pixel size
to `MainActivity`'s `renderSvg` channel, which rasterizes it (AndroidSVG →
Bitmap → PNG) off the platform thread; `_buildChart` displays the returned bytes
with `gaplessPlayback`.

This replaced an `AndroidView` PlatformView (viewType `svg_chart_view`). That
view was composited by Impeller as a `TextureLayer` / external texture, whose
`Image.getHardwareBuffer()` JNI call fatally aborts on some Vulkan devices
(Adreno / Android 12 — flutter/flutter#175267). Rendering bytes in pure Flutter
removes the only `TextureLayer` in the app and the entire crash path, with
identical AndroidSVG rasterization (same as the widget).

## Background Refresh (fully native)

Expand Down Expand Up @@ -230,7 +238,8 @@ the app process is alive.
`NativeSvgService.fetchWeather(lat, lon)` → Kotlin `WeatherFetcher` hits
Open-Meteo and caches the JSON to SharedPreferences.
2. **In-app chart**: Dart calls `generateSvg` → Kotlin reads the cache and
returns an SVG string → `NativeSvgChartView` renders it.
returns an SVG string → Dart calls `renderSvg` → Kotlin rasterizes it to PNG
bytes → `home_screen.dart` shows them with `Image.memory`.
3. **Widget chart**: native `onUpdate` reads the cache, generates light+dark
SVGs with `SvgChartGenerator`, rasterises via AndroidSVG → `Bitmap` →
`ImageView`.
Expand Down
7 changes: 4 additions & 3 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ Test a different build with `APP_PATH=/abs/path/to.apk npm test`.

- **Driver pin:** `uiautomator2@4.2.9` is the last driver compatible with Appium
2.x (5.x+ require Appium 3). Bump both together.
- **Charts** are hybrid-composition PlatformViews with no `resource-id`; they
expose a native `content-desc` (set in `SvgChartPlatformView.kt`) and are
located by accessibility-id.
- **Charts** are plain Flutter `Image` widgets (PNG rasterized natively), so a
normal `Semantics` reaches them: they carry both a `resource-id`
(`homeHourlyChart` / `homeWeeklyChart`) and a localized `content-desc` label
(`descriptionContains("48-hour")` / `"7-day"`). Locate by either.
- Flutter text surfaces as `content-desc`, not the `text` attribute — locate by
`resourceId` or `description*`, never `.text()`.
- CI: `.github/workflows/e2e.yml` (PR + manual) builds the x86_64 APK then runs
Expand Down
6 changes: 4 additions & 2 deletions e2e/a11y_ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ module.exports = {
homeLocationSelector: 'home_location_selector',
homeOpenMeteoLink: 'home_open_meteo_link',
homeGithubLink: 'home_github_link',
// Charts are hybrid-composition PlatformViews with no resource-id; they carry
// a native content-desc instead (locate via accessibility-id if needed).
// Charts are plain Flutter Image widgets, so a normal Semantics reaches them:
// they carry both a resource-id (below) and a localized content-desc label.
homeHourlyChart: 'home_hourly_chart',
homeWeeklyChart: 'home_weekly_chart',

// Location picker sheet
locationSearchField: 'location_search_field',
Expand Down
Loading
Loading