Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6d312fe
Add local and shared state to Remote Compose Android Pet widget
ashnohe Oct 12, 2026
b16e843
Make StoreActivity accessible only from within Android Pet widget
ashnohe Oct 12, 2026
71268ce
Remove unit tests and test-only bridge from Android Pet widget
ashnohe Oct 12, 2026
3588d11
Remove redundant onReceive and manual updateAppWidget from AndroidPet…
ashnohe Oct 12, 2026
28c7983
Simplify widget registration and clarify state comments in AndroidPet…
ashnohe Oct 12, 2026
b95dd8e
Remove unnecessary getIdForCreationState RemoteCanvas block from Andr…
ashnohe Oct 12, 2026
00b90fc
Simplify BuddyPreferences by removing BuddyRepository and redundant t…
ashnohe Oct 13, 2026
099eaa7
Remove redundant onUpdate override from AndroidPetWidget
ashnohe Oct 13, 2026
5aabd57
Simplify ClaimCreditButton, StoreButton, and WidgetActionBar
ashnohe Oct 13, 2026
310a809
Simplify StoreActivity state loading and card UI
ashnohe Oct 13, 2026
6d279cf
Match StoreActivity visual design to PetBuddyStore
ashnohe Oct 13, 2026
d141218
Move pet onTouchDown directly onto RemoteColumn and remove inner Remo…
ashnohe Oct 13, 2026
87106b2
Use RemoteImage instead of RemoteBox and RemoteCanvas in ClaimCreditB…
ashnohe Oct 13, 2026
d8c67c3
Simplify StoreActivity with dynamic colors, trimmed typography, and r…
ashnohe Oct 13, 2026
d32f388
Clean up StoreActivity by removing StoreEmptyStateCard, unused modifi…
ashnohe Oct 13, 2026
60f0252
Flatten StoreActivity Compose UI hierarchy and use M3 FilterChip
ashnohe Oct 13, 2026
209a7ae
Use LazyRow in BuddySelectorBar and simplify StoreTheme and modifiers
ashnohe Oct 13, 2026
0d7a46c
Use Scaffold topBar slot and Material 3 TopAppBar in StoreActivity
ashnohe Oct 13, 2026
3442480
Remove WidgetAssetProvider and decode drawables directly with toBitmap()
ashnohe Oct 13, 2026
9077895
refactoring
ashnohe Oct 13, 2026
a2e12c6
Address PR review feedback in README, StoreButton, and ClaimCreditButton
ashnohe Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions samples/user-interface/appwidgets/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
# AppWidgets samples

