Jetpack Glance 深度解析

2024-12-23 · 55-65 min · Glance 框架

Jetpack Glance 是 Google 推出的用于构建桌面小部件和穿戴设备界面的新框架,它允许开发者使用类似 Compose 的声明式 API 来创建这些界面。本文将深入探讨 Glance 的核心概念、架构设计、组件体系、状态管理以及实战技巧。

📚 官方参考

目录

I. Glance 核心架构

1.1 Glance 的工作原理

Glance 并非直接渲染 UI,而是作为一个"翻译器",将 Compose 风格的代码转换为 RemoteViews。


        Glance Composable
       ↓
  Glance Composition
       ↓
  Translation Layer
       ↓
   RemoteViews
       ↓
  System Launcher
        

关键理解:

1.2 核心类关系


        // 核心抽象类
abstract class GlanceAppWidget : GlanceAppWidgetReceiver() {
    // 定义状态存储方式
    open val stateDefinition: GlanceStateDefinition<*>?
    
    // 定义 UI 内容
    abstract suspend fun provideGlance(context: Context, id: GlanceId)
    
    // 尺寸模式
    open val sizeMode: SizeMode = SizeMode.Single
}

// 接收器基类
abstract class GlanceAppWidgetReceiver : BroadcastReceiver() {
    abstract val glanceAppWidget: GlanceAppWidget
}
        

1.3 基本项目结构


        // 1. 定义 Widget 类
class MyWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            MyWidgetContent()
        }
    }
}

// 2. 定义 Receiver
class MyWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyWidget()
}

// 3. 定义 UI
@Composable
fun MyWidgetContent() {
    Text("Hello Glance!")
}
        

II. Glance 组件详解

2.1 布局组件 (Layout Composables)

Glance 提供了与标准 Compose 类似但独立的布局组件。

Column - 垂直布局

        @Composable
