KOTLIN • JETPACK GLANCE API 26 to 16 (BAKLAVA) 2026 ARCHITECTURE

How to Create Widgets in Android Programmatically

A comprehensive engineering walkthrough: programmatic pinning via requestPinAppWidget, declarative UI with Jetpack Glance, DPI responsive scaling, and background WorkManager synchronization.

Hafiz Musa (Independent Developer) • October 2026 • 14 min read

1. The Mental Model: IPC & RemoteViews vs In-App UI

When developers first ask "how to create a widget in Android programmatically", they often assume they can instantiate an Android View or a Jetpack Compose composable in memory, pass it to an API, and mount it onto the home screen.

In Android's OS architecture, that is fundamentally impossible. Your app does not render the widget; the launcher application (e.g. Pixel Launcher, Nova Launcher, Samsung One UI Home) renders it.

Your widget lives in a separate OS process (com.android.launcher3 or OEM launcher). To bridge this boundary, Android uses Inter-Process Communication (IPC) powered by Binder. Historically, this meant constructing a RemoteViews object—a parcelable blueprint that instructs the launcher's layout engine on what to inflate.

What Changed in Modern Android?

In modern Android development, Google introduced Jetpack Glance. Glance lets you write familiar declarative Compose syntax (Text, Row, Column, Button), but instead of rendering directly to a Skia canvas, Glance's compiler translates your composable tree into an optimized, battery-conscious RemoteViews structure automatically.

2. Programmatic Widget Pinning (requestPinAppWidget)

Starting in Android 8.0 (API level 26, Oreo), developers no longer have to force users to manually pinch their home screen, search through a 100-app widget drawer, and drag an icon. You can trigger a native system confirmation dialog right from inside your settings screen, onboarding flow, or dashboard.

import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Build fun pinWidgetToHomeScreen(context: Context) { val appWidgetManager = AppWidgetManager.getInstance(context) val widgetProvider = ComponentName(context, MyGlanceWidgetReceiver::class.java) // 1. Verify that the user's default launcher supports programmatic pinning if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (appWidgetManager.isRequestPinAppWidgetSupported) { // 2. Create a callback PendingIntent (optional, fires upon placement confirmation) val successIntent = Intent(context, WidgetPinnedReceiver::class.java).apply { action = "ACTION_WIDGET_PINNED_SUCCESS" } val successPendingIntent = PendingIntent.getBroadcast( context, 0, successIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) // 3. Request the OS to display the native confirmation prompt appWidgetManager.requestPinAppWidget( widgetProvider, null, // Optional bundle with custom extras successPendingIntent ) } else { // Fallback: Notify user that their launcher does not support direct pinning } } }

Notice the use of PendingIntent.FLAG_IMMUTABLE. Beginning with Android 12 (API 31), explicitly declaring mutability flags is mandatory.

3. Android Version Compatibility & Launcher Checks

Because widget APIs have evolved dramatically across OS releases, your codebase must inspect runtime API levels.

Android Version API Level Major Widget Capabilities & Changes
Android 8.0 (Oreo) API 26 Introduced requestPinAppWidget(); background execution limits restricted BroadcastReceiver widget updates.
Android 12 (Snow Cone) API 31 Complete visual overhaul: dynamic Material You color theming, standardized rounded corners (system_app_widget_background_radius), responsive layouts.
Android 14 (Upside Down Cake) API 34 Strict foreground service limits. All recurring background widget updates must route through WorkManager.
Android 16 (Baklava) API 35/36 Predictive back gestures integration, refined Glance state definitions, and Edge-to-Edge enforcement.

You can check the full version requirements and active Play Store target SDK thresholds at any time using our free Android Version & API Checker.

4. Modern Declarative UI with Jetpack Glance

Gone are the days of wrestling with deeply nested XML layouts and fragile RemoteViews.setTextViewText() calls. Here is how to create a production-grade widget using Jetpack Glance.

Step A: Add Gradle Dependencies
// In your app/build.gradle.kts: dependencies { implementation("androidx.glance:glance-appwidget:1.1.0") implementation("androidx.glance:glance-material3:1.1.0") implementation("androidx.work:work-runtime-ktx:2.9.1") }
Step B: Implement the GlanceAppWidget
import android.content.Context import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.* import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider import androidx.compose.ui.graphics.Color class MetricCounterWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { Column( modifier = GlanceModifier .fillMaxSize() .background(Color(0xFF0F172A)) .padding(16.dp) .clickable(actionStartActivity<MainActivity>()), verticalAlignment = Alignment.Vertical.CenterVertically, horizontalAlignment = Alignment.Horizontal.CenterHorizontally ) { Text( text = "Daily Steps", style = TextStyle( color = ColorProvider(Color(0xFF94A3B8)), fontSize = 12.sp ) ) Spacer(modifier = GlanceModifier.height(4.dp)) Text( text = "8,420", style = TextStyle( color = ColorProvider(Color.White), fontSize = 24.sp, fontWeight = FontWeight.Bold ) ) } } } }
Step C: Declare the Receiver and AppWidgetProviderInfo XML
import androidx.glance.appwidget.GlanceAppWidgetReceiver class MetricCounterReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = MetricCounterWidget() }