This module demonstrates how to build Android AppWidgets with [Jetpack Glance](https://developer.android.com/jetpack/androidx/releases/glance).
This module demonstrates how to build Android AppWidgets with [Jetpack Glance](https://developer.android.com/jetpack/androidx/releases/glance) and [Remote Compose](https://developer.android.com/jetpack/androidx/releases/compose-remote).

The new set of APIs allows an application to build more engaging and beautiful widgets. Use this
sample together with the
[App widget guides](https://developer.android.com/guide/topics/appwidgets/overview).

# Pre-requisites

* Android 12 (API level 31)
* Android 12 (API level 31) for Jetpack Glance samples; Android 17 (API level 37) for Remote Compose samples
* Android Studio Chipmunk or newer

# Getting Started

## Remote Compose Widget Sample (`remote_compose`)

The [`remote_compose`](./src/main/java/com/example/platform/ui/appwidgets/remote_compose) directory contains the **Android Pet Widget** ([`AndroidPetWidget.kt`](./src/main/java/com/example/platform/ui/appwidgets/remote_compose/AndroidPetWidget.kt)) and [`StoreActivity.kt`](./src/main/java/com/example/platform/ui/appwidgets/remote_compose/StoreActivity.kt). This sample is a work in progress. We will add more features in the coming months.

### When to use Jetpack Glance vs. Remote Compose

For most cases, use [Jetpack Glance](https://developer.android.com/develop/ui/compose/glance) to build Android widgets. Jetpack Glance is a backwards-compatible library with developer tools (such as Canonical Layouts) that help you build high-quality widgets across form factors.

However, you cannot build some widget use cases with Jetpack Glance, such as widgets that require animations. Jetpack Glance uses the legacy `RemoteViews` system:
* `RemoteViews` cannot evaluate mathematical expressions on the host (for example, to continuously rotate an object).
* The Android system controls `RemoteViews` and limits the size and frequency of widget updates.

Remote Compose does **not** replace Jetpack Glance. Build most production widgets with Jetpack Glance for backwards compatibility and stable developer tools. Remote Compose widgets work best on Android 17 and higher devices.

> **Note:** Remote Compose is in alpha and the API surface can change. Send us your [feedback](https://issuetracker.google.com/issues/new?component=1984647&template=2233909).

## Canonical Widget Layout Samples

These [layouts](./src/main/java/com/example/platform/ui/appwidgets/glance/layout) demonstrate how
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@
</receiver>

<!-- Remote Compose Android Pet Widget -->
<activity
android:name=".remote_compose.StoreActivity"
android:exported="false"
android:label="@string/android_pet_store_title" />

<receiver
android:name=".remote_compose.AndroidPetWidget"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,77 @@
package com.example.platform.ui.appwidgets.remote_compose

import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.remote.creation.compose.action.valueChange
import androidx.compose.remote.creation.compose.layout.RemoteAlignment
import androidx.compose.remote.creation.compose.layout.RemoteBox
import androidx.compose.remote.creation.compose.layout.RemoteColumn
import androidx.compose.remote.creation.compose.layout.RemoteComposable
import androidx.compose.remote.creation.compose.layout.RemoteText
import androidx.compose.remote.creation.compose.modifier.RemoteModifier
import androidx.compose.remote.creation.compose.modifier.background
import androidx.compose.remote.creation.compose.modifier.fillMaxSize
import androidx.compose.remote.creation.compose.modifier.onTouchDown
import androidx.compose.remote.creation.compose.state.rc
import androidx.compose.remote.creation.compose.state.rememberMutableRemoteInt
import androidx.compose.remote.creation.compose.state.ri
import androidx.compose.remote.creation.compose.state.rs
import androidx.compose.remote.creation.compose.state.selectIfGe
import androidx.compose.remote.creation.compose.widgets.RemoteComposeWidget
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color

@SuppressLint("RestrictedApiAndroidX", "RestrictedApi")
class AndroidPetWidget : RemoteComposeWidget() {

@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
appWidgetIds.forEach { id ->
widget.updateRemoteView(context, appWidgetManager, this, id, -1)
BuddyPreferences.removeWidgetState(context, id)
}
}

@SuppressLint("RestrictedApiAndroidX", "RestrictedApi")
@RemoteComposable
@Composable
override fun Content(context: Context, widgetId: Int) {
val appContext = context.applicationContext ?: context
val coins = BuddyPreferences.getCoinsForWidget(appContext, widgetId)

val petTaps = rememberMutableRemoteInt(0)
val nextPetTaps = petTaps + 1.ri
// Evaluates to 1 (visible) in the host's expression graph when petTaps >= 3,
// or 0 (hidden) otherwise, allowing the coin button to appear after 3 taps
// without waking the app process.
val isClaimVisible = selectIfGe(petTaps, REQUIRED_TAPS_FOR_COIN.ri, 1.ri, 0.ri)

RemoteBox(
modifier = RemoteModifier.fillMaxSize().background(Color.White.rc),
contentAlignment = RemoteAlignment.Center
) {
RemoteText(
text = "Android Pet Coming Soon".rs,
color = Color.Black.rc
RemoteColumn(
modifier = RemoteModifier.onTouchDown(valueChange(petTaps, nextPetTaps)),
horizontalAlignment = RemoteAlignment.CenterHorizontally
) {
RemoteText(
text = "Android Pet".rs,
color = Color.Black.rc
)
RemoteText(
text = "Coins: $coins".rs,
color = Color.DarkGray.rc
)
}

WidgetActionBar(
context = appContext,
widgetId = widgetId,
isClaimVisible = isClaimVisible,
)
}
}

companion object {
const val REQUIRED_TAPS_FOR_COIN = 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.platform.ui.appwidgets.remote_compose

import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit

/**
* Snapshot of an Android Pet widget instance's state.
*/
data class WidgetBuddyState(
val widgetId: Int,
val coins: Int = 0,
)

/**
* Persists per-widget collected coins in [SharedPreferences].
*/
object BuddyPreferences {
const val PREFS_NAME = "buddy_prefs"
private const val KEY_COINS_PREFIX = "widget_coins_"

fun prefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

fun getCoinsForWidget(context: Context, widgetId: Int): Int =
prefs(context).getInt("$KEY_COINS_PREFIX$widgetId", 0)

fun addCoinsForWidget(context: Context, widgetId: Int, amount: Int = 1): Int {
val updated = (getCoinsForWidget(context, widgetId) + amount).coerceAtLeast(0)
prefs(context).edit {
putInt("$KEY_COINS_PREFIX$widgetId", updated)
}
return updated
}

fun removeWidgetState(context: Context, widgetId: Int) {
prefs(context).edit {
remove("$KEY_COINS_PREFIX$widgetId")
}
}

fun getActiveBuddyStates(context: Context): List<WidgetBuddyState> {
val manager = AppWidgetManager.getInstance(context)
val component = ComponentName(context, AndroidPetWidget::class.java)
return (manager.getAppWidgetIds(component) ?: IntArray(0))
.filter { it != AppWidgetManager.INVALID_APPWIDGET_ID }
.sorted()
.map { id ->
WidgetBuddyState(
widgetId = id,
coins = getCoinsForWidget(context, id),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.platform.ui.appwidgets.remote_compose

import android.annotation.SuppressLint
import android.content.Context
import androidx.compose.remote.creation.compose.layout.RemoteComposable
import androidx.compose.remote.creation.compose.layout.RemoteImage
import androidx.compose.remote.creation.compose.modifier.RemoteModifier
import androidx.compose.remote.creation.compose.modifier.padding
import androidx.compose.remote.creation.compose.modifier.size
import androidx.compose.remote.creation.compose.modifier.visibility
import androidx.compose.remote.creation.compose.state.RemoteInt
import androidx.compose.remote.creation.compose.state.rb
import androidx.compose.remote.creation.compose.state.rdp
import androidx.compose.remote.creation.compose.state.rs
import androidx.compose.remote.creation.compose.widgets.onClick
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.example.platform.ui.appwidgets.R

@SuppressLint("RestrictedApiAndroidX", "RestrictedApi")
@RemoteComposable
@Composable
fun ClaimCreditButton(
context: Context,
widgetId: Int,
isClaimVisible: RemoteInt,
) {
val imageBitmap = remember(context) {
ContextCompat.getDrawable(context, R.drawable.claim_credit_button)!!.toBitmap().asImageBitmap()
}

RemoteImage(
remoteBitmap = imageBitmap.rb,
contentDescription = "Claim Credit".rs,
modifier = RemoteModifier
.padding(end = 68.rdp, bottom = 12.rdp)
.size(48.rdp)
.visibility(isClaimVisible)
.onClick {
BuddyPreferences.addCoinsForWidget(context, widgetId)
},
)
}
Loading
Loading