fun VerticalLayoutExample() {
    Column(
        modifier = GlanceModifier
            .fillMaxSize()
            .padding(16.dp),
        verticalAlignment = Alignment.Vertical.Top,
        horizontalAlignment = Alignment.Horizontal.Start
    ) {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
}
        
Row - 水平布局

        @Composable
fun HorizontalLayoutExample() {
    Row(
        modifier = GlanceModifier
            .fillMaxWidth()
            .padding(16.dp),
        verticalAlignment = Alignment.Vertical.CenterVertically,
        horizontalAlignment = Alignment.Horizontal.SpaceBetween
    ) {
        Image(
            provider = ImageProvider(R.drawable.icon),
            contentDescription = "Icon"
        )
        Text("Title", style = TextStyle(fontSize = 18.sp))
        Button(text = "Action", onClick = { })
    }
}
        
Box - 层叠布局

        @Composable
fun StackedLayoutExample() {
    Box(
        modifier = GlanceModifier.size(200.dp),
        contentAlignment = Alignment.Center
    ) {
        // 背景层
        Image(
            provider = ImageProvider(R.drawable.background),
            contentDescription = null,
            modifier = GlanceModifier.fillMaxSize()
        )
        
        // 前景层
        Column(
            modifier = GlanceModifier.padding(16.dp),
            horizontalAlignment = Alignment.Horizontal.CenterHorizontally
        ) {
            Text("Overlay Text", style = TextStyle(color = ColorProvider(Color.White)))
            Button(text = "Action", onClick = { })
        }
    }
}
        

2.2 基础组件 (Basic Composables)

Text - 文本组件

        @Composable
fun TextExamples() {
    Column(verticalAlignment = Alignment.Vertical.Top) {
        // 基础文本
        Text(text = "Simple Text")
        
        // 样式化文本
        Text(
            text = "Styled Text",
            style = TextStyle(
                color = ColorProvider(Color.Blue),
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold,
                fontStyle = FontStyle.Italic,
                textAlign = TextAlign.Center,
                textDecoration = TextDecoration.Underline
            )
        )
        
        // 限制行数
        Text(
            text = "Long text that will be truncated...",
            maxLines = 1,
            style = TextStyle(
                textOverflow = TextOverflow.Ellipsis
            )
        )
    }
}
        
Image - 图片组件

        @Composable
fun ImageExamples() {
    Column {
        // 1. 资源图片
        Image(
            provider = ImageProvider(R.drawable.my_image),
            contentDescription = "My Image",
            modifier = GlanceModifier.size(64.dp)
        )
        
        // 2. 图标(tinted)
        Image(
            provider = ImageProvider(R.drawable.ic_star),
            contentDescription = "Star",
            colorFilter = ColorFilter.tint(ColorProvider(Color.Yellow)),
            modifier = GlanceModifier.size(24.dp)
        )
        
        // 3. 位图(Bitmap)
        val bitmap: Bitmap = loadBitmap()
        Image(
            provider = ImageProvider(bitmap),
            contentDescription = "Dynamic Image"
        )
    }
}
        
Button - 按钮组件

        @Composable
fun ButtonExamples() {
    Column(verticalAlignment = Alignment.Vertical.Top) {
        // 标准按钮
        Button(
            text = "Click Me",
            onClick = actionRunCallback<MyCallback>()
        )
        
        // 自定义样式按钮
        Button(
            text = "Styled Button",
            onClick = actionStartActivity<MainActivity>(),
            style = TextStyle(
                color = ColorProvider(Color.White),
                fontSize = 16.sp
            ),
            colors = ButtonDefaults.buttonColors(
                backgroundColor = ColorProvider(Color.Blue),
                contentColor = ColorProvider(Color.White)
            ),
            modifier = GlanceModifier
                .fillMaxWidth()
                .height(48.dp)
        )
        
        // 图标按钮
        Button(
            text = "Refresh",
            icon = ImageProvider(R.drawable.ic_refresh),
            onClick = actionRunCallback<RefreshCallback>()
        )
    }
}
        
CircularProgressIndicator - 加载指示器

        @Composable
fun LoadingExample() {
    Box(
        modifier = GlanceModifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        CircularProgressIndicator(
            color = ColorProvider(Color.Blue),
            modifier = GlanceModifier.size(48.dp)
        )
    }
}
        
LinearProgressIndicator - 进度条

        @Composable
fun ProgressExample(progress: Float) {
    Column {
        Text("Download Progress: ${(progress * 100).toInt()}%")
        LinearProgressIndicator(
            progress = progress,
            color = ColorProvider(Color.Green),
            backgroundColor = ColorProvider(Color.LightGray),
            modifier = GlanceModifier
                .fillMaxWidth()
                .height(8.dp)
        )
    }
}
        

2.3 复合组件 (Compound Composables)

Switch - 开关组件

        @Composable
fun SwitchExample() {
    var isEnabled by remember { mutableStateOf(false) }
    
    Row(
        verticalAlignment = Alignment.Vertical.CenterVertically,
        modifier = GlanceModifier.fillMaxWidth()
    ) {
        Text("Enable Notifications", modifier = GlanceModifier.defaultWeight())
        Switch(
            checked = isEnabled,
            onCheckedChange = actionRunCallback<ToggleSwitchCallback>()
        )
    }
}
        
CheckBox - 复选框

        @Composable
fun CheckBoxExample() {
    Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
        CheckBox(
            checked = true,
            onCheckedChange = actionRunCallback<ToggleCheckBoxCallback>()
        )
        Text("I agree to terms")
    }
}
        

2.4 懒加载列表 (LazyColumn)

Glance 1.0+ 支持懒加载列表。


        @Composable
fun LazyListExample(items: List<String>) {
    LazyColumn(
        modifier = GlanceModifier.fillMaxSize()
    ) {
        // 单个 item
        item {
            Text("Header", style = TextStyle(fontWeight = FontWeight.Bold))
        }
        
        // 多个 items
        items(items.size) { index ->
            ListItemRow(items[index])
        }
        
        // 带 key 的 items
        items(
            count = items.size,
            key = { index -> items[index] }
        ) { index ->
            ListItemRow(items[index])
        }
    }
}

@Composable
fun ListItemRow(text: String) {
    Row(
        modifier = GlanceModifier
            .fillMaxWidth()
            .padding(vertical = 8.dp, horizontal = 16.dp)
            .clickable(actionRunCallback<ItemClickCallback>()),
        verticalAlignment = Alignment.Vertical.CenterVertically
    ) {
        Image(
            provider = ImageProvider(R.drawable.ic_item),
            contentDescription = null,
            modifier = GlanceModifier.size(24.dp)
        )
        Spacer(modifier = GlanceModifier.width(8.dp))
        Text(text)
    }
}
        

