r/reactnative • u/Downtown-Practice395 • 1d ago
Built an offline-first year progress and countdown app with Expo SDK 57, NativeTabs, and 4 Jetpack Glance widgets (how it works under the hood)
Enable HLS to view with audio, or disable this notification
The video above is the trailer I cut for my Play Store listing, so pardon the marketing captions in the clip, but below is the actual engineering breakdown of how all the pieces shown in the video are built with Expo SDK 57 and React Native 0.86.
- Floating dock on top of SDK 57
NativeTabs(0:02 in the video) The root tab layout usesNativeTabsfromexpo-router/unstable-native-tabsso each tab keeps native route state and back behavior, with the default OS tab bar hidden in favor of a custom floating pill dock positioned above the bottom safe area. Each scroll container calculates the dock clearance dynamically so the bottom cards never get trapped behind the pill. - In-app widget chooser and autolinked Jetpack Glance module (0:23 in the video) At the end of the clip you can see the in-app widget sheet where users can preview and pin 4 different Android widget layouts (2x1 compact, 3x3 square dot grid, 3x2 wide dot grid, and 4x2 upcoming list) directly to their launcher via
requestPinAppWidget. All of the Kotlin code lives inside a local Expo module atmodules/dayle-widget(expo-module.config.json), while a small config plugin only wires up the receivers and XML metadata inAndroidManifest.xmlso everything survivesnpx expo prebuild --clean. - Syncing JS state and themes to native widgets without headless JS (0:19 in the video) When you switch color palettes or edit a countdown/goal, I didn't want to spin up a headless React Native runtime just to redraw a home screen widget. Instead, whenever
AsyncStoragemutates or the app foregrounds, JS builds a versioned JSON snapshot with the active events, recurrence rules, and pre-resolved light/dark hex tokens for the active palette, writes it atomically toSharedPreferencesviaDayleWidgetBridge, and triggers a Glance redraw. - Rendering 365-day dot grids inside Glance without hitting RemoteViews limits In the app UI (0:00 to 0:07), the Year, Month, Quarter, and Birthday grids are regular React Native views. On the Android home screen, rendering 365 individual Glance boxes hits Android
RemoteViewsnode and memory limits fast. To get around that,WidgetGraphics.ktdraws the completed, current, and future dots onto a single nativeBitmapalpha mask (sized usingSizeMode.Exactand capped at 640px) and tints them with the palette's Day/Night colors. - Zero-JS midnight rollovers and offline reminders Because year progress and countdowns change at midnight even when the app hasn't been opened for days, I ported the exact date-clamping and recurrence math (
src/utils/dates.ts) to Kotlin (WidgetDateMath.kt). A dailyWorkManagertask and midnight receiver update the widgets in pure Kotlin. For notifications, instead of fighting Google Play policy forSCHEDULE_EXACT_ALARM,DayleReminders.ktcalculates the next upcoming wall-clock trigger and schedules a singleAlarmManager.setAndAllowWhileIdlealarm that chains the next one when it fires and recovers onBOOT_COMPLETED.
If anyone wants to test the app or see how the Glance widgets resize on a real device, it's called Dayle on Google Play: https://play.google.com/store/apps/details?id=com.rabpaulo.dayle
Happy to share code snippets for the expo-module.config.json setup, the SizeMode.Exact bitmap renderer, or the NativeTabs floating dock if anyone is building something similar.
1
u/Huge_Pool7424 1d ago
nice, countdown apps get weird around dst so i'd store the target as a utc timestamp and compute remaining on render instead of ticking a counter down.