使用 Glance 创建应用微件
1. 开始之前
本 Codelab 将带你为 SociaLite 创建应用微件。首先构建一个简单的 Glance 微件,并将它加入 SociaLite 和主屏幕;随后用 Glance 组件和主题添加未配置时的初始状态;接着支持用户从微件中选择最喜爱的联系人;最后学习如何从应用更新微件。
前提知识
- 具备基础 Kotlin 知识。
- 完成过设置 Android Studio练习,或熟悉 Android Studio,并能在 Android 15 模拟器或运行 Android 15 的实体设备上测试应用。
- 具备基础 Hilt 知识。
- 具备基础 Compose 知识。Glance 不使用 Jetpack Compose 的 UI composable,但复用了其框架和编码风格。
本练习将学习什么
- 配置应用以支持微件。
- 使用 Glance 组件构建响应式布局。
- 使用
GlanceTheme,在用户的主屏幕上支持动态配色。 - 处理微件中的用户交互。
- 从应用更新微件。
所需条件
- 最新版本的 Android Studio。
- 运行 Android 12 或更高版本的测试设备或模拟器。
- Android 12 或更高版本的 SDK。

2. 准备环境
获取起始代码
- 如果已经完成“处理 Android 15 的强制边到边显示”或“添加预测性返回动画”练习,你已经拥有起始代码,可以直接进入“添加微件”部分。
- 从 GitHub 下载起始代码;也可以克隆仓库并切换到
codelab_improve_android_experience_2024分支:
git clone git@github.com:android/socialite.git
cd socialite
git checkout codelab_improve_android_experience_2024
3. 在 Android Studio 中打开 SociaLite,并在 Android 15 设备或模拟器上运行。你会看到类似下面的界面:

采用手势导航的 SociaLite。
3. 添加微件
什么是微件
微件是应用中可嵌入其他 Android 应用的一部分,最常见的位置是用户主屏幕。添加微件可以让用户快速开始常见任务、查看一眼可读的信息,并通过你的内容个性化设备。
什么是 Glance
Jetpack Glance 是一个使用类似 Compose 的 Kotlin API 编写微件的库。它具有 Compose 的若干优点,例如重组、Kotlin 声明式界面代码和有明确设计规范的组件。Glance 大幅减少了微件中手写 XML RemoteViews 的需求。
创建微件
Android 微件在 AndroidManifest 中声明为 <receiver> 元素。此接收器应导出,处理 android.appwidget.action.APPWIDGET_UPDATE action 的 Intent,并通过名为 android.appwidget.provider 的 metadata 元素提供微件配置文件。
为 SociaLite 添加微件

