⚠️ WarningThis is highly experimental and not part of any official Expo workflow.
An experimental Expo Config Plugin that enables developers to create and manage Android Glance widgets in their React Native Expo applications. This plugin automates the setup process, generates boilerplate code, and provides a convenient API for widget data management.
- 🚀 Automatic Setup: Automatically configures Android Glance dependencies and build settings
- 📱 Widget Generation: Generates boilerplate widget code with Kotlin and Glance
- 🔧 Configuration Management: Handles widget provider info, manifest entries, and permissions
- 💾 Data Management: Provides a simple API for widget data storage and updates
- 🎨 Asset Handling: Automatically copies widget assets and layouts
- 📋 Configuration Activities: Supports widget configuration screens
- 🔄 Live Updates: Update widgets from your React Native app
- 🛠️ Template Generation: Creates example widget files based on your configuration
npm install android-glance-widget-expo{
"expo": {
"plugins": [
[
"android-glance-widget-expo",
{
"glanceVersion": "1.1.1", // Optional, defaults to "1.1.1"
"kotlinVersion": "2.0.0", // Optional, defaults to "2.0.0"
"widgets": [
{
"widgetClassName": "Home",
"configurationActivity": "HomeWidgetConfigurationActivity",
"widgetProviderInfo": {
"description": "Home Widget",
"updatePeriodMillis": 1000,
"minWidth": "100dp",
"minHeight": "100dp",
"targetCellWidth": "100",
"targetCellHeight": "100",
"maxResizeWidth": "100dp",
"maxResizeHeight": "100dp",
"resizeMode": "horizontal",
"widgetCategory": "home_screen"
}
}
]
}
]
]
}
}The plugin creates a widgets folder in your project root with starter code based on your configuration:
widgets/
├── Home.kt
├── HomeReceiver.kt
├── HomeWidgetConfigurationActivity.kt (if configured)
└── res/
├── layout/
│ ├── home_initial_layout.xml
│ └── home_preview_layout.xml
└── drawables/
└── your_assets.png
Note: These files are generated automatically as starter code that you can customize!
npx expo prebuild --platform android --cleanDuring prebuild, the plugin automatically:
- Adds Dependencies: Configures Android Glance dependencies in
android/app/build.gradle - Generates Starter Code: Creates working widget files in
widgets/folder for each configured widget if they don't already exist - Copies Widget Code: Transfers the widget files from
widgets/to the Android project - Generates Provider Info: Creates XML widget provider info files
- Updates Manifest: Adds widget receivers and activities to
AndroidManifest.xml - Copies Assets: Transfers widget assets and layouts to appropriate Android directories
For each widget configured in your app.json, the plugin creates files in the widgets/ folder:
- Basic Glance widget implementation
- Scaffold with "Hello Widget" text
- Preview composable for Android Studio
- Extends
GlanceAppWidgetReceiver - Links to your widget class
- Optional configuration screen
- Only created if
configurationActivityis specified
your_widget_preview_layout.xml- Preview layoutyour_widget_initial_layout.xml- Initial layout
These files are working starter code that you can customize directly in the widgets/ folder.
Use the WidgetStorage API to communicate with your widgets:
import { WidgetStorage } from 'android-glance-widget-expo';
// Set data for widgets
WidgetStorage.set('message', 'Hello World!');
WidgetStorage.set('count', 42);
WidgetStorage.set('isActive', true);
WidgetStorage.set('user', { name: 'John', age: 30 });
// Get data (useful for reading back stored values)
const message = WidgetStorage.get('message') as string;
const count = WidgetStorage.get('count') as number;The WidgetStorage API stores data in Android SharedPreferences. Access this data in your widget code using:
// In your widget receiver or widget class
val sharedPrefs = context.getSharedPreferences(
context.packageName + ".glance_widget",
Context.MODE_PRIVATE
)
val messageFromApp = sharedPrefs.getString("message", "No Message") ?: "No Message"
val countFromApp = sharedPrefs.getInt("count", 0)
val isActiveFromApp = sharedPrefs.getBoolean("isActive", false)
// For objects and arrays, data is stored as JSON strings
val userJson = sharedPrefs.getString("user", "{}")
// You'll need to parse JSON manually in your widget code
// Example: val user = Gson().fromJson(userJson, User::class.java)- Storage Location: Android SharedPreferences with name
{packageName}.glance_widget - Data Types: Supports strings, numbers, booleans, objects (as JSON), and arrays.
After changing data with WidgetStorage.set(), you need to trigger a widget update to reflect the changes. Use the updateWidget() method:
// Update data
WidgetStorage.set('message', 'Updated message!');
WidgetStorage.set('count', 42);
// Trigger widget update
WidgetStorage.updateWidget('HomeReceiver');The update process follows this flow:
- JavaScript calls
updateWidget(): Pass the receiver class name (e.g., 'HomeReceiver') - Module sends broadcast: The native module constructs the full class name and sends an Android broadcast intent
- Widget receiver responds: Your widget receiver catches the broadcast and updates the widget
Your widget receiver should handle the update broadcast like this:
class HomeReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = Home()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
CoroutineScope(Dispatchers.IO).launch {
val glanceIds = GlanceAppWidgetManager(context).getGlanceIds(Home::class.java)
// Read updated data from SharedPreferences
val sharedPrefs = context.getSharedPreferences(
context.packageName + ".glance_widget",
Context.MODE_PRIVATE
)
val messageFromApp = sharedPrefs.getString("message", "No Message") ?: "No Message"
val countFromApp = sharedPrefs.getInt("count", 0)
glanceIds.forEach { id ->
updateAppWidgetState(
context = context,
glanceId = id
) {
// Update widget state with new data
it[stringPreferencesKey("message")] = messageFromApp
it[intPreferencesKey("count")] = countFromApp
}
// Trigger widget UI update
glanceAppWidget.update(context, id)
}
}
}
}
}- Receiver class name: Use just the class name (e.g., 'HomeReceiver'), not the full package name
- Automatic broadcast: The module automatically constructs the full class name:
{packageName}.widgets.{receiverClassName} - Async updates: Widget updates run in a coroutine to avoid blocking the main thread
- Multiple instances: The receiver updates all instances of the widget if multiple are placed on the home screen
| Method | Description |
|---|---|
set(key: string, value: any) |
Store data (string, number, boolean, object, array) |
get(key: string) |
Retrieve stored data |
remove(key: string) |
Remove a key |
has(key: string) |
Check if key exists |
clear() |
Clear all data |
getAllKeys() |
Get all keys |
updateWidget(receiverClassName: string) |
Trigger widget update |
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
glanceVersion |
string |
No | "1.1.1" |
AndroidX Glance library version |
kotlinVersion |
string |
No | "2.0.0" |
Kotlin compiler version |
widgets |
Widget[] |
Yes | - | Array of widget configurations |
| Option | Type | Description |
|---|---|---|
widgetClassName |
string |
Name of the widget class |
configurationActivity |
string |
Optional configuration activity |
widgetProviderInfo |
WidgetProviderInfo |
Widget provider settings |
| Option | Type | Description |
|---|---|---|
minWidth |
string |
Minimum width (e.g., "100dp") (Added in API level 3) |
minHeight |
string |
Minimum height (e.g., "100dp") (Added in API level 3) |
updatePeriodMillis |
number |
Update interval in milliseconds (Added in API level 3) |
previewImageFileName |
string |
Preview image file name (must be present in widgets/res/drawables/) (Added in API level 11) |
resizeMode |
"horizontal" | "vertical" | "none" | "horizontal|vertical" |
Resize mode options (Added in API level 12) |
minResizeWidth |
string |
Minimum resize width (Added in API level 14) |
minResizeHeight |
string |
Minimum resize height (Added in API level 14) |
widgetCategory |
"home_screen" | "keyguard" |
Widget category options (Added in API level 17) |
widgetFeatures |
"configuration_optional" | "reconfigurable" | "hide_from_picker" |
Widget features options (Added in API level 28) |
description |
string |
Widget description (Added in API level 31) |
targetCellWidth |
string |
Target cell width (Added in API level 31) |
targetCellHeight |
string |
Target cell height (Added in API level 31) |
maxResizeWidth |
string |
Maximum resize width (Added in API level 31) |
maxResizeHeight |
string |
Maximum resize height (Added in API level 31) |
Note: The plugin automatically creates API-level specific XML files to ensure backward compatibility. For example, if you use description (API 31), the plugin will create separate XML files for different Android versions, ensuring your widget works on older devices without newer features.
For more detailed information about these widget provider options, refer to the Android AppWidgetProviderInfo documentation.
API Reference: AppWidgetProviderInfo
- @EvanBacon/expo-apple-targets for the inspiring approach to native target generation and plugin architecture.