Android App Widgets Development
Android App Widgets are components placed on home screen and updated via AppWidgetProvider (subclass of BroadcastReceiver). Unlike iOS WidgetKit, Android widgets can respond to taps interactively and update without iOS's budget limitations — but with RemoteViews constraints.
Architecture: RemoteViews as Main Limitation
Android widget renders in launcher process, not app process. So can't use standard View — only RemoteViews. RemoteViews supports limited View set: TextView, ImageView, Button, LinearLayout, RelativeLayout, FrameLayout, GridLayout, ListView, GridView, StackView.
Can't use: RecyclerView, ConstraintLayout (before API 31), custom View, WebView. For collections — ListView or GridView via RemoteViewsFactory.
Starting Android 12 (API 31): RemoteViews supports CheckBox, RadioButton, Switch — interactive elements. Before 12 — only PendingIntent on tap.
AppWidgetProvider and Updates
class MyWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { widgetId ->
val views = buildRemoteViews(context)
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
onUpdate called by schedule from android:updatePeriodMillis in appwidget-provider XML. Minimum — 1800000ms (30 minutes). For more frequent updates need WorkManager or AlarmManager with explicit updateAppWidget.
WorkManager with PeriodicWorkRequest (minimum 15 minutes) + appWidgetManager.updateAppWidget — standard approach for widgets with regular data (rates, weather, schedule).
For push-driven updates: FirebaseMessagingService.onMessageReceived → appWidgetManager.updateAppWidget — widget updates instantly on push.
Collections: RemoteViewsFactory
ListView in widget with data from app:
class WidgetListFactory(private val context: Context) : RemoteViewsService.RemoteViewsFactory {
private var items: List<WidgetItem> = emptyList()
override fun onDataSetChanged() {
// Called in background thread
items = loadDataFromSharedPrefs(context)
}
override fun getViewAt(position: Int): RemoteViews {
val item = items[position]
return RemoteViews(context.packageName, R.layout.widget_list_item).apply {
setTextViewText(R.id.item_title, item.title)
val fillIntent = Intent().putExtra("item_id", item.id)
setOnClickFillInIntent(R.id.item_container, fillIntent)
}
}
}
setOnClickFillInIntent + setPendingIntentTemplate on ListView — pattern for handling item taps. Without setPendingIntentTemplate taps don't work.
Glance — Jetpack Compose for Widgets
Starting Glance 1.0 (stable): widgets in Compose-like syntax, without manual RemoteViews.
class MyGlanceWidget : GlanceAppWidget() {
@Composable
override fun Content() {
val data = currentState<MyWidgetData>()
Column(modifier = GlanceModifier.fillMaxSize().background(Color.White)) {
Text(text = data.title, style = TextStyle(fontSize = 16.sp))
Button(text = "Refresh", onClick = actionRunCallback<RefreshAction>())
}
}
}
Glance limited to Compose subset — not all modifiers and components available. GlanceModifier differs from standard Modifier. State via GlanceStateDefinition and updateAppWidgetState. Best choice for new projects on API 23+.
Widget Configuration by User
AppWidgetConfigure Activity — opens on widget add. User selects parameters (city, theme, displayed data). After selection: setResult(Activity.RESULT_OK, intent.putExtra(EXTRA_APPWIDGET_ID, widgetId)) — mandatory, else widget won't add.
In Glance: GlanceAppWidgetManager().startConfigureActivityIntent — launch configuration programmatically.
Timeframe: 3-5 days for one widget with data and configuration. With collection and push-updates — up to 1 week. Cost calculated individually.







