Skip to content

✨ Feature: Widget - #27

Merged
ngapp-dev merged 5 commits into
masterfrom
feature/widget
Oct 2, 2026
Merged

ngapp-dev merged 5 commits into
masterfrom
feature/widget

Conversation

@ngapp-dev

Copy link
Copy Markdown
Owner

Summary

feature/widget adds the first home-screen widget, "Price and nearest station", on Android (Jetpack Glance) and iOS (WidgetKit). It has 5 commits on top of master and touches 77 files (+5921 / −12
lines; 64 added, 13 modified). Main areas:

📊 Data layer

  • WidgetData (core:model), WidgetDataRepository (core:domain) and CompositeWidgetDataRepository (core:data) build the widget data from the existing station, price and location repositories.
  • Widgets only show the app's local data. They never call the API.

🧱 New widget/ module group

  • :widget:core holds what every widget shares:
    • WidgetPublisher re-renders the widgets when local data changes (a sync, a new location or a favourite change);
    • the appearance model, its per-widget storage, the palette and the shared Glance surfaces;
    • the generic settings screen.
  • :widget:nearest-station is the widget itself.
  • Widgets depend only on core:*, never on feature:*. A tap on a station opens it through the existing ecogas-map deep link.

🤖 Android widget (Glance)

  • Price tile and nearest-station tile with distance, address and status dot. The look follows the app's design tokens.
  • The layout follows the size: a narrow widget shows one tile and a wide one shows the chosen tiles. One tile on a wide widget takes half its width.
  • Per-widget settings screen (long press → Settings) with a live preview over the real wallpaper:
    • Show: Price / Distance / Both;
    • Theme: System / Light / Dark;
    • background and tile colour (black / white / blue / auto) and opacity;
    • imitated liquid glass with tint and strength, for the background and for the tiles.
  • The status dot gets a contrasting ring when it matches the tile colour.

🍏 iOS widget (WidgetKit)

  • New MetanMobileWidgetExtension target (iOS 17+), small and medium sizes.
  • The app writes a JSON snapshot into the App Group group.com.ngapp.metanmobile. The texts in it are already translated into the app's language, and the nearest station is already picked by the same Kotlin logic as
    on Android.
  • WidgetKit reloads through a Swift bridge (WidgetReloader), and only when the snapshot actually changed, because iOS rations reloads.
  • Settings in the system "Edit Widget" sheet: tiles, theme, background and tile colour, and opacity steps.
  • Supports the iOS 26 Clear and Tinted home screen modes (the system draws the glass). Strings in en/ru/be.

🐞 Fix

  • A cold start from a deep link (the widget or an App Link) crashed with You must call setGraph() before calling getGraph(). The link now waits until the NavHost graph is set. This affects both Android and iOS.

🧪 Tests

  • Kotlin:
    • CompositeWidgetDataRepository and WidgetPublisher (JVM + iOS simulator);
    • palette and settings storage;
    • the iOS snapshot and its JSON contract;
    • the Glance layout (glance-appwidget-testing + Robolectric).
  • Swift: new MetanMobileWidgetTests target (Swift Testing, no host app). It covers the JSON contract with Kotlin, the palette rules, the widget rules and ru/be translations.

👷 CI

  • The iOS job selects Xcode 27 and now also runs the widget's Kotlin tests on the iOS simulator and its Swift tests.
  • Caches for what was downloaded on every run: the Kotlin/Native toolchain (~/.konan), Swift packages and Robolectric's Android SDK jars.

🛠 Versions

  • Android 2.4.0 (2400), iOS 1.1.0 (3). The widget extension has the same version as the app, as App Store requires.

Review notes

  • CI on Xcode 27: if macos-latest doesn't ship Xcode 27 / iOS 27 simulators yet, the iOS job fails at setup-xcode. Then switch runs-on to a newer macOS image.
  • iOS widget on a device: its look, the "Edit Widget" settings and taps haven't been checked on a real iPhone yet. Only the data path was verified, on the iOS 27 simulator (the snapshot in the App Group).
  • App Group: group.com.ngapp.metanmobile has to exist in the Apple Developer account for release signing of both targets.
  • Location freshness: the widget's distance is as fresh as the last location the app saved. The widget doesn't request location on its own.
  • Lint gap (not from this PR): lintDebug doesn't analyse the Android code of KMP modules, so core:*, feature:* and widget:* androidMain aren't linted. This was checked with a deliberate probe error.
  • New library: androidx.glance:glance-appwidget 1.2.0, plus glance-appwidget-testing for tests.