III. 状态管理深入

3.1 使用 DataStore 管理状态

Glance 推荐使用 Preferences DataStore 来持久化小部件状态。


        // 1. 定义 Preferences Keys
object WidgetPreferences {
    val TEMPERATURE_KEY = intPreferencesKey("temperature")
    val CITY_KEY = stringPreferencesKey("city")
    val LAST_UPDATE_KEY = longPreferencesKey("last_update")
}

// 2. 定义 StateDefinition
object WeatherStateDefinition : GlanceStateDefinition<Preferences> {
    
    private const val DATA_STORE_FILE = "weather_widget_prefs"
    
    override suspend fun getDataStore(
        context: Context,
        fileKey: String
    ): DataStore<Preferences> {
        return context.dataStore
    }
    
    override fun getLocation(context: Context, fileKey: String): File {
        return context.dataStoreFile(DATA_STORE_FILE)
    }
    
    private val Context.dataStore by preferencesDataStore(name = DATA_STORE_FILE)
}

// 3. 在 GlanceAppWidget 中使用
class WeatherWidget : GlanceAppWidget() {
    
    override val stateDefinition = WeatherStateDefinition
    
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            // 读取状态
            val prefs = currentState<Preferences>()
            val temperature = prefs[WidgetPreferences.TEMPERATURE_KEY] ?: 0
            val city = prefs[WidgetPreferences.CITY_KEY] ?: "Unknown"
            
            WeatherWidgetContent(
                temperature = temperature,
                city = city
            )
        }
    }
}
        

3.2 更新状态


        // 在 ActionCallback 中更新状态
class RefreshWeatherCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        // 1. 获取新数据
        val weatherData = fetchWeatherFromApi()
        
        // 2. 更新 DataStore
        updateAppWidgetState(context, glanceId) { prefs ->
            prefs[WidgetPreferences.TEMPERATURE_KEY] = weatherData.temperature
            prefs[WidgetPreferences.CITY_KEY] = weatherData.city
            prefs[WidgetPreferences.LAST_UPDATE_KEY] = System.currentTimeMillis()
        }
        
        // 3. 触发 Widget 更新
        WeatherWidget().update(context, glanceId)
    }
}

// 在 Activity/Service 中更新状态
class WeatherUpdateWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        val weatherData = fetchWeatherFromApi()
        
        // 更新所有 Widget 实例
        GlanceAppWidgetManager(applicationContext)
            .getGlanceIds(WeatherWidget::class.java)
            .forEach { glanceId ->
                updateAppWidgetState(applicationContext, glanceId) { prefs ->
                    prefs[WidgetPreferences.TEMPERATURE_KEY] = weatherData.temperature
                    prefs[WidgetPreferences.CITY_KEY] = weatherData.city
                }
            }
        
        // 刷新所有实例
        WeatherWidget().updateAll(applicationContext)
        
        return Result.success()
    }
}
        

3.3 使用自定义状态类


        // 1. 定义状态数据类
@Serializable
data class WeatherState(
    val city: String = "Unknown",
    val temperature: Int = 0,
    val condition: WeatherCondition = WeatherCondition.UNKNOWN,
    val lastUpdate: Long = 0
)

enum class WeatherCondition {
    SUNNY, CLOUDY, RAINY, SNOWY, UNKNOWN
}

// 2. 定义自定义 StateDefinition
object CustomWeatherStateDefinition : GlanceStateDefinition<WeatherState> {
    
    private const val DATA_STORE_FILE = "custom_weather_state"
    private val Context.dataStore by dataStore(DATA_STORE_FILE, WeatherStateSerializer)
    
    override suspend fun getDataStore(
        context: Context,
        fileKey: String
    ): DataStore<WeatherState> {
        return context.dataStore
    }
    
    override fun getLocation(context: Context, fileKey: String): File {
        return context.dataStoreFile(DATA_STORE_FILE)
    }
}

// 3. 定义 Serializer
object WeatherStateSerializer : Serializer<WeatherState> {
    override val defaultValue: WeatherState = WeatherState()
    
    override suspend fun readFrom(input: InputStream): WeatherState {
        return Json.decodeFromString(input.readBytes().decodeToString())
    }
    