5. Widget DPI Sizing & Responsive Grid Math

One of the biggest frustrations Android engineers face is widget sizing bugs: a 2×2 widget looks perfect on an Android Pixel 9, but gets clipped or cropped on a Samsung Galaxy S25 or a Xiaomi device.

Android home screens are divided into a grid of cells (typically 4×5 or 5×5). The official Android formula for determining the minimum bounding box dimensions in DP is:

Standard Cell Dimension Formula: minWidth = (73 × N - 16) dp
minHeight = (73 × M - 16) dp
Where N is the number of horizontal columns, and M is the number of vertical rows.

For example:

  • 2 × 2 Widget: Width = (73 × 2 - 16) = 130 dp, Height = (73 × 2 - 16) = 130 dp
  • 4 × 2 Wide Widget: Width = (73 × 4 - 16) = 276 dp, Height = (73 × 2 - 16) = 130 dp

To convert these density-independent pixels into exact physical pixels across device screen buckets (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi), use our interactive DP to PX Converter and Screen DPI Calculator.

6. Background Data Sync via WorkManager

Android widgets do not stay active in memory. If your app needs to update a stock price, weather condition, step count, or prayer timetable, you cannot run a continuous background thread.

The correct, battery-efficient architecture is to dispatch periodic synchronization using Jetpack WorkManager, and then programmatically notify the Glance widget engine.

import android.content.Context import androidx.glance.appwidget.updateAll import androidx.work.CoroutineWorker import androidx.work.WorkerParameters class WidgetSyncWorker( private val context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { // 1. Fetch fresh data from local Room DB, DataStore, or API val latestData = fetchLatestMetrics() // 2. Trigger a programmatic re-render across all active widget instances MetricCounterWidget().updateAll(context) return Result.success() } }

7. Memory Limits (1MB Binder) & Common Pitfalls

The 1MB Binder Transaction Limit

Every RemoteViews update sent to the launcher travels across a Binder transaction. Android enforces a strict 1 megabyte buffer shared by all concurrent IPC transactions for the entire process. If you decode a full 4K or 1080p bitmap directly into an ImageView inside a widget, your app will crash with TransactionTooLargeException. Always downsample bitmaps to the exact physical pixel size of the widget cell before updating.

Key Pitfalls Checklist:
  • Opaque Action Handlers: Do not execute long-running operations inside widget click callbacks. Launch an explicit BroadcastReceiver, WorkManager task, or open an Activity.
  • Aggressive OEM Battery Killers: Manufacturers like Samsung (One UI) and Xiaomi (HyperOS) aggressively restrict background broadcasts. Ensure users are guided to whitelist your app from battery optimization when using background widgets (see our Samsung Battery & Background Fix Guide).
  • Adaptive Icon Assets: For widget preview headers on Android 12+, ensure you use vector XML or adaptive icons conforming to standard asset grids (see our App Icon Size Matrix).

Frequently Asked Questions

No. For security and user privacy, Android's OS sandboxing requires explicit confirmation. requestPinAppWidget triggers a native system UI showing the widget preview. The user must manually confirm placement.

Jetpack Glance provides declarative Compose ergonomics, strong type-safety, automatic state handling through DataStore, and responsive layout primitives (SizeMode.Responsive), while eliminating complex XML RemoteViews inflation boilerplate.

The standard updatePeriodMillis attribute in appwidget-provider XML has a minimum threshold of 30 minutes to preserve battery life. For updates needed on user actions (like tapping a button), updates occur instantaneously. For background periodic updates, use WorkManager with a minimum 15-minute interval.
About the Author • Hafiz Musa

Independent mobile developer and creator of AndroidRich since 2012. Specializing in native Android performance, client-side utility architecture, and privacy-first application development.