我们希望用户通过微件看到最喜爱的联系人,以及是否有来自此人的未读消息。有未读消息时,点击微件应进入该联系人的聊天界面。同时,借助 Glance 组件与主题,使用响应式布局和动态配色改善外观。
先为 SociaLite 添加静态“Hello World”微件,再逐步扩展功能。需要完成五项工作:添加 Glance 依赖;实现 GlanceAppWidget;创建 GlanceAppWidgetReceiver;通过 app widget info XML 配置微件;将接收器和微件信息加入 AndroidManifest.xml。
向项目添加 Glance
起始代码已在版本目录 libs.versions.toml 中加入 Glance 版本与库坐标:
[versions]
//..
glance = "1.1.1"
[libraries]
glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }
glance-material = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" }
build.gradle.kts
SociaLite 的 app/build.gradle.kts 也已包含 Glance 依赖:
dependencies {
...
implementation(libs.glance.appwidget)
implementation(libs.glance.material)
...
}
如果修改了这些文件,请同步项目以下载 Glance 库。
创建 GlanceAppWidget 和 GlanceAppWidgetReceiver
Android 通过广播接收器通知 SociaLite:微件被添加、需要更新或已被移除。Glance 提供抽象接收器 GlanceAppWidgetReceiver,它继承 AppWidgetProvider,并负责提供 GlanceAppWidget 实例。后者把 Glance composable 渲染为 RemoteViews。
起始代码已包含两个类:继承 GlanceAppWidget 的 SociaLiteAppWidget,以及继承 GlanceAppWidgetReceiver 的 SociaLiteAppWidgetReceiver。
- 进入
app/src/main/java/com/google/android/samples/socialite/下的widget包。 - 打开
SociaLiteAppWidget,其中重写了provideGlance。 - 把
TODO替换为provideContent调用,并传入微件的 composable。暂时只显示“Hello World”,稍后再增加功能:
package com.google.android.samples.socialite.widget
import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.text.Text
class SociaLiteAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Text("Hello World")
}
}
}
}
4. 打开 widget 包的 SociaLiteAppWidgetReceiver。目前只需提供微件实例,稍后再扩展功能。
5. 将 TODO 替换为 SociaLiteAppWidget() 构造调用:
package com.google.android.samples.socialite.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class SociaLiteAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = SociaLiteAppWidget()
}
现在可以配置 Android,让它显示微件并允许用户将微件添加到主屏幕。
添加 app-widget provider 信息
- 右键点击
res/xml,选择 New > XML resource file。 - 文件名输入
socialite_widget_info,根元素填appwidget-provider,点击 OK。此文件包含AppWidgetHost初次显示微件所需的 metadata。 - 向
socialite_widget_info.xml添加以下内容:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="3600000"
android:minHeight="128dp"
android:minWidth="128dp"
android:minResizeHeight="128dp"
android:minResizeWidth="128dp"
android:configure="com.google.android.samples.socialite.widget.SociaLiteAppWidgetConfigActivity"
android:widgetFeatures="configuration_optional|reconfigurable"
android:previewImage="@drawable/widget_preview"
android:maxResizeHeight="512dp"
android:maxResizeWidth="512dp"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:initialLayout="@layout/glance_default_loading_layout">
</appwidget-provider>
这些属性的用途如下:
| 属性名 | 说明 |
|---|---|
resizeMode |
允许水平和垂直调整微件大小。 |
targetCellWidth, targetCellHeight |
指定首次加入主屏幕时的默认尺寸。 |
updatePeriodMillis |
控制宿主何时可能决定刷新微件。应用只要正在运行且有新信息,就可以主动更新微件。 |
minResizeHeight,minResizeWidth |
规定微件可以缩小到的最小尺寸。 |
minHeight,minWidth |
规定加入主屏幕时的最小默认尺寸。 |
initialLayout |
在 Glance 渲染 composable 期间显示初始布局。 |
previewImage |
为微件选择器提供静态预览图。 |
widgetFeatures |
向宿主提示微件支持的功能,标志本身不会改变微件行为。本例表示添加前不强制配置,而且添加后仍可重新配置。 |
configure |
指定用于后续配置微件的 Activity 类名。 |
包括 API 31 及更高版本功能在内的完整属性列表,见 AppWidgetProviderInfo。
更新 AndroidManifest 并测试
最后更新 AndroidManifest.xml。在 application 内定义一个子元素 receiver,处理 APPWIDGET_UPDATE Intent,并向 Android Launcher 提供微件 metadata。
- 为
SociaLiteAppWidgetReceiver创建导出的接收器,把下列内容放入application元素内部:
<receiver
android:name=".widget.SociaLiteAppWidgetReceiver"
android:exported="true"
android:label="Favorite Contact">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/socialite_widget_info" />
</receiver>
2. 编译并运行应用。
3. 应用启动后,将微件加入主屏幕。例如在 Pixel 上,长按背景,选择 Widgets > SociaLite,然后添加微件。

目前微件显示“Hello World”,背景透明,外观和功能都很简单。下一节将加入更复杂的布局,并用 Material Design 配色改善外观。
4. 改善设计
当前静态微件仍缺少许多优秀微件应有的特性。好的微件应保持内容新鲜、简短,功能简单;用可调整大小的布局减少不自然的空隙;从微件宿主的背景中应用配色。深入讨论见微件设计指南。
添加 Scaffold
用 Glance Scaffold 作为微件的顶层组件。它提供简单的插槽 API,用于显示带 TitleBar 的微件界面,并将背景色设为 GlanceTheme.colors.widgetBackground、应用内边距。
1. 用以下代码替换 SociaLiteAppWidget 的实现:
package com.google.android.samples.socialite.widget
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.components.Scaffold
import androidx.glance.appwidget.components.TitleBar
import androidx.glance.appwidget.provideContent
import androidx.glance.layout.fillMaxSize
import androidx.glance.text.Text
import com.google.android.samples.socialite.R
class SociaLiteAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme() {
Content()
}
}
}
@Composable
private fun Content() {
Scaffold(titleBar = {TitleBar(startIcon = ImageProvider(R.drawable.ic_launcher_monochrome), title = "SociaLite")},
modifier = GlanceModifier.fillMaxSize()) {
Text("Hello World")
}
}
}
2. 重新运行应用,并向主屏幕添加一个新的微件实例,以查看更新。