    override suspend fun writeTo(t: WeatherState, output: OutputStream) {
        output.write(Json.encodeToString(t).encodeToByteArray())
    }
}

// 4. 在 Widget 中使用
class WeatherWidget : GlanceAppWidget() {
    override val stateDefinition = CustomWeatherStateDefinition
    
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            val state = currentState<WeatherState>()
            WeatherWidgetContent(state)
        }
    }
}
        

IV. Action 系统

4.1 Action 类型详解

Glance 提供了多种 Action 类型来处理用户交互。

actionStartActivity - 启动 Activity

        @Composable
fun ActivityLaunchExample() {
    Column {
        // 1. 启动不带参数的 Activity
        Button(
            text = "Open App",
            onClick = actionStartActivity<MainActivity>()
        )
        
        // 2. 启动带参数的 Activity
        Button(
            text = "View Details",
            onClick = actionStartActivity(
                activity = DetailActivity::class.java,
                parameters = actionParametersOf(
                    ActionParameters.Key<String>("item_id") to "123",
                    ActionParameters.Key<Int>("page") to 1
                )
            )
        )
        
        // 3. 使用 Intent 启动
        Button(
            text = "Custom Intent",
            onClick = actionStartActivity(
                Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
            )
        )
    }
}
        
actionRunCallback - 执行回调

        // 1. 定义 Callback
class UpdateDataCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        // 获取参数
        val itemId = parameters[ActionParameters.Key<String>("item_id")]
        
        // 执行业务逻辑
        val result = performUpdate(itemId)
        
        // 更新状态
        updateAppWidgetState(context, glanceId) { prefs ->
            prefs[resultKey] = result
        }
        
        // 刷新 Widget
        MyWidget().update(context, glanceId)
    }
}

// 2. 使用 Callback
@Composable
fun CallbackExample() {
    Button(
        text = "Update",
        onClick = actionRunCallback<UpdateDataCallback>(
            parameters = actionParametersOf(
                ActionParameters.Key<String>("item_id") to "456"
            )
        )
    )
}
        
actionSendBroadcast - 发送广播

        @Composable
fun BroadcastExample() {
    Button(
        text = "Send Broadcast",
        onClick = actionSendBroadcast(
            Intent("com.example.ACTION_CUSTOM").apply {
                putExtra("key", "value")
            }
        )
    )
}

// 接收广播
class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val value = intent.getStringExtra("key")
        // 处理广播
    }
}
        
actionStartService - 启动服务

        @Composable
fun ServiceExample() {
    Button(
        text = "Start Service",
        onClick = actionStartService<MyForegroundService>(
            isForegroundService = true
        )
    )
}
        

4.2 复合 Action(组合多个操作)


        @Composable
fun CompoundActionExample() {
    // 方式1:链式调用(不支持,需要自定义)
    // 方式2:使用自定义 Callback
    Button(
        text = "Complex Action",
        onClick = actionRunCallback<ComplexActionCallback>()
    )
}

class ComplexActionCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        // 步骤1:更新数据
        updateData()
        
        // 步骤2:发送通知
        sendNotification(context)
        
        // 步骤3:启动后台同步
        startSync(context)
        
        // 步骤4:更新 Widget
        updateAppWidgetState(context, glanceId) { prefs ->
            prefs[lastActionKey] = System.currentTimeMillis()
        }
        MyWidget().update(context, glanceId)
    }
}
        

4.3 带参数的 Action


        @Composable
fun ParameterizedActionExample(items: List<Item>) {
    LazyColumn {
        items(items.size) { index ->
            val item = items[index]
            
            Row(
                modifier = GlanceModifier
                    .fillMaxWidth()
                    .clickable(
                        actionRunCallback<ItemClickCallback>(
                            parameters = actionParametersOf(
                                ActionParameters.Key<String>("item_id") to item.id,
                                ActionParameters.Key<Int>("position") to index
                            )
                        )
                    )
            ) {
                Text(item.title)
            }
        }
    }
}

class ItemClickCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        val itemId = parameters[ActionParameters.Key<String>("item_id")]
        val position = parameters[ActionParameters.Key<Int>("position")]
        
        // 使用参数执行操作
        Log.d("ItemClick", "Clicked item $itemId at position $position")
    }
}
        

V. 尺寸适配与响应式设计

5.1 SizeMode 配置

