Working with App Widgets
1. Creating Widget Provider Class
| Property | Description | Values |
|---|---|---|
| AppWidgetProvider | Base class for implementing app widgets | android.appwidget.AppWidgetProvider |
| onUpdate | Called when widget is updated or first created | override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) |
| onEnabled | Called when the first widget is created | override fun onEnabled(context: Context) |
| onDisabled | Called when the last widget is deleted | override fun onDisabled(context: Context) |
Example: Basic Widget Provider Implementation
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
override fun onEnabled(context: Context) {
// When the first widget is created
}
override fun onDisabled(context: Context) {
// When the last widget is deleted
}
}
AppWidgetProvider is a subclass of BroadcastReceiver, so it can receive broadcasts like android.appwidget.action.APPWIDGET_UPDATE.
2. Defining Widget Layout
| Property | Description | Values |
|---|---|---|
| appWidgetProvider | Defines the widget's behavior and updates | android.appwidget.AppWidgetProvider |
| minWidth | Minimum width of the widget in dp | 180dp |
| minHeight | Minimum height of the widget in dp | 40dp |
| updatePeriodMillis | How often the widget updates (in milliseconds) | 30000 |
Example: Basic Widget Layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/widget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Widget" />
</LinearLayout>
res/layout/ directory and referenced in the AppWidgetProviderInfo XML.
3. Configuring Widget Metadata
| Field | Description | Value |
|---|---|---|
| appWidgetProvider | Class that extends AppWidgetProvider | com.example.MyWidgetProvider |
| label | User-facing name of the widget | @string/widget_label |
| initialLayout | Layout resource ID for widget UI | @layout/widget_layout |
| minWidth | Minimum width in dp | 250dp |
| minHeight | Minimum height in dp | 40dp |
| updatePeriodMillis | Update interval in milliseconds (0 = manual) | 30000 |
| previewImage | Preview image for widget in launcher | @drawable/widget_preview |
| resizeMode | Allowed resize behavior (none, horizontal, vertical, all) | AppWidgetManager.RESIZE_HORIZONTAL |
Example: Widget Metadata in XML
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:appWidgetProvider="com.example.MyWidgetProvider"
android:label="@string/widget_label"
android:initialLayout="@layout/widget_layout"
android:minWidth="250dp"
android:minHeight="40dp"
android:updatePeriodMillis="30000"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal" />
4. Updating Widget Content
| Method | Description | Notes |
|---|---|---|
| AppWidgetManager.updateAppWidget() | Updates the widget UI with new data | Must be called from a BroadcastReceiver or Service |
| RemoteViews.setImageViewResource() | Updates an image view with a new resource | Use for dynamic image updates (e.g., loading from network) |
| RemoteViews.setTextViewText() | Updates a text view with new content | Supports dynamic string updates |
| RemoteViews.setImageViewUri() | Updates an image view with a URI (e.g., from content provider) | Useful for loading images from external sources |
Example: Updating Widget Text
val remoteViews = RemoteViews(context.packageName, R.layout.widget_layout)
remoteViews.setTextViewText(R.id.widget_text, "Updated Text")
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.updateAppWidget(widgetId, remoteViews)
Example: Updating Widget Image
val remoteViews = RemoteViews(context.packageName, R.layout.widget_layout)
remoteViews.setImageViewResource(R.id.widget_image, R.drawable.new_image)
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.updateAppWidget(widgetId, remoteViews)
AppWidgetManager.updateAppWidget() method must be called from a background thread or service to avoid blocking the UI.
AlarmManager for periodic updates.
5. Handling Widget Clicks
| Click Type | Description | Implementation |
|---|---|---|
| Button Click | Triggers when user taps a button inside the widget | |
| App Launch | Opens the main app when widget is tapped | |
Example: Broadcast Receiver for Widget Clicks
class MyWidgetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "com.example.WIDGET_BUTTON_CLICKED") {
val widgetId = intent.getIntExtra("widgetId", 0)
// Handle button click logic here
}
}
}
AndroidManifest.xml6. Implementing Widget Configuration
Example: Basic AppWidgetProvider
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
| Property | Description | Values |
|---|---|---|
| AppWidgetProvider | Base class for widget providers | Extends BroadcastReceiver |
| onUpdate | Called when widget is updated | Override to update widget content |
| RemoteViews | Defines widget UI layout | Uses layout resource ID |
Example: Configuration Activity
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
class WidgetConfigActivity : Activity() {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.config_layout)
val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
}
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(Activity.RESULT_OK, resultValue)
finish()
}
}
7. Using Collection Widgets
Collection widgets in Android allow you to display a dynamic list of items, such as a list of notifications or a grid of images. These widgets are particularly useful for showing content that changes frequently.
| Property | Description | Values |
|---|---|---|
| appWidgetProvider | Class that handles widget updates and lifecycle events. | AppWidgetProvider |
| collectionItems | Array of items to display in the collection widget. | Array<WidgetItem> |
| layoutResource | Resource ID of the layout to use for each item in the collection. | R.layout.widget_item |
Example: Creating a Collection Widget
class MyCollectionWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
val widgets = getCollectionWidgets(context)
for (id in appWidgetIds) {
val intent = Intent(context, MyCollectionService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
val pendingIntent = PendingIntent.getService(
context,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val remoteViews = RemoteViews(context.packageName, R.layout.widget_collection)
remoteViews.setRemoteAdapter(R.id.listView, pendingIntent)
appWidgetManager.updateAppWidget(id, remoteViews)
}
}
private fun getCollectionWidgets(context: Context): List<WidgetItem> {
return listOf(
WidgetItem("Item 1", "Description 1"),
WidgetItem("Item 2", "Description 2")
)
}
}
Example: Service for Collection Items
class MyCollectionService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsService.RemoteViewsFactory {
return MyCollectionFactory(intent)
}
class MyCollectionFactory(private val intent: Intent) : RemoteViewsService.RemoteViewsFactory {
private var items = mutableListOf<WidgetItem>()
override fun onCreate() {
items = getCollectionItems()
}
override fun onDataSetChanged() {
items = getCollectionItems()
}
override fun onDestroy() {}
override fun getCount(): Int = items.size
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_item)
views.setTextViewText(R.id.title, items[position].title)
views.setTextViewText(R.id.description, items[position].description)
return views
}
override fun getLoadingView(): RemoteViews? = null
override fun getViewTypeCount(): Int = 1
override fun getItemId(position: Int): Long = position.toLong()
override fun hasStableIds(): Boolean = true
private fun getCollectionItems(): MutableList<WidgetItem> {
return mutableListOf(
WidgetItem("Item 1", "Description 1"),
WidgetItem("Item 2", "Description 2")
)
}
}
}
RemoteViewsService to provide the content dynamically.
Key Points
- Collection widgets are ideal for displaying lists of items.
- They require a
RemoteViewsServiceto populate the list dynamically. - Each item in the collection must have a corresponding layout resource.
8. Creating RemoteViewsService
A RemoteViewsService is a special type of service that provides data to an AppWidgetProvider for displaying dynamic content in a widget. It is particularly useful when you need to populate list views or other dynamic content within your app widget.
Example: Basic RemoteViewsService Implementation
import android.app.Service
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.IBinder
import android.widget.RemoteViewsService
class MyRemoteViewsService : RemoteViewsService() {
override fun onBind(intent: Intent): IBinder {
return RemoteViewsService.RemoteViewsFactory()
}
}
The RemoteViewsService must implement the RemoteViewsService.RemoteViewsFactory interface to provide data for the widget. This factory is responsible for creating and managing the RemoteViews that will be displayed in the widget.
RemoteViewsService runs in a separate process, so it's important to ensure that any data you pass between the service and the widget provider is properly serialized.
| Method | Description | Return Type |
|---|---|---|
| onCreate() | Called once when the service is created. Initialize any resources here. | void |
| onDataSetChanged() | Called when the data set changes. This is where you update your data source and notify the widget. | void |
| onDestroy() | Called when the service is destroyed. Clean up any resources here. | void |
| getCount() | Returns the number of items in the data set. | Int |
| getViewAt(position: Int) | Returns the RemoteViews for a specific item at the given position. |
RemoteViews |
| getLoadingView() | Returns a RemoteViews to display while the data is loading. |
RemoteViews |
| getViewTypeCount() | Returns the number of view types supported by this factory. | Int |
| getItemId(position: Int) | Returns the ID of the item at the given position. | Long |
| hasStableIds() | Indicates whether the IDs of items are stable across data changes. | Boolean |
Example: Implementing RemoteViewsFactory
class MyRemoteViewsFactory : RemoteViewsService.RemoteViewsFactory {
private var data: List<String> = emptyList()
override fun onCreate() {
// Initialize data
data = listOf("Item 1", "Item 2", "Item 3")
}
override fun onDataSetChanged() {
// Update data if needed
}
override fun onDestroy() {
// Clean up resources
}
override fun getCount(): Int = data.size
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_item)
views.setTextViewText(R.id.text_view, data[position])
return views
}
override fun getLoadingView(): RemoteViews? = null
override fun getViewTypeCount(): Int = 1
override fun getItemId(position: Int): Long = position.toLong()
override fun hasStableIds(): Boolean = true
}
9. Managing Widget Lifecycle
App Widgets in Android have a distinct lifecycle that developers must manage to ensure proper behavior and resource usage. The lifecycle includes creation, updates, and destruction phases.
| Phase | Description | Key Methods |
|---|---|---|
| Creation | Initial setup of the widget, including configuration and binding. | onUpdate(), onAppWidgetOptionsChanged() |
| Update | Periodic or triggered updates of the widget's UI. | onUpdate(), onReceive() |
| Deletion | When the widget is removed from the home screen. | onDeleted(), onDisabled() |
Example: Handling Widget Updates
class MyWidgetReceiver : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_layout)
// Update widget content
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
}
}
onUpdate() method is called when the widget is initially added or when the update interval specified in the configuration passes.
onUpdate() as it runs on the UI thread and can block the home screen.
Widget Lifecycle Summary
- onUpdate(): Called when the widget is added or updated.
- onDeleted(): Called when widgets are deleted from the home screen.
- onDisabled(): Called when the last instance of the widget is removed.
- onAppWidgetOptionsChanged(): Called when the widget options change (e.g., size).
10. Updating Widgets from App
Widgets can be updated from an app using AppWidgetManager and RemoteViews. Updates can be triggered manually or via broadcasts.
| Method | Description | Usage |
|---|---|---|
| updateAppWidget() | Updates a specific widget instance | AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) |
| notifyAppWidgetViewDataChanged() | Notifies widget that data has changed | AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, viewId) |
Example: Updating Widget Text
val appWidgetManager = AppWidgetManager.getInstance(context)
val views = RemoteViews(context.packageName, R.layout.widget_layout)
views.setTextViewText(R.id.widget_text, "Updated Text")
appWidgetManager.updateAppWidget(appWidgetId, views)
Example: Using Broadcast to Update Widget
val intent = Intent(context, WidgetUpdateReceiver::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.update_button, pendingIntent)
11. Using Glance for Compose Widgets
The Glance library is a lightweight framework for building Android app widgets using Jetpack Compose. It provides a declarative API to define widget layouts and logic, similar to how you would build UIs in a regular Compose application.
| Feature | Description | Notes |
|---|---|---|
| GlanceAppWidget | Base class for creating app widgets using Compose. | Extends AppWidget and uses GlanceAppWidgetReceiver. |
| GlanceAppWidgetReceiver | Receiver that handles widget updates and lifecycle events. | Must be declared in AndroidManifest.xml. |
| RemoteViews | Used to define the layout and behavior of widgets. | Supports Compose-based layouts via GlanceAppWidget. |
Example: Basic Glance Widget
class MyGlanceWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: AppWidgetId) = GlanceState(
appWidgetId = id,
layout = MyWidgetLayout
)
}
@Composable
fun MyWidgetLayout() {
Box(
modifier = GlanceModifier.fillMaxSize()
.background(Color.Blue)
) {
Text(
text = "Hello, Widget!",
modifier = GlanceModifier.fillMaxSize().padding(16.dp),
color = Color.White,
fontSize = 18.sp
)
}
}