记住,微件是显示在外部宿主中的 RemoteViews。后面会加入从应用自动更新微件的功能;在那之前,每次修改代码都需要通过微件选择器重新添加实例,才能看到变化。
现在的外观已有改善,但与其他微件相比,颜色似乎仍不协调。主屏幕微件通常应跟随用户主题设置。通过动态颜色 token,可以让微件主题适应设备壁纸和主题。
添加动态配色
将 Scaffold 背景设置为 widgetBackground,将 TitleBar 文本及 Text 的颜色设置为 onSurface。修改文字样式需要导入 Glance 的 TextStyle;修改 Scaffold 背景则设置其 backgroundColor 属性。
1. 在 SociaLiteAppWidget.kt 中增加导入:
//Add to the imports section of your Kotlin code.
import androidx.glance.text.TextStyle
2. 更新 Content composable:
Scaffold(
titleBar = {
TitleBar(
textColor = GlanceTheme.colors.onSurface,
startIcon = ImageProvider(R.drawable.ic_launcher_monochrome),
title = "SociaLite",
)
},
backgroundColor = GlanceTheme.colors.widgetBackground,
modifier = GlanceModifier.fillMaxSize(),
) {
Text(text = "Hello World", style = TextStyle(color = GlanceTheme.colors.onSurface))
}
3. 重新运行应用,并向主屏幕添加新的微件实例。
微件现在会与主屏幕的其他微件保持主题协调,并在更换壁纸或切换深色模式时自动更新颜色。对于色彩丰富的壁纸,微件还会适应其所在位置的背景。