Glance 提供了三种尺寸模式。


        class MyWidget : GlanceAppWidget() {
    
    // 模式1:Single - 单一尺寸(默认)
    override val sizeMode: SizeMode = SizeMode.Single
    
    // 模式2:Exact - 精确尺寸
    override val sizeMode: SizeMode = SizeMode.Exact
    
    // 模式3:Responsive - 响应式(多套布局)
    override val sizeMode: SizeMode = SizeMode.Responsive(
        setOf(
            DpSize(100.dp, 100.dp),  // 小
            DpSize(200.dp, 100.dp),  // 中
            DpSize(300.dp, 200.dp)   // 大
        )
    )
}
        

5.2 根据尺寸切换布局


        @Composable
fun ResponsiveWidget() {
    val size = LocalSize.current
    
    when {
        size.width < 150.dp -> SmallWidget()
        size.width < 250.dp -> MediumWidget()
        else -> LargeWidget()
    }
}

@Composable
fun SmallWidget() {
    Column(
        modifier = GlanceModifier.fillMaxSize().padding(8.dp),
        horizontalAlignment = Alignment.Horizontal.CenterHorizontally
    ) {
        Image(
            provider = ImageProvider(R.drawable.icon),
            contentDescription = null,
            modifier = GlanceModifier.size(32.dp)
        )
        Text("25°C", style = TextStyle(fontSize = 18.sp))
    }
}

@Composable
fun MediumWidget() {
    Row(
        modifier = GlanceModifier.fillMaxSize().padding(12.dp),
        verticalAlignment = Alignment.Vertical.CenterVertically
    ) {
        Image(
            provider = ImageProvider(R.drawable.icon),
            contentDescription = null,
            modifier = GlanceModifier.size(48.dp)
        )
        Spacer(modifier = GlanceModifier.width(8.dp))
        Column {
            Text("Beijing", style = TextStyle(fontSize = 14.sp))
            Text("25°C", style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold))
        }
    }
}

@Composable
fun LargeWidget() {
    Column(
        modifier = GlanceModifier.fillMaxSize().padding(16.dp)
    ) {
        Row(
            modifier = GlanceModifier.fillMaxWidth(),
            verticalAlignment = Alignment.Vertical.CenterVertically
        ) {
            Image(
                provider = ImageProvider(R.drawable.icon),
                contentDescription = null,
                modifier = GlanceModifier.size(64.dp)
            )
            Spacer(modifier = GlanceModifier.width(16.dp))
            Column {
                Text("Beijing", style = TextStyle(fontSize = 18.sp))
                Text("25°C", style = TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold))
                Text("Sunny", style = TextStyle(fontSize = 14.sp, color = ColorProvider(Color.Gray)))
            }
        }
        Spacer(modifier = GlanceModifier.height(16.dp))
        // 更多详细信息...
    }
}
        

5.3 使用 LocalSize 实现细粒度控制


        @Composable
fun AdaptiveTextSize() {
    val size = LocalSize.current
    
    val fontSize = when {
        size.width < 120.dp -> 14.sp
        size.width < 200.dp -> 16.sp
        else -> 20.sp
    }
    
    Text(
        text = "Adaptive Text",
        style = TextStyle(fontSize = fontSize)
    )
}
        

VI. 主题与样式

6.1 动态颜色(Android 12+)


        @Composable
fun DynamicColorWidget() {
    // 使用 ColorProvider 支持动态颜色
    Column(
        modifier = GlanceModifier
            .fillMaxSize()
            .background(
                day = Color.White,
                night = Color.Black
            )
            .padding(16.dp)
    ) {
        Text(
            text = "Dynamic Colors",
            style = TextStyle(
                color = ColorProvider(
                    day = Color.Black,
                    night = Color.White
                )
            )
        )
        
        Button(
            text = "Action",
            onClick = { },
            colors = ButtonDefaults.buttonColors(
                backgroundColor = ColorProvider(
                    day = Color(0xFF6200EE),
                    night = Color(0xFFBB86FC)
                )
            )
        )
    }
}
        

6.2 自定义主题系统


        // 定义主题对象
object WidgetTheme {
    val colors = WidgetColors(
        primary = Color(0xFF6200EE),
        onPrimary = Color.White,
        surface = Color.White,
        onSurface = Color.Black
    )
    