ngapps added 5 commits October 2, 2026 07:37
Android widget (Jetpack Glance) showing the CNG price and the nearest
station from the app's local data only - widgets never call the API.

- Data layer: WidgetData (core:model), WidgetDataRepository (core:domain),
  CompositeWidgetDataRepository (core:data) built from the existing
  station, price and location repositories.
- New top-level widget/ group: :widget:core holds what every widget
  shares (WidgetPublisher re-renders widgets when local data changes,
  appearance model and storage, palette, Glance surfaces, generic
  settings screen); :widget:nearest-station is the widget itself.
- Per-widget settings: Price / Distance / Both tiles, theme (system,
  light, dark), background and tile color + opacity, imitated liquid
  glass with tint and strength, live preview over the real wallpaper.
- Size-driven layout: a narrow widget keeps one tile, a wide one shows
  the chosen tiles; a single tile on a wide widget takes half its width.
- Status dot gets a contrasting ring when it matches the tile color.
- Tapping the station opens it via the existing ecogas-map deep link.

Fix: cold start from a deep link (widget or App Link) crashed with
"You must call setGraph() before calling getGraph()" - the link is now
kept pending until the NavHost graph is set.

Tests: repository, publisher (Android + iOS), palette, settings storage
and the Glance layout via glance-appwidget-testing + Robolectric.
WidgetKit counterpart of the Android widget, fed by the same shared
Kotlin pipeline: still no API calls from the widget.

- Kotlin: the app writes a compact JSON snapshot (price, the already
  picked nearest station, texts translated into the app's language)
  into the App Group group.com.ngapp.metanmobile, and asks WidgetKit to
  reload through a Swift bridge (WidgetReloader) - only when the
  snapshot actually changed, since iOS rations reloads. The same
  WidgetPublisher as on Android drives it, started in initSharedKoin.
- Swift (MetanMobileWidgetExtension, iOS 17+): small (one tile) and
  medium (chosen tiles) sizes, settings in the system "Edit Widget"
  sheet (tiles, theme, background/tile color and opacity steps), the
  Android palette rules ported, status dot ring, station deep links
  (metanmobile://ecogas-map/{code}/), iOS 26 Clear/Tinted rendering
  modes, en/ru/be String Catalog, Xcode previews.
- Xcode: widget extension target, App Group on both targets, ru/be
  project localizations.

Tests: snapshot mapping and its JSON contract with the Swift side, run
on the JVM and the iOS simulator. The nearest-station iOS test binary
pins compose foundation to the app's version (moko pulls 1.7.0).
New MetanMobileWidgetTests target (Swift Testing, no host app) that
compiles the widget's logic files directly, so it runs in seconds
without building the app or its Kotlin framework.

- JSON contract with the Kotlin snapshot, including a fixture captured
  from a simulator's App Group, every status, broken and newer files.
- Palette rules, matching the Android WidgetPaletteTest: auto colors per
  theme, explicit colors, opacity steps, text color, status dot ring.
- Widget rules: one tile in a small widget, themes, tap targets and the
  station link shape the app's deep link routing expects.
- Every widget string translated into ru and be.

The widget's decisions moved out of the SwiftUI view into
NearestStationWidgetRules.swift so they can be tested; behaviour is
unchanged. Shared schemes for the widget extension and its tests, so
they are available outside this machine (CI).
- Android 2.3.8 -> 2.4.0 (versionCode 2400).
- iOS app 1.0.1 (2) -> 1.1.0 (3). The widget extension gets the same
  version and build: App Store rejects an extension whose version
  differs from its app's.
- iOS job selects Xcode 27 and now also runs the widget's Kotlin tests
  on the iOS simulator and its Swift tests (MetanMobileWidgetTests) on
  an iOS 27 iPhone simulator picked from the runner. The Swift tests
  have no host app, so they don't build the app or its Kotlin framework.
- Caches what was downloaded on every run and isn't in Gradle's cache:
  the Kotlin/Native toolchain (~/.konan, ~1 GB), Swift packages
  (~900 MB) and Robolectric's Android SDK jars (~300 MB).
- Test results are uploaded when the iOS job fails.
@ngapp-dev ngapp-dev self-assigned this Oct 2, 2026
@ngapp-dev
ngapp-dev merged commit f36d7f1 into master Oct 2, 2026
2 of 3 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