添加未配置时的初始状态
接下来考虑微件的状态和配置。微件加入主屏幕后若仍需配置,通常最好先显示零状态(zero state),提示用户完成配置。本例将添加一个配置 Activity,并从零状态跳转到它。
起始代码提供了存储、访问和修改微件配置状态的类。你将添加代码,让微件界面显示这些状态,并通过 lambda action 处理点击。
查看微件模型
查看 com.google.android.samples.socialite.widget.model 包中的 WidgetModel、WidgetModelDao 和 WidgetModelRepository。这些类已包含在起始代码中,负责把微件状态持久化到 Room 数据库,并使用 Hilt 管理生命周期。
WidgetModel 包含 Android 分配的 widgetId、显示的 SociaLite 联系人的 contactId、用于显示的 displayName 与 photo,以及表示联系人是否有未读消息的布尔值。SociaLiteAppWidget 的 composable 会读取这些数据。
WidgetModelDao 是封装数据库访问的数据访问对象。WidgetModelRepository 提供创建、读取、更新、删除 WidgetModel 实例的便捷函数。这些类由 Hilt 创建,通过依赖注入进入应用。
打开 app/src/main/java/com/google/android/samples/socialite/widget/model/WidgetModel.kt。这是一个带 Entity 注解的 data 类。Android 为每个微件实例分配独立 ID,SociaLite 将它用作模型数据主键;每个模型记录对应联系人的基本信息和未读消息状态:
@Entity(
foreignKeys = [
ForeignKey(
entity = Contact::class,
parentColumns = ["id"],
childColumns = ["contactId"],
onDelete = ForeignKey.CASCADE,
),
],
indices = [
Index("widgetId"),
Index("contactId"),
],
)
data class WidgetModel(
@PrimaryKey val widgetId: Int,
val contactId: Long,
val displayName: String,
val photo: String,
val unreadMessages: Boolean = false,
) : WidgetState
零状态
让 Content 从 WidgetModelRepository 加载模型。若无模型,则显示 ZeroState;否则显示正常内容。目前正常内容暂时仍是“Hello World”,下一部分再改进。使用 when 表达式选择 ZeroState 或 Text 占位内容。
1. 在 provideGlance 中、composable 外部获取仓库和当前微件 ID,在 provideContent 之前加入:
override suspend fun provideGlance(context: Context, id: GlanceId) {
val widgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
val repository = WidgetModelRepository.get(context)
可能还需要这些导入:
import com.google.android.samples.socialite.widget.model.WidgetModel
import com.google.android.samples.socialite.widget.model.WidgetModelRepository
import com.google.android.samples.socialite.widget.model.WidgetState.Loading
import androidx.glance.appwidget.GlanceAppWidgetManager
2. 给 Content 增加仓库和微件 ID 参数,用它们加载模型;更新函数签名并加入:
private fun Content(repository: WidgetModelRepository, widgetId: Int) {
val model = repository.loadModel(widgetId).collectAsState(Loading).value
3. 如果 Android Studio 没有自动添加导入,请手动补上:
import androidx.compose.runtime.collectAsState
同时更新 provideGlance,把 ID 和仓库传给 Content:
override suspend fun provideGlance(context: Context, id: GlanceId) {
val widgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
val repository = WidgetModelRepository.get(context)
provideContent {
GlanceTheme {
Content(repository, widgetId)
}
}
}
4. 根据模型是否存在决定显示状态。将 Scaffold 和微件内容移入 ZeroState,把原来的 Scaffold 及其内容替换为:
when (model) {
is WidgetModel -> {Text("Hello World")}
else -> ZeroState(widgetId)
}
起始代码已在 com.google.android.samples.socialite.widget.ui 包中提供 ZeroState。
5. 如果 IDE 未自动导入,在 SociaLiteAppWidget 中加入:
import com.google.android.samples.socialite.widget.ui.ZeroState
6. 重新运行应用并添加新的微件实例。你会看到零状态组件和一个按钮;点击按钮会打开配置 Activity。下一节将从该 Activity 更新微件状态。


配置 Activity
查看 com.google.android.samples.socialite.widget.ui 包中的 ZeroState.kt:
@Composable
fun ZeroState(widgetId: Int) {
val widgetIdKey = ActionParameters.Key<Int>(AppWidgetManager.EXTRA_APPWIDGET_ID)
Scaffold(
titleBar = {
TitleBar(
modifier = GlanceModifier.clickable(actionStartActivity(MainActivity::class.java)),
textColor = GlanceTheme.colors.onSurface,
startIcon = ImageProvider(R.drawable.ic_launcher_monochrome),
title = "SociaLite",
)
},
backgroundColor = GlanceTheme.colors.widgetBackground,
modifier = GlanceModifier.fillMaxSize(),
) {
Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(
text = "Select Favorite Contact",
onClick = actionStartActivity<SociaLiteAppWidgetConfigActivity>(
parameters = actionParametersOf(widgetIdKey to widgetId),
),
)
}
}
}
Scaffold 已移入 ZeroState。TitleBar 的 clickable 修饰符打开 SociaLite 主 Activity。Glance Button 提示用户采取行动,点击后打开 SociaLiteAppWidgetConfigActivity,并把微件 ID 作为 Intent extra 传入。这两个操作都使用 Glance 的 actionStartActivity 便捷函数。详见处理用户交互。
1. 查看 SociaLiteAppWidgetConfigActivity 如何更新配置。它是微件的配置 Activity,从 Intent 中读取键为 AppWidgetManager.EXTRA_APPWIDGET_ID 的整数 extra。更多说明见允许用户配置应用微件。
2. 在该 Activity 中,把 ContactRow 的 onClick 属性里的 TODO 替换为:
{
coroutineScope.launch {
widgetModelRepository.createOrUpdate(
WidgetModel(
appWidgetId,
contact.id,
contact.name,
contact.iconUri.toString(),
false,
),
)
SociaLiteAppWidget().updateAll(this@SociaLiteAppWidgetConfigActivity)
val resultValue = Intent().putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId,
)
setResult(RESULT_OK, resultValue)
finish()
}
}
如果 Android Studio 未自动导入,补上:
import com.google.android.samples.socialite.widget.model.WidgetModel
import androidx.glance.appwidget.updateAll
import kotlinx.coroutines.launch
代码先通过仓库保存或更新包含所选联系人信息的 WidgetModel,再调用挂起函数 updateAll 更新主屏幕上的所有微件。应用的任何位置都可以调用该函数。最后设置配置 Activity 的返回结果,表示更新成功,并结束 Activity。
3. 运行应用并重新放置微件,查看新的零状态:

4. 点击 Select favorite contact,进入配置界面:


5. 选择联系人后,微件会更新,但还不会显示联系人;下一部分会添加此功能。
管理微件数据
- 打开 App inspection 工具,必要时连接到进程,选择 Database inspector 标签页,查看应用数据库。
- 在微件中选择最喜爱的联系人,确认微件显示“Hello World”。回到 App inspection,Widget model 表中应有该微件的一条记录。可能需要刷新表,或启用 Live updates 才能看到变化。

