You can add native Android home screen widgets to a React Native Expo app by writing a small amount of Kotlin with Jetpack Glance and wiring it into your project through a custom Expo config plugin. You can't render React components directly on a widget, because Android runs widgets in the launcher process using RemoteViews, outside your JS runtime. In 2026, Glance 1.1.x is stable, Expo prebuild is the default, and the workflow is finally clean enough that shipping a widget feels closer to a config-plugin exercise than a native platform slog.
Android widgets run in the launcher process using RemoteViews, so React components can't render on them. You write Kotlin and (optionally) Jetpack Compose-style Glance code.
Jetpack Glance 1.1 (stable since April 2024, patched through 1.1.1 in 2025) is Google's recommended API in 2026, and it compiles down to RemoteViews for you.
Expo prebuild plus a custom config plugin injects the widget XML metadata, AndroidManifest receiver, and Kotlin sources. You never edit android/ by hand.
Data flows from RN to the widget through SharedPreferences (or DataStore) written from a small Expo Module or react-native-shared-preferences.
Widget taps open the app through a PendingIntent whose URI matches your Expo Router deep link scheme.
Widgets do not work in Expo Go. You need a development build (eas build --profile development or npx expo run:android).
How Android widgets actually work in 2026
An Android home screen widget isn't part of your app's normal Activity. It's drawn by the launcher (Pixel Launcher, One UI Home, Nova, whatever) using RemoteViews, a serialisable description of a limited subset of Views that can be marshalled across process boundaries. That's why you can't mount a React tree there: the launcher never loads your JS engine, and the widget's UI has to be describable as RemoteViews before the launcher ever inflates it.
The pieces you have to supply are an AppWidgetProvider subclass (a BroadcastReceiver the OS calls into to build views), an XML metadata file describing sizes and resize behaviour, an entry in your AndroidManifest.xml, and a preview image for the widget picker. Android 12 added targetCellWidth/targetCellHeight for pixel-independent sizing, Android 12L introduced new corner-radius tokens, and Android 15 tightened background restrictions further. Every current guide has to consider all three baselines, because your minSdk in Expo is almost certainly still 24.
The other thing worth being pragmatic about: the OS decides when it will let your widget update. updatePeriodMillis has a floor of 30 minutes and is unreliable on modern Doze-mode devices. For anything time-sensitive you push updates from your app or from WorkManager, not by asking the OS to poll.
Jetpack Glance vs classic AppWidgetProvider
In 2026 you have two supported ways to build a widget: the classic RemoteViews + XML layouts route with AppWidgetProvider, or Jetpack Glance, Google's Compose-flavoured DSL that compiles to RemoteViews under the hood. Glance 1.1 hit stable in April 2024, and the 1.1.x patches (last shipped as 1.1.1 in mid-2025) fixed the last of the ridiculous state-restoration bugs. Honestly, I wouldn't write a new widget with the classic API today unless I had a specific reason, like an extreme minSdk, a shared Views codebase, or an existing RemoteViews layout I already trust. Glance is the pragmatic default.
Dimension
Jetpack Glance
Classic AppWidgetProvider + XML
API style
Composable functions (@Composable)
Imperative XML + RemoteViews
State
GlanceStateDefinition (DataStore-backed)
Roll your own SharedPreferences
Actions
actionRunCallback, actionStartActivity
PendingIntent boilerplate
Sizing / responsive layouts
LocalSize, sizeMode = Responsive
Multiple XML files, manual switching
Learning curve
Familiar if you know Compose
Familiar if you know old Android Views
Debug ergonomics
Preview functions with @GlancePreview
Reload widget on device to see anything
Recommended in 2026
Yes, for new work
Only for maintenance
I've shipped both. The classic path is more code, more places to make small mistakes (mismatched view ids, forgotten setViewVisibility), and much worse when your widget has more than one size. Glance's downside is a slightly larger binary and one more layer between you and the wire format, but the developer time saved is worth it.
Setting up an Expo development build for widgets
Widgets are native code, so they can't ship inside Expo Go. Start from an Expo SDK 55+ project with a development build. If you're still on the classic managed workflow with no android/ folder, that's fine; the config plugin below runs during expo prebuild and generates everything. Here's my preferred setup:
npx create-expo-app@latest my-app --template default
cd my-app
npx expo install expo-dev-client
# Reserve control of the native project
npx expo prebuild --clean
# Verify it still builds locally before adding widget code
npx expo run:android
Make sure your app.json has an explicit android.package. The widget's Kotlin package needs to match, and refactoring package names after you've declared receivers in the manifest is a headache I only want to feel once. Set compileSdkVersion to 34 or higher (Expo SDK 55 already does). Glance needs Kotlin 1.9+ and Compose compiler 1.5.4+, which Expo's Android template already handles.
If you aren't already familiar with Expo's plugin system, our Expo config plugins guide walks through withXcodeProject, withAndroidManifest, and the mod system in general. Everything below assumes you've read that or already know how mods work.
Writing the Glance widget in Kotlin
Create a folder at the project root (I use widgets/android) and put your Kotlin sources and XML there. The config plugin will copy them into android/app/src/main/ during prebuild. Here is a minimal Glance widget that renders a headline and a count from SharedPreferences and opens the app on tap.
Two files usually go beside it: widgets/android/res/xml/headline_widget_info.xml describing the widget metadata, and widgets/android/res/drawable/widget_preview.png for the picker screenshot. The metadata file is where you set minWidth/minHeight, the resize policy, the target cells, and the preview drawable:
The plugin has three jobs: copy the widget Kotlin sources into android/app/src/main/java/<package>/widget/, copy the XML metadata into res/xml/, and inject the <receiver> block into the manifest. Save this as plugins/withAndroidWidget.js at the project root.
Then run npx expo prebuild --clean and npx expo run:android. So, if the plugin fails silently (and it will, occasionally), run expo prebuild with EXPO_DEBUG=1 and grep the output for your receiver name. Nine times out of ten the mod ran, but the widget XML didn't copy because you nested the folder one level too deep. I hit that exact bug shipping my first Glance widget and lost half a Tuesday to it.
Sharing data from React Native to the widget
The widget lives outside your JS engine, so you can't pass props to it. Instead you write to a store the widget can read on wake. In practice that means SharedPreferences (simple) or DataStore (recommended by Glance, needs a bit more setup). I use SharedPreferences for anything under a few dozen keys.
Write a small Expo Module, or grab react-native-shared-preferences. A hand-rolled module is about 30 lines of Kotlin and gives you a typed API. I prefer that route, and it's portable if you also need a widget on iOS (see our React Native iOS widgets guide for the App Group equivalent).
// From your React Native code
import * as WidgetBridge from './modules/widget-bridge';
async function pushHeadline(headline: string, unreadCount: number) {
await WidgetBridge.setSharedData({
headline,
unread_count: unreadCount,
});
await WidgetBridge.requestUpdate();
}
The requestUpdate() call sends an intent that pokes the widget to redraw. Inside your Kotlin module it looks like this:
fun requestUpdate(context: Context) {
val intent = Intent(context, HeadlineWidgetReceiver::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context)
.getAppWidgetIds(ComponentName(context, HeadlineWidgetReceiver::class.java))
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
}
context.sendBroadcast(intent)
}
Deep linking from a widget tap into Expo Router
A tap opens your app via a PendingIntent that fires an ACTION_VIEW intent with your custom scheme. If you already have Expo Router universal links configured, the widget just needs to point at the same URI. Our Expo Router deep linking guide covers the scheme and Android App Links setup end-to-end; the widget side is the intent above.
One footgun to watch for: in Glance you use actionStartActivity with an Intent, not a PendingIntent, because Glance builds the PendingIntent for you. If you drop back to classic RemoteViews you have to construct the PendingIntent by hand, and since Android 12 you must pass FLAG_IMMUTABLE or your app will crash on install:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://feed"))
val pending = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
Updating widgets in the background
You've got three practical update paths in 2026: schedule with WorkManager, react to a push notification, or update opportunistically when the user opens the app. For a news or feed widget I usually combine a foreground refresh (on app close) with a periodic WorkManager job every 60 minutes. That gives you fresh data without draining battery, and it survives Doze mode where updatePeriodMillis would silently die.
class WidgetRefreshWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
// fetch fresh data, write to SharedPreferences
val prefs = applicationContext.getSharedPreferences("widget_data", Context.MODE_PRIVATE)
prefs.edit().putString("headline", fetchLatestHeadline()).apply()
// ask Glance to re-render
HeadlineWidget().updateAll(applicationContext)
return Result.success()
}
}
Schedule it once at app start with PeriodicWorkRequestBuilder<WidgetRefreshWorker>(60, TimeUnit.MINUTES). On Android 15 you should also check Android's background restriction docs if you plan to update more frequently. The OS will silently drop your job if you exceed the fair-share budget.
Testing and debugging Android widgets
Two commands do most of the work. To force a redraw without touching the launcher:
adb shell am broadcast \
-a android.appwidget.action.APPWIDGET_UPDATE \
-n com.example.myapp/.widget.HeadlineWidgetReceiver
To inspect state and logs, filter Logcat by the widget tag Glance emits: adb logcat | grep -i "glance\|widget". In Android Studio, the Widget preview tool in the layout inspector renders @GlancePreview composables without deploying to a device. That saved me hours the first time I built a responsive widget.
Here's the checklist I run before shipping any widget. Does it render at the minimum size (single 2x1 cell)? Does it render at 4x2 without truncating? Does the tap open the deep link even when the app is fully killed? Does the OS lightScheme/darkScheme colour pair look right in both? Does the widget survive a locale change? And does uninstalling and reinstalling the app leave stale widgets in a sensible empty state? If any of those answer "no", fix it before you ship, because you'll get a bug report about it within a week.
Shipping widgets to the Play Store
There's no separate Play Console flag for widgets, but three things get flagged during pre-launch review often enough that they're worth mentioning. First, your preview image (the drawable you set in previewImage) needs to look decent at multiple densities. A 320x180 XHDPI PNG is the sweet spot in 2026. Second, the widget's description string is user-facing in the picker; write it in strings.xml and localise it if you localise the rest of the app. Third, background data usage from your WorkManager schedule counts against your app's foreground service and network budgets. Apps that pull megabytes per hour to refresh a widget get flagged in Vitals.
If your widget shows content that requires a signed-in user, remember to handle the signed-out state gracefully. A widget in "please open the app" mode is fine; a widget stuck on a spinner or showing another user's data after logout is a rejection risk. I also strongly recommend adding a small integration test that mounts the widget via AppWidgetHost, because it will catch crashes on OEM launchers (Samsung and Xiaomi in particular) that you'll never see in the emulator.
Frequently Asked Questions
Can React Native make Android home screen widgets?
Not directly. Android widgets render outside your JS engine using RemoteViews, so React components can't mount there. What you can do is write the widget in Kotlin (Jetpack Glance is the pragmatic 2026 choice) and use an Expo config plugin to keep the native code inside your Expo project. Data flows from React Native to the widget through SharedPreferences or DataStore.
What is Jetpack Glance in Android?
Jetpack Glance is Google's Compose-flavoured API for building App Widgets and Wear OS tiles. You write @Composable functions and Glance compiles them to RemoteViews the launcher can render. Glance 1.1 has been stable since April 2024 and is the recommended way to build new widgets in 2026.
How often do Android widgets update automatically?
The updatePeriodMillis value in your widget XML has a 30-minute floor, and on modern Doze-optimised devices even that is unreliable. For anything time-sensitive, push updates from your app when the user closes it, or schedule a WorkManager job. Both approaches survive background restrictions where updatePeriodMillis silently fails.
Why doesn't my Android widget update after data changes?
Ninety percent of the time it's because you wrote to SharedPreferences but never asked the widget to redraw. Call HeadlineWidget().updateAll(context) from Glance, or send an AppWidgetManager.ACTION_APPWIDGET_UPDATE broadcast for classic widgets. The other ten percent is caching; verify with adb shell dumpsys appwidget that the launcher actually asked for the new view.
Do Android widgets work with Expo Go?
No. Widgets are native code that has to be compiled into the APK, and Expo Go ships a fixed native binary, so custom widgets can't appear there. You need a development build (eas build --profile development or npx expo run:android) that includes your config plugin's output.
Add Stripe to a React Native Expo app in 2026 with PaymentSheet, Apple Pay, Google Pay, SetupIntent, and 3D Secure. Full config-plugin setup, testing, and webhook confirmation.