    val typography = WidgetTypography(
        title = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold),
        body = TextStyle(fontSize = 14.sp),
        caption = TextStyle(fontSize = 12.sp, color = ColorProvider(Color.Gray))
    )
}

data class WidgetColors(
    val primary: Color,
    val onPrimary: Color,
    val surface: Color,
    val onSurface: Color
)

data class WidgetTypography(
    val title: TextStyle,
    val body: TextStyle,
    val caption: TextStyle
)

// 使用主题
@Composable
fun ThemedWidget() {
    Column(
        modifier = GlanceModifier
            .fillMaxSize()
            .background(WidgetTheme.colors.surface)
            .padding(16.dp)
    ) {
        Text("Title", style = WidgetTheme.typography.title)
        Text("Body text", style = WidgetTheme.typography.body)
        Text("Caption", style = WidgetTheme.typography.caption)
    }
}
        

VII. 高级特性

7.1 多实例管理


        class MultiInstanceWidget : GlanceAppWidget() {
    
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // 每个实例可以有独立的状态
        val instanceState = getInstanceState(context, id)
        
        provideContent {
            WidgetContent(
                instanceId = id.toString(),
                config = instanceState
            )
        }
    }
    
    private suspend fun getInstanceState(context: Context, id: GlanceId): InstanceConfig {
        val prefs = context.dataStore.data.first()
        return InstanceConfig(
            city = prefs[stringPreferencesKey("city_${id}")] ?: "Beijing",
            showDetails = prefs[booleanPreferencesKey("details_${id}")] ?: false
        )
    }
}

// 更新特定实例
suspend fun updateSpecificInstance(context: Context, glanceId: GlanceId, city: String) {
    updateAppWidgetState(context, glanceId) { prefs ->
        prefs[stringPreferencesKey("city_${glanceId}")] = city
    }
    MultiInstanceWidget().update(context, glanceId)
}

// 更新所有实例
suspend fun updateAllInstances(context: Context) {
    val glanceIds = GlanceAppWidgetManager(context)
        .getGlanceIds(MultiInstanceWidget::class.java)
    
    glanceIds.forEach { glanceId ->
        updateSpecificInstance(context, glanceId, "Shanghai")
    }
}
        

7.2 配置 Activity 集成


        class WidgetConfigActivity : AppCompatActivity() {
    
    private var glanceId: GlanceId? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 获取 GlanceId
        val extras = intent.extras
        if (extras != null) {
            glanceId = GlanceAppWidgetManager(this)
                .getGlanceIdBy(extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID
                ))
        }
        
        setContent {
            ConfigScreen(
                onSave = { config ->
                    lifecycleScope.launch {
                        saveConfig(config)
                        finish()
                    }
                }
            )
        }
    }
    
    private suspend fun saveConfig(config: WidgetConfig) {
        glanceId?.let { id ->
            updateAppWidgetState(this, id) { prefs ->
                prefs[cityKey] = config.city
                prefs[themeKey] = config.theme
            }
            MyWidget().update(this, id)
        }
    }
}
        

7.3 WorkManager 集成实现定期更新


        class WidgetUpdateWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        return try {
            // 1. 获取新数据
            val data = fetchDataFromNetwork()
            
            // 2. 更新所有 Widget 实例
            val glanceIds = GlanceAppWidgetManager(applicationContext)
                .getGlanceIds(MyWidget::class.java)
            
            glanceIds.forEach { glanceId ->
                updateAppWidgetState(applicationContext, glanceId) { prefs ->
                    prefs[dataKey] = data.toString()
                    prefs[lastUpdateKey] = System.currentTimeMillis()
                }
            }
            
            // 3. 刷新 UI
            MyWidget().updateAll(applicationContext)
            
            Result.success()
        } catch (e: Exception) {
            Log.e("WidgetUpdateWorker", "Update failed", e)
            Result.retry()
        }
    }
}

// 调度 Worker
fun scheduleWidgetUpdates(context: Context) {
    val updateRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
        15, TimeUnit.MINUTES
    )
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        )
        .build()
    
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "WidgetUpdate",
        ExistingPeriodicWorkPolicy.KEEP,
        updateRequest
    )
}
        

VIII. 性能优化与调试

8.1 性能优化建议

减少不必要的更新

        // ❌ 不好:频繁更新
class BadWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // 每次都重新计算
        val heavyData = performHeavyComputation()
        provideContent {
            Content(heavyData)
        }
    }
}

// ✅ 好:缓存和条件更新
class GoodWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val prefs = currentState<Preferences>()
        val lastUpdate = prefs[lastUpdateKey] ?: 0L
        val now = System.currentTimeMillis()
        
        // 只在需要时更新
        val data = if (now - lastUpdate > 5 * 60 * 1000) {
            val newData = performHeavyComputation()
            updateAppWidgetState(context, id) { p ->
                p[dataKey] = newData
                p[lastUpdateKey] = now
            }
            newData
        } else {
            prefs[dataKey] ?: ""
        }
        
        provideContent {
            Content(data)
        }
    }
}
        
图片优化

        // ✅ 使用合适尺寸的图片
@Composable
fun OptimizedImage() {
    Image(
        provider = ImageProvider(R.drawable.icon_small), // 使用小尺寸图片
        contentDescription = null,
        modifier = GlanceModifier.size(24.dp)
    )
}

// ✅ 异步加载并缓存
class ImageCacheManager(private val context: Context) {
    private val cache = LruCache<String, Bitmap>(10 * 1024 * 1024) // 10MB
    
    suspend fun loadImage(url: String): Bitmap? {
        return cache.get(url) ?: downloadAndCache(url)
    }
    
    private suspend fun downloadAndCache(url: String): Bitmap? {
        return withContext(Dispatchers.IO) {
            try {
                val bitmap = downloadBitmap(url)
                cache.put(url, bitmap)
                bitmap
            } catch (e: Exception) {
                null
            }
        }
    }
}
        

8.2 调试技巧

日志调试

        class DebugWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        Log.d("DebugWidget", "provideGlance called for id: $id")
        
        val prefs = currentState<Preferences>()
        Log.d("DebugWidget", "Current state: $prefs")
        
        provideContent {
            DebugContent()
        }
    }
}

@Composable
fun DebugContent() {
    val size = LocalSize.current
    Log.d("DebugWidget", "Current size: ${size.width} x ${size.height}")
    
    Column {
        Text("Debug Mode")
        Text("Size: ${size.width} x ${size.height}")
    }
}
        
使用 Preview(有限支持)

        // Glance Preview 功能有限,主要用于布局预览
@Preview
@Composable
fun WidgetPreview() {
    // 模拟数据
    WeatherWidgetContent(
        temperature = 25,
        city = "Beijing",
        condition = WeatherCondition.SUNNY
    )
}
        

8.3 常见问题排查

问题1:Widget 不更新

        // 解决方案:确保调用 update
class RefreshCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        updateAppWidgetState(context, glanceId) { prefs ->
            prefs[dataKey] = "new data"
        }
        
        // ⚠️ 必须调用 update
        MyWidget().update(context, glanceId)
    }
}
        
问题2:状态不持久化

        // ❌ 错误:使用 remember(不持久化)
@Composable
fun BadState() {
    var count by remember { mutableStateOf(0) }  // 不会保存
    Button(text = "Count: $count", onClick = { count++ })
}

// ✅ 正确:使用 DataStore
@Composable
fun GoodState() {
    val prefs = currentState<Preferences>()
    val count = prefs[countKey] ?: 0
    Button(
        text = "Count: $count",
        onClick = actionRunCallback<IncrementCallback>()
    )
}

class IncrementCallback : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        updateAppWidgetState(context, glanceId) { prefs ->
            val current = prefs[countKey] ?: 0
            prefs[countKey] = current + 1
        }
        MyWidget().update(context, glanceId)
    }
}
        
问题3:点击事件不响应

        // ❌ 错误:使用标准 Compose 的 clickable
Row(modifier = Modifier.clickable { }) {  // 不工作
    Text("Click me")
}

// ✅ 正确:使用 Glance 的 clickable
Row(
    modifier = GlanceModifier.clickable(
        actionRunCallback<ClickCallback>()
    )
) {
    Text("Click me")
}
        

总结

Jetpack Glance 是构建桌面小部件的现代化方案,核心要点:

架构理解

组件使用

状态管理

Action 系统

响应式设计

性能优化

---

推荐阅读

📚 官方参考
Jetpack Glance Documentation - Android Developers
Glance App Widget Samples - GitHub
Build app widgets with Glance - Android Developers