3. 再添加一个微件并选择另一联系人;同样可能需要刷新表或启用实时更新。
4. 移除微件,观察数据库中仍残留模型记录。
可以在 SociaLiteAppWidgetReceiver 中重写 onDeleted 清理记录。调用 WidgetModelRepository.cleanupWidgetModels 即可清理孤立模型;仓库由 Hilt 管理,因此通过依赖注入获取实例。
5. 给接收器类加上 Hilt 的 AndroidEntryPoint 注解,并注入 WidgetModelRepository。
6. 在重写的 onDeleted 方法中调用清理函数:
package com.google.android.samples.socialite.widget
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import com.google.android.samples.socialite.widget.model.WidgetModelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SociaLiteAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = SociaLiteAppWidget()
@Inject
lateinit var repository: WidgetModelRepository
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
repository.cleanupWidgetModels(context)
}
}
7. 重新运行应用。移除主屏幕微件时,App inspector 中相应的模型行现在也应消失。
5. 添加联系人界面,并在收到新消息时更新
最后实现联系人界面,并在联系人有未读消息时更新微件。
1. 查看 model 包中的 WidgetModelRepository,其中的 updateUnreadMessagesForContact 根据联系人 ID 更新相关微件。以下代码已存在,无需再次添加:
//Don't add this code.
fun updateUnreadMessagesForContact(contactId: Long, unread: Boolean) {
coroutineScope.launch {
widgetModelDao.modelsForContact(contactId).filterNotNull().forEach { model ->
widgetModelDao.update(
WidgetModel(model.widgetId, model.contactId, model.displayName, model.photo, unread)
)
SociaLiteAppWidget().updateAll(appContext)
}
}
}
方法接收联系人 ID contactId 与表示未读状态的布尔值 unread。它通过 WidgetModelDao 找到所有显示该联系人的模型,更新已读状态,再调用 Glance 的 SociaLiteAppWidget().updateAll 更新用户主屏幕上的微件。
了解状态更新机制后,创建联系人界面、发送消息并观察更新。把 FavoriteContact composable 加入 SociaLiteAppWidget 布局,并根据状态决定显示没有新消息还是有新消息。
2. 查看 com.google.android.samples.socialite.widget.ui 中的 FavoriteContact.kt。以下代码已存在,无需添加:
//Don't add this code.
@Composable
fun FavoriteContact(model: WidgetModel, onClick: Action) {
Column(
modifier = GlanceModifier.fillMaxSize().clickable(onClick)
.background(GlanceTheme.colors.widgetBackground).appWidgetBackground()
.padding(bottom = 8.dp),
verticalAlignment = Alignment.Vertical.Bottom,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
) {
Image(
modifier = GlanceModifier.fillMaxWidth().wrapContentHeight().defaultWeight()
.cornerRadius(16.dp),
provider = ImageProvider(model.photo.toUri()),
contentScale = ContentScale.Crop,
contentDescription = model.displayName,
)
Column(
modifier = GlanceModifier.fillMaxWidth().wrapContentHeight().padding(top = 4.dp),
verticalAlignment = Alignment.Vertical.Bottom,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
) {
Text(
text = model.displayName,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
color = (GlanceTheme.colors.onSurface),
),
)
Text(
text = if (model.unreadMessages) "New Message!" else "No messages",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = (GlanceTheme.colors.onSurface),
),
)
}
}
}
3. 在 SociaLiteAppWidget 的 Content 中,用 FavoriteContact 替换 Text("Hello World")。它接收 WidgetModel 和通过 Glance actionStartActivity 创建的 Action。
4. 在 when 中为 WidgetModel 分支加入调用,其他情况继续显示 ZeroState:
when (model) {
is WidgetModel -> FavoriteContact(model = model, onClick = actionStartActivity(
Intent(LocalContext.current.applicationContext, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.setData("https://socialite.google.com/chat/${model.contactId}".toUri()))
)
else -> ZeroState(widgetId)
}
5. 如果 IDE 未自动导入,补上:
import com.google.android.samples.socialite.widget.ui.FavoriteContact
import androidx.glance.appwidget.action.actionStartActivity
import android.content.Intent
import com.google.android.samples.socialite.MainActivity
import androidx.core.net.toUri
6. 运行应用。
7. 选择最喜爱的联系人,发送一条消息,并在对方回复前立即退出应用。回复到达时,微件状态应改变。
8. 点击微件打开聊天,回到主屏幕时观察状态再次更新。

6. 完成
完成本练习后,你已经学习了如何使用 Glance 编写微件:适应不同主屏幕的外观,处理用户输入,并主动更新内容。
要获取 main 分支中的解决方案代码,如果已下载 SociaLite,执行:
git checkout main
否则重新克隆仓库,查看默认的 main 分支:
git clone git@github.com:android/socialite.git











暂无评论内容