Repository navigation
✨ Feature: Widget - #27
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
feature/widgetadds the first home-screen widget, "Price and nearest station", on Android (Jetpack Glance) and iOS (WidgetKit). It has 5 commits on top ofmasterand touches 77 files (+5921 / −12lines; 64 added, 13 modified). Main areas:
📊 Data layer
WidgetData(core:model),WidgetDataRepository(core:domain) andCompositeWidgetDataRepository(core:data) build the widget data from the existing station, price and location repositories.🧱 New
widget/module group:widget:coreholds what every widget shares:WidgetPublisherre-renders the widgets when local data changes (a sync, a new location or a favourite change);:widget:nearest-stationis the widget itself.core:*, never onfeature:*. A tap on a station opens it through the existingecogas-mapdeep link.🤖 Android widget (Glance)
🍏 iOS widget (WidgetKit)
MetanMobileWidgetExtensiontarget (iOS 17+), small and medium sizes.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 ason Android.
WidgetReloader), and only when the snapshot actually changed, because iOS rations reloads.🐞 Fix
You must call setGraph() before calling getGraph(). The link now waits until the NavHost graph is set. This affects both Android and iOS.🧪 Tests
CompositeWidgetDataRepositoryandWidgetPublisher(JVM + iOS simulator);glance-appwidget-testing+ Robolectric).MetanMobileWidgetTeststarget (Swift Testing, no host app). It covers the JSON contract with Kotlin, the palette rules, the widget rules and ru/be translations.👷 CI
~/.konan), Swift packages and Robolectric's Android SDK jars.🛠 Versions
Review notes
macos-latestdoesn't ship Xcode 27 / iOS 27 simulators yet, the iOS job fails atsetup-xcode. Then switchruns-onto a newer macOS image.group.com.ngapp.metanmobilehas to exist in the Apple Developer account for release signing of both targets.lintDebugdoesn't analyse the Android code of KMP modules, socore:*,feature:*andwidget:*androidMainaren't linted. This was checked with a deliberate probe error.androidx.glance:glance-appwidget1.2.0, plusglance-appwidget-testingfor tests.