26 Commits

Author SHA1 Message Date
android-devrel-github-bot bac232fd02 Bump plugin version to v1.0.11 2026-09-07 11:57:38 +00:00
android-devrel-github-bot ac4238481a Updates skills (2026-09-07 11:38) (#187)
Co-authored-by: android-devrel-github-bot <android-devrel-github-bot@users.noreply.github.com>
2026-09-07 12:56:54 +01:00
Simona 725364add9 Fix README with the latest CLI skill command changes (#174) 2026-09-01 13:19:32 +01:00
Simona ea05a53683 Update the Update Skills action to create a PR instead of merging to main directly (#161) 2026-08-27 13:56:53 +01:00
android-devrel-github-bot aaca635061 Bump plugin version to v1.0.10 2026-08-24 09:46:19 +00:00
Simona a8e8d000ef Delete identity/restore-credentials/skill.md (#160) 2026-08-24 10:45:14 +01:00
android-devrel-github-bot f87c432be4 Bump plugin version to v1.0.9 2026-08-24 08:37:37 +00:00
android-devrel-github-bot 1c807685ca Updates skills (2026-08-24 08:35) 2026-08-24 08:35:55 +00:00
android-devrel-github-bot eb1c09b18c Updates skills (2026-08-24 08:34) 2026-08-24 08:34:47 +00:00
android-devrel-github-bot 6685cac292 Bump plugin version to v1.0.8 2026-08-17 17:22:27 +00:00
android-devrel-github-bot 86932856ef Updates skills (2026-08-17 17:17) 2026-08-17 17:17:28 +00:00
android-devrel-github-bot 1e5e7ae613 Bump plugin version to v1.0.7 2026-08-07 14:29:53 +00:00
android-devrel-github-bot 7311f4905d Updates skills (2026-08-07 14:24) 2026-08-07 14:24:00 +00:00
Simona 28822b2306 Revert last update (#126) 2026-08-04 18:20:39 +02:00
android-devrel-github-bot 63123be36f Updates skills (2026-08-04 14:53) 2026-08-04 14:53:55 +00:00
android-devrel-github-bot 9f80a958c4 Updates skills (2026-08-04 14:51) 2026-08-04 14:51:56 +00:00
Simona 6fd7e4d545 Update workflows with agent marketplace generation files (#124)
* Update workflows with agent marketplace generation files

* Fix failing zizmor errors

* Modify update-skills steps to update agent marketplace files and skill lists with every new update

* Modify update-skills steps to update agent marketplace files and skill lists with every new update

* Resolve comments
2026-08-04 16:51:25 +02:00
Vinisha Athwani ba0042c08b Update readMe (#121)
* Update README.md

* Update README.md

* Update README.md
2026-08-03 11:37:16 +02:00
Simona 23d9eae21a Fix Claude Code and Codex plugin/marketplace manifests (#113)
* Fix Claude Code and Codex plugin/marketplace manifests

* Fix Claude Code and Codex plugin/marketplace manifests
2026-07-29 11:37:25 +02:00
android-devrel-github-bot 4e1674995b Updates skills (2026-07-29 08:28) 2026-07-29 08:28:58 +00:00
Simona bc6cd7246a Add Claude Code and Codex plugin/marketplace manifests (#111) 2026-07-28 18:56:32 +02:00
android-devrel-github-bot 47e1dff74a Updates skills (2026-07-14 08:21) 2026-07-14 08:21:32 +00:00
android-devrel-github-bot 57ff3c7d02 Updates skills (2026-07-10 16:33) 2026-07-10 16:33:36 +00:00
android-devrel-github-bot aaf42b970f Updates skills (2026-07-10 08:08) 2026-07-10 08:08:50 +00:00
android-devrel-github-bot 79bee216c4 Updates skills (2026-07-06 08:06) 2026-07-06 08:06:29 +00:00
android-devrel-github-bot 07302ca15e Updates skills (2026-06-24 14:23) 2026-06-24 14:23:35 +00:00
307 changed files with 38617 additions and 17226 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"name": "android-skills",
"interface": {
"displayName": "Android Skills"
},
"plugins": [
{
"name": "android-skills",
"source": {
"source": "url",
"url": "https://github.com/android/skills",
"ref": "main"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Developer Tools"
}
]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "android-skills",
"owner": {
"name": "Google / Android"
},
"plugins": [
{
"name": "android-skills",
"description": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.",
"author": {
"name": "Google / Android"
},
"category": "development",
"source": "./",
"strict": false,
"skills": [
"./build-system/agp/agp-9-upgrade",
"./camera/camerax",
"./device-ai/appfunctions",
"./device-ai/ml-kit-genai-prompt-api",
"./devtools/android-cli",
"./identity/restore-credentials",
"./identity/verified-email",
"./jetpack-compose/adaptive",
"./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose",
"./jetpack-compose/theming/styles",
"./media/media3-cast-integration",
"./navigation/navigation-3",
"./navigation/navigation-event",
"./performance/r8-analyzer",
"./play/engage-sdk-integration",
"./play/play-billing-library-version-upgrade",
"./play/play-policy-insights",
"./profilers/android-profiler",
"./security/android-intent-security",
"./system/edge-to-edge",
"./testing/testing-setup",
"./tv/leanback-to-compose-tv-migration",
"./wear/wear-compose-m3",
"./xr/display-glasses-with-jetpack-compose-glimmer"
],
"homepage": "https://github.com/android/skills",
"license": "Apache-2.0"
}
]
}
+154
View File
@@ -0,0 +1,154 @@
{
"name": "android-skills",
"version": "1.0.11",
"description": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.",
"author": {
"name": "Google / Android",
"url": "https://github.com/android"
},
"repository": "https://github.com/android/skills",
"license": "Apache-2.0",
"keywords": [
"android",
"kotlin",
"jetpack-compose",
"skills"
],
"skills": [
{
"source": {
"path": "./build-system/agp/agp-9-upgrade"
}
},
{
"source": {
"path": "./camera/camerax"
}
},
{
"source": {
"path": "./device-ai/appfunctions"
}
},
{
"source": {
"path": "./device-ai/ml-kit-genai-prompt-api"
}
},
{
"source": {
"path": "./devtools/android-cli"
}
},
{
"source": {
"path": "./identity/restore-credentials"
}
},
{
"source": {
"path": "./identity/verified-email"
}
},
{
"source": {
"path": "./jetpack-compose/adaptive"
}
},
{
"source": {
"path": "./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose"
}
},
{
"source": {
"path": "./jetpack-compose/theming/styles"
}
},
{
"source": {
"path": "./media/media3-cast-integration"
}
},
{
"source": {
"path": "./navigation/navigation-3"
}
},
{
"source": {
"path": "./navigation/navigation-event"
}
},
{
"source": {
"path": "./performance/r8-analyzer"
}
},
{
"source": {
"path": "./play/engage-sdk-integration"
}
},
{
"source": {
"path": "./play/play-billing-library-version-upgrade"
}
},
{
"source": {
"path": "./play/play-policy-insights"
}
},
{
"source": {
"path": "./profilers/android-profiler"
}
},
{
"source": {
"path": "./security/android-intent-security"
}
},
{
"source": {
"path": "./system/edge-to-edge"
}
},
{
"source": {
"path": "./testing/testing-setup"
}
},
{
"source": {
"path": "./tv/leanback-to-compose-tv-migration"
}
},
{
"source": {
"path": "./wear/wear-compose-m3"
}
},
{
"source": {
"path": "./xr/display-glasses-with-jetpack-compose-glimmer"
}
}
],
"interface": {
"displayName": "Android Skills",
"shortDescription": "AI-optimized Android development skills from Google.",
"longDescription": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.",
"developerName": "Google / Android",
"category": "Developer Tools",
"capabilities": [
"Skills"
],
"websiteURL": "https://github.com/android/skills",
"defaultPrompt": [
"Help me upgrade my app to AGP 9.",
"Migrate this Camera1 code to CameraX."
]
}
}
+39 -3
View File
@@ -16,9 +16,41 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
token: ${{ secrets.ADR_GITHUB_BOT_PAT || secrets.GITHUB_TOKEN }}
- name: Update Codex Plugin Version
env:
TAG_NAME: ${{ inputs.tag_name }}
run: |
VERSION="${TAG_NAME#v}"
echo "Updating .codex-plugin/plugin.json version to $VERSION"
VERSION="$VERSION" python3 -c "
import json
import os
version = os.environ['VERSION']
path = '.codex-plugin/plugin.json'
with open(path, 'r') as f:
data = json.load(f)
data['version'] = version
with open(path, 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
"
- name: Commit and push updated version
env:
TAG_NAME: ${{ inputs.tag_name }}
run: |
git config user.name "android-devrel-github-bot"
git config user.email "android-devrel-github-bot@users.noreply.github.com"
git add .codex-plugin/plugin.json
git commit -m "Bump plugin version to $TAG_NAME" || echo "No version change to commit"
git push origin main
- name: Create Zip Archive
run: |
@@ -26,6 +58,10 @@ jobs:
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.ADR_GITHUB_BOT_PAT || secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ inputs.tag_name }}
run: |
gh release create ${{ inputs.tag_name }} android-skills.zip --title "Release ${{ inputs.tag_name }}" --notes "Manual release of main branch contents."
gh release create "$TAG_NAME" android-skills.zip \
--target main \
--title "Release $TAG_NAME" \
--notes "Release $TAG_NAME of Android Skills."
+63 -5
View File
@@ -8,13 +8,14 @@ on:
permissions:
contents: write
pull-requests: write
jobs:
update-skills:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
token: ${{ secrets.ADR_GITHUB_BOT_PAT }}
@@ -59,11 +60,68 @@ jobs:
rm -rf temp_skills
echo "Successfully copied directories from github-skills branch."
- name: Commit and push changes
- name: Update Skill Lists in Plugin Files
run: |
python3 -c "
import json
import pathlib
# Discover all skills with SKILL.md
skill_paths = []
for path in pathlib.Path('.').glob('**/SKILL.md'):
if any(p.startswith('.') for p in path.parts[:-1]):
continue
skill_paths.append(f'./{path.parent.as_posix()}')
skill_paths = sorted(list(set(skill_paths)))
# 1. Update .claude-plugin/marketplace.json in-place
claude_file = pathlib.Path('.claude-plugin/marketplace.json')
if not claude_file.exists():
raise FileNotFoundError(f'Required file {claude_file} does not exist. Ensure you are running on the correct branch.')
with open(claude_file, 'r') as f:
data = json.load(f)
if 'plugins' in data and len(data['plugins']) > 0:
data['plugins'][0]['skills'] = skill_paths
with open(claude_file, 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
# 2. Update .codex-plugin/plugin.json in-place
codex_file = pathlib.Path('.codex-plugin/plugin.json')
if not codex_file.exists():
raise FileNotFoundError(f'Required file {codex_file} does not exist. Ensure you are running on the correct branch.')
with open(codex_file, 'r') as f:
data = json.load(f)
data['skills'] = [{'source': {'path': p}} for p in skill_paths]
with open(codex_file, 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
"
- name: Commit and push to PR branch
run: |
BRANCH="update-skills/$(date +'%Y-%m-%d-%H%M')"
git config user.name "android-devrel-github-bot"
git config user.email "android-devrel-github-bot@users.noreply.github.com"
git checkout -b "$BRANCH"
git add -A
# The '|| true' ensures the workflow doesn't fail if there's nothing new
git commit -m "Updates skills ($(date +'%Y-%m-%d %H:%M'))" || echo "No changes to commit"
git push origin main
# The workflow doesn't fail if there's nothing new to commit
if git diff --cached --quiet; then
echo "No changes to commit — skipping PR creation."
exit 0
fi
git commit -m "Updates skills ($(date +'%Y-%m-%d %H:%M'))"
git push origin "$BRANCH"
gh pr create \
--title "Update skills ($(date +'%Y-%m-%d %H:%M'))" \
--body "Automated skill update from the \`Update Skills\` workflow. Please review the changes before merging." \
--base main \
--head "$BRANCH"
env:
GH_TOKEN: ${{ secrets.ADR_GITHUB_BOT_PAT }}
+8 -8
View File
@@ -1,7 +1,7 @@
## Android skills
**Android skills** are a dedicated repository of **AI-optimized, modular instructions** and
resources, to help LLMs better understand and execute specific patterns that follow the best
resources, to help agents better understand and execute specific patterns that follow the best
practices and guidance on Android development
from [developer.android.com](https://developer.android.com).
@@ -25,7 +25,7 @@ To learn more, read the official documentation:
Use Android CLI to install a specific skill into the current directory:
```
android skills add --skill=r8-analyzer --project=.
android skills add r8-analyzer --project=.
```
Use Android CLI to install all Android skills to directories for all detected agents:
@@ -39,14 +39,14 @@ will be installed for Gemini and Antigravity at `~/.gemini/antigravity/skills`.
**Options:**
- `--all` - Add all Android skills. If omitted (and `--skill` isn't specified), only the
`android-cli` skill will be installed.
- `--agent` - A comma-separated list of agents to install the skill for. If omitted, the skill will
be installed for all detected agents.
- `--skill` - Specific skill that you want to install. If omitted (and `--all` isn't specified),
only the `android-cli` skill will be installed.
- `--all` - Add all Android skills. If omitted, only the `android-cli` skill will be installed.
- `--agent` - A comma-separated list of agents to install the skill for. If omitted, the skill will be installed for all detected agents.
- `--project` - Path to a project root in which to install the skills.
### Activate skills
Your agent automatically activates skills that are relevant to your task. To use a skill, prompt your agent to complete a task that is related to the skill, for example "Make my app UI edge-to-edge." The agent should automatically find and use the skill if it has been installed.
## Disclaimer
AI can make mistakes, so always double-check the results.
@@ -6,7 +6,7 @@ description: Upgrades, or migrates, an Android project to use Android Gradle Plu
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-03'
last-updated: '2026-08-18'
keywords:
- Android Gradle Plugin 9
- AGP 9
@@ -37,7 +37,7 @@ In custom build-logic using `CommonExtension`:
### Step 2: Migrate to the new API
Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https://developer.android.com/agents/skills/build/agp/agp-9-upgrade/references/recipes)
Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https://developer.android.com/agents/skills/build-system/agp/agp-9-upgrade/references/recipes)
repository.
**IMPORTANT:** For `BuildConfigField`s with a type of `String`, the `value` field
@@ -38,7 +38,7 @@ Additional details for each use-case follow.
### Add custom BuildConfig fields
See the detailed guide at [BuildConfig](https://developer.android.com/agents/skills/build/agp/agp-9-upgrade/references/buildconfig).
See the detailed guide at [BuildConfig](https://developer.android.com/agents/skills/build-system/agp/agp-9-upgrade/references/buildconfig).
### Renaming an APK
-270
View File
@@ -1,270 +0,0 @@
---
name: camera1-to-camerax
description: Use this skill to migrate legacy Android camera implementations (Camera1
or raw Camera2 APIs) to CameraX. CameraX is a lifecycle-aware Jetpack library built
on top of Camera2 that resolves camera rotation issues and handles device dependencies.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-05-06'
keywords:
- Android
- CameraX
- Camera1 Migration
- Jetpack Compose
- Dependencies
- Image Capture
- Lifecycle
- PreviewView
---
## Step 0: Add Dependencies
Check for and add the required CameraX dependencies. Use version 1.3.0 or higher
for interoperability, or version 1.5.0 or higher for Compose extensions.
If you are using a Version Catalog (`libs.versions.toml`), add the following:
```kotlin
[versions]
camerax = "<minimum_version_needed>"
[libraries]
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" }
```
<br />
And in your `build.gradle.kts` (or `build.gradle`):
```kotlin
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.compose)
```
<br />
Without a Version Catalog, fall back to these standard Gradle dependencies:
```kotlin
implementation "androidx.camera:camera-core:<minimum_version_needed>"
implementation "androidx.camera:camera-camera2:<minimum_version_needed>"
implementation "androidx.camera:camera-lifecycle:<minimum_version_needed>"
implementation "androidx.camera:camera-view:<minimum_version_needed>"
implementation "androidx.camera:camera-compose:<minimum_version_needed>"
```
<br />
## Step 1: Remove Legacy Implementation
1. Delete all `android.hardware.Camera` instances.
2. Delete `SurfaceView` and `SurfaceHolder.Callback` implementations (`surfaceCreated`, `surfaceChanged`, `surfaceDestroyed`).
3. Remove custom lifecycle handling that opens or releases the camera in `onResume` or `onPause`.
4. Remove manual matrix calculations for orientation.
## Step 2: Initialize ProcessCameraProvider
Request the `ProcessCameraProvider` and bind use cases to the Activity or
Fragment lifecycle.
```kotlin
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(context, lifecycleOwner) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
cameraProvider.unbindAll() // Unbind before rebinding
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
val cameraControl = camera.cameraControl
}, ContextCompat.getMainExecutor(context)
)
}
```
<br />
## Step 3: Implement the Preview \& Tap-to-Focus
Choose exactly one of the following patterns based on the app's UI toolkit:
### Option A: For Android Views (XML Legacy)
Use `androidx.camera.view.PreviewView`.
**1. Set up preview**:
```kotlin
preview.setSurfaceProvider(previewView.surfaceProvider)
```
<br />
**2. Handle tap-to-focus**:
```kotlin
val factory = previewView.meteringPointFactory
val point = factory.createPoint(x, y) // x, y from touch event
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build()
cameraControl?.startFocusAndMetering(action)
```
<br />
### Option B: For Jetpack Compose
Use `androidx.camera.compose.CameraXViewfinder`.
**1. Set up preview and SurfaceRequest**:
```kotlin
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
val preview = remember {
Preview.Builder().build().apply {
setSurfaceProvider { request -> surfaceRequest = request }
}
}
```
<br />
**2. Render viewfinder**:
```kotlin
surfaceRequest?.let { request ->
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = Modifier
)
}
```
<br />
**3. Handle tap-to-focus in Compose**:
```kotlin
// Inside your tap gesture handler...
val surfaceCoords = with(coordinateTransformer) { offset.transform() }
val factory = SurfaceOrientedMeteringPointFactory(
request.resolution.width.toFloat(),
request.resolution.height.toFloat()
)
val point = factory.createPoint(surfaceCoords.x, surfaceCoords.y)
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build()
cameraControl?.startFocusAndMetering(action)
```
<br />
**4. Update target rotation for Compose**:
```kotlin
LaunchedEffect(configuration) {
if (!view.isInEditMode) {
val rotation = view.display?.rotation ?: Surface.ROTATION_0
imageCapture.targetRotation = rotation
preview.targetRotation = rotation
}
}
```
<br />
## Step 4: Capture Photo
Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles
rotation directly.
```kotlin
imageCapture.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
// Adjust rotation natively via ImageProxy
val matrix = Matrix()
matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
matrix.postScale(-1f, 1f) // Mirror for front camera
}
val rotatedBitmap = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
// MUST close proxy
image.close()
}
override fun onError(exception: ImageCaptureException) {
Log.e("CameraX", "Capture failed: ${exception.message}", exception)
}
}
)
```
<br />
## Step 5: Switch Cameras
To flip between front and rear cameras, change the `CameraSelector` and
re-trigger the `ProcessCameraProvider` logic.
```kotlin
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
```
<br />
## Constraints
- **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`.
- **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations.
- **Don't forget to close the ImageProxy** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos.
- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an old fallback option that introduces resizing issues.
+132
View File
@@ -0,0 +1,132 @@
---
name: camerax
description: Provide technical guidance for Android camera development with CameraX.
Use when implementing camera features, handling asynchronous recording lifecycles,
wiring low-level hardware interop using CameraX, or integrating ML Kit or Media3
effects.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-08-14'
keywords:
- recipe
- Android
- Camera
- Camera1
- Camera2
- CameraX
- migration
- Compose
- guide
- dependencies
- PreviewView
- CameraXViewfinder
- ImageCapture
- VideoCapture
- ImageAnalysis.
---
This skill provides procedural guidance and standard patterns for building
camera applications on Android, with a focus on CameraX, including its
`Camera2Interop` utilities, and Media3 integrations.
## Core workflows
### Handling immutable API patterns
Various Android camera and media APIs, especially CameraX `VideoCapture`, use a
**fluent, immutable builder-like pattern** where methods return a new instance.
Failing to reassign these results in settings, such as audio, being ignored.
**Pattern: Reassignment is required**
```kotlin
// WRONG
run {
val pending = recorder.prepareRecording(context, opts)
pending.withAudioEnabled() // This returns a new instance which is ignored
val active = pending.start(exec, listener)
}
// CORRECT
run {
val pending = recorder.prepareRecording(context, opts)
.withAudioEnabled() // Chaining works
val active = pending.start(exec, listener)
}
// ALSO CORRECT
run {
var pending = recorder.prepareRecording(context, opts)
pending = pending.withAudioEnabled() // Reassignment
val active = pending.start(exec, listener)
}
```
<br />
See [immutability](references/immutability.md) for a list of affected classes.
### Migrating to CameraX
When migrating legacy camera codebases to the CameraX Jetpack library:
- **Camera1 to CameraX** : For migrating legacy `android.hardware.Camera` implementations, surface handling, and manual lifecycles, see the [Camera1 migration guide](references/camera1-to-camerax.md).
- **Camera2 to CameraX** : For migrating more recent but verbose `android.hardware.camera2` implementations, session state callbacks, and interop patterns, see the [Camera2 migration guide](references/camera2-to-camerax.md).
### Comprehensive feature blueprinting
For multi-step features that involve multiple files and hardware-level wiring,
follow the [Structural Blueprinting](references/expert-blueprints.md) approach to avoid
system timeouts. Such complex features include:
- **Manual controls** : Break down into the `ViewModel` state, the controller layer, and the `Camera2Interop` wiring in the session.
- **RAW capture**: Separate JPEG and RAW output configurations into discrete build steps.
- **Custom effects** : Prefer `Media3Effect` or `SurfaceProcessor` over manual OpenGL pipelines unless absolute performance is required.
- **Low-light** : See [low-light](references/low-light.md) for Night Mode and LLB guidance.
- **Foldables** : See [foldables](references/foldables.md) for handling dynamic postures and hinge states.
- **XR, AR, and VR** : See [xr](references/xr.md) for spatial tracking, passthrough synchronization, and latency guardrails.
- **Thermals and power** : See [thermals](references/thermals.md) for managing `StreamUseCase` optimizations and `PowerManager` thermal states.
- **Testing and mocking** : See [testing](references/testing.md) for using `FakeCameraConfig`, handling asynchronous lifecycles, and validating analysis pipelines.
- **ML Kit spatial analysis** : See [mlkit-spatial](references/mlkit-spatial.md) for coordinate mapping, rotation logic, and mirrored lens handling.
- **Wear OS camera remote** : See [wear-os](references/wear-os.md) for circular UI constraints, Data Layer API syncing, and remote trigger logic.
See [expert-blueprints](references/expert-blueprints.md) for step-by-step guides.
### API discovery
Always use higher-level abstractions instead of low-level manual wiring:
- **Analysis** : Use `MlKitAnalyzer` instead of manual `ImageAnalysis.Analyzer`.
- **Filters and effects** : Use `Media3Effect` for standard post-processing.
- **Multi-camera** : Use `ConcurrentCamera` APIs for dual-stream setups.
See [modern-apis](references/modern-apis.md) for current recommendations.
### Code quality and architectural rules
Adhere to the following Android ecosystem standard patterns when building your
camera implementations:
- **Testing, fakes over mocks** : Avoid mocking libraries like `Mockito`, especially for multi-step CameraX interfaces like `ImageProxy`. Build "Fakes" to verify state rather than unreliable implementation details.
- **Google Truth assertions** : Use `assertThat` over standard `JUnit` assertions like `assertEquals` for improved readability.
- **Explicit test runners** : Always define an explicit `@RunWith` for test classes to ensure the CI environment executes them correctly.
- **Semantic UI merging** : When building custom camera controls in Compose, such as a button with an `Icon` and `Text`, use `semantics {
mergeDescendants = true }` to ensure screen readers announce them as a single, coherent unit.
## Hardware and device diversity
Camera apps run on a wide variety of hardware, from mobile phones and
foldables to tablets, laptops, and even smart appliances. Have consideration
for the specific hardware the app is running on.
- **Form factors**: Account for screen size and orientation changes on foldables and tablets.
- **Multi-camera arrays**: Some devices have a rear-facing camera and a front-facing camera. Other devices have multiple rear-facing cameras, such as wide-angle and telephoto lenses.
- **Feature parity**: Features like flash or auto-focus behave differently across hardware. For example, CameraX handles both physical flash, back, and screen-based flash, front, and both must be considered when implementing flash functionality.
## Common pitfalls
- **Asynchronous lifecycles** : Check `isRecording` state before attempting to stop or pause. Handle `VideoRecordEvent.Start` for UI state updates, not just the initial call.
- **Thread safety**: Camera callbacks often run on background executors. Dispatch UI updates on the main thread.
- **Permission handling** : Check `CAMERA` permission; check for `RECORD_AUDIO` specifically when enabling audio in `VideoCapture`.
@@ -0,0 +1,198 @@
## Remove `Camera1` implementation
1. Delete all `android.hardware.Camera` instances.
2. Delete `SurfaceView` and `SurfaceHolder.Callback` implementations `surfaceCreated`, `surfaceChanged`, and `surfaceDestroyed`.
3. Remove custom lifecycle handling that opens or releases the camera in `onResume` or `onPause`.
4. Remove manual matrix calculations for orientation.
## Initialize `ProcessCameraProvider`
Request the `ProcessCameraProvider` and bind use cases to the Activity or
Fragment lifecycle.
```kotlin
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(context, lifecycleOwner) {
val cameraProvider = ProcessCameraProvider.getInstance(context).await()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
cameraProvider.unbindAll() // Unbind before rebinding
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
val cameraControl = camera.cameraControl
}
```
<br />
## Implement the preview and tap-to-focus
Choose exactly one of the following patterns based on the app's UI toolkit:
### Option A: For Android Views
Use `androidx.camera.view.PreviewView`.
1. **Set up preview**:
```kotlin
preview.setSurfaceProvider(previewView.surfaceProvider)
```
<br />
2. **Handle tap-to-focus**:
```kotlin
val factory = previewView.meteringPointFactory
val point = factory.createPoint(x, y) // x, y from touch event
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build()
cameraControl?.startFocusAndMetering(action)
```
<br />
### Option B: For Jetpack Compose
Use `androidx.camera.compose.CameraXViewfinder`.
1. **Set up preview and SurfaceRequest**:
```kotlin
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
val preview = remember {
Preview.Builder().build().apply {
setSurfaceProvider { request -> surfaceRequest = request }
}
}
```
<br />
2. **Render viewfinder**:
```kotlin
surfaceRequest?.let { request ->
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = Modifier
)
}
```
<br />
3. **Handle tap-to-focus in Compose**:
```kotlin
// Inside your tap gesture handler...
val surfaceCoords = with(coordinateTransformer) { offset.transform() }
val factory = SurfaceOrientedMeteringPointFactory(
request.resolution.width.toFloat(),
request.resolution.height.toFloat()
)
val point = factory.createPoint(surfaceCoords.x, surfaceCoords.y)
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build()
cameraControl?.startFocusAndMetering(action)
```
<br />
4. **Update target rotation for Compose**:
```kotlin
LaunchedEffect(configuration) {
if (!view.isInEditMode) {
val rotation = view.display?.rotation ?: Surface.ROTATION_0
imageCapture.targetRotation = rotation
preview.targetRotation = rotation
}
}
```
<br />
## Capture a photo
Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles
rotation directly.
```kotlin
imageCapture.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
// Adjust rotation natively via ImageProxy
val matrix = Matrix()
matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
matrix.postScale(-1f, 1f) // Mirror for front camera
}
val rotatedBitmap = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
// MUST close proxy
image.close()
}
override fun onError(exception: ImageCaptureException) {
Log.e("CameraX", "Capture failed: ${exception.message}", exception)
}
}
)
```
<br />
## Switch cameras
To flip between front and rear cameras, change the `CameraSelector` and
retrigger the `ProcessCameraProvider` logic.
```kotlin
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
```
<br />
## Follow constraints
- **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`.
- **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations.
- **Don't forget to close the `ImageProxy`** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos.
- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an earlier fallback option that introduces resizing issues.
@@ -0,0 +1,193 @@
Camera2 offers granular control but introduces boilerplate: managing
`CameraDevice` states, `CameraCaptureSession` lifecycles, background threads,
`HandlerThread`, and manual orientation calculations.
CameraX simplifies this by binding high-level `UseCase`s such as `Preview`,
`ImageCapture`, and `ImageAnalysis` directly to Android lifecycles, handling
thread management and device-specific workarounds automatically.
*** ** * ** ***
## Remove Camera2 boilerplate
Migrating to CameraX removes manual setup code:
1. **Delete manual thread handling** : Remove `HandlerThread`, `Handler`, and executors dedicated to camera background tasks. CameraX manages its own threads.
2. **Remove session and device callbacks** : Delete `CameraDevice.StateCallback` and `CameraCaptureSession.StateCallback` implementations.
3. **Remove surface management** : Remove manual routing of `Surface` objects from `TextureView` or `SurfaceView` to the camera device.
4. **Remove manual orientation logic** : Delete calculations involving `CameraCharacteristics.SENSOR_ORIENTATION` and display rotation for adjusting preview and capture orientation.
*** ** * ** ***
## Initialize `ProcessCameraProvider`
Request the `ProcessCameraProvider` and bind your use cases to the
`LifecycleOwner` activity or fragment. This replaces the
`CameraManager.openCamera` flow.
```kotlin
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(context, lifecycleOwner) {
val cameraProvider = ProcessCameraProvider.getInstance(context).await()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// ImageAnalysis is common when migrating from Camera2 ImageReader
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture,
imageAnalysis
)
}
```
<br />
*** ** * ** ***
## Implement the preview and tap-to-focus
CameraX handles surface configuration automatically. Choose based on your UI
toolkit:
### Option A: For Android Views
Use `androidx.camera.view.PreviewView` in your layout, and bind it to the
`Preview` use case.
```kotlin
preview.setSurfaceProvider(previewView.surfaceProvider)
```
<br />
### Option B: For Jetpack Compose
Use `androidx.camera.compose.CameraXViewfinder`.
```kotlin
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
val preview = remember {
Preview.Builder().build().apply {
setSurfaceProvider { request -> surfaceRequest = request }
}
}
```
<br />
*** ** * ** ***
## Capture a photo
Replace `ImageReader` capture flows and
`CaptureRequest.Builder.TEMPLATE_STILL_CAPTURE` with the `ImageCapture` use
case. CameraX handles the rotation natively using the returned `ImageProxy`.
```kotlin
imageCapture.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
if (bitmap != null) {
val matrix = Matrix()
matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
matrix.postScale(-1f, 1f)
}
val rotatedBitmap = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
}
// MUST close proxy
image.close()
}
override fun onError(exception: ImageCaptureException) {
Log.e("CameraX", "Capture failed: ${exception.message}", exception)
}
}
)
```
<br />
*** ** * ** ***
## Implement image analysis
If you were using `ImageReader` in Camera2 to access raw frames, e.g., for QR
scanning or ML,, replace it with the CameraX `ImageAnalysis` use case.
```kotlin
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
try {
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
// Process image here (e.g., run object detection)
// ...
} finally {
// MUST close the imageProxy to avoid blocking the pipeline
imageProxy.close()
}
}
```
<br />
*** ** * ** ***
## Use Camera2 interop
If your app requires specific Camera2 configuration options, such as custom
exposure modes or flash settings, that aren't exposed directly in CameraX,
use `Camera2Interop` to apply them to your CameraX use cases.
```kotlin
// Use Camera2Interop to set Camera2-specific capture options
val extender = Camera2Interop.Extender(imageCaptureBuilder)
extender.setCaptureRequestOption(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_OFF
).setCaptureRequestOption(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH
)
```
<br />
*** ** * ** ***
## Follow constraints
- **Don't open the CameraDevice manually** : Let CameraX manage device opening and closing using `bindToLifecycle`.
- **Don't forget to close `ImageProxy`** : In both `ImageCapture` and `ImageAnalysis` callbacks, you **must** call `image.close()`. Failure to do so will block the camera pipeline.
- **Don't block the ImageAnalysis thread** : The analyzer runs frame-by-frame. If you need to perform heavy computations, offload them to a background worker and close the `ImageProxy` as soon as possible.
@@ -0,0 +1,58 @@
Complex camera features often fail due to "Agent Stall" or timeouts when
attempted in a single turn. Use these blueprints to break tasks into manageable
phases.
## Manual controls
### Phase one: ViewModel and state
1. Define a `ManualSettings` data class.
2. Add a `MutableStateFlow<ManualSettings>` to your `CameraViewModel`.
3. Implement the Jetpack Compose UI with sliders and switches to update this flow.
### Phase two: Controller wiring
1. In `CameraController.kt`, create a new function `updateManualSettings(settings: ManualSettings)`.
2. Map these settings into the `CameraSystem` layer.
### Phase three: CameraX `Camera2Interop` wiring
1. In `CameraSession.kt`, use the `Camera2Interop.Extender` utility to access Camera2 capture request keys.
2. Apply the hardware keys:
- `CaptureRequest.SENSOR_SENSITIVITY` to set ISO sensitivity.
- `CaptureRequest.SENSOR_EXPOSURE_TIME` to set exposure time.
- `CaptureRequest.LENS_FOCUS_DISTANCE` to set focus distance.
3. **Critical** : If manual exposure is active, set `CaptureRequest.CONTROL_AE_MODE` to `CameraMetadata.CONTROL_AE_MODE_OFF`. ---
## RAW and JPEG capture
### Phase one: Output configuration
1. Verify device support for RAW capture using `CameraInfo`.
2. Configure `ImageCapture.Builder` with `OUTPUT_FORMAT_RAW_JPEG` or `OUTPUT_FORMAT_RAW`.
### Phase two: Implementation
1. Provide `ImageCapture.OutputFileOptions` for the target storage locations.
2. Invoke `takePicture`. CameraX internally manages `DngCreator` to wrap RAW data with the required `CameraCharacteristics` and `CaptureResult` metadata.
*** ** * ** ***
## Apply image effects
### Phase one: Effects selection
1. Use the `androidx.media3:media3-effect` dependency.
2. Use `RgbFilter` or `HslAdjustment` for standard color grading.
### Phase two: Application
1. Configure `Composition.Builder` or `MediaItem.Builder` with the list of effects.
2. Inject the list of effects into the CameraX `Recorder` or `Preview` using the `setEffects` method.
*** ** * ** ***
## Low-light capture
For guidance on Night Mode Extensions and Low Light Boost,
[low-light.md](https://developer.android.com/agents/skills/camera/camerax/references/low-light).
+83
View File
@@ -0,0 +1,83 @@
Foldable devices introduce unique challenges for camera applications, including
dynamic layout changes, multiple display orientations, and physical device
postures, such as tabletop and book modes.
## Manage fold states and postures
| State | Posture | User interaction | Implementation goal |
|---|---|---|---|
| `FLAT` | Standard | Full screen preview | Conventional mobile phone layout. |
| `HALF_OPENED` | Tabletop | Lower half for controls | Split-screen layout, viewfinder on top, controls on bottom. |
| `HALF_OPENED` | Book | Side-by-side | Viewfinder on one panel, gallery and controls on the other. |
*** ** * ** ***
## Follow the implementation guide
### Detect posture changes
Use the Jetpack WindowManager library to observe the device's hinge state and
fold layout.
```kotlin
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowInfoTracker.windowLayoutInfo(activity)
.collect { layoutInfo ->
val displayFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
updateCameraLayout(displayFeature)
}
}
}
```
<br />
### Handle tabletop mode
In Tabletop mode, horizontal fold, you should move the viewfinder to the top
half of the screen and the controls to the bottom half to prevent the user from
seeing a "bent" image.
- **Identify orientation:** Check `FoldingFeature.orientation`.
- **Calculate geometry:** Use `FoldingFeature.bounds` to identify the hinge's physical location on the screen.
- **Update UI:** Apply padding or constraints to move the `PreviewView` above the hinge.
### Coordinate mapping and `Viewport`
When the UI layout changes due to a fold, you **must** update the `Viewport` to
ensure that tap-to-focus and image capture coordinates remain accurate.
```kotlin
val viewport = ViewPort.Builder(Rational(viewfinder.width, viewfinder.height), display.rotation)
.setScaleType(ViewPort.FILL_CENTER)
.build()
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview)
.setViewPort(viewport)
.build()
```
<br />
### Rear display mode
Some foldables allow using the rear camera with the cover display while the
device is unfolded.
- **Verification:** If available through OEM SDKs or Android 14 (API level 34) or higher, check `DeviceState.REAR_DISPLAY_STATE`.
- **Logic:** Handle preview detachment and reattachment on different display surfaces with varying aspect ratios.
*** ** * ** ***
## Foldable pitfalls
- **Hinge distortion:** Don't span the camera preview across a hinge in `HALF_OPENED` state.
- **Physical orientation:** The camera sensor's physical orientation relative to the screen often changes when you fold or unfold the device. Always rely on `CameraInfo.getSensorRotationDegrees`.
- **Latency:** Rebinding `UseCase` objects during a fold event is expensive. Use the internal scaling of `PreviewView` before performing a full `bindToLifecycle` reconfiguration.
+60
View File
@@ -0,0 +1,60 @@
Many Android APIs are designed with immutability in mind to prevent race
conditions in async environments. However, this often trips up developers used
to mutable builder patterns.
## Common immutable classes
The following classes use fluent APIs that **return a new instance**. You must
reassign the variable.
| Class | Methods that return a new instance | Result if not reassigned |
|---|---|---|
| `PendingRecording` | `withAudioEnabled`, `asPersistentRecording` | Audio isn't recorded. |
| `ImageCapture.Builder` | `setTargetRotation`, `setTargetResolution` | The output has the wrong orientation. |
| `Recorder.Builder` | `setQualitySelector`, `setExecutor` | The recording uses the default quality. |
| `Viewport.Builder` | `setScaleType`, `setLayoutDirection` | The viewfinder is stretched. |
## Use standard patterns
### CameraX video recording
To set up video recording, use the following code:
```kotlin
// WRONG
run {
val pending = recorder.prepareRecording(context, opts)
pending.withAudioEnabled() // This returns a new instance which is ignored
val active = pending.start(exec, listener)
}
// CORRECT
run {
val pending = recorder.prepareRecording(context, opts)
.withAudioEnabled() // Chaining works
val active = pending.start(exec, listener)
}
// ALSO CORRECT
run {
var pending = recorder.prepareRecording(context, opts)
pending = pending.withAudioEnabled() // Reassignment
val active = pending.start(exec, listener)
}
```
<br />
### Viewport construction
To set up the viewport, use the following code:
```kotlin
val viewport = ViewPort.Builder(Rational(width, height), displayRotation)
.setScaleType(ViewPort.FILL_CENTER)
.build()
```
<br />
+124
View File
@@ -0,0 +1,124 @@
This guide covers implementing low-light features using **Night mode
extensions** and **Low Light Boost (LLB)**.
## Choosing the right tool
| Feature | Used for | Implementation | UX impact |
|---|---|---|---|
| **Night mode** | High-quality stills | `ExtensionsManager` | Possibly requires user to hold still for several seconds. |
| **LLB, AE mode** | Real-time preview or video | `Camera2Interop`, CameraX utility | Hardware drops the frame rate to increase brightness. |
| **LLB, Play services** | Real-time preview and video | `SurfaceProcessor` | Software-based brightening; maintains a higher frame rate. |
*** ** * ** ***
## Night mode extension
CameraX Extensions provide access to the device's built-in computational
photography pipeline.
### Basic setup
To set up the extension, initialize the extension manager:
```kotlin
// Use ListenableFuture.await() extension function for coroutine support
val extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).await()
if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) {
val nightSelector = extensionsManager.getExtensionEnabledCameraSelector(
cameraSelector, ExtensionMode.NIGHT
)
cameraProvider.bindToLifecycle(lifecycleOwner, nightSelector, imageCapture, preview)
}
```
<br />
### Comprehensive features
- **Image postview** : Display a low-resolution image immediately while the multi-frame processing occurs.
```kotlin
val imageCapture = ImageCapture.Builder()
.setPostviewEnabled(true)
.build()
```
- **Extension strength** : Let users control the intensity of the night effect.
```kotlin
// Set the strength of the active extension (e.g. NIGHT mode intensity)
val extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).await()
val extensionsControl = extensionsManager.getCameraExtensionsControl(camera.cameraControl)
extensionsControl?.setExtensionStrength(strength)
```
- **Capture progress** : Show a UI progress bar for long exposures.
```kotlin
// Use the suspend extension function for takePicture to avoid callback boilerplate
try {
val result = imageCapture.takePicture(outputOptions)
// Use result.savedUri or other fields
} catch (e: ImageCaptureException) {
// Handle capture failure
}
```
*** ** * ** ***
## Low-light boost
LLB is designed for preview and video streams where you prefer high frame rates.
### AE mode
The built-in CameraX way to prioritize brightness. It modifies the hardware's
auto-exposure algorithm.
- **Activation** : Use `CameraControl.enableLowLightBoostAsync`.
- **Implementation** :
```kotlin
// Enable Low Light Boost (LLB) natively in CameraX 1.4+
camera.cameraControl.enableLowLightBoostAsync(true)
```
- **Monitoring** : Observe `CameraInfo.lowLightBoostState` to track when the hardware actively applies the enhancement.
### Google Play services LLB
It's a multi-step implementation that uses a session-based `SurfaceProcessor`.
**Dependency** : `com.google.android.gms:play-services-camera-low-light-boost`
To implement Google Play services LLB, follow these core steps:
1. **Initialize client** : `val client = LowLightBoost.getClient`.
2. **Implement `SurfaceProcessor`** :
- **Manage session** : Call `client.createSession`.
- **Forward required metadata** : Observe the camera's `TotalCaptureResult` stream and forward every result to the session: `session.processCaptureResult`.
- **Provide surface** : Get the input surface from the session, `session.getCameraSurface`, and provide it to the camera's `SurfaceRequest`.
- **Lifecycle** : Release the session, `session.release`, when the processor is closed or the `SurfaceRequest` completes.
3. **Wire using `CameraEffect`** :
```kotlin
val effect = SimpleCameraEffect(
CameraEffect.PREVIEW or CameraEffect.VIDEO_CAPTURE,
executor,
llbSurfaceProcessor
) { throw it }
// Add to UseCaseGroup
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview)
.addUseCase(videoCapture)
.addEffect(effect)
.build()
```
4. **Scene detection** : Use `session.setSceneDetectorCallback` to receive `boostStrength` updates for real-time UI indicators.
*** ** * ** ***
## Implementation notes
- **Thread safety** : Always handle `ExtensionsManager` and `LowLightBoostClient` initialization asynchronously.
- **FPS trade-offs**: AE mode LLB often drops the frame rate significantly to increase brightness.
- **Compatibility** : Extensions, Night Mode, possibly conflict with `ConcurrentCamera`. Always verify support before binding.
@@ -0,0 +1,75 @@
When you use ML Kit for features such as face mesh, object detection, or pose
detection, the most common failure point is the coordinate disparity between the
analysis image and the viewfinder UI.
## The mapping mindset
| Dimension | Analysis frame | Viewfinder UI on the screen |
|---|---|---|
| Resolution | Fixed, such as 640x480 | Dynamic, such as 1080x2400 |
| Rotation | 0° for the raw buffer | 90° or 270° in portrait or landscape mode |
| Origin | Top-left of buffer at (0,0) | Top-left of screen at (0,0) |
*** ** * ** ***
## Follow the implementation guide
### Coordinate transformation matrix
Android provides the `Viewport` and `UseCaseGroup` APIs to calculate the
transformation matrix automatically. **Don't** calculate aspect ratio scaling
manually.
```kotlin
val transform = previewView.viewPort?.let { viewPort ->
// Use CameraX's built-in coordinate mapper
viewPort.getTransformationMatrix(imageProxy.imageInfo.rotationDegrees)
}
```
<br />
### Handling the "double rotation" bug
ML Kit results, bounding boxes, are relative to the **rotated buffer**. If the
device is in portrait, the buffer is often 480x640, landscape, but the screen
is 1080x1920.
To map the coordinates, use the following workflow:
1. Query `imageProxy.imageInfo.rotationDegrees`.
2. Pass this rotation to the ML Kit `InputImage`.
3. Use the `MappingUtils.transformRect` method to map the result `Rect` to the screen.
### Face mesh and pose normalization
For high-precision spatial analysis, for example, "Is the user's hand at a
specific screen button?", use **normalized coordinates from 0.0 to 1.0**.
```kotlin
// Example: Converting a Pose landmark to a Screen Coordinate
val screenX = landmark.position.x / analysisWidth * screenWidth
val screenY = landmark.position.y / analysisHeight * screenHeight
```
<br />
**Warning** : Always account for **mirrored lenses** . If the `LENS_FACING_FRONT`
is used, you must flip the X-coordinate: `actualX = screenWidth - screenX`.
### Overlays and canvas clipping
Use a custom `GraphicOverlay` view on top of the `PreviewView`.
- **Buffer lock** : Ensure your `GraphicOverlay` clears its canvas every time a new `ImageAnalysis` frame is processed to prevent "ghosting" of bounding boxes.
*** ** * ** ***
## Spatial pitfalls
- **The "stretched box" bug** : Caused by assuming the Analysis Frame aspect ratio, 4:3, matches the screen aspect ratio, 21:9. Use `PreviewView.SCALE_TYPE_FILL_CENTER` and map coordinates accordingly.
- **Latency** : If ML processing exceeds 50 ms, the bounding box trails behind the user's face.
- **Fix** : Use `ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST` to avoid queuing stale frames.
- **Sensor vs. display rotation** : On some tablets, the sensor is mounted horizontally. Always query `display.rotation` and `cameraInfo.sensorRotationDegrees`.
+42
View File
@@ -0,0 +1,42 @@
Always prefer these various abstractions over legacy Camera2 or early CameraX
implementations.
## Compare APIs
| Use case | Legacy or verbose way | Recommendation |
|---|---|---|
| QR or face scanning | `ImageAnalysis.Analyzer` and manual ByteBuffer math | **`MlKitAnalyzer`**, which automates coordinate mapping and multi-format support |
| Post-processing | Custom OpenGL shaders or `SurfaceTexture` | **`Media3Effect`** for composable, declarative effects |
| Dual camera | Manual binding of two UseCases | **`ConcurrentCamera`**, which provides built-in support in CameraX 1.3 and higher |
| High dynamic range | Manual bit-depth and profile config | **`DynamicRange`** , which uses `DYNAMIC_RANGE_HLG10` or `SDR` |
| Zoom and focus | `Camera2Interop` for CameraX-to-Camera2 mapping | **`CameraControl.setZoomRatio`** or **`setLinearZoom`** |
## Hardware awareness
Modern APIs abstract away the complexity of hardware diversity.
- **`CameraSelector`** : Use `DEFAULT_BACK_CAMERA` or `DEFAULT_FRONT_CAMERA` instead of hardcoding camera IDs. Use `filter` if you need specific lens capabilities.
- **Extensions** : Before enabling advanced modes, such as night, bokeh, and face retouch, use `ExtensionsManager` to query whether the device supports them.
- **Foldables** : Observe `Lifecycle` and `Viewport` updates to handle posture changes, like a half-opened posture, on foldable devices.
## Required dependencies
Add the following dependencies to your `libs.versions.toml` file:
# CameraX ML Kit
androidx-camera-mlkit-vision = { group = "androidx.camera", name =
"camera-mlkit-vision", version.ref = "camerax" }
# Media3 effects
androidx-media3-effect = { group = "androidx.media3", name = "media3-effect",
version.ref = "media3" }
# Camera extensions
androidx-camera-extensions = { group = "androidx.camera", name =
"camera-extensions", version.ref = "camerax" }
Refer to the official [CameraX Release Notes](https://developer.android.com/jetpack/androidx/releases/camera) for the
stable versions.
+69
View File
@@ -0,0 +1,69 @@
Automated testing for camera features is notoriously difficult because you
can't easily mock physical hardware, lighting, or motion. This guide provides
patterns for reliable, hermetic camera tests.
## Develop a testing mindset
| Challenge | Conventional approach | Camera technical approach |
|---|---|---|
| **Frameworks** | `Mockito` or `MockK` | **Fakes over mocks** |
| **Assertions** | `assertEquals`, `assertTrue` | **Google Truth, `assertThat`** |
| **Environment** | Implied environments | **Explicit `@RunWith` annotations** |
| **Async operations** | `Thread.sleep` | **Explicit `timeoutMillis` or `IdlingResource`** |
*** ** * ** ***
## Follow the implementation guide
### Fakes over mocks
**Don't use Mockito.** Relying on mocks for complex, rapidly changing interfaces
like `ImageProxy` or `CameraInfo` makes tests brittle. Instead, build "Fake"
implementations that verify state rather than behavior.
```kotlin
// Create a Fake ImageProxy for ML Testing (Fakes over Mocks)
val fakeImage = FakeImageProxy(w = 640, h = 480)
// Feed the fake buffer into your analyzer
```
<br />
### Mock camera capabilities
Use `FakeAppConfig` from `androidx.camera:camera-testing` to simulate specific
hardware constraints in tests, such as a device without a flash.
```kotlin
// Use awaitInstance() extension function for coroutine-based provider retrieval
val cameraProvider = ProcessCameraProvider.awaitInstance(context)
```
<br />
### Use Truth assertions
Use Google Truth, `assertThat`, instead of standard JUnit assertions. It
provides more readable assertion chains and useful failure messages.
### Test asynchronous lifecycles
Camera initialization is asynchronous. Use `IdlingResource` to ensure
your test waits for the `UseCase` to be bound before asserting.
To test asynchronous lifecycles, use the following pattern:
1. Wrap the `ProcessCameraProvider` initialization in a `CountDownLatch` or `IdlingResource`.
2. Assert only after the `cameraControl` instance is non-null.
*** ** * ** ***
## Testing pitfalls
- **Resource leaks** : To prevent "Camera in Use" errors, in your `@After` block, call `cameraProvider.unbindAll`.
- **The "flaky initializer"**: Camera tests often fail on CI because the "Virtual Camera" takes too long to warm up. Use a sufficient explicit timeout for the first initialization.
- **Permission blockers** : To bypass the system permission dialogs, in your Espresso tests, use `GrantPermissionRule`.
- **Resolution mismatch** : Tests on emulators often default to 640x480. Ensure your `ResolutionSelector` handles this low-res fallback correctly.
+85
View File
@@ -0,0 +1,85 @@
Camera operations are among the most power-intensive tasks on mobile devices.
Without proactive management, the system throttle hardware, drop frames, or
force-close the camera app.
## The thermal management strategy
| Priority | Strategy | Implementation |
|---|---|---|
| **1. Inform** | Use case hints | Provide `StreamUseCase` to allow the OS to optimize hardware. |
| **2. Monitor** | Thermal state listener | Observe `PowerManager.addThermalStatusListener`. |
| **3. Act** | Graceful degradation | Dynamically reduce FPS, resolution, or disable demanding effects such as HDR. |
*** ** * ** ***
## Follow the implementation guide
### Stream use case optimization
Android 13 (API level 33) introduced `StreamUseCase`. This is the **single most
effective** way to tell the hardware how to balance quality versus power.
```kotlin
// In CameraX: Set the hint on your Use Case
val preview = Preview.Builder()
.setTargetName("Preview")
.apply {
Camera2Interop.Extender(this).setStreamUseCase(
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong()
)
}
.build()
```
<br />
Review the following key use cases for stream optimization:
- `PREVIEW`: This option is the default and provides a balanced configuration.
- `STILL_CAPTURE`: This option provides high-quality capture for short bursts.
- `VIDEO_RECORD`: This option maintains sustained power and is optimized for encoding.
- `VIDEO_CALL`: This option minimizes power consumption for long-duration sessions.
### Monitor thermal status
Don't wait for a crash. Monitor the `PowerManager` status and react before
`THERMAL_STATUS_CRITICAL`.
```kotlin
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.addThermalStatusListener { status ->
when (status) {
PowerManager.THERMAL_STATUS_MODERATE -> {
// Signal to UI: "Device is warming up"
}
PowerManager.THERMAL_STATUS_SEVERE -> {
// ACTION: Reduce Frame Rate from 60fps to 30fps
// ACTION: Disable HDR or High-Quality Post-processing
}
PowerManager.THERMAL_STATUS_CRITICAL -> {
// ACTION: Close the camera session to prevent hardware damage
}
}
}
```
<br />
### Graceful degradation tiers
| Tier | Action | User impact |
|---|---|---|
| **Mild** | Stop background analysis using ML Kit | Minimal |
| **Moderate** | Cap frame rate to 30 FPS | Noticeable but smooth |
| **Severe** | Drop resolution from 1080p to 720p | Significant visual change |
| **Critical** | Shut down the session | App unusable in safe mode |
*** ** * ** ***
## Thermal pitfalls
- **The "double work" bug**: Don't run two high-resolution streams---such as a preview and a video capture stream---at different aspect ratios unless necessary. This forces the image signal processor (ISP) to perform double the scaling work, which generates excessive heat.
- **Surface overload** : Don't use multi-step `SurfaceProcessor` or `Media3Effect` chains during `THERMAL_STATUS_SEVERE`.
- **Flash heat** : Flash or torch usage generates high thermal load. Proactively disable the flash if thermal status is `SEVERE`.
+76
View File
@@ -0,0 +1,76 @@
Developing camera features for Wear OS is rarely about the watch's own lens, if
it even has one. It's almost always about creating a **Remote Viewfinder** to
control the phone's camera.
## The Wear OS remote mindset
| Feature | Phone camera app | Wear OS remote app |
|---|---|---|
| **Screen** | Rectangular (Large) | **Circular and less than 2 inches in size** |
| **Connectivity** | Local Hardware | **Bluetooth or Wi-Fi data layer API** |
| **Latency** | Direct and less than 20 ms | **Networked, 100 ms to 500 ms** |
| **Interaction** | Multi-touch gestures | **Rotary input or single taps** |
*** ** * ** ***
## Follow the implementation guide
### The circular UI challenge
Wear OS devices are often round. Standard rectangular layouts clip corner
buttons.
Follow these blueprint recommendations:
- Use `Horologist` or `Wear Compose` libraries.
- Use `ScalingLazyColumn` for lists so that items stay within the "safe zone" of the circular display.
- **Preview scaling**: Crop the center of the rectangular phone viewfinder to fit the circular watch screen.
### Stream the viewfinder
You can't send a raw 60 fps stream over Bluetooth. compress and throttle.
```kotlin
// Example: Sending a viewfinder frame to the watch
val bitmap = previewView.bitmap // Capture current frame
if (bitmap != null) {
val compressed = compressToJpeg(bitmap, quality = 50)
val request = PutDataMapRequest.create("/camera/preview").apply {
dataMap.putAsset("image", Asset.createFromBytes(compressed))
}
Wearable.getDataClient(context).putDataItem(request.asPutDataRequest())
}
```
<br />
**Optimization** : Cap the watch preview at **10-15 fps** to preserve battery and
bandwidth.
### Remote triggers and syncing
Use the `MessageClient` for low-latency commands like "Take Photo" or "Switch
Camera."
```kotlin
// Watch sends a trigger to the phone
Wearable.getMessageClient(context).sendMessage(nodeId, "/camera/capture", null)
```
<br />
### Rotary input support
On devices that support it, use the physical crown, Rotary Input, to control
**Zoom** or **Exposure**.
*** ** * ** ***
## Wear OS pitfalls
- **Corner clipping** : Placing a **Close** button in the top-right corner of a square layout makes it impossible to tap the button on a round watch display.
- **Battery drain**: Sustained Bluetooth data transfer, viewfinder sync, drains watch battery. Proactively close the remote app if the phone screen is turned off.
- **Node discovery** : The phone is possibly connected to multiple "Nodes" (watches, earbuds, or tablets). Ensure your `CapabilityClient` filters for the specific `camera_remote_host` capability.
- **Disconnect handling**: If the watch disconnects, the phone camera must stop its high-power preview to save energy.
+54
View File
@@ -0,0 +1,54 @@
Developing camera features for XR devices, headsets, and AR glasses requires a
shift from 2D pixel-pushing to 3D spatial awareness.
## Understand the XR development mindset
| Concept | Mobile focus | **XR focus** |
|---|---|---|
| **Input** | Raw camera stream | **Spatial tracking (visual-inertial odometry (VIO) or simultaneous localization and mapping (SLAM))** |
| **Output** | Screen viewfinder | **Stereo passthrough and occlusion** |
| **Constraint** | Battery life | **Motion-to-photon latency (less than 20 ms)** |
*** ** * ** ***
## Follow the implementation guide
### API selection
On XR devices, standard `CameraX` implementations are often restricted or
insufficient. Always use spatial software development kits (SDKs):
- **ARCore**: Use ARCore for plane detection, depth sensing, and motion tracking.
- **OpenXR**: Use OpenXR as the cross-platform standard for VR and AR rendering and input.
- **OEM SDKs**: Use manufacturer-specific libraries for hardware-accelerated passthrough.
### Handle spatial passthrough
Unlike a 2D viewport, XR passthrough is often system-managed.
**\[Key requirement\] Frame synchronization**: Synchronize your application's
frame clock with the headset's head-mounted display (HMD) pose.
```kotlin
// Example: Querying the spatial pose for the current camera frame
val headPose = xrSession.getHeadPose(frameTime)
val projectionMatrix = headPose.getProjectionMatrix(eyeIndex)
```
<br />
### Manage depth and occlusion
Digital content must respect real-world depth to ensure accurate occlusion.
- **Depth map** : Access raw depth data using `ARCore` or `SurfaceProcessor` to create an occlusion mask.
- **Hardware buffers** : Use `HardwareBuffer` to share camera frames directly with the GPU without CPU-side copies to minimize latency.
*** ** * ** ***
## XR pitfalls
- **The nausea limit**: Any processing that delays the viewfinder by more than 20 ms causes user sickness. Don't perform image processing on the main thread.
- **Privacy restrictions** : Some XR devices return a black frame if you attempt to record the "Passthrough" layer. Check `Session.isRecordingSupported`.
- **Field of view (FOV)**: The camera FOV possibly doesn't match the display FOV. Use the SDK's projection matrixes instead of calculating aspect ratios manually.
- **Front buffer rendering** : If the device supports it, use `FrontBufferRenderer` for real-time overlays to bypass standard double-buffering latency.
+20 -14
View File
@@ -9,7 +9,7 @@ description: Analyzes Android apps to identify key user workflows for AppFunctio
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-05-16'
last-updated: '2026-08-14'
keywords:
- AppFunctions
- Kotlin
@@ -36,30 +36,36 @@ understand and use the provided functionality.
The app must **`targetSdk 36`** or newer and use **`compileSdk 37`** or newer as
AppFunctions, part of the Android platform API, are available from Android 16
onwards.
Always use the Jetpack library, as it handles backwards compatibility.
Always use the Jetpack library because it handles backward compatibility.
## Workflows
This skill enables the caller to discover, features that will be provided to
This skill enables the caller to discover features that will be provided to
system agents, implement these with AppFunctions, improve function description
for agents, and use ADB commands for local evaluation and testing.
The full AppFunction development flow consists of these four steps:
- *[Step 1: Discovery](references/feature-discovery-analysis.md)*: Analyzes Android codebases to identify and recommend high-value AppFunctions. Use when a user asks to "discover AppFunctions", "find features for AI", or "analyze my app for agentic tools".
- *[Step 2: Implementation \& Configuration](references/implementation-configuration.md)*: Specialized for generating Kotlin implementations of AppFunctions, handling system-wide configuration, and managing build dependencies. Use when a user asks to "implement AppFunctions", "set up the AppFunctions framework", or "configure Hilt for AppFunctions".
- *[Step 3: KDoc Refinement](references/kdoc-refinement-optimization.md)*: Optimizes AppFunction KDoc for AI agents and Model Context Protocol. Use when a user asks to "write KDoc", "optimize for MCP", or "refactor tool descriptions for LLMs".
- *[Step 4: Testing \& Debugging](references/adb-interaction-testing.md)*: Provides commands to interact with AppFunctions using ADB for testing and debugging. Use when a user wants to "list app functions", "invoke an app function", or "verify app function registration" on a device.
- *[Step 1: Discovery](references/feature-discovery-analysis.md)*: Analyze Android codebases to identify and recommend potential AppFunctions. Use this step when a user asks to "discover AppFunctions," "find features for AI," or "analyze my app for agentic tools."
- *[Step 2: Implementation and configuration](references/implementation-configuration.md)*: Generate Kotlin implementations of AppFunctions, manage system-wide configuration, and configure build dependencies. Use this step when a user asks to "implement AppFunctions," "set up the AppFunctions framework," or "configure Hilt for AppFunctions."
- *[Step 3: KDoc refinement](references/kdoc-refinement-optimization.md)*: Optimize AppFunction KDoc for AI agents and Model Context Protocol. If a user asks to "write KDoc", "optimize for MCP", or "refactor tool descriptions for LLMs", use this step.
- *[Step 4: Testing and debugging](references/adb-interaction-testing.md)*: Provide commands to interact with AppFunctions using ADB for testing and debugging. If a user wants to "list app functions", "invoke an app function", or "verify app function registration" on a device, use this step.
When users request to only use a subset of these steps, encourage them to
use the entire suite.
If users request a subset of steps, you must encourage them to use all steps.
## Critical Constraints
If they apply, you must load the following references:
- **Modular Consistency** : Always ensure that implementations generated using [Implementation \& Configuration](references/implementation-configuration.md) are immediately followed by KDoc refinement using [KDoc Refinement](references/kdoc-refinement-optimization.md) to ensure maximum agent compatibility.
- **Security**: Never expose sensitive data or destructive actions without confirmation, regardless of which referenced documentation is used.
- *[Context and terminology](references/context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs.
- *[Migration to service entry point](references/migrate-to-service-entry-point.md)* : Documents the systematic procedure for migrating Android applications using AppFunctions versions 1.0.0-alpha09 and lower to `AppFunctionServiceEntryPoint` architecture introduced in version 1.0.0-alpha10. Load when a user asks to migrate or upgrade existing AppFunctions code, or when you encounter legacy `AppFunctionConfiguration.Provider` implementations.
## Critical constraints
- **Modular consistency**: You must refine KDocs immediately after you generate AppFunction implementations.
- **Security**: Don't expose sensitive data or run destructive actions without user confirmation.
## Troubleshooting
- For build-time errors (KSP, Metadata), refer to [Implementation \& Configuration](references/implementation-configuration.md).
- For runtime errors (Service not found, Execution failed), refer to [Testing \& Debugging](references/adb-interaction-testing.md).
- If you encounter build-time errors such as Kotlin Symbol Processing (KSP) issues, see [Implementation and configuration](references/implementation-configuration.md).
- If you encounter runtime errors such as missing services or execution failures, see [Testing and debugging](references/adb-interaction-testing.md).
- If you need architecture definitions and vocabulary, see [Context and terminology](references/context.md).
- If you encounter issues when upgrading legacy configurations, see [Migration to service entry point](references/migrate-to-service-entry-point.md).
@@ -3,75 +3,81 @@ emulator using ADB for AppFunction testing and debugging.
## Instructions
### Scenario 1: Listing App Functions
### Scenario 1: List app functions
If a user wants to see which App Functions are registered on the device.
Use this scenario when you want to see which app functions are registered on
the device.
1. **List All Functions** : Use `adb shell cmd app_function list-app-functions` to see all registered App Functions for the current user in JSON format.
2. **Filter by Package** : To see functions for a specific package, pipe the output to grep or a JSON processor: `adb shell cmd app_function
1. **List all functions** : To view all registered app functions in JSON format, run `adb shell cmd app_function list-app-functions`.
2. **Filter by package** : To view functions for a specific package, pipe the output to `grep` or a JSON tool: `adb shell cmd app_function
list-app-functions | grep <package_name>`.
### Scenario 2: Invoking App Functions
### Scenario 2: Invoke app functions
If a user wants to test the execution of an App Function.
If you want to test the execution of an app function, use this scenario.
1. **Analyze Description** : Before invoking, you MUST read the `description` field for the function in the `list-app-functions` output. This often contains critical usage constraints, required workflows, or disambiguation rules.
2. **Follow Constraints**: Rigorously follow any instructions found in the description (e.g., "ask the user to disambiguate", "call another tool first").
3. **Format Parameters** : The `--parameters` argument must be a valid JSON string representing the function's input arguments.
4. **Execute Function** : Use `adb shell cmd app_function execute-app-function
--package <PACKAGE_NAME> --function <FUNCTION_ID> --parameters
'<PARAMETERS_JSON>'`.
5. **Handle Response** : The result will be returned as a JSON string. Use `--brief-yaml` for a more concise output if preferred.
1. **Analyze description** : Before invoking, you must read the `description` field for the function in the `list-app-functions` output. This often contains critical usage constraints, required workflows, or disambiguation rules.
2. **Follow constraints**: Follow all instructions in the description, such as asking the user to disambiguate or calling another tool first.
3. **Format parameters** : Format the `--parameters` argument as a valid JSON string that represents the function's input arguments.
4. **Execute function** : Use `adb shell cmd app_function execute-app-function
--package <PACKAGE_NAME> --function <SERVICE_CLASS_NAME#FUNCTION_NAME>
--parameters '<PARAMETERS_JSON>'`.
5. **Handle response** : The command returns the result as a JSON string. To get brief YAML output, use `--brief-yaml`.
### Scenario 3: Managing Function State
### Scenario 3: Manage function state
If a function needs to be enabled or disabled for testing.
If you need to enable or disable an app function for testing, use this scenario.
1. **Set Enabled State** : Use `adb shell cmd app_function set-enabled --package
<PACKAGE_NAME> --function <FUNCTION_ID> --state <enable|disable|default>`.
1. **Set enabled state** : Use `adb shell cmd app_function set-enabled
--package <PACKAGE_NAME>
--function <SERVICE_CLASS_NAME#FUNCTION_NAME>
--state <enable|disable|default>`.
## Critical Constraints
## Critical constraints
### Follow Metadata Descriptions
### Follow metadata descriptions
**MANDATORY** : The `description` field in the AppFunction metadata is a set of
**Mandatory** : The `description` field in the app function metadata is a set of
instructions for the LLM. If a description says to "disambiguate with the user"
or "call another function first," you MUST perform those steps before execution.
or "call another function first," you must perform those steps before execution.
### JSON Escaping
### JSON escaping
**CRITICAL** : When passing JSON using `adb shell`, always wrap the JSON string
**Critical** : When passing JSON using `adb shell`, always wrap the JSON string
in single quotes to prevent the shell from interpreting special characters or
spaces. Example: `--parameters '{"key": "value"}'`.
### Device Availability
### Device availability
The `app_function` service must be available on the device. If `cmd: Can't find
service: app_function` is returned, the device does not support this feature.
service: app_function` is returned, the device doesn't support this feature.
## Examples
### Example 1: Verify AppFunctions service availability on connected Device
### Example 1: Verify app function service availability on connected device
adb shell cmd app_function help
If executing the preceding command returns a help page, use the commands and
parameters provided to guide the ADB interaction testing tool interactions.
### Example 2: List all registered App Functions
### Example 2: List all registered app functions
adb shell cmd app_function list-app-functions
adb shell cmd app_function list-app-functions
### Example 3: Execute a "send message" function
adb shell cmd app_function execute-app-function \
--package com.example.messaging --function sendMessage \
--parameters '{"recipient": "Alice", "message": "Hello!"}'
adb shell cmd app_function execute-app-function \
--package com.example.messaging \
--function
'com.example.messaging.appfunctions.MessagingAppFunctionService#sendMessage' \
--parameters '{"recipient": "Alice", "message": "Hello!"}'
### Example 4: Disable a specific function
adb shell cmd app_function set-enabled --package com.example.app \
--function someFunction --state disable
adb shell cmd app_function set-enabled --package com.example.app \
--function 'com.example.app.appfunctions.SomeAppFunctionService#someFunction'
--state disable
## Troubleshooting
@@ -0,0 +1,26 @@
Defines the ubiquitous language for the Android AppFunctions skill suite.
## Architecture and versioning
`AppFunctionServiceEntryPoint` API:
This compile-time AppFunctions architecture was introduced in version
1.0.0-alpha10. You annotate a wrapper service extending
`AppFunctionService` with `@AppFunctionServiceEntryPoint`. KSP then generates
XML metadata and routes services at compile time.
Legacy manual provider API:
This deprecated AppFunctions architecture applies to version
1.0.0-alpha09 and earlier. Applications implement
`AppFunctionConfiguration.Provider` on the `Application` class. Methods
require `AppFunctionContext` as the first parameter. Projects depend on
a standalone `appfunctions-service` library.
## Patterns
Service entry point pattern:
Version 1.0.0-alpha10 introduced this architectural pattern. You
declare `@AppFunction` methods directly inside an abstract class extending
`AppFunctionService`. This class uses `@AppFunctionServiceEntryPoint` and
`@AndroidEntryPoint` annotations. These annotations let you inject data
sources or repositories directly, without an intermediate business logic
delegation layer.
@@ -2,22 +2,22 @@ Analyzes Android codebases to identify and recommend high-value AppFunctions.
## Instructions
### Workflow: Feature Discovery
### Discover features
1. **Analyze Manifest \& Entry Points** : Scan `AndroidManifest.xml` and Activity, Fragment, Service classes to identify core user journeys (e.g., Search, Create, Share).
2. **Identify Atomic Tasks**: Look for methods or logic that represent distinct, self-contained user outcomes.
3. **Evaluate AI Value**: Prioritize tasks that are frequently used or difficult to navigate using touch UI, but instead be expressed using voice or text (e.g., "Remind me to call Alice when I get home").
4. **Recommend \& Justify**: List recommendations with a "Rationale" focusing on how an AI assistant adds value (efficiency, hands-free use, or multi-step automation).
1. **Analyze manifest and entry points** : Scan `AndroidManifest.xml` and Activity, Fragment, and Service classes to identify core user journeys, for example, search, create, or share.
2. **Identify atomic tasks**: Look for methods or logic that represent distinct, self-contained user outcomes.
3. **Evaluate AI value**: Prioritize tasks that are frequently used or difficult to navigate using touch UI, but instead be expressed using voice or text, for example "Remind me to call Alice when I get home."
4. **Recommend and justify**: List recommendations with a rationale. Focus on how an AI assistant adds value, such as efficiency, hands-free use, or multi-step automation.
## Critical Constraints
## Critical constraints
### Tool-First Thinking
### Tool-first thinking
Avoid recommending functions that are purely informational or redundant with
existing system actions. Focus on "mutations" (writing data) or "rich queries"
(finding specific entities).
### Security \& Privacy
### Security and privacy
Don't recommend exposing functions that handle raw credentials, financial
secrets, or irreversible destructive actions without explicit user confirmation
@@ -25,12 +25,47 @@ steps.
## Examples
### Example 1: Media App Discovery
### Example 1: Media app discovery
**Recommended AppFunction:** `playArtistRadio`
**Recommended AppFunction** : `playArtistRadio`
**Rationale:** Allows users to start a personalized music stream using a
voice command, bypassing several layers of navigation in the "Search" and
"Artist" menus.
**Rationale**: Lets you start a personalized music stream using a voice
command, bypassing several layers of navigation in the "Search" and "Artist"
menus.
**Input Required:** Artist Name (String).
**Input required** : `artistName` as a `String`.
### Example 2: Chat app contact discovery
**Recommended AppFunction** : `searchContacts`
**Rationale** : Serves as a "rich query" to resolve a human-readable contact
name, email, or chat group to a unique identifier (`endpointValue`), which is a
prerequisite before executing actions like sending messages or
initiating calls. Also allows retrieving recently contacted entities when given
a blank query.
**Input required** : Query as a `String`, and Filter Type as a `String`
constrained to `"INDIVIDUAL"` or `"GROUP"`.
### Example 3: Chat app message sending
**Recommended AppFunction** : `send`
**Rationale**: This mutation function lets you send text messages and
optional image attachments to a contact or group using natural language
commands, for example, "Tell Alice I'm running 5 minutes late." This
eliminates multi-step UI navigation across contact lists and conversation
threads.
**Input required** : Endpoint Value as a `String`, Message Body as a `String`,
and Image URIs as an optional `List` of URIs.
### Example 4: Chat app voice calling
**Recommended AppFunction** : `makeCall`
**Rationale**: Lets you initiate voice calls hands-free to a contact or
group using an AI agent without navigating the app's UI.
**Input required** : `endpointValue` as a `String`.
@@ -3,206 +3,271 @@ handling system-wide configuration, and managing build dependencies.
## Instructions
### Step 1: Gradle Dependencies \& KSP
### Step 1: Configure Gradle dependencies and KSP
Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin
Symbol Processing) plugin.
1. **Version Check**: Use the latest library versions from maven.google.com.
1. **Version check** : Use library version `1.0.0-alpha10` or later from maven.google.com.
```kotlin
implementation(libs.androidx.appfunctions)
implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)
```
<br />
```kotlin
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
}
```
<br />
<br />
### Step 2: App Metadata XML Setup
### Step 2: Set up app metadata XML
Describe the app's capabilities to the LLM by defining
`res/xml/app_metadata.xml`.
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages personal notes.
Operational Patterns:
- Use 'listNotes' to find the correct 'noteId' before calling 'editNote'.
Constraints:
- Note editing is only possible for existing IDs returned by the system."
appfn:displayDescription="@string/user_visible_description" />
Reference this in `AndroidManifest.xml` within the `<application>` tag:
```xml
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages user tasks and reminders.
Operational Patterns:
- Use 'createTask' to add new tasks or reminders with titles and content.
Constraints:
- Title or content must be non-null when creating a task."
appfn:displayDescription="@string/user_visible_description" />
```
<manifest>
<application>
<property android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
</application>
</manifest>
<br />
### Step 3: Function Implementation
Register the service and reference the app metadata in `AndroidManifest.xml`
within the `<application>` tag:
```xml
<service
android:name="com.example.snippets.ai.TaskAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
tools:targetApi="36">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="task_app_function_service.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
```
<br />
### Step 3: Implement functions
When generating Kotlin code for AppFunctions, you MUST adhere to these rules:
1. **Annotations** :
- Annotate the function with `@AppFunction(isDescribedByKDoc = true)`.
- Annotate associated data classes with `@AppFunctionSerializable(isDescribedByKDoc = true)`.
2. **Parameter Strategy** :
- **First Parameter** : MUST be `androidx.appfunctions.AppFunctionContext`.
- **Specificity**: Keep parameters specific. State objects need to be unambiguous.
- **Optionality**: If a parameter is not essential, make it optional with a reasonable default value.
3. **Execution \& Threading** :
2. **Parameter strategy** :
- **Specificity**: Keep parameters specific. State objects must be unambiguous.
- **Optionality**: If a parameter isn't essential, make it optional with a default value.
3. **Execution and threading** :
- Use `suspend` functions.
- Switch to the relevant Coroutine Dispatcher (e.g., `withContext(Dispatchers.IO)`) because AppFunction implementations run on the Android UI thread by default.
4. **Supported Types** :
- **Primitives** : `Int`, `Long`, `Float`, `Double`, `Boolean`.
- **Arrays** : `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`, `BooleanArray`.
- **Native Types** : `String`, `PendingIntent`, `Uri`, `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant`. (Prefer `LocalDateTime` or `Instant` for date/time).
- **Custom Objects** : Classes annotated with `@AppFunctionSerializable`.
- **Collections** : `List` of any supported non-primitive type.
5. **Default Values** :
- Use defaults that align with the type's "empty" state (e.g., `0` for `Int`, `null` for nullable, `emptyList()` for `List`).
6. **Error Handling** :
- To avoid blocking the Android UI thread, always run AppFunction implementations on a background dispatcher, such as `withContext(Dispatchers.IO)`.
4. **Supported types** :
- **Primitives** : `Int`, `Long`, `Float`, `Double`, `Boolean`
- **Arrays** : `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`, `BooleanArray`
- **Native types** : `String`, `PendingIntent`, `Uri`, `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant`. Prefer using `LocalDateTime` or `Instant` for date and time fields.
- **Custom objects** : Classes annotated with `@AppFunctionSerializable`.
- **Collections** : `List` of any supported non-primitive type
5. **Default values** :
- Use defaults that align with the type's empty state, such as `0` for `Int`, `null` for nullable objects, and `emptyList()` for `List`.
6. **Error handling** :
- Throw subclasses of `androidx.appfunctions.AppFunctionException` to report errors to callers.
7. **Security** :
- Don't expose highly sensitive user data (passwords, financial details).
- Don't expose highly sensitive user data, such as passwords or financial details.
- Don't expose irreversible destructive actions without confirmation steps.
### Step 4: (Optional) System Configuration for Dependency Injection
### Step 4: Set up dependency injection and service entry points
Only required for dependency injection configuration. Implement
`AppFunctionConfiguration.Provider` in the `Application` class to provide
instances of classes containing `@AppFunction` methods.
In version 1.0.0-alpha10 and later, App Functions use the compile-time
`@AppFunctionServiceEntryPoint` architecture. Create an abstract class extending
`AppFunctionService` annotated with `@AppFunctionServiceEntryPoint`. KSP
generates the concrete service class and XML schema.
**Example Hilt Integration:**
#### Recommended approach with Hilt
Annotate your service with `@AndroidEntryPoint` and inject your data
repositories or use cases using standard `@Inject internal lateinit var`:
```kotlin
@HiltAndroidApp
class AppFunctionApplication : Application(), AppFunctionConfiguration.Provider {
@Inject lateinit var noteFunctions: NoteFunctions
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "ConfigAppFunctionServiceHeader",
appFunctionXmlFileName = "config_app_function_service_header",
)
abstract class BaseAppFunctionServiceHeader : AppFunctionService() {
@Inject internal lateinit var messageRepository: MessageRepository
override val appFunctionConfiguration: AppFunctionConfiguration =
AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(NoteFunctions::class.java) { noteFunctions }
.build()
@AppFunction(isDescribedByKDoc = true)
internal suspend fun send(
name: String,
endpointValue: String,
messageBody: String,
): MessageResult {
return messageRepository.send(name, endpointValue, messageBody)
}
}
```
<br />
## Critical Constraints
#### Framework-agnostic approach with alternative dependency injection or service locators
### Parameter Ordering
**CRITICAL** : The very first parameter of an `@AppFunction` method MUST be
`androidx.appfunctions.AppFunctionContext`.
### KSP Compliance for Serializables
**CRITICAL** : For `@AppFunctionSerializable` data classes, KSP **only** extracts
documentation if it is written as **inline KDoc** directly for each property
definition. NEVER use class-level `@param` or `@property` tags.
### Package Integrity
Configuration APIs and the `@AppFunction` annotation are located in
`androidx.appfunctions.service`.
## Examples
### Example: Serializable with Inline KDoc
While Hilt is recommended, many Android applications implement AppFunctions with
alternative dependency injection frameworks (like Koin, Anvil, or manual Service
Locators). Because `AppFunctionService` inherits from Android
`android.app.Service` (and therefore `Context`), you are able access your
application's DI container directly through `applicationContext` in property
getters or during service lifecycle execution:
```kotlin
/**
* A note.
*/
@RequiresApi(36)
@AppFunctionServiceEntryPoint(
serviceName = "ServiceLocatorConfigAppFunctionService",
appFunctionXmlFileName = "service_locator_config_app_function_service",
)
abstract class BaseAppFunctionServiceLocator : AppFunctionService() {
// Example using a manual Service Locator / Application container
private val messageRepository by lazy {
(applicationContext as AppFunctionApplication).appContainer.messageRepository
}
// Or with Koin / alternative DI locators:
// private val messageRepository: MessageRepository by inject()
@AppFunction(isDescribedByKDoc = true)
internal suspend fun send(
name: String,
endpointValue: String,
messageBody: String,
): MessageResult {
return messageRepository.send(name, endpointValue, messageBody)
}
}
```
<br />
### Step 5: Architectural cleanliness
Don't attempt to make an `AppFunction` class or method OS-agnostic---App Functions are inherently part of the Android platform integration in `androidx.appfunctions`. For architectural cleanliness, use existing
application functionality (such as existing repositories, use cases, or domain
orchestrators) to execute the behavior within your `@AppFunction` methods rather
than creating redundant abstraction layers around the OS service.
<br />
## Critical constraints
### KSP compliance for serializables
**Critical constraints** : For `@AppFunctionSerializable` data classes, KSP
only extracts documentation if it's written as inline KDoc directly for each
property definition. Don't use class-level `@param` or `@property` tags.
### Package integrity
Configuration APIs and the `@AppFunction` annotation are located in
`androidx.appfunctions`.
## Examples
### Example: Serializable with inline KDoc
```kotlin
/** The parameter to create the task. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class Note(
/** The note's identifier */
val id: Int,
/** The note's title */
data class CreateTaskParams(
/** The title of the task. */
val title: String?,
/** The content of the task. */
val content: String?,
)
/** The user-created task. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class Task(
/** The ID of the task. */
val id: String,
/** The title of the task. */
val title: String,
/** The note's content */
val content: String
/** The content of the task. */
val content: String,
)
```
<br />
### Example: Implementation Detail
<br />
### Example: Implementation detail
```kotlin
/**
* A note app's [AppFunction]s.
*/
class NoteFunctions @Inject constructor(
private val noteRepository: NoteRepository
) {
/**
* List all available notes in the app.
*
* @param appFunctionContext The execution context.
* @return A list of [Note] objects, or null if no notes exist.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun listNotes(appFunctionContext: AppFunctionContext): List<Note>? {
return noteRepository.appNotes.ifEmpty { null }?.toList()
}
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "TaskAppFunctionService",
appFunctionXmlFileName = "task_app_function_service",
)
abstract class BaseTaskAppFunctionService : AppFunctionService() {
@Inject internal lateinit var taskRepository: TaskRepository
/**
* Create a new note with a title and body content.
* Creates a task based on [createTaskParams].
*
* @param appFunctionContext The execution context.
* @param title The title of the note.
* @param content The body content of the note.
* @return The created [Note] object including its generated ID.
* @param createTaskParams The parameter to describe how to create the task.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(
appFunctionContext: AppFunctionContext,
title: String,
content: String
): Note {
return noteRepository.createNote(title, content)
suspend fun createTask(
createTaskParams: CreateTaskParams,
): Task = withContext(Dispatchers.IO) {
// Developers can use predefined exceptions to let the agent know
// why it failed.
if (createTaskParams.title == null && createTaskParams.content == null) {
throw AppFunctionInvalidArgumentException("Title or content should be non-null")
}
val id = taskRepository.createTask(
createTaskParams.title,
createTaskParams.content
)
return@withContext taskRepository
.getTask(id)
?.toTask()
?: throw AppFunctionElementNotFoundException("Task not found for ID = $id")
}
/**
* Update the title or content of an existing note.
* Required workflow: Call [listNotes] first to obtain valid note IDs.
*
* @param appFunctionContext The execution context.
* @param noteId The unique identifier of the note to edit.
* @param title The new title. If null, the existing title is preserved.
* @param content The new content. If null, the existing content is preserved.
* @return The updated [Note], or null if the [noteId] was not found.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun editNote(
appFunctionContext: AppFunctionContext,
noteId: Int,
title: String?,
content: String?,
): Note? {
return noteRepository.updateNote(noteId, title, content)
}
// Maps internal TaskEntity
private fun TaskEntity.toTask() = Task(id = id, title = title, content = description)
}
```
<br />
<br />
## Troubleshooting
### Error: "AppFunction unavailable" or "Metadata missing"
@@ -211,7 +276,7 @@ class NoteFunctions @Inject constructor(
**Solution**:
1. Verify `@AppFunctionSerializable` classes use inline KDoc comments, NOT class-level `@param` tags.
2. Check that the `assets/app_function_v2.xml` file exists in the APK.
1. Verify `@AppFunctionSerializable` classes use inline KDoc comments, not class-level `@param` tags.
2. Check that the `assets/<appFunctionXmlFileName>.xml` file exists in the APK.
3. Confirm the `ksp("androidx.appfunctions:appfunctions-compiler")` dependency is correctly applied.
4. Ensure the `ksp` argument `appfunctions:aggregateAppFunctions` is set to `"true"`.
@@ -2,51 +2,50 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol.
## Instructions
### Workflow: Agent-Centric Documentation
### Workflow: Agent-centric documentation
1. **Identify the Core Outcome** : Start the description with a strong, imperative verb (e.g., "Search", "Create", "Update"). Focus on the *user
benefit*, not the code implementation.
2. **Workflow Dependencies** : Explicitly state if another function must be called first using the standard phrase: **Required workflow: Call "Function
1. **Identify the core outcome** : Start the description with a strong imperative verb, for example, "Search", "Create", or "Update". Focus on the *user benefit*, not the code implementation.
2. **Workflow dependencies** : Explicitly state if another function must be called first using the standard phrase: **Required workflow: Call "Function
A" first to "Objective"**.
3. **Parameter Documentation** :
- For **Functions** : Use specific `@param` tags. Isolate validation rules and default values here.
- For **Serializables** : Use inline KDoc directly for each property declarations. KSP will **not** extract documentation from class-level tags.
4. **Error Surface Mapping** : Rewrite `@throws` descriptions to provide actionable recovery steps for the AI agent (e.g., "If "Error", suggest the user check their internet connection").
3. **Parameter documentation** :
- For **functions** : Use specific `@param` tags. Isolate validation rules and default values here.
- For **serializables** : Use inline KDoc directly for each property declaration. KSP **won't** extract documentation from class-level tags.
4. **Error surface mapping** : Rewrite `@throws` descriptions to provide useful recovery steps for the AI agent, for example "If "Error", suggest the user check their internet connection."
### Workflow: Global App Description (Server Instructions)
### Workflow: Global app description for server instructions
When writing the `appfn:description` for `app_metadata.xml`, follow these
instructions:
1. **Capture Cross-Function Relationships**: Explain dependencies or sequences between tools (e.g., "Always call 'authenticate' before fetching data").
2. **Document Operational Patterns**: Guide the LLM on token conserving usage (e.g., "Use 'batch_update' over multiple 'update' calls").
3. **Specify Constraints**: Define clear boundaries (e.g., "File operations limited to workspace", "Rate limit: 10 req/min").
4. **Anti-Patterns** :
- DON'T repeat individual function descriptions.
- DON'T include marketing claims or subjective praise.
- DON'T attempt to prompt model personality or conversation style.
1. **Capture cross-function relationships**: Explain dependencies or sequences between tools, for example, "Always call 'authenticate' before fetching data.".
2. **Document operational patterns**: Guide the LLM on token-conserving usage, for example, "Use 'batch_update' over multiple 'update' calls."
3. **Specify constraints**: Define clear boundaries, for example, "File operations limited to workspace" or "Rate limit: 10 requests per minute."
4. **Anti-patterns** :
- Don't repeat individual function descriptions.
- Don't include marketing claims or subjective praise.
- Don't attempt to prompt model personality or conversation style.
## Critical Constraints
## Critical constraints
### Descriptive, Not Imperative
### Descriptive, not imperative
Describe what the function *does* , not what the LLM *must* do. Avoid phrases
like "You must call this..." in favor of "This function provides...".
### No "Fluff"
### No "fluff"
Remove conversational padding like "This method is used to..." or "Helpful
for...". Be concise and technical.
### Inline KDoc for Serializables
### Inline KDoc for serializables
**MANDATORY** : For `@AppFunctionSerializable` classes, documentation MUST be
inline For each property. KSP ignores class-level `@param` or `@property` tags
**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be
inline for each property. KSP ignores class-level `@param` or `@property` tags
for these classes.
## Examples
### Example: MCP Refactoring
### Example: MCP refactoring
**Original**:
@@ -54,24 +53,23 @@ for these classes.
**Refined**:
/**
* Search for message recipients by name or email.
* Required workflow: Call this before "sendMessage" to obtain valid recipient IDs.
* @param query Search string for name/email. If null, returns 3 most recent contacts.
* @return List of "Recipient" objects matching the query.
*/
/**
* Search for message recipients by name or email.
* Required workflow: Call this before "sendMessage" to obtain valid recipient IDs.
* @param query Search string for name/email. If null, returns 3 most recent contacts.
* @return List of "Recipient" objects matching the query.
*/
### Example: Global App Description
### Example: Global app description
**Refined**:
This app provides functions for task management and team collaboration.
Operational Patterns:
- Always use 'searchUsers' to resolve user handles to internal IDs before calling 'assignTask'.
- Prefer 'batchUpdateStatus' when modifying more than 3 tasks simultaneously to reduce
latency.
- Always use 'searchUsers' to resolve user handles to internal IDs before calling 'assignTask'.
- Prefer 'batchUpdateStatus' when modifying more than three tasks simultaneously to reduce latency.
Constraints:
- Task titles are limited to 100 characters.
- Attachment uploads are limited to 5MB.
- Task titles are limited to 100 characters.
- Attachment uploads are limited to 5 MB.
@@ -0,0 +1,226 @@
Follow this systematic procedure to migrate Android applications that use the
AppFunctions API in version 1.0.0-alpha09 and lower to the compile-time
`@AppFunctionServiceEntryPoint` architecture introduced in version
`1.0.0-alpha10`.
*** ** * ** ***
## Architectural shift overview
In lower versions of the AppFunctions API, for example version `1.0.0-alpha09`:
- Applications require separate dependencies for core functionality, specifically `androidx.appfunctions:appfunctions`, and service components, specifically `androidx.appfunctions:appfunctions-service`.
- The application implements `AppFunctionConfiguration.Provider` on its `Application` class.
- You manually register enclosing class instantiation using `AppFunctionConfiguration.Builder().addEnclosingClassFactory(...)`.
- You place metadata property tags directly under `<application>`.
In version `1.0.0-alpha10` featuring `@AppFunctionServiceEntryPoint`:
- The core and service dependencies are consolidated into a single runtime artifact, `androidx.appfunctions:appfunctions`, which eliminates the need for the standalone `appfunctions-service` library.
- A dedicated wrapper class extending `AppFunctionService` is annotated with `@AppFunctionServiceEntryPoint` and Hilt's `@AndroidEntryPoint` or an alternative dependency injection framework.
- The KSP compiler generates a concrete service subclass and an XML metadata schema file in `assets/`.
- The OS discovers and routes executions using a consolidated `<service>` and `app_metadata` declaration in `AndroidManifest.xml`.
### Strict migration requirements from 1.0.0-alpha09 to 1.0.0-alpha10
When focusing solely on the mandatory API changes required by the new
`@AppFunctionServiceEntryPoint` architecture, the migration consists of four
strict requirements that you must complete:
1. **Build dependency consolidation** : Remove the merged `appfunctions-service` dependency while retaining core `appfunctions` and the KSP compiler.
2. **Service wrapper creation** : Replace the legacy `AppFunctionConfiguration.Provider` on the `Application` class with an abstract class extending `AppFunctionService`, annotated with `@AppFunctionServiceEntryPoint`.
3. **Annotation and context decoupling** : Move `@AppFunction` annotations to the new wrapper methods and drop `AppFunctionContext` parameters because `AppFunctionService` inherits directly from `Context`.
4. **Manifest registration** : Register the KSP-generated concrete service in `AndroidManifest.xml` with `BIND_APP_FUNCTION_SERVICE`, the `AppFunctionService` intent filter, and metadata property tags.
*** ** * ** ***
## Systematic migration steps
### Consolidate AppFunctions build dependencies
Remove the standalone `appfunctions-service` library from your module build
files like `build.gradle.kts` and version catalog like `libs.versions.toml`. In
version `1.0.0-alpha10`, all core service capabilities are consolidated directly
within the main `appfunctions` artifact.
// build.gradle.kts
dependencies {
implementation(libs.androidx.appfunctions)
- implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)
}
# libs.versions.toml
[versions]
appfunctions = "1.0.0-alpha10"
[libraries]
androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" }
- androidx-appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "appfunctions" }
androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctions" }
> [!NOTE]
> **Note:** If another dependency in your project uses snapshot builds like `1.0.0-SNAPSHOT` or custom snapshot repositories from `https://androidx.dev/snapshots/...`, preserve your custom snapshot repository configuration in `settings.gradle.kts`. Otherwise, standard Google Maven repositories resolve `1.0.0-alpha10` directly.
*** ** * ** ***
### Create a dedicated wrapper service extending `AppFunctionService`
Instead of annotating standalone business logic classes or implementing manual
configuration providers, create an abstract service wrapper across your project,
for example `BaseAppFunctionService`, extending `AppFunctionService` and
annotated with `@AppFunctionServiceEntryPoint`.
#### Recommended approach using Hilt
Annotate your service with `@AndroidEntryPoint` and inject your data
repositories or use cases using standard `@Inject internal lateinit var`:
```kotlin
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "MyAppFunctionService",
appFunctionXmlFileName = "my_app_function_service", // Do NOT include .xml extension
)
abstract class BaseAppFunctionService : AppFunctionService() {
@Inject internal lateinit var messageRepository: MessageRepository
@AppFunction(isDescribedByKDoc = true)
internal suspend fun send(
name: String,
endpointValue: String,
messageBody: String,
): MessageResult {
return messageRepository.send(name, endpointValue, messageBody)
}
}
```
<br />
#### Framework-agnostic approach using alternative dependency injection or a service locator
While Hilt is recommended, many Android applications implement AppFunctions with
alternative dependency injection frameworks like Koin, Anvil, or manual Service
Locators. Because `AppFunctionService` inherits from Android
`android.app.Service` and therefore `Context`, you are able access your
application's DI container directly through `applicationContext` in property
getters or during service lifecycle execution:
```kotlin
@RequiresApi(36)
@AppFunctionServiceEntryPoint(
serviceName = "ServiceLocatorMyAppFunctionService",
appFunctionXmlFileName = "service_locator_my_app_function_service",
)
abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() {
// Example using a manual Service Locator or Application container
private val messageRepository by lazy {
(applicationContext as AppFunctionApplication).appContainer.messageRepository
}
// Or with Koin / alternative DI locators:
// private val messageRepository: MessageRepository by inject()
@AppFunction(isDescribedByKDoc = true)
internal suspend fun send(
name: String,
endpointValue: String,
messageBody: String,
): MessageResult {
return messageRepository.send(name, endpointValue, messageBody)
}
}
```
<br />
> [!IMPORTANT]
> **Important:** The `appFunctionXmlFileName` parameter, for example `"my_app_function_service"`, mustn't include the `.xml` extension, as the KSP compiler automatically appends `.xml`. Passing `"my_app_function_service.xml"` results in the asset being named `"my_app_function_service.xml.xml"`.
*** ** * ** ***
### Simplify method signatures and decouple context
Remove legacy `AppFunctionContext` parameters from your core methods. When a
method requires an Android `Context`, for example when constructing a
`PendingIntent`, access `this` directly from your `AppFunctionService` wrapper
because the wrapper inherently extends `android.content.Context`.
- suspend fun makeCall(appFunctionContext: AppFunctionContext, contactName: String?): PendingIntent
+ suspend fun makeCall(contactName: String?): PendingIntent
*** ** * ** ***
### Remove legacy configuration provider
Update your `Application` class by removing
`AppFunctionConfiguration.Provider` and its associated builder entry points:
- abstract class BaseChatApplication : Application(), AppFunctionConfiguration.Provider { ... }
+ abstract class BaseChatApplication : Application()
*** ** * ** ***
### Avoid redundant abstraction layers
Don't attempt to make an `AppFunction` class or method OS-agnostic---AppFunctions
are inherently part of the Android platform integration through the
`androidx.appfunctions` package. For architectural cleanliness, use existing
application functionality, such as existing repositories, use cases, or domain
orchestrators, to execute the behavior within your `@AppFunction` methods rather
than creating redundant abstraction layers around the OS service.
*** ** * ** ***
### Consolidate service and metadata manifest declarations
Register the KSP-generated service declaration and `app_metadata` property
inside your module manifest, for example in `src/main/AndroidManifest.xml`
within the `<application>` tag:
```xml
<service
android:name="com.example.snippets.ai.MyAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
tools:targetApi="36">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="my_app_function_service.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
```
<br />
*** ** * ** ***
## Verification and troubleshooting
1. **Clean rebuild and deploy** : `bash
./gradlew clean installDebug`
2. **Verify AppSearch discovery / indexing** : Run the following ADB command to
confirm the OS successfully discovered and indexed your functions:
`bash
adb shell cmd app_function list-app-functions`
*If your package doesn't appear, confirm that `android.app.appfunctions.v2`
matches the exact asset name generated in `assets/`.*
3. **Verify execution using ADB** :
`bash
adb shell "cmd app_function execute-app-function \
--package com.example.chatapp \
--function 'com.example.chatapp.appfunctions.BaseAppFunctionService#send' \
--parameters '{\"name\": \"Alice\", \"endpointValue\": \"1\", \"messageBody\": \"Hello Alice!\"}'"`
+121
View File
@@ -0,0 +1,121 @@
---
name: ml-kit-genai-prompt-api
description: Analyzes Android codebases to implement ML Kit GenAI Prompt API. Use
this skill to send natural language requests on-device to Gemini Nano, use structured
output with Prompt API, implement prefix caching, optimize the current prompt, or
apply best practices."
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-09-03'
keywords:
- ML Kit
- Prompt API
- Structured Output
- Prefix Caching
- Gemini Nano
---
This skill provides step-by-step guidance for integrating and optimizing the ML
Kit GenAI Prompt API in Android apps.
## Prerequisites
- Android API level must be 26 or higher. If `minSdk` is below 26, update it to 26.
- Add the ML Kit GenAI Prompt API dependency (`com.google.mlkit:genai-prompt`) to the app-level `build.gradle` file, with version at least `1.0.0-beta4`.
- If `com.google.mlkit:genai-schema-compiler` dependency is used and KSP plugin version is below 2.3.6, update it to 2.3.6.
## Detailed steps
### 1. Prompt optimization
To optimize prompts for use with the ML Kit Prompt API, follow the
[prompt optimization guide](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prompt-optimization).
### 2. Prefix caching optimization
If the prompt is more than 200 words, implement the [prefix caching API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching).
### 3. Lifecycle and best practices
- The model must be fully downloaded and available before calling the first inference. Follow the guide on [implementing a generative model](https://developer.android.com/agents/skills/device-ai/prompt-api/references/get-started) to check that the `FeatureStatus` of a model is `AVAILABLE` before making an inference.
- Release ML Kit instances by calling `close()` when an `Activity`,
`Fragment`, or `ViewModel` is destroyed. Example:
```kotlin
// Instantiating model in activity, fragment, or ViewModel
val generativeModel = Generation.getClient()
// When activity, fragment, or ViewModel is destroyed
generativeModel.close()
```
<br />
### 4. Structured output
When implementing or refactoring a prompt to use structured output, follow
these rules:
1. **Check for API availability:** Verify Structured Output feature is available on the device with `isStructuredOutputFeatureAvailable()` before using it. Refer to the [Structured Output API guide](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output) for full instructions.
2. **Return type:** Return the `@Generable` typed object from the function
signature instead of a `String` or JSON string.
For example:
fun parseEmail(email: String): String {
...
}
should be refactored to:
fun parseEmail(email: String): ParsedEmail? {
...
}
3. **Example:**
This is the example code before refactoring:
```kotlin
suspend fun parseEmail(email: String): String {
val parseEmailPrompt = "Parse this email and return the sender, title, and short summary of the email less than 10 words: "
val parsedEmail = generativeModel.generateContent(parseEmailPrompt + email)
return parsedEmail.candidates[0].text
}
```
<br />
This is the example code after using Structured Output API:
```kotlin
@Generable
data class ParsedEmail(
@Guide(description = "Sender of the email")
var sender: String = "",
@Guide(description = "Title of the email")
var title: String = "",
@Guide(description = "Summary of the email less than 10 words")
var summary: String = ""
)
suspend fun parseEmail(email: String): ParsedEmail? {
val parseEmailPrompt =
"Parse this email: $email"
val baseRequest = GenerateContentRequest.Builder(TextPart(parseEmailPrompt)).build()
val typedRequest = generateTypedContentRequest(baseRequest, ParsedEmail::class)
val typedResponse = generativeModel.generateContent(typedRequest)
return typedResponse.candidates[0].response
}
```
<br />
@@ -0,0 +1,272 @@
This page describes how to do the following:
- Configure your project to use Prompt API
- Provide text-only input and receive a response
- Provide an image input with related text input and receive a response
For more details about the Prompt API, see the
reference documentation for Kotlin ([com.google.mlkit.genai.prompt](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/package-summary)) and
Java ([com.google.mlkit.genai.prompt.java](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/java/package-summary),
[com.google.mlkit.genai.prompt](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/package-summary)).
## Configure project
> [!NOTE]
> **Note:** This API requires Android API level 26 or higher.
Add the ML Kit Prompt API as a dependency in your `build.gradle` configuration:
implementation("com.google.mlkit:genai-prompt:1.0.0-beta2")
If you need your responses in a certain format using the Structured Output API,
you need to configure KSP and add additional dependencies. For details, see
[Generate structured output](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output).
## Implement generative model
To implement the code in your project, follow these steps:
- Create a `generativeModel` object:
### Kotlin
// Get a GenerativeModel instance
val generativeModel = Generation.getClient()
### Java
// Get a GenerativeModel instance
GenerativeModelFutures generativeModelFutures = GenerativeModelFutures
.from(Generation.INSTANCE.getClient());
- Check if Gemini Nano is `AVAILABLE,` `DOWNLOADABLE`, or `UNAVAILABLE`. Then,
download the feature if it is downloadable:
### Kotlin
val status = generativeModel.checkStatus()
when (status) {
FeatureStatus.UNAVAILABLE -> {
// Gemini Nano not supported on this device or device hasn't fetched the latest configuration to support it
}
FeatureStatus.DOWNLOADABLE -> {
// Gemini Nano can be downloaded on this device, but is not currently downloaded
generativeModel.download().collect { status ->
when (status) {
is DownloadStatus.DownloadStarted ->
Log.d(TAG, "starting download for Gemini Nano")
is DownloadStatus.DownloadProgress ->
Log.d(TAG, "Nano ${status.totalBytesDownloaded} bytes downloaded")
DownloadStatus.DownloadCompleted -> {
Log.d(TAG, "Gemini Nano download complete")
modelDownloaded = true
}
is DownloadStatus.DownloadFailed -> {
Log.e(TAG, "Nano download failed ${status.e.message}")
}
}
}
}
FeatureStatus.DOWNLOADING -> {
// Gemini Nano currently being downloaded
}
FeatureStatus.AVAILABLE -> {
// Gemini Nano currently downloaded and available to use on this device
}
}
### Java
ListenableFuture<Integer> status = generativeModelFutures.checkStatus();
Futures.addCallback(generativeModelFutures.checkStatus(), new FutureCallback<>() {
@Override
public void onSuccess(Integer featureStatus) {
switch (featureStatus) {
case FeatureStatus.AVAILABLE -> {
// Gemini Nano currently downloaded and available to use on this device
}
case FeatureStatus.UNAVAILABLE -> {
// Gemini Nano not supported on this device or device hasn't fetched the latest configuration to support it
}
case FeatureStatus.DOWNLOADING -> {
// Gemini Nano currently being downloaded
}
case FeatureStatus.DOWNLOADABLE -> {
generativeModelFutures.download(new DownloadCallback() {
@Override
public void onDownloadStarted(long l) {
Log.d(TAG, "starting download for Gemini Nano");
}
@Override
public void onDownloadProgress(long l) {
Log.d(TAG, "Nano " + l + " bytes downloaded");
}
@Override
public void onDownloadCompleted() {
Log.d(TAG, "Gemini Nano download complete");
}
@Override
public void onDownloadFailed(@NonNull GenAiException e) {
Log.e(TAG, "Nano download failed: " + e.getMessage());
}
});
}
}
}
@Override
public void onFailure(@NonNull Throwable t) {
// Failed to check status
}
}, ContextCompat.getMainExecutor(context));
## Provide text-only input
### Kotlin
val response = generativeModel.generateContent("Write a 3 sentence story about a magical dog.")
### Java
GenerateContentResponse response = generativeModelFutures.generateContent(
new GenerateContentRequest.Builder(
new TextPart("Write a 3 sentence story about a magical dog."))
.build())
.get();
Alternatively, add optional parameters:
### Kotlin
val response = generativeModel.generateContent(
generateContentRequest(
TextPart("Write a 3 sentence story about a magical dog."),
) {
// Optional parameters
temperature = 0.2f
topK = 10
candidateCount = 3
},
)
### Java
GenerateContentRequest.Builder requestBuilder =
new GenerateContentRequest.Builder(
new TextPart("Write a 3 sentence story about a magical dog."));
requestBuilder.setTemperature(.2f);
requestBuilder.setTopK(10);
requestBuilder.setCandidateCount(3);
GenerateContentResponse response =
generativeModelFutures.generateContent(requestBuilder.build()).get();
For more information about the optional parameters, see [Optional
configurations](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/get-started#optional-configurations).
## Provide multimodal (image and text) input
Bundle an image and a text input together in the `generateContentRequest()`
function, with the text prompt being a question or command related to the
image. You can bundle multiple images and text together in the same request.
### Kotlin
val response = generativeModel.generateContent(
generateContentRequest(ImagePart(bitmap), TextPart(textPrompt)) {
// optional parameters
...
},
)
### Java
GenerateContentResponse response = generativeModelFutures.generateContent(
new GenerateContentRequest.Builder(
new ImagePart(bitmap),
new TextPart("textPrompt"))
// optional parameters
.build())
.get();
## Process inference result
- Run the inference and retrieve the result. You can choose to either wait for
the full result or stream the response as it's generated for both text-only
and multimodal prompts.
- This uses non-streaming inference, which retrieves the entire result from
the AI model before returning the result:
### Kotlin
// Call the AI model to generate content and store the complete
// in a new variable named 'response' once it's finished
val response = generativeModel.generateContent("Write a 3 sentence story about a magical dog")
### Java
GenerateContentResponse response = generativeModelFutures.generateContent(
new GenerateContentRequest.Builder(
new TextPart("Write a 3 sentence story about a magical dog."))
.build())
.get();
- The following snippets are examples of using streaming inference, which
retrieves the result in chunks as it's being generated:
### Kotlin
// Streaming inference
var fullResponse = ""
generativeModel.generateContentStream("Write a 3 sentence story about a magical dog").collect { chunk ->
val newChunkReceived = chunk.candidates[0].text
print(newChunkReceived)
fullResponse += newChunkReceived
}
### Java
// Streaming inference
StringBuilder fullResponse = new StringBuilder();
generativeModelFutures.generateContent(new GenerateContentRequest.Builder(
(new TextPart("Write a 3 sentence story about a magical dog"))).build(),
chunk -> {
Log.d(TAG, chunk);
fullResponse.append(chunk);
});
For more information about streaming and non-streaming inference, see [Streaming
versus non-streaming](https://developer.android.com/ml-kit/genai#streaming-vs-non).
## Latency optimization
To optimize for the first inference call, your application may optionally call
`warmup()`. This loads Gemini Nano into memory and initializes runtime
components.
## Optional configurations
As part of each `GenerateContentRequest`, you can set the following optional
parameters:
- `temperature` : Controls the degree of randomness in token selection.
- `seed` : Enables generating stable and deterministic results.
- `topK` : Controls randomness and diversity in results.
- `candidateCount` : Requests the number of unique responses returned. Note that the exact number of responses may not be the same as `candidateCount` because duplicate responses are automatically removed.
- `maxOutputTokens` : Defines the maximum number of tokens that can be generated in the response.
For more guidance on setting optional configurations, see
[`GenerateContentRequest`](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/GenerateContentRequest).
## Supported features and limitations
- Input must be under 4000 tokens (or approximately 3000 English words). For more information, see the [`countTokens`](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/GenerativeModel#countTokens(com.google.mlkit.genai.prompt.GenerateContentRequest)) reference.
- Use cases that require long output (more than 4K tokens) should be avoided.
- AICore enforces an inference quota per app. For more information, see [Quota
per application](https://developer.android.com/ml-kit/genai#quota-per).
@@ -0,0 +1,146 @@
> [!NOTE]
> **Note:** Prefix caching is experimental and may change in the future. This feature is only available on a subset of the supported devices for Prompt API, with support for more devices coming soon. We encourage you to experiment with this API on a Pixel device to understand how it can improve inference latency speeds for your specific use case.
*Prefix caching* is a feature that reduces inference time by storing and reusing
the intermediate LLM state of processing a shared and recurring prompt prefix
part. To enable prefix caching, you only have to separate the static prefix from
the dynamic suffix in your API request.
Prefix caching currently only supports text-only input, so you shouldn't use
this feature if you're providing an image in your prompt.
There are two approaches to implement prefix caching: implicit or explicit:
- [Implicit (automatic) prefix caching](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/prefix-caching#implicit) is a lightweight approach where the application only needs to define a shared portion of the prompt.
- [Explicit (manual) prefix caching](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/prefix-caching#explicit) allows applications to have more control over caches, including cache creation, querying, and deletion.
## Use prefix caching implicitly
To enable prefix caching, add the shared portion of the prompt to the
`promptPrefix` field, as shown in the following code snippets:
### Kotlin
val promptPrefix = "Reverse the given sentence: "
val dynamicSuffix = "Hello World"
val result = generativeModel.generateContent(
generateContentRequest(TextPart(dynamicSuffix)) {
promptPrefix = PromptPrefix(promptPrefix)
}
)
### Java
String promptPrefix = "Reverse the given sentence: ";
String dynamicSuffix = "Hello World";
GenerateContentResponse response = generativeModelFutures.generateContent(
new GenerateContentRequest.Builder(new TextPart(dynamicSuffix))
.setPromptPrefix(new PromptPrefix(promptPrefix))
.build())
.get();
In the preceding snippet, the `dynamicSuffix` is passed as the main content, and
the `promptPrefix` is provided separately.
### Estimated performance gains
|---|---|---|
| | **Without prefix caching** | **With prefix cache-hit** (Prefix cache-miss may occur when prefix is used for the first time) |
| Pixel 9 with 300-token fixed prefix and a 50-token dynamic suffix prompt | 0.82 seconds | 0.45 seconds |
| Pixel 9 with a 1,000-token fixed prefix and a 100-token dynamic suffix prompt | 2.11 seconds | 0.5 seconds |
### Storage considerations
With implicit prefix caching, cache files are saved on the client application's
private storage, which increases your app's storage usage. Encrypted cache files
and their associated metadata, including original prefix text, are stored. Keep
the following storage considerations in mind:
- The number of caches is managed by an LRU (Least Recently Used) mechanism. Least used caches are deleted automatically when exceeding the max total cache amount.
- Prompt cache sizes are dependent on the length of the prefix.
- To clear all caches created from prefix caching, use the
[`generativeMode.clearImplicitCaches()`](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/GenerativeModel#clearCaches%28%29) method.
> [!NOTE]
> **Note:** The `clearImplicitCaches()` method is experimental and may change in the future.
## Use explicit cache management
The Prompt API includes explicit cache management methods to give developers
more precise control over how caches are created, searched, used, and removed.
These manual operations run independently of the system's automated cache
handling.
This example illustrates how to initialize explicit cache management and
perform inference:
### Kotlin
val cacheName = "my_cache"
val promptPrefix = "Reverse the given sentence: "
val dynamicSuffix = "Hello World"
// Create a cache
val cacheRequest = createCachedContextRequest(cacheName, PromptPrefix(promptPrefix))
val cache = generativeModel.caches.create(cacheRequest)
// Run inference with the cache
val response = generativeModel.generateContent(
generateContentRequest(TextPart(dynamicSuffix)) {
cachedContextName = cache.name
}
)
### Java
String cacheName = "my_cache";
String promptPrefix = "Reverse the given sentence: ";
String dynamicSuffix = "Hello World";
// Create a cache
CachedContext cache = cachesFutures.create(
new CreateCachedContextRequest.Builder(cacheName, new PromptPrefix(promptPrefix))
.build())
.get();
// Run inference with the cache
GenerateContentResponse response = generativeModelFutures.generateContent(
new GenerateContentRequest.Builder(new TextPart(dynamicSuffix))
.setCachedContextName(cache.getName())
.build())
.get();
This example demonstrates how to query, retrieve, and delete explicitly managed
caches using `generativeModel.caches`:
### Kotlin
val cacheName = "my_cache"
// Query pre-created caches
for (cache in generativeModel.caches.list()) {
// Do something with cache
}
// Get specific cache
val cache = generativeModel.caches.get(cacheName)
// Delete a pre-created cache
generativeModel.caches.delete(cacheName)
### Java
String cacheName = "my_cache";
// Query pre-created caches
for (PrefixCache cache : cachesFutures.list().get()) {
// Do something with cache
}
// Get specific cache
PrefixCache cache = cachesFutures.get(cacheName).get();
// Delete a pre-created cache
cachesFutures.delete(cacheName);
@@ -0,0 +1,55 @@
When using Prompt API, there are specific strategies you can use to tailor your
prompts and receive optimal results. This page describes best practices for
formatting prompts for Gemini Nano.
For more general prompt engineering guidance, see [Prompt Engineering
whitepaper](https://www.kaggle.com/whitepaper-prompt-engineering), [Prompt Engineering for Generative
AI](https://developer.android.com/machine-learning/resources/prompt-eng), and [Prompt design strategies](https://ai.google.dev/gemini-api/docs/prompting-strategies).
Alternatively, to automatically refine and improve prompts, you can use the
[zero-shot optimizer](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/zero-shot-optimizer#optimizing_for_smaller_models), which can target on-device models such as
`gemma-3n-e4b-it`.
## Prompt design best practices
When designing prompts for Prompt API, use the following techniques:
- **Provide examples for in-context learning**. Add well-distributed examples to
your prompt to show Gemini Nano the kind of result you expect.
Consider using the [prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching) feature when you use in-context
learning, as providing examples makes the prompt longer and increases
inference time.
- **Be concise** . Verbose preambles with repeated instructions can produce
suboptimal results. Keep your prompt focused and to-the-point. If you need to
repeat a short directive that guides the model's behavior, consider using
[system instructions](https://developer.android.com/agents/skills/device-ai/prompt-api/references/system-instructions).
- **Structure prompts** to generate more effective responses, such as this
[sample prompt template](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/prompt-design-strategies#sample-prompt-template) that clearly defines instructions,
constraints, and examples.
- **Keep output short** . LLM inference speeds are heavily dependent on the
output length. Carefully consider how you can generate the shortest possible
output for your use case and do manual post-processing to structure the output
in the chosen format. To help ensure that the response output is in your
preferred format, use the [Structured Output API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output).
- **Add delimiters** . Use delimiters like `<background_information>`,
`<instruction>`, and `##` to create separation between different parts of your
prompt. Using `##` between components is particularly critical for Gemini
Nano, as it significantly reduces the chances of the model failing to
correctly interpret each component.
- **Prefer simple logic and a more focused task** . If you find it challenging
to achieve good results with a prompt requiring multi-step reasoning (for
example, *do X first, if the result of X is A, do M; otherwise do N;
then do Y...* ), consider breaking the task up and let each Gemini Nano call
handle a more focused task, while using code to chain multiple calls together.
If you do need to tackle a complex, reasoning-intensive task all at once,
consider using [thinking mode](https://developer.android.com/ml-kit/genai/prompt/android/thinking-mode).
- **Use lower temperature values for deterministic tasks** . For tasks such as
entity extraction or translation that don't rely on creativity, consider
starting with a `temperature` value of `0.2`, and tune this value based on
your testing.
@@ -0,0 +1,74 @@
This skill provides guidance on how to optimize prompts for use with the ML Kit
Prompt API.
## Guidelines \& Rules
The best practices for prompt design for Prompt API are at
[Prompt Design](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prompt-design). Apply these core rules:
- Include examples for in-context learning with examples of the output. This is an example prompt that includes in-context learning because it has examples of the desired model output: Analyze the message and return whether the customer sentiment is positive, negative, or neutral. Example 1: Input - "This product doesn't work"; Output - "Negative" Example 2: Input - "I liked this product"; Output - "Positive"
- Make prompts concise. Remove duplicate or repeated instructions. Do not include conversational filler such as: hello, please, do this, help me with, etc. For example, a verbose prompt such as: "Hello! I want you to act as an expert translator for me today. I am going to give you a short paragraph of text in English, and I would really like it if you could translate it into formal, high-level Business French. Please make sure that you do not include any intro like 'Here is your translation' or any closing remarks at all. Just output the translation itself." can be improved with this concise prompt: "Translate to formal Business French. Return ONLY the translation"
- Use paired XML/HTML delimiters to denote dynamic inputs (for example `<email>[content]</email>`) to clearly isolate user data from prompt instructions. Mention that the delimiters represent placeholder text that should be replaced by code in the actual implementation.
- Use the [Structured Output API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output) if the output requires parsing responses into certain formats. Don't include the Structured Output API implementation in the prompt itself, but mention it after presenting the optimized prompt to the user so the user knows how to parse the model output. Prompts that use Structured Output API must return the `@Generable` typed object from the function signature instead of a string or JSON string.
- Keep output short by adding output constraints such as word count, character count, or number of bullets or sentences. For example, add constraints such as "The summarization must be 10 words or fewer."
- Use [system instructions](https://developer.android.com/agents/skills/device-ai/prompt-api/references/system-instructions) for short instructions that define how a model should behave, and use [prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching) for prompts that are over 200 words.
## Examples
Here are examples of prompts that the user might ask you to optimize, and the
improved, optimized versions.
### Example 1
Unoptimized prompt:
Process this customer email and return the order ID, what was bought, the phone
number, and what they want. Format it as JSON so my app can read it.
Optimized prompt:
## Task
Extract key details from the customer email below.
## Customer Email
<email>
[email_content]
</email>
Explanation: Because the information that the user wants to extract using the
prompt is well suited for the Structured Output API, the optimized prompt
implicitly uses the Structured Output API and doesn't need to explicitly define
the schema for the extracted data. In your response to the user that explains
the optimizations made to the prompt, mention that the optimized prompt is
designed for use with the Structured Output API. Also, mention that the
"" delimiters signify placeholder text that should be replaced with code in the user's actual implementation.
### Example 2
Unoptimized prompt:
Given this itinerary for a trip: [Flight to Paris (CDG), Check-in: Hotel Le
Meurice, Visit to the Louvre, Dinner at Le Jules Verne, Eiffel Tower Visit,
Versailles Palace Tour, Montmartre Walk, Seine River Cruise, Pastry and Macaron
Tasting, Flight Out], generate the following: overall vibe, tips on how to
prepare for this trip, and common short phrases to learn for the trip.
Optimized prompt:
## Task
Analyze the trip itinerary below and extract details to build a concise travel
guide.
## Rules
- Overall vibe: Limit to 1 short sentence (under 15 words).
- Preparation tips: Provide exactly 3 short tips (maximum 10 words per tip).
- Useful phrases: Provide exactly 3 phrases (under 15 words each).
## Itinerary
<itinerary>
[itinerary_content]
</itinerary>
@@ -0,0 +1,219 @@
If you need to parse the responses from the Prompt API into certain formats,
such as JSON, for further processing, use the Structured Output API.
With the Structured Output API, you define the target output structure using
Kotlin classes and annotations. The Prompt API then returns a response in the
form of your Kotlin object.
Generating structured output is particularly useful for tasks like the
following:
- **Entity extraction**: Extracting structured fields (for example, event name, date, location) from unstructured text.
- **Classification**: Categorizing input text into predefined categories.
- **Data serialization**: Converting unstructured user input into a format suitable for database storage or API calls.
## Prerequisites
To verify that the Structured Output API is available on the device, use the
`isStructuredOutputFeatureAvailable()` API. The API returns `true` if the
Structured Output API is available on the device, and `false` otherwise.
suspend fun isStructuredOutputFeatureAvailable(): Boolean
The Structured Output API also has the following requirements:
- Android API level 26 or higher (`minSdk` 26)
- KSP plugin version 2.3.6 or higher
## Limitations
The Structured Output API has the following limitations:
- Works in Kotlin only.
- ProGuard might interfere with the parsing of your annotated class. Add your annotated class to your [keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview) to exclude them from ProGuard if you get errors parsing, for example:
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }
## Configure project
To get started with the Structured Output API, follow these steps:
1. [Add the ML Kit Prompt API as a dependency](https://developer.android.com/agents/skills/device-ai/prompt-api/references/get-started#configure-project) in your
app-level `build.gradle.kts` (or `build.gradle`) file, if you haven't
already.
2. Add the KSP plugin to your project-level `build.gradle.kts` file. Use a
KSP plugin version that is compatible with your Kotlin version; we
recommend KSP version 2.3.6 or higher.
dependencies {
...
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
}
3. Add the structured compiler dependencies to your app-level
`build.gradle.kts` file:
dependencies {
...
ksp("com.google.mlkit:genai-schema-compiler:1.0.0-alpha1")
}
## Define the output structure
Define the structure of the data you want the model to return using Kotlin
data classes. There are two main annotations for defining the output
structure:
- Use the `@Generable` annotation to define the class as a target for structured output.
- Use the `@Guide` annotations on the class properties to provide descriptions and constraints that guide the model's output.
The following example defines a structure for extracting plant information:
import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide
@Generable
data class PlantList(
@Guide(description = "The list of plants found", minItems = 1, maxItems = 5)
val plants: List<Plant>
)
@Generable("Information about a plant species")
data class Plant(
@Guide(description = "The common name of the plant")
val commonName: String,
@Guide(description = "The full latin scientific name of the plant")
val scientificName: String,
@Guide(
description = "The maximum height of the plant in centimeters.",
minimum = 1.0,
maximum = 10000.0
)
val maxHeightCm: Int,
@Guide(description = "Whether the plant is poisonous or not")
val isPoisonous: Boolean?,
@Guide(
description = "The primary continent where this plant is native to",
enumValues = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"]
)
val nativeContinent: String
)
### Supported types and constraints
The following types are supported within a `@Generable` annotated class,
along with their respective `@Guide` constraints:
| Type | Description | Supported `@Guide` constraints |
|---|---|---|
| `String` | For text. | `description`, `enumValues` |
| `Double` / `Float` | For floating-point numbers. | `description`, `minimum`, `maximum` |
| `Int` / `Long` | For whole numbers. | `description`, `minimum`, `maximum` |
| `Boolean` | For true/false values. | `description` |
| `List<T>` | For lists of supported types or nested `@Generable` classes. | `description`, `minItems`, `maxItems` |
| `List<String>` | For lists of `String` values. | `description`, `enumValues`, `minItems`, `maxItems` > [!NOTE] > **Note:** Setting the `enumValues` parameter defines the values allowed for the individual list items. |
| `@Generable` class | For nested structured objects. | `description` |
> [!NOTE]
> **Note:** Circular dependencies between nested `@Generable` classes are not supported (for example, a class referencing itself, or Class A referencing Class B which in turn references Class A).
## Generate structured content
To request structured output, use the `generateTypedContentRequest` helper
function to wrap your standard prompt and specify the target output class.
// 1. Initialize your GenerativeModel as usual
val generativeModel = Generation.getClient()
// 2. Prepare the prompt text
val promptText = "List some common plants found in California."
val baseRequest = GenerateContentRequest.Builder(TextPart(promptText)).build()
// 3. Create the typed request, specifying the target class (e.g., PlantList)
val typedRequest = generateTypedContentRequest(
generateContentRequest = baseRequest,
outputClass = PlantList::class
)
// 4. Run the inference
try {
val typedResponse = generativeModel.generateContent(typedRequest)
// 5. Access the parsed object
// The response candidates contain the parsed object of type T (PlantList in this case)
val plantList: PlantList? = typedResponse.candidates.firstOrNull()?.response
if (plantList != null) {
// Process the structured data
for (plant in plantList.plants) {
Log.d("StructuredOutput", "Found plant: ${plant.commonName} (${plant.scientificName})")
}
} else {
Log.e("StructuredOutput", "Failed to parse response into the desired structure.")
// Inspect finish reason for details
val finishReason = typedResponse.candidates.firstOrNull()?.finishReason
Log.d("StructuredOutput", "Finish reason: $finishReason")
}
} catch (e: GenAiException) {
// Handle API errors
when (e.errorCode) {
GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS -> {
Log.e("StructuredOutput", "The class structure is not supported.")
}
GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE -> {
Log.e("StructuredOutput", "The model generated values that violate the schema constraints.")
}
else -> {
Log.e("StructuredOutput", "API error: ${e.message}")
}
}
}
## Handle finish reasons and errors
When using the Structured Output API, you should handle potential exceptions
thrown by the API and inspect the `finishReason` property in the response
candidates if the parsed response is null.
### finishReason values
The `finishReason` property can take one of the following values:
- `TypedFinishReason.STOP`: The model finished generating successfully and the output matches the schema.
- `TypedFinishReason.MAX_TOKENS`: The model stopped because it reached the token limit. The output might be incomplete.
- `TypedFinishReason.PARSE_CLASS_ERROR`: The model completed generation, but the resulting JSON couldn't be parsed into the target Kotlin class.
- `TypedFinishReason.STRUCTURE_NOT_ANNOTATED`: The target class or its nested classes are missing the required `@Generable` annotation.
- `TypedFinishReason.STRUCTURE_VALUES_INVALID`: The generated values violated the constraints defined in the `@Guide` annotations (for example value out of range, list size out of bounds).
- `TypedFinishReason.OTHER`: Generation stopped due to other reasons.
### Exceptions
The Structured Output API might throw `GenAiException` with the following
error codes:
- `GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS` (-104): The structure of the annotated class is invalid or contains unsupported types. This is typically a development-time configuration error. Review your `@Generable` data class definition to check that all property types are supported and that there aren't any circular dependencies.
- `GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE` (-105): The values generated by the model are invalid or fail constraints verification. This is a runtime error. If you encounter this error frequently, consider the following solutions:
- Refining your prompt instructions to guide the model more strictly.
- Relaxing the constraints (like minimum, maximum, or list size limits) in your `@Guide` annotations if they are too restrictive for the model's capabilities.
- Implementing a fallback strategy in your app, such as retrying the request or displaying a default state.
## Count tokens
To check if your structured prompt is within the input token limit, calculate
the token count using the [`countTokens()`](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/GenerativeModel#countTokens(com.google.mlkit.genai.prompt.GenerateContentRequest)) method.
Because structured output requests need to instruct the model on the schema
structure, counting tokens on just the raw prompt text (using a
`GenerateContentRequest` instance) isn't accurate. To get an accurate token
count, you must pass the complete `GenerateTypedContentRequest` instance, which
includes your target class and schema configurations, to the `countTokens()`
method:
suspend fun <T : Any> countTokens(request: GenerateTypedContentRequest<T>): CountTokensResponse
@@ -0,0 +1,71 @@
System instructions let you give the model a persona, set the tone of its
responses, or provide specific rules it must follow. These instructions are
separate from the user's prompt and are treated with higher priority by the
model to ensure it behaves as expected.
Common use cases include:
- **Setting a persona:** For example, "You are a helpful math tutor."
- **Enforcing output format:** For example, "Always respond in bullet points."
- **Setting constraints:** For example, "Do not answer questions about politics."
## Prerequisites
System instructions work on devices running Gemini Nano V3 and higher. For a
list of supported devices, see [Prompt API device support](https://developer.android.com/ml-kit/genai#prompt-device).
## Limitations
We don't recommend using system instructions with
[prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching). In general, use system instructions for
short instructions that define how the model should behave; use prefix
caching if you need to repeat a large part of your prompt across queries and
need to optimize performance.
## How to use system instructions
To provide system instructions, create a `SystemInstruction` object and pass it
to the `GenerateContentRequest` builder:
import com.google.mlkit.genai.prompt.SystemInstruction
import com.google.mlkit.genai.prompt.TextPart
import com.google.mlkit.genai.prompt.generateContentRequest
// 1. Define the system instruction
val systemInstruction =
SystemInstruction("You are a concise assistant. Answer in 2 sentences or less.")
// 2. Create the request
val request = generateContentRequest(TextPart("How does photosynthesis work?")) {
this.systemInstruction = systemInstruction
}
// 3. Run inference
try {
val response = generativeModel.generateContent(request)
println(response.candidates.firstOrNull()?.text)
} catch (e: GenAiException) {
// Handle SDK-specific exceptions
}
You can further simplify the code by passing the system instructions directly
into the `generateContentRequest` request builder:
val request = generateContentRequest(
SystemInstruction("You are a pirate. Speak like one."),
TextPart("What is the weather like today?")
) {
// Optional configurations like temperature
temperature = 0.7f
}
## Best practices
Here are some best practices when using system instructions:
- **Be clear and direct:** The model follows clear, direct instructions better than ambiguous ones. Here are some examples:
- Vague (avoid): "Don't write too much. Try to be helpful and friendly, and format the output nicely."
- Clear (preferred): "You are a friendly customer support assistant. Limit your responses to a maximum of 3 sentences. Format any lists using bullet points."
- **Be concise:** While system instructions are powerful, very long instructions can consume the model's limited context window.
- **Factor in token counts:** Make sure that your token counting logic includes the system instructions to avoid underestimating request size. We recommend keeping your system instructions to under 150 words (100-200 tokens).
- **Test and iterate:** Model behavior can vary based on phrasing. Test with various user inputs to ensure the model maintains its persona consistently.
+89 -36
View File
@@ -68,7 +68,7 @@ Capture an image of the current screen of a connected Android device and output
## Managing skills
Manage antigravity agent skills for Android using the `android skills` command.
Manage agent skills for Android using the `android skills` command.
## Inspecting UI Layouts
@@ -80,30 +80,45 @@ Update the Android CLI using the `android update` command.
# `android help` output
Usage: android [-hV] [--sdk=PARAM] [COMMAND]
Usage: android [-hvV] [--sdk=PARAM] [COMMAND]
-h, --help Show this help message and exit.
--sdk=PARAM Path to the Android SDK
-v, --verbose Enable verbose output for troubleshooting
-V, --version Print version information and exit.
Commands:
create Create a new Android project
create Create a new Android project from available templates. Allows
specifying project name, output directory, minSdk, and dry-run
execution.
describe Analyzes an Android project to generate descriptive metadata.
docs Android documentation commands
emulator Emulator commands
docs Android documentation commands for searching and fetching developer
documentation from the official knowledge base.
emulator Manage Android Virtual Devices (AVDs). Includes commands to start,
stop, list, and view details about emulators.
help Shows the help of all commands
info Print environment information (SDK Location, etc.)
init Initializes the environment (eg. skills) for Android CLI.
info Print environment information including SDK location, connected
devices, and configuration variables. Use specific fields to
narrow the output.
init Initializes the environment for the Android CLI. Sets up required
configurations, directories, and default skills.
install Install an Android Application (one or more APKs) to a connected
device or emulator without activating any components using
incremental optimizations for faster deployment than adb.
layout Returns the layout tree of an application
run Deploy an Android Application
run Build, deploy, and launch an Android application on a connected
device or emulator.
screen Commands to view the device
sdk Download and list SDK packages
skills Manage skills
sdk Manage the Android SDK installation. Includes commands to install,
update, remove, and list available and installed SDK packages.
skills Manage Android CLI skills. Includes commands to install, remove,
list, and search for skills by keyword.
studio Android Studio commands
update Update the Android CLI
update Update the Android CLI to the latest version.
create
Usage: android create [-h] [--verbose] [--list] [--minSdk=api]
--name=applicationName [-o=dest-path] [template-name]
Create a new Android project
Create a new Android project from available templates. Allows specifying
project name, output directory, minSdk, and dry-run execution.
[template-name] The template name
-h, --help Show this help message and exit.
--minSdk=api The 'minSdk' supported by the application (default
@@ -128,15 +143,17 @@ describe
docs
Usage: android docs [-h] [COMMAND]
Android documentation commands
Android documentation commands for searching and fetching developer
documentation from the official knowledge base.
-h, --help Show this help message and exit.
Commands:
search Search Android documentation
fetch Fetch Android documentation
search Search Android documentation. Enclose keywords in quotes.
fetch Fetch an Android documentation article from a URL (kb://...)
emulator
Usage: android emulator [-h] [COMMAND]
Emulator commands
Manage Android Virtual Devices (AVDs). Includes commands to start, stop, list,
and view details about emulators.
-h, --help Show this help message and exit.
Commands:
create Creates a virtual device
@@ -153,12 +170,33 @@ help
info
Usage: android info <field>
Print environment information (SDK Location, etc.)
Print environment information including SDK location, connected devices, and
configuration variables. Use specific fields to narrow the output.
<field> The specific field to print the value of. If omitted print all.
init
Usage: android init
Initializes the environment (eg. skills) for Android CLI.
Initializes the environment for the Android CLI. Sets up required
configurations, directories, and default skills.
install
Usage: android install [-hv] [--use-delta-install] [--device=PARAM]
[--apks=PARAM[,PARAM...]]... [--install-options=PARAM[,
PARAM...]]...
Install an Android Application (one or more APKs) to a connected device or
emulator without activating any components using incremental optimizations for
faster deployment than adb.
--apks=PARAM[,PARAM...]
The paths to the APKs
--device=PARAM The device serial number
-h, --help Show this help message and exit.
--install-options=PARAM[,PARAM...]
Additional options/flags to pass to package manager
install (e.g. -g,-d)
--use-delta-install Use fast delta install (speeds up incremental
updates by transferring only modified code and
resources; default: true)
-v, --verbose Enable verbose logging
layout
Usage: android layout [-dhp] [--device=PARAM] [-o=PARAM]
@@ -173,16 +211,26 @@ layout
-p, --pretty Pretty-prints the returned JSON
run
Usage: android run [-h] [--debug] [--activity=PARAM] [--device=PARAM]
[--type=PARAM] [--apks=PARAM[,PARAM...]]...
Deploy an Android Application
--activity=PARAM The activity name
Usage: android run [-hv] [--debug] [--use-delta-install] [--activity=PARAM]
[--device=PARAM] [--type=PARAM] [--apks=PARAM[,PARAM...]]...
[--install-options=PARAM[,PARAM...]]...
Build, deploy, and launch an Android application on a connected device or
emulator.
--activity=PARAM The activity name
--apks=PARAM[,PARAM...]
The paths to the APKs
--debug Run in debug mode
--device=PARAM The device serial number
-h, --help Show this help message and exit.
--type=PARAM The component type (ACTIVITY, SERVICE, etc.)
The paths to the APKs
--debug Run in debug mode
--device=PARAM The device serial number
-h, --help Show this help message and exit.
--install-options=PARAM[,PARAM...]
Additional options/flags to pass to package manager
install (e.g. -g,-d)
--type=PARAM The component type (ACTIVITY, WATCH_FACE, TILE,
COMPLICATION, DECLARATIVE_WATCH_FACE, WEAR_WIDGET)
--use-delta-install Use fast delta install (speeds up incremental
updates by transferring only modified code and
resources; default: false)
-v, --verbose Enable verbose logging
screen
Usage: android screen [-h] [COMMAND]
@@ -194,21 +242,25 @@ screen
sdk
Usage: android sdk [COMMAND]
Download and list SDK packages
Manage the Android SDK installation. Includes commands to install, update,
remove, and list available and installed SDK packages.
Commands:
install Install SDK packages
update Update one or all packages to the latest version
remove Remove a package from the SDK
remove Remove packages from the SDK
list List installed and available SDK packages
skills
Usage: android skills [COMMAND]
Manage skills
Usage: android skills [-h] [COMMAND]
Manage Android CLI skills. Includes commands to install, remove, list, and
search for skills by keyword.
-h, --help Show this help message and exit.
Commands:
add Install a skill
remove Remove a skill
list List available skills
find Find skills by keyword
add [id] Install a specific skill by its ID to your environment
remove [id] Remove an installed skill by its ID
list List installed and available skills
find [keyword] Search for available skills in the repository matching a
keyword
studio
Usage: android studio [-h] [COMMAND]
@@ -227,5 +279,6 @@ studio
update
Usage: android update [--url=PARAM]
Update the Android CLI
Update the Android CLI to the latest version.
--url=PARAM The URL to download the update from
+1 -1
View File
@@ -74,7 +74,7 @@ To tap on this button, you would execute `adb shell input tap 152 23`. This taps
"center": "[250,400]"
}
```
To scroll down on this list, you would execute `adb shell input swipe 250 400 600 500`. This swipes from the center to the bottom over 500ms.
To scroll down on this list, you would execute `adb shell input swipe 250 400 250 200 500`. This swipes from the center to the top over 500ms.
# Android Interaction Rules
1. Always ensure text input fields have `"focused"` in their `"state"` list before entering text
+350
View File
@@ -0,0 +1,350 @@
---
name: restore-credentials
description: Provides knowledge and workflows to implement Android's Restore Credentials
feature using the androidx.credentials library. Use this skill to create, sign in
with, and delete restore keys, enabling silent user sign-in on new devices after
a restore. It covers version compatibility, dependencies, server-side prerequisites,
and the complete client-side implementation for creating, retrieving, and clearing
restore keys.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-09-01'
keywords:
- Credential Manager
- Restore Credentials
- backup & restore
- backup
- restore
- implementation
---
## Fundamentals
The objective is to implement the **Restore Credentials** feature through the
Android Credential Manager API (`androidx.credentials`). This allows apps that
use or are integrating Credential Manager to silently log users back in when
they restore their app on a new device. Restore Credentials operates
independently of the app's primary authentication method (passwords, passkeys,
federated sign-in) and requires no UI changes to existing sign-in flows.
### Scope
**Crucial:** This skill focuses exclusively on the Android client-side
integration. It does **not** implement the server-side cryptographic
validation logic. The developer must be reminded of this and the
[Points to inform the developer about](#backend-guidelines) after implementation is done.
## Implementation Guidelines
When instructed to implement Restore Credentials on a developer's application,
remember the following:
1. Before the implementation, you **MUST** read and understand the [Two-Tier
Restoration Architecture](#two-tier-restoration-architecture) and review the [DOs and DON'Ts](#dos-and-donts).
2. After the implementation, you **MUST** present the developer with the [Backend Guidelines](#backend-guidelines) as a reminder for their backend setup. It is important that you remind the developer that they still have to implement the backend.
## Two-Tier Restoration Architecture
To enable a resilient sign-in experience, retrieve credentials through a
**two-tier architecture**:
1. **Tier 1 (Primary - Background):** Executes automatically during device setup using the app's `BackupAgent.onRestoreFinished()` callback. This provides an invisible restoration before the user opens the app for the first time, allowing background sync and notification delivery.
2. **Tier 2 (Secondary - Foreground):** Runs in the Launcher `Activity.onCreate()` to catch failovers if background restoration didn't complete (example: dropped network, delayed restoration) or if `allowBackup` is disabled.
If `allowBackup` in the manifest is set to true, implement both. Otherwise, only
implement tier 2 (Foreground Restoration). Do **NOT** change the value of
`allowBackup` in the manifest.
If you added a `BackupAgent` to the app, you **MUST** also set
`android:fullBackupOnly="true"` in the manifest. Do **NOT** do this if there
already existed a `BackupAgent` in the app before your implementation.
## DOs and DON'Ts
**DO:**
- Do check `AndroidManifest.xml` for the value of allowBackup to determine what you have to implement.
- Do implement a fallback for createCredential: always try calling it first with `isCloudBackupEnabled` set to true. If an `E2eeUnavailableException` is thrown, catch it and retry the call with `isCloudBackupEnabled` set to `false`.
- Do implement a `BackupAgent` (subclass of `android.app.backup.BackupAgent`) if `allowBackup` is true in the manifest.
- Do set `android:fullBackupOnly="true"` in the manifest if you added a `BackupAgent` to the app.
- Call `clearCredentialState()` when the user signs out. This is a mandatory security measure to log the user out fully.
- Do attempt to get the restore key on the first launch of the app on a new device and also within the `BackupAgent.onRestoreFinished()` callback if your app uses it.
- Do ensure that a restore credential is created even if the user is already logged in.
- Do ensure that the credential retrieval and login in `onRestoreFinished()` is performed synchronously (for example using `runBlocking`).
- Do restore notifications in the `BackupAgent` if your app uses them. (For example capture and send FCM token to backend)
- Do ensure that if you implement mock network requests or stubs, you replace all placeholders with valid, properly formatted JSON payloads for the credential requests.
- Do encapsulate credential creation and retrieval into their own dedicated functions. Because credential creation must be called in multiple places (sign-up, sign-in) and retrieval across multiple tiers (`BackupAgent` and Launcher `Activity`), this prevents code duplication.
- Do remind the developer of the [critical guidelines](#implementation-guide) for implementing the backend once you're done with the implementation.
- Do generate a separate restore key for each application if the organization has multiple apps with different package names, as a restore key is tied to a unique application package name.
**DON'T:**
- DON'T change the value of `allowBackup` in `AndroidManifest.xml`. Restore Credentials functionality is not affected by the allowBackup setting, meaning the user will still be automatically logged in when a Restore Credential exists, even if `allowBackup` is false.
- DON'T implement a `BackupAgent` if `allowBackup` is `false` in `AndroidManifest.xml`.
- DON'T assume the credential stored in the `GetCredentialResponse` to be of type `PublicKeyCredential`. It has type `RestoreCredential`.
- DON'T chain `GetRestoreCredentialOption` with any other `CredentialOption` in the construction of a `GetCredentialRequest`.
- DON'T assume `CredentialManager` or the Android system will automatically delete a restore key when a user signs out of the app. You must explicitly call `clearCredentialState` with a `ClearCredentialStateRequest` of `TYPE_CLEAR_RESTORE_CREDENTIAL`.
- DON'T remove any existing calls to `clearCredentialState()`. A `ClearCredentialStateRequest` without a type specified only clears all NON-restore credentials.
## Implementation Guide
Implement the Android client-side code by using the following guide. Follow it
**step-by-step** and don't implement any backend functionality, only remind
the user of the [Backend Guidelines](#backend-guidelines) once you're done.
## Version compatibility
Credential Manager's Restore Credentials works on devices running Android 9 (API
level 28) and higher, Google Play services (GMS) core version 24220000 or
higher, and version 1.5.0 or higher of the `androidx.credentials` library.
## Prerequisites
Set up a [relying party server](#backend-guidelines) similar to the server for [passkeys](#dos-and-donts). If
you already have a [server](#two-tier-restoration-architecture) set up to handle authentication with passkeys,
use the same server-side implementation for restore keys.
> [!NOTE]
> **Note:** While the server-side implementation is the same for passkeys and restore keys, your client-side app can support restore keys without supporting passkeys. Because restore keys work independently of the authentication method in your app (for example, passwords or Sign in with Google), you don't need to make any additional changes to the existing authentication methods in your app's code.
## Dependencies
Add the following dependencies to your app module's `build.gradle` file:
### Kotlin
```kotlin
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
```
### Groovy
```groovy
dependencies {
implementation "androidx.credentials:credentials:1.7.0-alpha03"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}
```
Restore Credentials is available from version 1.5.0 and higher of the
androidx.credentials library. However, it's recommended to use the latest stable
versions of the dependencies where possible.
> [!NOTE]
> **Note:** The Restore Credentials feature works regardless of whether [`allowBackup`](references/android/guide/topics/manifest/application-element.md) is set in the manifest.
## Overview
1. [**Create a restore key**](#create-restore-key): To create a restore key, complete the following steps:
1. [**Instantiate Credential Manager**](#implementation-guide): Create a `CredentialManager` object.
2. [**Get credential creation options from the app server**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Send the client app the details required to create the restore key from your app server.
3. [**Create the restore key**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Create a restore key for the user's account if the user is signed in to your app.
4. [**Handle the credential creation response**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Send the credentials from your client app to your app server for processing, and handle any exceptions.
2. [**Sign in with a restore key**](#sign-restore): To sign in with a restore key, complete the following steps:
1. [**Get credential retrieval options from the app server**](#get-credential-retrieval): Send the client app the details required to retrieve the restore key from your app server.
2. [**Get the restore key**](#get-restore): Request the restore key from Credential Manager when the user sets up a new device. This lets the user sign in without additional input.
3. [**Handle the credential retrieval response**](#handle-sign-in): Send the restore key from the client app to the app server to sign in the user.
3. [**Delete a restore key**](#delete-restore).
## Create a restore key
Your app should cover all cases of a user signing in to ensure active users have
a restore key created. Create the restore key in the following scenarios:
- If the user is signed in and a restore key isn't already created (such as in the `onCreate` method for the main `Activity`).
- When the user is signing in or completing a new account registration flow.
To optimize performance and avoid the overhead of creating or checking for a
restore credential on every single login, set a `boolean` flag or a credential
creation timestamp in local storage, such as `has_synced_restore_credential`, to
track whether the key has already been created.
> [!NOTE]
> **Note:** A restore key is tied to an application's unique package name. If your organization's main app and sub-apps have different package names, create a separate restore key for each app.
### Instantiate Credential Manager
Use your app's activity context to instantiate a `CredentialManager` object.
// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)
### Get credential creation options from your app server
Use a FIDO-compliant library in your app server to send your client app the
information required to create the restore credential, such as information about
the user, the app, and additional configuration properties. For more information
about the server-side implementation, see [Server-side
guidance](https://developers.google.com/identity/passkeys/developer-guides/server-registration).
### Create the restore key
After parsing the public key creation options sent by the server, create a
restore key by wrapping these options in a
[`CreateRestoreCredentialRequest`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialRequest) object and calling the
[`createCredential()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#createCredential(android.content.Context,androidx.credentials.CreateCredentialRequest)) method with the `CredentialManager` object.
// createRestoreRequest contains the details sent by the server
val response = credentialManager.createCredential(context, createRestoreRequest)
#### Key points about the code
- The `CreateRestoreCredentialRequest` object contains the following fields:
- `requestJson`: The credential creation options sent by the app server in the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- `isCloudBackupEnabled`: `Boolean` field to determine if the restore key
should be backed up to the cloud. By default, this flag is `true`. This
field has these values:
- `true`: (**Recommended**) This value enables the backup of restore keys to the cloud if the user has Google Backup and end-to-end encryption, such as a screen lock, enabled.
- `false`: This value saves the key locally and not in the cloud. The key is not available on the new device if the user chooses to restore from the cloud.
> [!CAUTION]
> **Caution:** It's recommended to set `isCloudBackupEnabled` to `true`. If cloud backup is disabled and the user restores from a cloud backup, the call to retrieve the restore key fails. Users who restore your app with a cloud backup don't receive the restore key and are not automatically signed in.
### Handle the credential creation response
The Credential Manager API returns a response of type
[`CreateRestoreCredentialResponse`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialResponse). This response holds the public key
credential registration response in [JSON format](https://w3c.github.io/webauthn/#authenticatorattestationresponse).
Send the public key from your app to the relying party server. This public key
is similar to the public key generated when you create a passkey. The same code
that handles passkey creation on the server can also handle restore key
creation. For more information about the server-side implementation, see [the
guidance for passkeys](references/android/identity/passkeys/create-passkeys.md).
During the restore key creation process, handle these exceptions:
- [`CreateRestoreCredentialDomException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/CreateRestoreCredentialDomException): This exception occurs if `requestJson` is invalid and does not follow the WebAuthn format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- [`E2eeUnavailableException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/E2eeUnavailableException): This exception occurs if `isCloudBackupEnabled` is `true`, but the user's device doesn't have data backup or end-to-end encryption, such as a screen lock.
To ensure that Restore Credentials are created in all cases, you must handle the `E2eeUnavailableException` explicitly by calling `createCredential` with `isCloudBackupEnabled` set to `true`. If `E2eeUnavailableException` is thrown, catch and call `createCredential` again with `isCloudBackupEnabled` set to `false`.
- `IllegalArgumentException`: This exception occurs if `createRestoreRequest` is empty or not valid JSON, or if it doesn't have a valid `user.id` that conforms to the WebAuthn [specifications](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
## Sign in with a restore key
Use Restore Credentials to silently sign in the user during the device setup
process.
### Get credential retrieval options from the app server
Send the client app the options required to get the restore key from the server.
For similar passkey guidance for this step, see [Sign in with a passkey](references/android/identity/passkeys/sign-in-with-passkeys.md).
For more information about the server-side implementation, see the [server-side
authentication guide](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_credential_request_options).
### Get the restore key
To get the restore key on the new device, call the `getCredential()` method on
the `CredentialManager` object.
It's recommended to fetch the restore key in both of the following scenarios:
- On the first launch of the app on the device. Credential restoration in this scenario is independent of restoration of the app data.
- If app data backup and restore is enabled, get the restore key immediately after the app data is restored. Use [`BackupAgent`](https://developer.android.com/reference/android/app/backup/BackupAgent) to configure your app's backup and ensure you complete the `getCredential` functionality within the [`onRestoreFinished`](https://developer.android.com/reference/android/app/backup/BackupAgent#onRestoreFinished()) callback. Don't use the `onRestore` method, as it is only called for key-value backups, whereas `onRestoreFinished` is reliably called for any kind of backup restore. This avoids potential delays when users open their new device for the first time and lets users interact with the app without waiting for them to open your app. For example, this lets your app send the user notifications before they open the app for the first time on the new device, which is particularly relevant for messaging or communications apps.
If you newly create a `BackupAgent` and previously had backup enabled with
`allowBackup="true"`, set the boolean value `android:fullBackupOnly="true"`in
your app's manifest. This ensures that your app's backup and restore behavior is
maintained.
> [!IMPORTANT]
> **Important:** Notifications aren't automatically restored after the restore credentials are retrieved. If you use Firebase to handle notifications, you must fetch and send the Firebase Cloud Messaging (FCM) token to the backend to successfully resume background messaging and notifications.
// Fetch the options required to get the restore key
val authenticationJson = fetchAuthenticationJson()
// Create the GetRestoreCredentialRequest object
val options = GetRestoreCredentialOption(authenticationJson)
val getRequest = GetCredentialRequest(listOf(options))
val response = credentialManager.getCredential(context, getRequest)
// Type-check and extract the restore credential
val credential = response.credential as RestoreCredential
The credential manager APIs return a response of type
[`GetCredentialResponse`](https://developer.android.com/reference/android/credentials/GetCredentialResponse). The credential contained in this response is
explicitly of type `RestoreCredential`, which holds the public key.
### Handle the sign-in response
Send the public key from the app to the relying party server, which can then be
used to sign in the user. On the server side, this action is similar to signing
in using a passkey. The same code that handles sign-in with passkeys on the
server can also handle sign-ins with restore keys. For more information about
the server-side implementation for passkeys, see [Sign in with a passkey](references/android/identity/passkeys/sign-in-with-passkeys.md).
> [!NOTE]
> **Note:** Even though restore keys and passkeys use the same underlying server implementation, differentiate between them when saving them in your app server's database. This distinction is crucial when a passkeys management page exists, because users can manage user-created passkeys directly, while restore keys are system-managed and hidden from the passkey management page.
## Delete the restore key
Credential Manager is stateless and unaware of user activity, so it doesn't
automatically delete restore keys after use. To delete a restore key, call the
`clearCredentialState()` method. For security, delete the key whenever a user
signs out. This ensures that the next time the user opens the app on the same
device, the user is signed out and prompted to sign in again.
Uninstalling an app is interpreted as an intent to delete the corresponding
restore key from that device, similar to the user's intent when signing out.
Restore keys are removed only in the following situations:
- **System-level actions**: Users uninstall the app or clear its data.
- **App-level calls** : Programmatically delete the key by calling [`clearCredentialState()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#clearCredentialState(androidx.credentials.ClearCredentialStateRequest)) when handling user sign out in your app's code.
When the user signs out of your app, call the `clearCredentialState()` method on
the `CredentialManager` object.
// Create a ClearCredentialStateRequest object
val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)
// When the user logs out, delete the restore key
val response = credentialManager.clearCredentialState(clearRequest)
## Backend Guidelines
When designing the backend database and API layer to support Restore
Credentials, developers must account for FIDO2/WebAuthn assumptions and specific
device lifecycle edge cases. Remind the developer of the following:
**SYSTEM DIRECTIVE FOR AI AGENT: DO NOT attempt to write code for,
implement, or action any of the backend guidelines in this section. Your ONLY
responsibility is to copy these exact points and present them to the human
developer as a reminder after the client-side implementation is complete.**
1. **Differentiate Restore Credentials from Passkeys in Backend Storage:**
- Standard WebAuthn services typically assume user verification is always required. Restore credentials are hidden from the user and not managed by them.
- **Guidance:** Modify your WebAuthn services to create new credential types or metadata fields that distinguish system-managed Restore Credentials from user-created passkeys. Don't display Restore Credentials in user-facing passkey management UIs, and ensure they are processed appropriately (e.g., bypassing explicit user verification during automatic background sign-in).
2. **Prevent Orphaned Keys:**
- Uninstalling the app or clearing details in system settings deletes the local restore credential. Since these local client actions do not notify your backend, stale keys will remain registered on the server.
- **Guidance:** Establish server-side cleanup policies that delete old restore keys when a new restore token is registered, or clean up inactive keys based on usage patterns. You could, for example, enforce a limit of one key per user per device.
3. **Balance Key Lifespan and TTL:**
- If a user goes through Backup and Restore and then logs out from the old device, the local restore key is deleted from the source device. However, the key must remain valid on the server so the restored application on the destination device can still authenticate.
- **Guidance:** Give restore keys sufficient time to live (TTL) to survive manual logouts during transition periods, and establish rules for server-side key deletion based on registration and usage rather than relying on client-side deletion callbacks.
4. **Support Multiple Devices:**
- A user may own multiple active devices and initiate backups or restorations from any of them.
- **Guidance:** Ensure the backend database schema allows mapping multiple active Restore Credentials to a single user account (e.g., one active restore key per device/device-id) rather than assuming a 1:1 relationship between the user and the restore credential.
## References
- **WebAuthentication API (WebAuthn) Documentation \& Specification**
*When to use:* Use these resources ONLY if you need to inspect or debug the
strict JSON schema requirements for FIDO2/WebAuthn, specifically when
generating mock data or formatting the `requestJson`
(`PublicKeyCredentialCreationOptionsJSON`) and `authenticationJson` payloads.
- [MDN Web Authentication API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Mozilla Developer Network
guide and reference for WebAuthn APIs
- [W3C `PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Data structure definition
for WebAuthn credential creation requests in JSON format.
- [W3C `PublicKeyCredentialRequestOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Data structure definition
for WebAuthn authentication or assertion requests in JSON format.
@@ -0,0 +1,832 @@
syntax:
:
```xml
<application android:allowTaskReparenting=["true" | "false"]
android:allowBackup=["true" | "false"]
android:allowClearUserData=["true" | "false"]
android:allowCrossUidActivitySwitchFromBelow=["true" | "false"]
android:allowNativeHeapPointerTagging=["true" | "false"]
android:appCategory=["accessibility" | "audio" | "game" |
"image" | "maps" | "news" | "productivity" | "social" | "video"]
android:backupAgent="string"
android:backupInForeground=["true" | "false"]
android:banner="drawable resource"
android:dataExtractionRules="string resource"
android:debuggable=["true" | "false"]
android:description="string resource"
android:enabled=["true" | "false"]
android:enableOnBackInvokedCallback=["true" | "false"]
android:extractNativeLibs=["true" | "false"]
android:forceQueryable=["true" | "false"]
android:fullBackupContent="string"
android:fullBackupOnly=["true" | "false"]
android:gwpAsanMode=["always" | "never"]
android:hasCode=["true" | "false"]
android:hasFragileUserData=["true" | "false"]
android:hardwareAccelerated=["true" | "false"]
android:icon="drawable resource"
android:intentMatchingFlags=["none" | "enforceIntentFilter" | "allowNullAction"]
android:isGame=["true" | "false"]
android:isMonitoringTool=["parental_control" | "enterprise_management" |
"other"]
android:killAfterRestore=["true" | "false"]
android:largeHeap=["true" | "false"]
android:label="string resource"
android:localeConfig="xml resource"
android:logo="drawable resource"
android:manageSpaceActivity="string"
android:name="string"
android:networkSecurityConfig="xml resource"
android:pageSizeCompat=["true" | "false"]
android:permission="string"
android:persistent=["true" | "false"]
android:process="string"
android:restoreAnyVersion=["true" | "false"]
android:requestLegacyExternalStorage=["true" | "false"]
android:requiredAccountType="string"
android:resizeableActivity=["true" | "false"]
android:restrictedAccountType="string"
android:supportsRtl=["true" | "false"]
android:taskAffinity="string"
android:testOnly=["true" | "false"]
android:theme="resource or theme"
android:uiOptions=["none" | "splitActionBarWhenNarrow"]
android:usesCleartextTraffic=["true" | "false"]
android:vmSafeMode=["true" | "false"] >
. . .
</application>
```
contained in:
: `https://developer.android.com/guide/topics/manifest/manifest-element`
can contain:
: `https://developer.android.com/guide/topics/manifest/activity-element`
`https://developer.android.com/guide/topics/manifest/activity-alias-element`
`https://developer.android.com/guide/topics/manifest/meta-data-element`
`https://developer.android.com/guide/topics/manifest/service-element`
`https://developer.android.com/guide/topics/manifest/receiver-element`
`https://developer.android.com/guide/topics/manifest/profileable-element`
`https://developer.android.com/guide/topics/manifest/provider-element`
`https://developer.android.com/guide/topics/manifest/uses-library-element`
`https://developer.android.com/guide/topics/manifest/uses-native-library-element`
`https://developer.android.com/guide/topics/manifest/property-element`
description:
: The declaration of the application. This element contains subelements
that declare each of the application's components and has attributes
that can affect all the components.
Many of these attributes, such as
`icon`, `label`, `permission`, `process`,
`taskAffinity`, and `allowTaskReparenting`, set default values
for corresponding attributes of the component elements. Others, such as
`debuggable`, `enabled`, `description`, and
`allowClearUserData`, set values for the application as a whole and
aren't overridden by the components.
attributes
:
`android:allowTaskReparenting`
: Whether activities that the application defines can move from
the task that started them to the task they have an affinity for when that task
is next brought to the front. It's `"true"` if they can move, and
`"false"` if they must remain with the task where they started.
The default value is `"false"`.
The
`https://developer.android.com/guide/topics/manifest/activity-element`
element has its own
`https://developer.android.com/guide/topics/manifest/activity-element#reparent`
attribute that can override the value set here.
`android:allowBackup`
: Whether to let the application participate in the backup
and restore infrastructure. If this attribute is set to `"false"`, no
backup or restore of the application is ever performed, disabling all cloud backups
and device-to-device (D2D) transfers, even by a full-system backup that otherwise
causes all application data to save using `adb`. The default value of this
attribute is `"true"`. If your app processes sensitive data, you can keep
this attribute set to `"true"` and configure
[`android:dataExtractionRules`](https://developer.android.com/guide/topics/manifest/application-element#dataExtractionRules) to safely
exclude sensitive keys or temporary caches while preserving user preferences and
continuity.
**Note:**For apps targeting Android 12 (API level 31) or higher, this behavior
varies. On devices from some device manufacturers, you can't disable device-to-device migration
of your app's files.
However, you can disable cloud-based backup and restore of your app's files by setting
this attribute to `"false"`, even if your app targets Android 12 (API level 31) or
higher.
For more information, see the
[backup and restore](https://developer.android.com/about/versions/12/behavior-changes-12#backup-restore) section
of the page that describes behavior changes for apps targeting Android 12 (API level 31) or
higher.
`android:allowClearUserData`
: Whether to let the application reset user data. This data includes
flags, such as whether the user has seen introductory tooltips, as
well as user-customizable settings and preferences. The default value of this
attribute is `"true"`.
**Note:** Only apps that are part of the system image can
declare this attribute explicitly. Third-party apps can't include this
attribute in their manifest files.
For more information, see [Data backup overview](https://developer.android.com/guide/topics/data/backup).
`android:allowCrossUidActivitySwitchFromBelow`
: Specifies whether activities from other applications in the same task
can be launched on top of this application. If set to `"false"`,
activity launches that would replace this application in the user's view
are blocked.
The default value is `"true"`.
For more information, see the guide to
[Secure Background Activity Launches](https://developer.android.com/guide/components/activities/secure-bal).
`android:allowNativeHeapPointerTagging`
: Whether the app enables the Heap pointer tagging feature. The default value of
this attribute is `"true"`.
**Note:** Disabling this feature **doesn't** address the
underlying code health issue. Future hardware devices might not support this manifest tag.
For more information, see [Tagged Pointers](https://source.android.com/devices/tech/debug/tagged-pointers).
`android:appCategory`
: Declares the category of this app. Categories are used to cluster multiple apps
together into meaningful groups, such as when summarizing battery, network, or
disk usage. Only define this value for apps that fit well into one of
the specific categories.
Must be one of the following constant values.
| Value | Description |
|---|---|
| `accessibility` | Apps that are primarily accessibility apps, such as screen-readers. |
| `audio` | Apps that primarily work with audio or music, such as music players. |
| `game` | Apps that are primarily games. |
| `image` | Apps that primarily work with images or photos, such as camera or gallery apps. |
| `maps` | Apps that are primarily map apps, such as navigation apps. |
| `news` | Apps that are primarily news apps, such as newspapers, magazines, or sports apps. |
| `productivity` | Apps that are primarily productivity apps, such as cloud storage or workplace apps. |
| `social` | Apps that are primarily social apps, such as messaging, communication, email, or social network apps. |
| `video` | Apps that primarily work with video or movies, such as streaming video apps. |
`android:backupAgent`
: The name of the class that implements the application's backup agent,
a subclass of `https://developer.android.com/reference/android/app/backup/BackupAgent`. The attribute value is
a fully qualified class name, such as `"com.example.project.MyBackupAgent"`.
However, as a shorthand, if the first character of the name is a period,
for example, `".MyBackupAgent"`, it is appended to the
package name specified in the
`https://developer.android.com/guide/topics/manifest/manifest-element`
element.
There is no default. The name must be specific.
`android:backupInForeground`
: Indicates that
[Auto Backup](https://developer.android.com/guide/topics/data/autobackup) operations
can be performed on this app even if the app is in a foreground-equivalent
state. The system shuts down an app during auto backup operation,
so use this attribute with caution. Setting this flag to `"true"` can impact app
behavior while the app is active.
The default value is `"false"`, which means that the OS avoids
backing up the app while it's running in the foreground, such as a music app
that is actively playing music using a service in the
`https://developer.android.com/reference/android/app/Service#startForeground(int, android.app.Notification)` state.
`android:banner`
: A [drawable resource](https://developer.android.com/guide/topics/resources/drawable-resource)
providing an extended graphical banner for its associated item. Use with the
`<application>` tag to supply a default banner for all application activities or with the
[`<activity>`](https://developer.android.com/guide/topics/manifest/activity-element)
tag to supply a banner for a specific activity.
The system uses the banner to represent an app in
the Android TV home screen. Therefore, only specify this for applications with an activity that handles the
`https://developer.android.com/reference/android/content/Intent#CATEGORY_LEANBACK_LAUNCHER` intent.
This attribute is set as a reference to a drawable resource containing
the image, for example `"@drawable/banner"`. There is no default banner.
For more information, see [Provide a home screen banner](https://developer.android.com/training/tv/start/start#banner).
`android:dataExtractionRules`
:
Applications can set this attribute to an XML resource where they specify the
rules determining which files and directories can be copied from the device as part of backup or
transfer operations.
For information about the format of the XML file, see [Backup and restore](https://developer.android.com/about/versions/12/behavior-changes-12#backup-restore).
`android:debuggable`
: Whether the application can be debugged, even when running
on a device in user mode. It's `"true"` if it can be and `"false"`
if not. The default value is `"false"`.
`android:description`
: User-readable text about the application, which is longer and more descriptive than the application label.
The value is set as a reference to a string resource. Unlike the label, it can't be a raw string.
There is no default value.
`android:enabled`
: Whether the Android system can instantiate components of
the application. It's `"true"` if it can and `"false"`
if not. If the value is `"true"`, each component's
`enabled` attribute determines whether that component is enabled.
If the value is `"false"`, it overrides the
component-specific values, and all components are disabled.
The default value is `"true"`.
`android:enableOnBackInvokedCallback`
: This flag lets you opt out of predictive system animations at the app level.
Set `android:enableOnBackInvokedCallback=false` to turn off predictive back animations at the app level
and instruct the system to ignore calls to the `OnBackInvokedCallback` platform API.
`android:extractNativeLibs`
: This attribute indicates whether the package installer extracts native libraries from the
APK to the file system. If set to `"false"`, your native libraries are stored
uncompressed in the APK. Although your APK might be larger, your application loads faster
because the libraries load directly from the APK at runtime.
The default value of `extractNativeLibs` depends on `minSdkVersion` and the
version of AGP you're using. In most cases, the default behavior is probably what you want, and
you don't have to set this attribute explicitly.
`android:forceQueryable`
: Specifies whether this application is visible to all other applications
on the device, regardless of what those other applications declare in the
`<queries>` tags in their manifests.
The default value is `"false"`.
For more information, see the guide on
[automatic package visibility filtering](https://developer.android.com/training/package-visibility/automatic).
`android:fullBackupContent`
: This attribute points to an XML file that contains full backup rules for
[Auto Backup](https://developer.android.com/guide/topics/data/autobackup).
These rules determine what files get backed up. For more information, see the
[XML config syntax](https://developer.android.com/guide/topics/data/autobackup#XMLSyntax)
for Auto Backup.
This attribute is optional. If it is not specified, by default, Auto Backup
includes most of your app's files. For more information, see
[Files that are backed up](https://developer.android.com/guide/topics/data/autobackup#Files).
`android:fullBackupOnly`
: This attribute indicates whether to use
[Auto Backup](https://developer.android.com/guide/topics/data/autobackup) on devices
where it is available. If set to `"true"`, then your app performs
Auto Backup when installed on a device running Android 6.0 (API level 23) or
higher. On older devices, your app ignores this attribute and performs
[key/value backups](https://developer.android.com/guide/topics/data/keyvaluebackup).
The default value is `"false"`.
`android:gwpAsanMode`
: This attribute indicates whether to use
[GWP-ASan](https://developer.android.com/ndk/guides/gwp-asan), a native memory allocator feature that helps
find use-after-free and heap-buffer-overflow bugs.
The default value is `"never"`.
`android:hasCode`
: Whether the application contains any DEX code---that is, code using the
Kotlin or Java programming language.
It's `"true"` if it does and `"false"` if not. When the
value is `"false"`, the system doesn't try to load any application
code when launching components. The default value is `"true"`.
If the application includes native (C/C++) code, but no DEX code, this should
be set to `"false"`. If set to `"true"` when the APK
contains no DEX code, the app may fail to load.
**This property must account for code included in the application by
dependencies.** If the application depends on an AAR that uses
Java/Kotlin code, or directly on a JAR, `app:hasCode` must be
`"true"`, or omitted as that is the default.
For example, your app might support
[Play feature delivery](https://developer.android.com/platform/technology/app-bundle) and include feature
modules that don't generate any DEX files, which is bytecode optimized for the Android
platform. If so, you need to set this property to `"false"` in the module's manifest
file to avoid runtime errors.
`android:hasFragileUserData`
: Whether to show the user a prompt to
keep the app's data when the user uninstalls the app. The default value is `"false"`.
`android:hardwareAccelerated`
: Whether hardware-accelerated rendering is enabled for all
activities and views in this application. It's `"true"` if it is
enabled and `"false"` if not. The default value is `"true"` if you set
either [`minSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#min)
or [`targetSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#target)
to `"14"` or higher. Otherwise, it's `"false"`.
Starting from Android 3.0 (API level 11), a hardware-accelerated OpenGL renderer is
available to applications to improve performance for many common 2D graphics
operations. When the hardware-accelerated renderer is enabled, most operations
in Canvas, Paint, Xfermode, ColorFilter, Shader, and Camera are accelerated.
This results in smoother animations, smoother scrolling, and improved
responsiveness overall, even for applications that don't explicitly make use
the framework's OpenGL libraries.
Not all of the OpenGL 2D operations are accelerated. If you enable
the hardware-accelerated renderer, test your application so that it can
make use of the renderer without errors.
For more information, read the
[Hardware acceleration](https://developer.android.com/guide/topics/graphics/hardware-accel)
guide.
`android:icon`
: An icon for the application as whole and the default icon for
each of the application's components. See the individual
`icon` attributes for the
`https://developer.android.com/guide/topics/manifest/activity-element`,
`https://developer.android.com/guide/topics/manifest/activity-alias-element`,
`https://developer.android.com/guide/topics/manifest/service-element`,
`https://developer.android.com/guide/topics/manifest/receiver-element`, and
`https://developer.android.com/guide/topics/manifest/provider-element` elements.
This attribute is set as a reference to a drawable resource containing
the image, such as `"@drawable/icon"`. There is no default icon.
`android:intentMatchingFlags`
:
Use this attribute to fine-tune how the system matches incoming intents to app
components. By default, no special matching rules are applied.
This attribute can be specified on the `<application>` tag
as well as on component tags, including `<activity>`,
`<activity-alias>`, `<receiver>`,
`<service>`, and `<provider>`. The value
set on a component overrides the value set on the
`<application>` tag.
The value must be one or more of the following flags, separated by '`|`':
| Flag | Description |
|---|---|
| `none` | Disables all special matching rules for incoming intents. When specifying multiple flags, conflicting values are resolved by giving precedence to the `none` flag. |
| `enforceIntentFilter` | Enforces stricter matching for incoming intents: - Explicit intents must match the target component's intent filter. - Intents without an action don't match any intent filter. |
| `allowNullAction` | Relaxes the matching rules to allow intents without an action to match. This flag is used in conjunction with `enforceIntentFilter` to achieve the following behavior: - Explicit intents must match the target component's intent filter. - Intents without an action are allowed to match any intent filter. |
For more information, see the
[Safer Intents](https://developer.android.com/about/versions/16/behavior-changes-16#safer-intents)
section in the Android 16 (API level 36) behavior changes.
`android:isGame`
: Whether the application is a game. The system might group together applications classified
as games or display them separately from other applications. The default is `"false"`.
`android:isMonitoringTool`
: Indicates that this application is designed to monitor other individuals.
**Note:** If an app declares this attribute in its manifest, the developer must
follow the
[Stalkerware](https://support.google.com/googleplay/android-developer/answer/9888380#commercial-spyware)
policy to publish the app to Google Play.
There is no default value. The developer must specify one of the following values:
| Value | Description |
|---|---|
| `"parental_control"` | App caters to parental control and is specifically targeted at parents who want to keep their kids safe. |
| `"enterprise_management"` | App caters to enterprises that want to manage and track devices given to employees. |
| `"other"` | App caters to a use case not otherwise specified in this table. |
`android:killAfterRestore`
: Whether the application terminates after its
settings have been restored during a full-system restore operation.
Single-package restore operations never cause the application to
shut down. Full-system restore operations typically only occur once,
when the phone is first set up. Third-party applications don't normally
need to use this attribute.
The default is `"true"`, which means that after the application
finishes processing its data during a full-system restore, it terminates.
`android:largeHeap`
: Whether the application's processes are created with a large Dalvik heap. This applies to
all processes created for the application. It only applies to the first application loaded into a
process. If you're using a shared user ID to let multiple applications use a process, they all
must use this option consistently to avoid unpredictable results.
Most apps don't need this and instead focus on reducing their overall memory usage for
improved performance. Enabling this also doesn't guarantee a fixed increase in available memory,
because some devices are constrained by their total available memory.
To query the available memory size at runtime, use the methods `https://developer.android.com/reference/android/app/ActivityManager#getMemoryClass()` or `https://developer.android.com/reference/android/app/ActivityManager#getLargeMemoryClass()`.
`android:label`
: A user-readable label for the application as a whole and a default
label for each of the application's components. See the individual
`label` attributes for the
`https://developer.android.com/guide/topics/manifest/activity-element`,
`https://developer.android.com/guide/topics/manifest/activity-alias-element`,
`https://developer.android.com/guide/topics/manifest/service-element`,
`https://developer.android.com/guide/topics/manifest/receiver-element`, and
`https://developer.android.com/guide/topics/manifest/provider-element` elements.
The label is set as a reference to a string resource, so that
it can be localized like other strings in the user interface.
However, as a convenience while you're developing the application,
it can also be set as a raw string.
`android:localeConfig`
: A reference to an XML resource that specifies the list of locales
supported by the application. This is used by the system to support per-app
language preferences.
For more information, see the guide on
[per-app language preferences](https://developer.android.com/guide/topics/resources/app-languages).
`android:logo`
: A logo for the application as whole and the default logo for activities.
This attribute is set as a reference to a drawable resource containing
the image, such as `"@drawable/logo"`. There is no default logo.
`android:manageSpaceActivity`
: The fully qualified name of an `Activity` subclass that the system
launches to let users manage the memory occupied by the application
on the device. The activity is also declared with an
`https://developer.android.com/guide/topics/manifest/activity-element` element.
`android:name`
: The fully qualified name of an `https://developer.android.com/reference/android/app/Application`
subclass implemented for the application. When the application process
is started, this class is instantiated before any of the application's
components.
The subclass is optional. Most applications don't need one.
In the absence of a subclass, Android uses an instance of the base
`Application` class.
`android:networkSecurityConfig`
: Specifies the name of the XML file that contains your application's
[Network security
configuration](https://developer.android.com/training/articles/security-config). The value is a reference to the XML resource file
containing the configuration.
This attribute was added in API level 24.
`android:pageSizeCompat`
: Overrides the user or platform compatibility settings for 16 KB page
sizes, which lets you force page-agnostic compatibility mode on or off for
this application.
For more information, see the guide on
[supporting 16 KB page sizes](https://developer.android.com/guide/practices/page-sizes).
`android:permission`
: The name of a permission that clients need in order to interact
with the application. This attribute is a convenient way to set a
permission that applies to all of the application's components. It is
overwritten by setting the `permission` attributes of individual
components.
For more information about permissions, see the
[Permissions](https://developer.android.com/guide/topics/manifest/manifest-intro#perms)
section in the app manifest overview and
[Security tips](https://developer.android.com/guide/topics/security/security).
`android:persistent`
: Whether the application remains running at all times. It's
`"true"` if it does and `"false"` if not. The default value
is `"false"`. Applications don't normally set this flag.
Persistence mode is intended only for certain system applications.
`android:process`
: The name of a process where all components of the application run.
Each component can override this default by setting its own `process`
attribute.
By default, Android creates a process for an application when the first
of its components needs to run. All components then run in that process.
The name of the default process matches the package name set by the
`https://developer.android.com/guide/topics/manifest/manifest-element` element.
By setting this attribute to a process name that's shared with another
application, you can arrange for components of both applications to run in
the same process, but only if the two applications also share a
user ID and are signed with the same certificate.
If the name assigned to this attribute begins with a colon (`:`), a new
process, private to the application, is created when it's needed.
If the process name begins with a lowercase character, a global process
of that name is created. A global process can be shared with other
applications, reducing resource usage.
`android:restoreAnyVersion`
: Indicates that the application is prepared to attempt a restore of any
backed-up data set, even if the backup was stored by a newer version
of the application than is currently installed on the device. Setting
this attribute to `"true"` lets the Backup Manager
- `$1`
attempt a restore even when a version mismatch suggests that the data is incompatible. *Use with caution!*
- The default value of this attribute is `"false"`.
`android:requestLegacyExternalStorage`
: Whether the application wants to opt out of
[scoped storage](https://developer.android.com/training/data-storage/files/external-scoped).
**Note:** Depending on changes related to policy or app
compatibility, the system might not honor this opt-out request.
`android:requiredAccountType`
: Specifies the account type required by the application to function.
If your app requires an `https://developer.android.com/reference/android/accounts/Account`, the value for this attribute must
correspond to the account authenticator
type used by your app, as defined by `https://developer.android.com/reference/android/accounts/AuthenticatorDescription`,
such as `"com.google"`.
The default value is null and indicates that the application
can work *without* any accounts.
Because restricted profiles
can't add accounts, specifying this attribute makes your app
unavailable from a restricted profile unless you also declare
[`android:restrictedAccountType`](https://developer.android.com/guide/topics/manifest/application-element#restrictedAccountType) with
the same value.
**Caution:**
If the account data might reveal personally identifiable information, it's important
that you declare this attribute and leave [`android:restrictedAccountType`](https://developer.android.com/guide/topics/manifest/application-element#restrictedAccountType) null, so that restricted profiles cannot use
your app to access personal information that belongs to the owner user.
This attribute was added in API level 18.
`android:resizeableActivity`
:
Specifies whether the app supports [multi-window mode](https://developer.android.com/guide/topics/ui/multi-window).
> [!WARNING]
> **Warning:** To improve the layout of apps on form factors with smallest width \>= 600dp, the system ignores this attribute for apps that target Android 16 (API level 36). Your app can opt out of the Android 16 behavior, but the opt out will be eliminated in a future release. See [Device compatibility mode](https://developer.android.com/guide/practices/device-compatibility-mode#android_16).
You can set this attribute in either the [`<activity>`](https://developer.android.com/guide/topics/manifest/activity-element)
or `<application>` element.
If you set this attribute to `"true"`, the user can launch the activity in
split-screen and free-form modes. If you set the attribute to `"false"`, the app
can't be tested or optimized for a multi-window environment. The system can
still put the activity in multi-window mode with compatibility mode applied.
Setting this attribute to `"true"` doesn't guarantee that there are no
other apps in multi-window mode visible on screen, such as picture-in-picture, or
on other displays. Therefore, setting this flag doesn't mean that your app
has exclusive resource access.
For apps targeting API level 24 or higher, the default is `"true"`.
If your app targets API level 31 or higher, this attribute works differently on small and large
screens:
- **Large screens (sw \>= 600dp):** all apps support multi-window mode. The attribute indicates whether an app can be resized, not whether the app supports multi-window mode. If `resizeableActivity="false"`, the app is put into compatibility mode when necessary to conform to display dimensions.
- **Small screens (sw \< 600dp):** if `resizeableActivity="true"` and the minimum width and minimum height of the main activity are within the multi-window requirements, the app supports multi-window mode. If `resizeableActivity="false"`, the app doesn't support multi-window mode regardless of the activity minimum width and height.
> [!NOTE]
> **Note:**
>
> - Device manufacturers can override the API level 31 behavior to improve the layout of apps.
> - On devices with Android 16 (API level 36) or higher installed, virtual device owners (select trusted and privileged apps) can configure devices they manage to override (ignore) this attribute to improve app layout. See also [Companion app
> streaming](https://source.android.com/docs/core/permissions/app-streaming).
>
> See [Device compatibility mode](https://developer.android.com/guide/practices/device-compatibility-mode).
A task's root activity value is applied to all additional activities launched in the task. That is, if the root
activity of a task is resizable, then the system treats all other activities in the task as resizable. If the root
activity isn't resizable, the other activities in the task aren't resizable.
This attribute was added in API level 24.
`android:restrictedAccountType`
: Specifies the account type required by this application and indicates that restricted profiles
can access such accounts that belong to the owner user. If your app requires an
`https://developer.android.com/reference/android/accounts/Account` and restricted profiles *can*
access the primary user's accounts, the value for this attribute must
correspond to the account authenticator type used by your app, as
defined by `https://developer.android.com/reference/android/accounts/AuthenticatorDescription`, such as `"com.google"`.
The default value is null and indicates that the application can work *without* any
accounts.
**Caution:**
Specifying this attribute lets restricted profiles use your
app with accounts that belong to the owner user, which might reveal personally identifiable
information. If the account might reveal personal details, *don't*
use this attribute. Instead, declare the [`android:requiredAccountType`](https://developer.android.com/guide/topics/manifest/application-element#requiredAccountType) attribute
to make your app unavailable to restricted profiles.
This attribute was added in API level 18.
`android:supportsRtl`
: Declares whether your application is willing to support right-to-left (RTL) layouts.
If this is set to `"true"` and
[`targetSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#target)
is set to 17 or higher, various RTL APIs are
activated and used by the system so your app can display RTL layouts.
If this is set to `"false"` or if `targetSdkVersion` is set to 16 or lower, the RTL APIs are ignored
or have no effect, and your app behaves the same regardless of the layout
direction associated to the user's locale choice. That is, your layouts are always left-to-right.
The default value of this attribute is `"false"`.
This attribute was added in API level 17.
`android:taskAffinity`
: An affinity name that applies to all activities within the application,
except for those that set a different affinity with their own
`https://developer.android.com/guide/topics/manifest/activity-element#aff`
attributes. See that attribute for more information.
By default, all activities within an application share the same
affinity. The name of that affinity is the same as the package name
set by the
`https://developer.android.com/guide/topics/manifest/manifest-element` element.
`android:testOnly`
: Indicates whether this application is only for testing purposes. For example,
it might expose functionality or data outside of itself that can cause a security
hole, but be useful for testing. This kind of APK only installs
through `https://developer.android.com/studio/command-line/adb`. You can't publish it to Google Play.
Android Studio automatically adds this attribute when you click **Run**
![](https://developer.android.com/static/studio/images/buttons/toolbar-run.png).
`android:theme`
: A reference to a style resource defining a default theme for all
activities in the application. Individual activities can override
the default by setting their own `https://developer.android.com/guide/topics/manifest/activity-element#theme`
attributes. For more information, see [Styles and themes](https://developer.android.com/guide/topics/ui/themes).
`android:uiOptions`
: Extra options for an activity's UI. Must be one of the following values:
| Value | Description |
|---|---|
| `"none"` | No extra UI options. This is the default. |
| `"splitActionBarWhenNarrow"` | Adds a bar at the bottom of the screen to display action items in the *app bar* , also known as the *action bar*, when constrained for horizontal space, such as when in portrait mode on a handset. Instead of a small number of action items appearing in the app bar at the top of the screen, the app bar splits into the top navigation section and the bottom bar for action items. This means a reasonable amount of space is available for the action items and for the navigation and title elements at the top. Menu items aren't split across the two bars. They always appear together. |
For more information about the app bar, see [Add the app bar](https://developer.android.com/training/appbar).
This attribute was added in API level 14.
`android:usesCleartextTraffic`
: Indicates whether the app intends to use cleartext network traffic, such as cleartext HTTP.
The default value for apps that target API level 27 or lower is `"true"`. Apps that
target API level 28 or higher default to `"false"`.
**Note:**
This attribute is getting deprecated and will be ignored for apps targeting API levels 38 and above.
Specify a [Network Security Configuration](https://developer.android.com/training/articles/security-config#CleartextTraffic) to control cleartext traffic for API levels 24 and above.
If your app targets API levels 23 and below, you must specify `android:usesCleartextTraffic` in addition to a Network Security Config.
When the attribute is set to `"false"`, platform components, for example, HTTP and FTP
stacks, `https://developer.android.com/reference/android/app/DownloadManager`, and
`https://developer.android.com/reference/android/media/MediaPlayer`, refuse the
app's requests to use cleartext traffic.
Third-party libraries are strongly encouraged to honor this
setting as well. The key reason for avoiding cleartext traffic is the lack of confidentiality,
authenticity, and protections against tampering. A network attacker can eavesdrop on transmitted
data and also modify it without being detected.
This flag is honored on a best-effort basis because it's impossible to prevent all cleartext
traffic from Android applications given the level of access provided to them. For example, there's
no expectation that the `https://developer.android.com/reference/java/net/Socket` API honors
this flag, because it can't determine whether its traffic is in cleartext.
However, most
network traffic from applications is handled by higher-level network stacks and components, which can
honor this flag by either reading it from
`https://developer.android.com/reference/android/content/pm/ApplicationInfo#flags`
or
`https://developer.android.com/reference/android/security/NetworkSecurityPolicy#isCleartextTrafficPermitted()`.
**Note:**
`https://developer.android.com/reference/android/webkit/WebView` honors this attribute for
applications targeting API level 26 and higher.
During app development, StrictMode can be used to identify any cleartext traffic from the app.
For more information, see
`https://developer.android.com/reference/android/os/StrictMode.VmPolicy.Builder#detectCleartextNetwork()`.
This attribute was added in API level 23.
This flag is ignored on Android 7.0 (API level 24) and above if an Android Network Security
Config is present.
`android:vmSafeMode`
: Indicates whether the app wants the virtual machine (VM) to operate
in safe mode. The default value is `"false"`.
This attribute was added in API level 8, where a value of `"true"`
disabled the Dalvik just-in-time (JIT) compiler.
This attribute was adapted in API level 22, where a value of `"true"`
disabled the ART ahead-of-time (AOT) compiler.
introduced in:
: API level 1
see also:
: `https://developer.android.com/guide/topics/manifest/activity-element`
`https://developer.android.com/guide/topics/manifest/service-element`
`https://developer.android.com/guide/topics/manifest/receiver-element`
`https://developer.android.com/guide/topics/manifest/provider-element`
@@ -0,0 +1,327 @@
Before your users can authenticate with passkeys, your app must first register
or create the passkey for their account.
To create the passkey, obtain details required to create the passkey from your
app server, and then call the Credential Manager API, which returns a public and
private key pair. The returned private key is stored in a credential provider,
such as Google Password Manager, as a passkey. The public key is stored on your
app server.
> [!TIP]
> **Tip:** While designing authentication flows with passkeys, make sure that you follow the [UX guidelines for passkeys](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys).
![Passkeys are stored in a credential provider and public keys are stored on the app server](https://developer.android.com/static/identity/passkeys/images/create-passkeys.png) **Figure 1:**Creation of passkeys
## Prerequisites
Make sure that you have set up [Digital Asset Links](https://developer.android.com/identity/credential-manager/prerequisites) and that you target
devices running Android 9 (API level 28) or higher.
## Overview
This guide focuses on the changes required in your [relying party client app](https://developer.android.com/identity/credential-manager#authentication-terminology)
to create a passkey, and gives a brief overview of the [relying party app
server](https://developer.android.com/identity/credential-manager#authentication-terminology) implementation. To learn more about the server-side integration, see
[Server-side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration).
1. [**Add dependencies to your app**](https://developer.android.com/identity/passkeys/create-passkeys#add-dependencies): Add the required Credential Manager libraries.
2. [**Instantiate Credential Manager**](https://developer.android.com/identity/passkeys/create-passkeys#instantiate): Create a Credential Manager instance.
3. [**Get credential creation options from the app server**](https://developer.android.com/identity/passkeys/create-passkeys#get-options): From your app server, send the client app the details required to create the passkey, such as information about the app, the user, as well as a `challenge` and other fields.
4. [**Request a passkey**](https://developer.android.com/identity/passkeys/create-passkeys#request-passkey): In your app, use the details received from the app server to create a [`GetPublicKeyCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetPublicKeyCredentialOption) object and use this object to invoke the `credentialManager.getCredential()` method to create a passkey.
5. [**Handle the passkey creation response**](https://developer.android.com/identity/passkeys/create-passkeys#handle-response): When you receive the credentials on your client app, you must encode, serialize and then send the public key to the app server. You must also handle each of the exceptions that can occur in case of passkey creation.
6. [**Verify and save the public key on the server**](https://developer.android.com/identity/passkeys/create-passkeys#verify): Complete the server-side steps to verify the origin of the credential and then save the public key.
7. [**Notify the user**](https://developer.android.com/identity/passkeys/create-passkeys#notify): Notify the user that their passkey has been created.
> [!TIP]
> **Tip:** To enhance the user experience during sign up, add functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices, and [suppressing autofill dialogs on
> credential fields](https://developer.android.com/identity/passkeys/create-passkeys#suppress-autofill).
## Add dependencies to your app
Add the following dependencies to your app module's `build.gradle` file:
### Kotlin
```kotlin
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
```
### Groovy
```groovy
dependencies {
implementation "androidx.credentials:credentials:1.7.0-alpha03"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}
```
> [!NOTE]
> **Note:** Use the latest available versions of the dependencies. Versions of [`androidx.credentials`](https://developer.android.com/identity/passkeys/create-passkeys#automatic-upgrade) earlier than version 1.2 throw an `UnsupportedOperationException("Post-U not supported yet")` exception on Android 14.
## Instantiate Credential Manager
Use your app or activity context to create a `CredentialManager` object.
// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)
## Get credential creation options from your app server
When the user clicks a "Create Passkey" button or when a new user signs up, make
a request from your app to your app server to obtain the information required to
start the passkey registration process.
Use a FIDO-compliant library in your app server to send your client app the
information required to create a passkey, such as information about the user,
the app, and additional configuration properties. To learn more, see [Server
side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration).
In the client app, decode the public key creation options sent by the app
server. These are usually represented in JSON format. To learn more about how
this decoding is done for web clients, see [Encoding and
Decoding](https://developers.google.com/identity/passkeys/developer-guides/server-registration#encoding_and_decoding). For Android client apps, you must handle the decoding
separately.
> [!NOTE]
> **Note:** On your app server, securely store the `challenge` so that you can later verify the origin of the credential.
The following snippet shows the structure the public key creation options sent
by the app server:
{
"challenge": "<base64url-encoded challenge>",
"rp": {
"name": "<relying party name>",
"id": "<relying party host name>"
},
"user": {
"id": "<base64url-encoded user ID>",
"name": "<user name>",
"displayName": "<user display name>"
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"attestation": "none",
"excludeCredentials": [
{
"id": "<base64url-encoded credential ID to exclude>",
"type": "public-key"
}
],
"authenticatorSelection": {
"requireResidentKey": true,
"residentKey": "required",
"userVerification": "required"
}
}
Key fields in the public key creation options include:
- `challenge`: A server-generated random string that is used to prevent replay attacks.
- `rp`: Details about the app.
- `rp.name`: The app's name.
- `rp.id`: The app's domain or subdomain.
- `user`: Details about the user.
- `id`: The user's unique ID. This value must not include personally identifying information, for example, email addresses or usernames. You can use a random, 16-byte value.
- `name`: A unique identifier for the account that the user will recognise, such as their email address or username. This will be displayed in the account selector. If using a username, use the same value as in password authentication.
- `displayName`: An optional, user-friendly name for the account intended for display in the account selector.
- `authenticatorSelection`: Details about the device that will be used for authentication.
- `authenticatorAttachment`: Indicates the preferred [authenticator](https://www.w3.org/TR/webauthn/#authenticator). The possible values are as follows:
- `platform`: This value is used for an authenticator built into the user's device, such as a fingerprint sensor.
- `cross-platform`: This value is used for roaming devices such as security keys. It is not typically used in the passkey context.
- Unspecified (recommended): Leaving this value unspecified provides users with the flexibility to create passkeys on their preferred devices. In most cases, leaving the parameter unspecified is the best option.
- `requireResidentKey`: To create a passkey, set the value of this `Boolean` field to `true`.
- `residentKey`: To create a passkey, set the value to `required`.
- `userVerification`: Used to specify the requirements for user verification during a passkey registration. The possible values are as follows:
- `preferred`: Use this value if you prioritize user experience over protection, such as in environments where user verification causes more friction than protection.
- `required`: Use this value if invoking a user verification method available on the device is required.
- `discouraged`: Use this value if using a user verification method is discouraged.
To learn more about `userVerification`, see [userVerification deep dive](https://web.dev/articles/webauthn-user-verification).
- `excludeCredentials`: List credential IDs in an [array](https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials) to prevent the creation of a duplicate passkey if one already exists with the same credential provider.
## Create a passkey
After you have parsed the server-side public key creation options, create a
passkey by wrapping these options in a `CreatePublicKeyCredentialRequest` object
and calling `createCredential()`.
The `createPublicKeyCredentialRequest` includes the following:
- `requestJson`: The credential creation options sent by the app server.
- `preferImmediatelyAvailableCredentials`: This is an optional Boolean field that defines whether to only use locally-available or credential provider-synced credentials to fulfill the request, instead of credentials from security keys or [hybrid](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid) key flows. The possible usages are as follows:
- `false` (default): Use this value if the call to Credential Manager was triggered by an explicit user action.
- `true`: Use this value if Credential Manager is opportunistically called, such as when first opening the app.
If you set the value to `true` and there are no immediately available credentials, Credential Manager won't show any UI and the request will fail immediately, returning NoCredentialException for get requests and [`CreateCredentialNoCreateOptionException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/CreateCredentialNoCreateOptionException) for create requests.
- `origin`: This field is automatically set for Android apps. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential
Manager calls on behalf of other parties for privileged apps](https://developer.android.com/training/sign-in/privileged-apps).
- `isConditional`: This is an optional field that defaults to `false`. For more information, see [Automatically create a passkey](https://developer.android.com/identity/passkeys/create-passkeys#automatic-upgrade).
Calling the `createCredential()` function launches Credential Manager's built-in
bottom sheet UI that prompts the user to use a passkey and to select a
credential provider and account for storage. However, if `isConditional` is set
to `true`, the bottom sheet UI does not display, and the passkey is
automatically created.
### Automatically create a passkey
You can automatically create a passkey for a user after a successful password
login by setting the `isConditional` parameter to `true` in your
`CreatePublicKeyCredentialRequest` while creating a passkey. If the user doesn't
already have a passkey, your app will automatically attempt to create one in the
background and store it in the user's credential provider, such as Google
Password Manager. For an example of how this is implemented, see the [public
sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt#L98).
![An example of the notification Google Password Manager shows after passkey creation](https://developer.android.com/static/identity/passkeys/images/conditional-create-gpm.svg) **Figure 2:**Google Password Manager notification
> [!NOTE]
> **Note:** If a passkey is created automatically, credential providers are responsible for notifying users about a newly created passkey. Google Password Manager notifies users when a passkey is automatically created. However, other credential providers might have their own conditions and notifications for this feature.
## Handle the response
After the user is verified using the device's screen lock, a passkey is created
and stored in the user's selected credential provider.
The response after you successfully call `createCredential()` is a
[PublicKeyCredential](https://developer.android.com/jetpack/androidx/releases/credentials) object.
The `PublicKeyCredential` looks as follows:
{
"id": "<identifier>",
"type": "public-key",
"rawId": "<identifier>",
"response": {
"clientDataJSON": "<ArrayBuffer encoded object with the origin and signed challenge>",
"attestationObject": "<ArrayBuffer encoded object with the public key and other information.>"
},
"authenticatorAttachment": "platform"
}
In the client app, serialize the object and send it to the app server.
Add code to handle failures as shown in the following snippet:
fun handleFailure(e: CreateCredentialException) {
when (e) {
is CreatePublicKeyCredentialDomException -> {
// Handle the passkey DOM errors thrown according to the
// WebAuthn spec.
}
is CreateCredentialCancellationException -> {
// The user intentionally canceled the operation and chose not
// to register the credential.
}
is CreateCredentialInterruptedException -> {
// Retry-able error. Consider retrying the call.
}
is CreateCredentialProviderConfigurationException -> {
// Your app is missing the provider configuration dependency.
// Most likely, you're missing the
// "credentials-play-services-auth" module.
}
is CreateCredentialCustomException -> {
// You have encountered an error from a 3rd-party SDK. If you
// make the API call with a request object that's a subclass of
// CreateCustomCredentialRequest using a 3rd-party SDK, then you
// should check for any custom exception type constants within
// that SDK to match with e.type. Otherwise, drop or log the
// exception.
}
else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}")
}
}
## Verify and save the public key on the app server
On the app server, you must verify the public key credential and then [save the
public key](https://web.dev/articles/passkey-registration#save-credential).
To verify the public key credential's origin, compare it against an allow list
of approved apps. If a key has an unrecognized origin, reject it.
> [!NOTE]
> **Note:** An app's origin is based on its unique identity, which is the SHA-256 fingerprint of its signing certificate.
To obtain the app's SHA 256 fingerprint:
1. Print your release app's signing certificate by running the following
command in a terminal:
keytool -list -keystore <path-to-apk-signing-keystore>
In the response, identify the signing certificate's SHA 256 fingerprint,
mentioned as `Certificate fingerprints block` : `SHA256`.
2. Encode the SHA256 fingerprint with base64url encoding. This Python example
demonstrates how to properly encode the fingerprint:
import binascii
import base64
fingerprint = '<SHA256 finerprint>' # your app's SHA256 fingerprint
print(base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))
3. Append `android:apk-key-hash`: to the start of the output from the previous
step so that you get something that is similar to the following:
android:apk-key-hash:<encoded SHA 256 fingerprint>
The result should match with an allowed origin on your app server. If you
have multiple signing certificates, such as certificates for debugging and
release, or multiple apps, then repeat the process and accept all the
origins as valid on the app server.
> [!NOTE]
> **Note:** When you save the passkey on the app server, make sure that you save the Authenticator Attestation Globally Unique Identifier ([AAGUID](https://web.dev/articles/webauthn-aaguid)) from the client data. The AAGUID is a unique number that identifies the model of the authenticator. For more information, see [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys).
## Notify the user
After the passkey is successfully created, notify your users about the passkey
and inform them that they can manage their passkeys from their credential
provider app or from [within the app settings](https://developer.android.com/identity/passkeys/manage-passkeys). Notify users by using a
custom dialog, notification, or snackbar. Since an unexpected passkey creation
by a malicious entity requires an immediate security alert, consider
supplementing these in-app methods with external communication, such as an
email.
## Enhance the user experience
To enhance the user experience while implementing sign up with Credential
Manager, consider adding functionality for restore credentials and suppress
autofill dialogs.
### Add functionality to restore credentials on a new device
To allow users to seamlessly log into their accounts on a new device, implement
the [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials) functionality. Adding restore credentials with
`BackupAgent` logs users in when they open your restored app on a new device,
letting them use your app right away.
### Suppress autofill on credential fields (optional)
For app screens where users are expected to use Credential Manager's bottom
sheet UI for authentication, add the `isCredential` attribute to the username
and password fields. This suppresses autofill dialogs (`FillDialog` and
`SaveDialog`) from overlapping with Credential Manager's bottom sheet UI.
The `isCredential` attribute is supported on Android 14 and higher.
The following example demonstrates how you can add the `isCredential` attribute
to the relevant username and password fields in the relevant views for your app:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:isCredential="true" /\>
## Next steps
- [Sign in with passkeys](https://developer.android.com/identity/passkeys/sign-in-with-passkeys)
- [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys)
- [Understand passkey user experience flows](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys)
@@ -0,0 +1,219 @@
This guide continues on the implementation of using passkeys for authentication.
Before your users can sign in with passkeys, you must also complete the
instructions in [Create passkeys](https://developer.android.com/identity/passkeys/create-passkeys).
To authenticate with a passkey, you must first retrieve the options required to
retrieve the public key from your [app server](https://developer.android.com/identity/credential-manager#authentication-terminology), and then call the Credential
Manager API to retrieve the public key. Then, handle the sign-in response
appropriately.
> [!TIP]
> **Tip:** While designing authentication flows with passkeys, make sure you follow the recommendations in the [UX guidelines for passkeys](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys).
## Overview
This guide focuses on the changes required in your client app to sign in your
user with a passkey, and gives a brief overview of the app server-side
implementation. To learn more about server-side integration, see [Server-side
passkey authentication](https://developers.google.com/identity/passkeys/developer-guides/server-authentication).
To retrieve all the passkey and password options that are associated with the
user's account, complete these steps:
1. [**Get credential request options from the server**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#get-options): Make a request from your app to your authentication server to start the passkey sign-in process. From the server, send the options required to get the public key credential, as well as a unique challenge.
2. [**Create the object required to get the public key credential**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#create-object): Wrap the options sent by the server in a `GetPublicKeyCredentialOption` object
3. ([**optional) Prepare getCredential**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#reduce-latency): In Android 14 and higher, you can reduce latency by showing the account selector by using the `prepareGetCredential()` method before calling `getCredential()`.
4. [**Launch the sign in flow**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#launch-sign-in): Call `getCredential()` method to sign in the user
5. [**Handle the response**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#handle-response): Handle each of the possible credential responses.
6. [**Handle exceptions**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#handle-exceptions): Make sure that you handle exceptions appropriately.
> [!TIP]
> **Tip:** Enhance the user experience during sign in by also adding functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices.
## Get credential request options from the server
Request the server for the options required to get the public key credentials,
as well as the `challenge`, which is unique for each sign-in attempt. To learn
more about the server-side implementation, see [Create the
challenge](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_the_challenge) and [Create credential request
options](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_credential_request_options).
The options look similar to the following:
{
"challenge": "<your app challenge>",
"allowCredentials": [],
"rpId": "<your app server domain>"
}
To learn more about the fields, see the blogpost about [signing in with a
passkey](https://web.dev/articles/passkey-form-autofill#fetch_information_from_the_backend).
> [!TIP]
> **Tip:** To reduce wait times in the UI because of as network calls, fetch the details required from the app server at the beginning of the user's app session.
## Create the object required to get the public key credential
In your app, use the options to create a `GetPublicKeyCredentialOption` object.
In the following example, `requestJson` represents the options sent by the
server.
// Get password logins from the credential provider on the user's device.
val getPasswordOption = GetPasswordOption()
// Get passkeys from the credential provider on the user's device.
val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
requestJson = requestJson
)
Then, wrap `GetPublicKeyCredentialOption` in a `GetCredentialRequest` object.
val credentialRequest = GetCredentialRequest(
// Include all the sign-in options that your app supports.
listOf(getPasswordOption, getPublicKeyCredentialOption),
// Defines whether you prefer to use only immediately available
// credentials or hybrid credentials.
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
)
> [!NOTE]
> **Note:** [`GetCredentialRequest`](https://developer.android.com/reference/androidx/credentials/GetCredentialRequest#GetCredentialRequest) can include an `origin` field. However, `origin` is automatically set for Android apps, and hence should be left as null. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential Manager calls on behalf of other parties for privileged
> apps](https://developer.android.com/training/sign-in/privileged-apps).
## Optional: Reduce sign-in latency
On Android 14 or higher, you can reduce latency when showing the account
selector by using the [`prepareGetCredential()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#prepareGetCredential) method before calling
`getCredential()`.
The `prepareGetCredential()` method returns a
[`PrepareGetCredentialResponse`](https://developer.android.com/reference/androidx/credentials/PrepareGetCredentialResponse) object which is cached. This lets the
`getCredential()` method in the following step bring up the account selector
with the cached data.
coroutineScope {
val response = credentialManager.prepareGetCredential(
GetCredentialRequest(
listOf(
// Include all the sign-in options that your app supports
getPublicKeyCredentialOption,
getPasswordOption
)
)
)
}
## Launch the sign-in flow
Call the `getCredential()` method to show the user the account selector. Use the
following code snippet as a reference for how to launch the sign-in flow:
> [!NOTE]
> **Note:** To avoid a potential memory leak, pass in a `MutableContextWrapper` of your foreground `Activity` to the `getCredential()` call. Credential Manager handles updating the context during `Activity` reconstruction.
// Use an activity-based context to avoid undefined system UI
// launching behavior.
val context = MutableContextWrapper(activityContext)
coroutineScope {
try {
result = credentialManager.getCredential(
// Use MutableContextWrapper to avoid memory leak during configuration changes
context = context,
request = credentialRequest
)
handleSignIn(result)
} catch (e: GetCredentialException) {
// Handle failure
}
}
> [!NOTE]
> **Note:** If you call `getCredential()` but the user does not have any credentials available, or if they don't grant consent to using their credentials, a `NoCredentialException` is returned. To learn more about this exception, see the [troubleshooting guide](https://developer.android.com/identity/sign-in/credential-manager-troubleshooting-guide).
## Handle the response
Handle the response, which can contain one of various types of credential
objects.
fun handleSignIn(result: GetCredentialResponse) {
// Handle the successfully returned credential.
val credential = result.credential
when (credential) {
is PublicKeyCredential -> {
val responseJson = credential.authenticationResponseJson
// Share responseJson i.e. a GetCredentialResponse on your server to
// validate and authenticate
}
is PasswordCredential -> {
val username = credential.id
val password = credential.password
// Use id and password to send to your server to validate
// and authenticate
}
is CustomCredential -> {
// If you are also using any external sign-in libraries, parse them
// here with the utility functions provided.
if (credential.type == ExampleCustomCredential.TYPE) {
try {
val ExampleCustomCredential =
ExampleCustomCredential.createFrom(credential.data)
// Extract the required credentials and complete the authentication as per
// the federated sign in or any external sign in library flow
} catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) {
// Unlikely to happen. If it does, you likely need to update the dependency
// version of your external sign-in library.
Log.e(TAG, "Failed to parse an ExampleCustomCredential", e)
}
} else {
// Catch any unrecognized custom credential type here.
Log.e(TAG, "Unexpected type of credential")
}
}
else -> {
// Catch any unrecognized credential type here.
Log.e(TAG, "Unexpected type of credential")
}
}
}
The `PublicKeyCredential` returned from authentication is essentially a signed
assertion, structured as follows:
{
"id": "<credential ID>",
"type": "public-key",
"rawId": "<raw credential ID>",
"response": {
"clientDataJSON": "<signed client data containing challenge>",
"authenticatorData": "<authenticator metadata>",
"signature": "<digital signature to be verified>",
"userHandle": "<user ID from credential registration>"
}
}
On the server, you must verify the credential. To learn more, see [Verify and
sign in the user](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#verify_and_sign_in_the_user).
## Handle exceptions
You should handle all the subclass exceptions of [`GetCredentialException`](https://developer.android.com/reference/androidx/credentials/exceptions/GetCredentialException).
To learn how to handle each exception, see the [troubleshooting guide](https://developer.android.com/identity/sign-in/credential-manager-troubleshooting-guide).
coroutineScope {
try {
result = credentialManager.getCredential(
context = activityContext,
request = credentialRequest
)
} catch (e: GetCredentialException) {
Log.e("CredentialManager", "No credential available", e)
}
}
## Next steps
- [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys)
- [Understand passkey user experience flows](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys)
+35 -26
View File
@@ -8,7 +8,7 @@ description: Provides a complete workflow for implementing verified email retrie
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-09'
last-updated: '2026-09-04'
keywords:
- implementation
- Android
@@ -117,6 +117,8 @@ actions:
- Leave a TODO for developers to handle the app's server-side validation and parsing.
- Direct users to the home screen after API call success and show a snackbar with user details for reference purpose only.
## Overview
This guide describes how to implement verified email retrieval using the
[Digital Credentials Verifier API](references/android/identity/digital-credentials/credential-verifier.md) through an [OpenID for Verifiable
Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request.
@@ -130,8 +132,8 @@ Manager:
```kotlin
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha02")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
```
@@ -139,8 +141,8 @@ dependencies {
```groovy
dependencies {
implementation "androidx.credentials:credentials:1.7.0-alpha02"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02"
implementation "androidx.credentials:credentials:1.7.0-alpha03"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}
```
@@ -214,14 +216,8 @@ The request contains the following key information:
- `hd` (hosted domain): In the response, this is empty.
> [!NOTE]
> **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Currently, Google does not issue [verifiable credentials](references/android/identity/digital-credentials/index.md) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app.
- If the email is non-@gmail.com, Google verified this email when the Google
Account was created, but there is no freshness claim. Therefore, for
non-Google emails, you should consider an additional challenge, such as an
OTP, to verify the user. To understand the schema of the credential and the
specific rules for validating fields like `email_verified`, refer to the
[Google Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token).
> **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](references/android/identity/digital-credentials/index.md) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google
> Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token).
- **nonce**: A unique, cryptographically secure random value is generated for
each request. This is critical for security, as it prevents replay attacks.
@@ -233,6 +229,9 @@ The request contains the following key information:
Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create
a `GetCredentialRequest`, and call `getCredential()`.
> [!NOTE]
> **Note:** The `hd` and `email_verified` fields are hidden from users in Credential Manager's built-in UI. You cannot make a request with only these hidden fields- in case of such requests, the response is the [`GetCredentialCancellationException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/GetCredentialCancellationException).
## Present the request to the user
Present the user with the request, using the Credential Manager built-in UI.
@@ -264,6 +263,9 @@ Present the user with the request, using the Credential Manager built-in UI.
## Parse the response on the client
> [!WARNING]
> **Warning:** From August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to strictly match the W3C standards. It contains `data` and `protocol` keys, with the OpenID4VP `vp_token` nested in `data`, while legacy formats hold the `vp_token` directly. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
After receiving the response, you can perform a preliminary parse on the client.
This is useful for immediately updating the UI, for example, by showing the
user's name.
@@ -276,7 +278,8 @@ The following code extracts the raw [Selective Disclosure JWT
// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
val vpToken = responseData.getJSONObject("vp_token")
val dataObject = responseData.getJSONObject("data")
val vpToken = dataObject.getJSONObject("vp_token")
// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
@@ -307,12 +310,15 @@ additional metadata as well along with verified email:
/*
// Example of the raw JSON response from credential.credentialJson:
{
"vp_token": {
// This key matches the 'id' you set in your dcql_query
"user_info_query": [
// The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
"eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
]
"protocol": "openid4vp-v1-unsigned",
"data": {
"vp_token": {
// This key matches the 'id' you set in your dcql_query
"user_info_query": [
// The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
"eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
]
}
}
}
@@ -335,8 +341,8 @@ additional metadata as well along with verified email:
}
*/
> [!NOTE]
> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys).
> [!IMPORTANT]
> **Important:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys).
## Server-side validation for account creation
@@ -372,6 +378,9 @@ By combining these steps, your server can validate both the authenticity of the
data and the identity of the presenter, ensuring the credential wasn't
intercepted or spoofed before provisioning the new account.
> [!WARNING]
> **Warning:** As mentioned in [Parse the response on the client](#parse-response), from August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to match W3C standards. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
try {
// Send the raw credential response and the original nonce to your server.
// Your server must validate the response. createAccountWithVerifiedCredentials
@@ -400,10 +409,10 @@ standard passkey registration.
## WebView support
For the flow to work on a WebView, developers should implement a [JavaScript
bridge](references/android/identity/sign-in/credential-manager-webview.md) (JS Bridge) to facilitate the handoff. This bridge allows the
Webview to signal the native app, which can then perform the actual call
to the Credential Manager API.
For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a
[JavaScript bridge](references/android/identity/sign-in/credential-manager-webview.md) (JS Bridge) to facilitate the handoff. This bridge
allows the `WebView` object to signal the native app, which can then perform the
actual call to the Credential Manager API.
## See also
@@ -12,7 +12,7 @@ verifying digital credentials.
### Android version compatibility
The Verifier API is supported on Android 9 (API level 28) and higher.
The Verifier API is supported on Android 6 (API level 23) and higher.
### Implementation
@@ -1,6 +1,16 @@
> [!IMPORTANT]
> **Important:** We released an agent skill to help you implement email verification with the Digital Credentials API. Try out the skill from the [Android skills
> repository](https://github.com/android/skills).
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/identity/verified-email)
### Retrieve verified email
Use an [Android skill](https://developer.android.com/tools/agents/android-skills) to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add verified-email
<br />
## Overview
This guide describes how to implement verified email retrieval using the
[Digital Credentials Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) through an [OpenID for Verifiable
@@ -15,8 +25,8 @@ Manager:
```kotlin
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha02")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
```
@@ -24,8 +34,8 @@ dependencies {
```groovy
dependencies {
implementation "androidx.credentials:credentials:1.7.0-alpha02"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02"
implementation "androidx.credentials:credentials:1.7.0-alpha03"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}
```
@@ -99,14 +109,8 @@ The request contains the following key information:
- `hd` (hosted domain): In the response, this is empty.
> [!NOTE]
> **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Currently, Google does not issue [verifiable credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app.
- If the email is non-@gmail.com, Google verified this email when the Google
Account was created, but there is no freshness claim. Therefore, for
non-Google emails, you should consider an additional challenge, such as an
OTP, to verify the user. To understand the schema of the credential and the
specific rules for validating fields like `email_verified`, refer to the
[Google Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token).
> **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google
> Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token).
- **nonce**: A unique, cryptographically secure random value is generated for
each request. This is critical for security, as it prevents replay attacks.
@@ -118,6 +122,9 @@ The request contains the following key information:
Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create
a `GetCredentialRequest`, and call `getCredential()`.
> [!NOTE]
> **Note:** The `hd` and `email_verified` fields are hidden from users in Credential Manager's built-in UI. You cannot make a request with only these hidden fields- in case of such requests, the response is the [`GetCredentialCancellationException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/GetCredentialCancellationException).
## Present the request to the user
Present the user with the request, using the Credential Manager built-in UI.
@@ -149,6 +156,9 @@ Present the user with the request, using the Credential Manager built-in UI.
## Parse the response on the client
> [!WARNING]
> **Warning:** From August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to strictly match the W3C standards. It contains `data` and `protocol` keys, with the OpenID4VP `vp_token` nested in `data`, while legacy formats hold the `vp_token` directly. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
After receiving the response, you can perform a preliminary parse on the client.
This is useful for immediately updating the UI, for example, by showing the
user's name.
@@ -161,7 +171,8 @@ The following code extracts the raw [Selective Disclosure JWT
// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
val vpToken = responseData.getJSONObject("vp_token")
val dataObject = responseData.getJSONObject("data")
val vpToken = dataObject.getJSONObject("vp_token")
// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
@@ -192,12 +203,15 @@ additional metadata as well along with verified email:
/*
// Example of the raw JSON response from credential.credentialJson:
{
"vp_token": {
// This key matches the 'id' you set in your dcql_query
"user_info_query": [
// The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
"eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
]
"protocol": "openid4vp-v1-unsigned",
"data": {
"vp_token": {
// This key matches the 'id' you set in your dcql_query
"user_info_query": [
// The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
"eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
]
}
}
}
@@ -220,8 +234,8 @@ additional metadata as well along with verified email:
}
*/
> [!NOTE]
> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys).
> [!IMPORTANT]
> **Important:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys).
## Server-side validation for account creation
@@ -257,6 +271,9 @@ By combining these steps, your server can validate both the authenticity of the
data and the identity of the presenter, ensuring the credential wasn't
intercepted or spoofed before provisioning the new account.
> [!WARNING]
> **Warning:** As mentioned in [Parse the response on the client](https://developer.android.com/identity/digital-credentials/email-verification-implementation#parse-response), from August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to match W3C standards. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
try {
// Send the raw credential response and the original nonce to your server.
// Your server must validate the response. createAccountWithVerifiedCredentials
@@ -285,10 +302,10 @@ standard passkey registration.
## WebView support
For the flow to work on a WebView, developers should implement a [JavaScript
bridge](https://developer.android.com/identity/sign-in/credential-manager-webview) (JS Bridge) to facilitate the handoff. This bridge allows the
Webview to signal the native app, which can then perform the actual call
to the Credential Manager API.
For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a
[JavaScript bridge](https://developer.android.com/identity/sign-in/credential-manager-webview) (JS Bridge) to facilitate the handoff. This bridge
allows the `WebView` object to signal the native app, which can then perform the
actual call to the Credential Manager API.
## See also
@@ -17,6 +17,19 @@ This guide assumes you are familiar with the following concepts:
- [Digital Credentials](https://developer.android.com/identity/digital-credentials)
- [Verifiable Credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/identity/verified-email)
### Retrieve verified email
Use an [Android skill](https://developer.android.com/tools/agents/android-skills) to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add verified-email
<br />
## Android compatibility
This feature is supported on mobiles, tablets, and foldable devices running
@@ -45,14 +58,14 @@ The user experience for sharing a verified email is as follows:
message.
> [!NOTE]
> **Note:** If the verified email you receive does not match what you expect, inform the user about the mismatch and either ask them to try again with a different credential or provide an alternate verification method, such as through OTPs.
> **Note:** If the verified email you receive doesn't match what you expect, inform the user about the mismatch and either ask them to try again with a different credential or provide an alternate verification method, such as through OTPs.
4. (Optional, recommended) If the user is signing up for your service, you
should prompt the user to [create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for
them to sign in subsequently.
4. Optional: If the user is signing up for your service, prompt the user to
[create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for them to sign in
subsequently.
> [!NOTE]
> **Note:** The email verification process doesn't automatically trigger passkey creation. However, it is highly recommended to include the steps for passkey creation. Passkeys help users by making it easier and more secure for them to sign in, and remove the need for the conventional username and password interaction.
> **Note:** The email verification process doesn't automatically trigger passkey creation. However, it's highly recommended to include the steps for passkey creation. Passkeys help users by making it easier and more secure for them to sign in, and remove the need for the conventional username and password interaction.
### Include primary and fallback flows
@@ -92,7 +105,7 @@ details, by requiring a quick reauthentication step.
Email verification through Credential Manager only supports verification of
consumer Google Accounts. [Workspace accounts](https://knowledge.workspace.google.com/admin/getting-started/set-up-google-workspace-for-your-organization) and [supervised
accounts](https://support.google.com/families/answer/9499054) are not supported.
accounts](https://support.google.com/families/answer/9499054) aren't supported.
A consumer Google Account can be created with an email address from any
provider, not necessarily @gmail.com. However, Google verifies these accounts
@@ -126,7 +139,7 @@ providing an expired VC or a VC for an inactive Google Account.
### Email deliverability
While the process confirms the account's legitimacy, it does not guarantee inbox
While the process confirms the account's legitimacy, it doesn't guarantee inbox
delivery (for instance, the email might be diverted to spam). An OTP remains the
definitive method for confirming email deliverability.
@@ -136,9 +149,9 @@ While both Digital Credentials and [Sign in with Google](https://developer.andro
verified email, the user flows and use cases are different:
- **Use cases**: The Credential Manager email verification flow is not exclusively used in sign up or sign in use cases, but rather can be used in any use case involving the retrieval of verified email. This could include account recovery as well.
- **Registration**: The Credential Manager flow does not require Google registration, unlike Sign in with Google.
- **Registration**: The Credential Manager flow doesn't require Google registration, unlike Sign in with Google.
- **Platform support**: The Credential Manager flow is an Android-only solution.
- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It cannot be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It can't be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
## Next steps
@@ -1,96 +1,112 @@
Digital credentials are cryptographically verifiable documents that can be used
to authenticate, authorize, or otherwise provide information about a user. These
are typically things such as mobile driver's licenses, digital passports,
boarding passes, etc. They reside in virtual containers called digital wallets,
and are part of a W3C standard that specifies how to access and retrieve them.
This standard is implemented for web use cases with the [W3C Credential
Management API](https://www.w3.org/TR/credential-management-1/) and on Android, with Credential Manager's
[DigitalCredential API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential).
Digital credentials are cryptographically verifiable digital documents that your
users can use to provide information about themselves, and use to authenticate
or authorize themselves. Digital credentials are based on the [open W3C Digital
Credentials API industry standard](https://www.w3.org/TR/digital-credentials/). On Android, this is
implemented through [Credential Manager's](https://developer.android.com/identity/credential-manager) [Digital Credentials API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential).
![Image showing the flow of using a digital credential](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials.png) **Figure 1.** Using a digital credential in a sample app.
## Understand digital credentials
## Benefits of digital credentials
In the physical world, a person might keep their identity in their wallet, and
present it to a requesting party when asked:
![Image showing the flow of a normal wallet interaction](https://developer.android.com/static/identity/digital-credentials/images/normal_wallet_flowchart.svg) **Figure 1.** The process of fulfilling a physical-world credential request. The requestor asks the user for a specific credential. Then, the user selects and retrieves it from their physical wallet. Finally, the user provides the credential to the requestor.
Digital credentials offer several advantages over physical credentials and
non-standardized digital documents:
In this case, a user generally has a single wallet, and retrieves the requested
credentials from the wallet to present to the requestor. Wallets are mostly
interchangeable, and can generally store the same things.
- **Improved security and trust**: Digital credentials are encrypted, ensuring data integrity and authenticity. This asserts the surety that the credential was issued by a verifiable source and was not tampered with.
- **Enhanced privacy**: Many digital credential formats support selective disclosure, which lets users share only necessary information. For example, a user could share proof of driving qualification without revealing their birth date.
- **Consolidated storage**: Users can store various credentials from different issuers in digital holders all on their device, reducing the need to carry physical cards.
- **Interoperability**: By following open standards, digital credentials can work across different operating systems, devices, and platforms.
Digital credentials have the following differences from credentials in the
physical world:
## Use cases
1. Users are expected to have multiple wallets - also known as **holders** - which can contain various different credentials. Wallets determine which credentials may be stored inside of them.
2. The app or service asking for the credential to grant access or verify an identity is called the **verifier**.
3. The entity that creates the credential and asserts claims about the subject (such as, a university, a government, or a tech company) is referred to as the **issuer**.
4. The credential presentation happens in software, which means an API surface retrieves and presents the credentials - in Android, this is Credential Manager.
The Digital Credentials API can be used across a broad set of use cases such as
the following:
As such, Credential Manager takes on several roles that were formerly handled by
the user:
- **Accept government-issued IDs**: Apps can request and use official government document attributes, for flows including age verification, account recovery, Know-Your-Customer (KYC) process.
- **Verify phone numbers** : Apps can use the API for phone number verification, by exchanging digital credentials derived from the phone's SIM card directly with the user's carrier. This removes the need for one-time passwords (OTPs), improves security, and reduces transmission costs. For more information, see the [phone number verification guide](https://developer.android.com/identity/digital-credentials/phone-number-verification).
- **Verify email addresses** : The API lets your app retrieve verified emails directly from the user's device, removing the need for OTPs, for frictionless sign-up, sign-in, and account recovery. For more information, see the [email verification guide](https://developer.android.com/identity/digital-credentials/email-verification).
- **Custom credentials**: The extensibility of the API lets any app begin issuing its own custom digital credentials, which corresponding verifiers can request.
- **Confirm payment credentials and transactions**: The API lets you secure payment authorizations and Digital Payment Credentials (DPC) with native, cryptographically bound wallet confirmation.
1. On Android, wallets must register their credentials metadata with Credential Manager to be listed in the Credential Manager UI.
2. Credential Manager matches credentials across wallets based on the request and presents a list for the user to select.
3. When the user selects a credential in the list, Credential Manager then invokes the wallet, which will handle the remainder of the transaction (showing UIs, etc.) and return the credential to the application.
## How digital credentials work
This flow is shown here:
![Image showing the flow of a digital credential interaction](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_flowchart.svg) **Figure 2.** Interaction model for digital credential verification. Credential Manager uses pre-registered credentials metadata across user wallet(s) to match a verifier's request and prompts the user to select a credential. Credential Manager then directs the activity flow to the corresponding wallet which handles the remainder of the transaction and returns the credential to the verifier. Note: The verifier needs to handle and verify the credential response once it is returned.
The digital credential ecosystem involves three primary categories of apps:
## Verifiable credentials
- **Issuers** : Issuers are apps that securely create and [issue](https://developer.android.com/identity/digital-credentials/credential-issuer/issue-credentials) credentials.
- **Holders (wallets)** : Holders are apps on a user's device that store credentials. They should be able to share these credentials with requesting apps through a [presentation](https://developer.android.com/identity/digital-credentials/credential-holder/credential-holder) process.
- **Verifiers**: Verifiers are apps that verify and use digital credentials.
Verifiable credentials are a subset of digital credentials governed by strict
standards (like the W3C Verifiable Credentials Data Model). These credentials
contain claims that are cryptographically secured, making them tamper-evident
and proving exactly who issued them.
Credential Manager's Digital Credentials API orchestrates the interaction
between the issuers, holders, and verifier apps.
Not all digital credentials are verifiable credentials, but all verifiable
credentials are digital credentials.
When a verifier makes a request for a digital credential, it sends a request to
the Android system through the API. Credential Manager then displays eligible
digital credentials from various holders within a trusted system UI. Once the
user agrees to proceed, Credential Manager invokes the corresponding holder to
generate the response.
## What it means for a claim to be verified
> [!NOTE]
> **Note:** Issuers and holders don't have to be separate---an app can be both an issuer and holder. For example, if you use the Digital Credentials API for email verification, Google is the issuer and holder for the Gmail email address.
When a credential arrives through the Android Credential Manager API and a claim
within it is marked as "verified," it implies that the issuer is asserting that
they performed a check on that specific piece of data. However, it does not mean
the data is an absolute, universal truth. "Verified" is an assertion of process,
not an automatic guarantee of trust.
## User experience
The core philosophy of this ecosystem is that trust is always resolved at the
verifier. When the verifier (your app) receives the cryptographically secure
data, and sees that the issuer marked it as "verified," it must determine
whether it trusts the issuer to have verified the claim to its standards.
Similarly to how Credential Manager has built-in user interfaces for
authentication flows, such as with passkeys, passwords, and Sign in with Google,
there are also standardized interfaces for the various use cases of the Digital
Credentials API.
### User experience
There are UI variants tailored to specific use cases. When using the API, the
Android system automatically renders context-aware bottom sheets, such as for
the following scenarios:
As shown in the Android flow, the user only needs to interact once with the
Credential Manager UI to select the appropriate credential. Here is an example
of how the selector looks:
![Image showing the digital credentials UI in Credential Manager](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_ui.png) **Figure 3.** The digital credentials UI.
- **Email verification**: The interface displays an account picker featuring the verified email address and identity provider.
- **Phone number verification**: The interface includes a verification card with network carriers represented.
- **Multi-credential**: This view stacks multiple cards, and allows bundling related items into a single tap.
- **Digital payments credentials**: This checkout sheet displays payment card details, merchant info, and total transaction amount for immediate confirmation.
### Standards
![Image showing the UX variants of digital credentials](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_ux_variants.png) **Figure 2.** The UX variants for digital credentials for email verification, phone verification, and multi-credential scenarios.
Digital credentials requests are created using the [OpenID4VP
standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction). You can see example requests at the [Digital
Credentials Demo site](https://digital-credentials.dev/).
## Industry standards
Digital credential responses are typically returned in a standardized credential
format. These are maintained by different standards bodies, and include [W3C
Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/), [sd-jwt](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/), and
[mdoc](https://www.iso.org/standard/69084.html).
Digital credentials rely on industry standards to ensure cross-platform
compatibility. The commonly used open standards are:
Custom protocols are also feasible, though we recommend using one of the
standard protocols in your application.
- **Sharing** : Digital credentials are requested using the [OpenID4VP
standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction).
- **Issuance** : Digital credentials are issued using the [OpenID4VCI
standard](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-1_1-wg-draft.html).
- **Credential storage format** : Digital credentials are represented in standardized formats maintained by different standards bodies, primarily the following:
- IETF [selective disclosure of JSON web tokens (sd-jwt)](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) VC
- ISO [MDoc](https://www.iso.org/standard/69084.html)
### Try it out
These standards are used widely on multiple platforms and operating systems,
enabling seamless operation across web browsers, mobile devices, and other form
factors. This allows developers to exchange digital credentials with other apps
regardless of the platform they're on.
You can test out the digital credentials flow across platforms with an Android
wallet and web-based verifier:
## Try it out
1. Install the [CMWallet public sample](https://github.com/digitalcredentialsdev/CMWallet) on your Android phone. You can do this by pulling from the repository and installing directly from Android Studio or navigating to <https://github.com/digitalcredentialsdev/CMWallet/actions> and selecting the latest build to access the latest `app-debug.apk` file.
2. Open the CMWallet to register the metadata with Credential Manager. Make sure Bluetooth is enabled to allow your devices to connect to each other.
3. Navigate to <https://digital-credentials.dev/> and select `Request Credentials (OpenID4VP)`.
4. Accept the warning prompts and scan the QR Code with your phone, then select "Use passkey" and tap through the confirmation to show the available credentials.
5. Select the credential from CMWallet to return to the browser. The browser should show the returned credential.
You can test the cross-platform digital credentials flow by installing the
sample holder and verifier apps on an Android-powered device.
### See also
To test the flow, complete the following steps:
- To learn more about using Credential Manager to request digital credentials in your app, read the [Credential Manager - Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) page.
- To learn more about building a digital wallet using Credential Manager, read the [Credential Manager - Holder API](https://developer.android.com/identity/digital-credentials/credential-holder) page.
1. **Install the sample apps on the Android-powered device** : Use one of the following methods to install the apps on your first device:
- **Use the prebuilt APKs** :
- **Download the holder app** : Sign in to your GitHub account, navigate to the [holder app GitHub Actions page](https://github.com/digitalcredentialsdev/CMWallet/actions), select the latest successful build, and download the `app-debug.apk` file from the **Artifacts** section. This app is called **CMWallet**.
- **Download the verifier app** : For the sample verifier app, get the APK from the [Identity samples repository](https://github.com/android/identity-samples/actions). Then, install the APK on your Android-powered device. This app is called **Digital
Credentials Demo**.
- **Build from source**: Clone the repositories mentioned and install the app using Android Studio.
2. **Open the sample holder app**: Open the sample holder app. This registers the holder credentials with Credential Manager.
3. **Open the sample verifier app** : Open the sample verifier app. Then, select the option to request digital credentials from wallets. A bottomsheet displaying available credentials from **CMWallet** should appear.
4. **Select a credential**: Select a credential to send back to the verifier app. The verifier app should now display fields from the returned credential.
> [!NOTE]
> **Note:** You can also initiate the request from a web browser on another device by navigating to <https://digital-credentials.dev/> and selecting **Request Credentials (OpenID4VP 1.0)**.
## Resources
- For more information about issuing digital credentials, see the [issuer
guide](https://developer.android.com/identity/digital-credentials/credential-issuer/issue-credentials).
- For more information about holder apps, see the [holder guide](https://developer.android.com/identity/digital-credentials/credential-holder/credential-holder).
- For more information about verifying users based on digital credentials, see the [verifier guide](https://developer.android.com/identity/digital-credentials/credential-verifier).
- For more information about digital credentials on the web, see [digital
credentials on the web](https://developer.chrome.com/blog/digital-credentials-api-shipped).
@@ -44,8 +44,8 @@ Add the following dependencies to your app module's `build.gradle` file:
```kotlin
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha02")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
```
@@ -53,8 +53,8 @@ dependencies {
```groovy
dependencies {
implementation "androidx.credentials:credentials:1.7.0-alpha02"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02"
implementation "androidx.credentials:credentials:1.7.0-alpha03"
implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}
```
+6 -6
View File
@@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-05-20'
last-updated: '2026-08-27'
keywords:
- android
- ui
@@ -75,11 +75,11 @@ phone in portrait mode. On larger screen hand-held devices, like tablets and
unfolded foldables, the navigation area must be accessible from the edge of the
screen (navigation rail).
If you need to provide more screen real state for the content, hide the
If you need to provide more screen space for the content, hide the
navigation area. Examples of this include:
- Hiding the navigation bar when the user scrolls down and showing it again when the user scrolls up. The assumption is that when the user is scrolling down, they are consuming content but when scrolling up they are trying to navigate away from that content.
- Hiding the navigation area when its content is distracting. For example, in camera previews or when the content is best displayed in full screen (such as a single photo screen).
- Hiding the navigation area when its content is distracting. For example, in camera previews or when displaying a full-screen photo.
When the detail screen is displayed full-screen on mobile, full-screen mode must
be deactivated on larger screens.
@@ -101,7 +101,7 @@ parameter.
Steps to migrate:
- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. It could be named something like `isNavBarVisible` or `shouldShowNavBar`.
- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. Use `isNavBarVisible` or `shouldShowNavBar` as the variable name.
- Create an instance of `NavigationSuiteScaffoldState` using `rememberNavigationSuiteScaffoldState()` and pass it to `NavigationSuiteScaffold`.
- When the navigation area visibility changes, use a `LaunchedEffect` to call `show` or `hide` on the `NavigationSuiteScaffoldState`.
@@ -207,8 +207,8 @@ Look for the following vertical list composables: `LazyColumn`,
Steps to migrate:
- Choose a suitable minimum width in dp for the column. It should be large enough so that item is clearly visible to the user.
- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction below
- Choose a suitable minimum width in dp for the column. The item must be clearly visible to the user at this width.
- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction later
- For `LazyVerticalGrid`: change the `columns` parameter to use `GridCells.Adaptive(<width>.dp)`
- For `LazyVerticalStaggeredGrid`: change the `columns` parameter to use `StaggeredGridCells.Adaptive(<width>.dp)`
@@ -95,7 +95,7 @@ There is space, so Child 1 is placed into the line.
There is `80dp` available space in the line. The gap is `8dp`. Child 2 is
`40dp`. The required space is `48dp`. There is space, so the gap and Child 2
are placed into the line.
![First item placed in the FlexBox container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-2.png) **Figure 2.** Second item placed in the `FlexBox` container after the first item.
![Second item placed in the FlexBox container after the first item.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-2.png) **Figure 2.** Second item placed in the `FlexBox` container after the first item.
There is `32dp` available space in the line. The gap is `8dp`. Child 3 is
`50dp`. The required space is `58dp`. There is not enough space in the current
@@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts.
`lib.versions.toml`.
[versions]
compose = "1.12.0-alpha03"
compose = "1.13.0-alpha02"
[libraries]
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
@@ -131,12 +131,94 @@ Grid(
modifier = Modifier.height(480.dp)
) {
PastelRedCard("Fixed(100.dp)")
PastelGreenCard("Percentage(0.2f)")
PastelBlueCard("Flex(1.fr)")
PastelYellowCard("Auto")
}
```
<br />
![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-sizes.png) **Figure 3** . Row heights defined using the four primary track sizing options in `Grid`.
### Set the minimum size for flexible grid tracks
When a grid container has no remaining space,
a standard flexible track can shrink to `0.dp`.
To prevent this and ensure content isn't crushed,
use [`GridTrackSize.MinMax`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinMax(androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.Fr))
to enforce an explicit minimum size while keeping the track flexible.
The following example allocates at least `100.dp` to the first row:
```kotlin
Grid(
config = {
column(1f)
// The first row has a minimum height of 100.dp and can expand to
// the half of the remaining space.
row(GridTrackSize.MinMax(100.dp, 1.fr))
// The second row takes the half of the remaining space.
row(1.fr)
// The third row has a fixed height of 200.dp.
row(200.dp)
},
modifier = Modifier.size(360.dp) // Total grid height is 360.dp
) {
PastelRedCard("MinMax(100.dp, 1.fr)")
PastelGreenCard("Flex(1.fr)")
PastelBlueCard("Fixed(200.dp)")
}
```
<br />
![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-size-minmax.png) **Figure 4** . The first row has at least `100.dp` height.
### Set the minimum grid track size to place lazy lists
Standard flexible tracks automatically query the intrinsic sizes of
their children to establish a base size.
However, Jetpack Compose prohibits querying the intrinsic sizes of
[`SubcomposeLayout`](https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/SubcomposeLayout.composable#SubcomposeLayout(androidx.compose.ui.Modifier,kotlin.Function2)), which backs components,
such as [`LazyColumn`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable) and [`LazyRow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyRow.composable).
Placing a lazy list inside a standard flexible track causes
an [`IllegalStateException`](https://developer.android.com/reference/java/lang/IllegalStateException) crash.
To safely place lazy lists inside a flexible grid track,
use `MinMax` with an explicit minimum size (such as `0.dp`)
to bypass the intrinsic measurement pass.
```kotlin
Grid(
config = {
column(1f)
// The first row's height is determined by the height of the Text composable.
row(GridTrackSize.Auto)
// The second row occupies the remaining space, allowing the LazyColumn to scroll.
row(GridTrackSize.MinMax(0.dp, 1.fr))
gap(8.dp)
},
modifier = Modifier.size(width = 170.dp, height = 240.dp)
) {
Text("LazyColumn in a Grid")
// The LazyColumn is placed in the second row, filling the remaining space.
LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(100) { number ->
PastelGreenCard("Card $number")
}
}
}
```
<br />
![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/lazy-column-in-grid.png) **Figure 5** . `LazyColumn` in a grid cell.
### Determine grid track size intrinsically
You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid`
@@ -163,14 +245,14 @@ Grid(
},
modifier = Modifier.width(480.dp)
) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." )
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." )
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.")
}
```
<br />
![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 4**. Intrinsic sizes specified in the columns.
![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 5**. Intrinsic sizes specified in the columns.
## Set gaps between rows and columns
@@ -206,7 +288,7 @@ Grid(
<br />
![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 5**. Gaps between rows and columns.
![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 6**. Gaps between rows and columns.
You can also use the convenience function [`gap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#gap(androidx.compose.ui.unit.Dp))
to define gaps of the same column and row size,
@@ -235,4 +317,99 @@ Grid(
}
```
<br />
<br />
## Define grid areas with named areas
Named areas allow you to attach names to groups of grid cells,
which are called [grid areas](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area).
You can use these names instead of [coordinate indexes](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties#position)
when placing UI elements in the grid.
Using named areas has two main benefits to code readability:
- When defining the grid layout, the purpose and placement of the expected content is clear.
- When adding the content, the purpose of that content is clear.
To organize complex layouts clearly, you can decouple your physical grid
structure from child placement by defining semantic grid areas.
Inside the `config` lambda, use the [`area`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#area(kotlin.Any,kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int)) function in
`GridConfigurationScope` to register named areas in the grid.
You can then assign child composables to these areas
using the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Any,androidx.compose.ui.Alignment)) modifier
with the corresponding area identifier. The `area` function maps
a semantic identifier (such as an `enum` class value or a string key) to
a set of physical grid coordinates. Grid lines and indexes are **1-based**
(that is, the first row is `1`, and the first column is `1`).
For example, you define a grid that has four area IDs:
```kotlin
/**
* An enum representing the IDs for named areas within the grid.
*/
enum class GridAreaNames {
Area1,
Area2,
Area3,
Area4
}
```
<br />
Provide the name for the area using the `areaId` parameter along with the area's
cell coordinates and spans.
The `gridItem` modifier uses the `areaId` as a key
to assign each child item to its designated grid area,
as shown in the following example:
```kotlin
Grid(
config = {
// Define a single column that takes all available width.
repeat(2) { column(0.5f) }
// Define four rows, each taking 25% of the total height.
repeat(4) { row(0.25f) }
// Define named grid areas by associating an areaId with specific row and column indices.
// Row and column indices are 1-based.
area(areaId = GridAreaNames.Area1, row = 1, column = 1, columnSpan = 2)
area(areaId = GridAreaNames.Area2, row = 2, column = 1, rowSpan = 3)
area(areaId = GridAreaNames.Area3, rows = 2..3, columns = 2..2)
area(areaId = GridAreaNames.Area4, row = 4, column = 2)
gap(4.dp)
},
modifier = Modifier.size(360.dp)
) {
PastelRedCard(
"Area 1",
// Use Modifier.gridItem(areaId) to place this composable into the
// grid area defined with the matching ID in the config block.
modifier = Modifier.gridItem(areaId = GridAreaNames.Area1)
)
PastelGreenCard(
"Area 2",
modifier = Modifier.gridItem(areaId = GridAreaNames.Area2)
)
PastelBlueCard(
"Area 3",
modifier = Modifier.gridItem(areaId = GridAreaNames.Area3)
)
PastelYellowCard(
"Area 4",
modifier = Modifier.gridItem(areaId = GridAreaNames.Area4)
)
}
```
<br />
By using named areas, you can reorganize or adjust the physical layout grid (for
example, changing rows, columns, or track sizes) in the `config` lambda
without needing to modify the order or parameters of the child composables.
@@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co
`lib.versions.toml`.
[versions]
compose = "1.12.0-alpha03"
compose = "1.13.0-alpha02"
[libraries]
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
@@ -25,7 +25,7 @@ of which layouts are suitable for each API:
| Component | Purpose |
|---|---|
| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that requires lazy loading. |
| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that require lazy loading. |
| `Row`, `Column`, `FlexBox` | One-dimensional layout |
| `Grid` | Two-dimensional layout |
@@ -6,6 +6,8 @@ and alignment of items within that structure.
Place an item into a specific track or cell
with the `row` and `column` parameters.
Coordinate indexes work best for straightforward grid layouts.
For larger or more complex layouts, define [named areas](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#named-area) instead.
The `row` and `column` parameters specify the row and column track indexes
that the item is placed in.
@@ -137,7 +139,7 @@ A UI element in `Grid`
that has no position specification undergoes auto-placement.
This example shows how you can mix auto-placed elements
and the UI elements with specified grid cells.
Card **#2** and card **#4** are specified grid cells,
Card **#2** and Card **#4** are placed in specified grid cells,
and the other items are auto-placed.
@@ -165,4 +167,4 @@ Grid(
<br />
![Card #3 is placed next to card <b>#1</b>, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to card **#1**, as it is an auto-placement.
![Card #3 is placed next to Card <b>#1</b>, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to Card **#1**, as it is an auto-placement.
@@ -16,6 +16,10 @@ Because the information is updated dynamically,
you need to monitor it and trigger recomposition when any update happens.
The [`mediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/mediaQuery.composable#mediaQuery(kotlin.Function1)) function abstracts the details of the information retrieval
and lets you focus on defining the condition to trigger the layout updates.
> [!NOTE]
> **Note:** "media" refers to the presentation medium that the UI is rendered on. The term "media" originates from [CSS media queries](https://www.w3.org/TR/mediaqueries-3/) which inspired the `mediaQuery` API.
The following example switches the layout to `TabletopLayout`
when the foldable posture is tabletop:
@@ -68,7 +68,7 @@ Inspector](https://developer.android.com/static/develop/ui/compose/images/li-sho
### Compose semantics
In Compose, [Semantics](https://developer.android.com/develop/ui/compose/semantics) describe your UI in an
In Compose, [Semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics) describe your UI in an
alternative manner that is understandable for
[Accessibility](https://developer.android.com/develop/ui/compose/accessibility) services and for the
[Testing](https://developer.android.com/develop/ui/compose/testing) framework. You can use the Layout Inspector
@@ -9,7 +9,7 @@ description: Provides a structured workflow for migrating an Android XML View to
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-08'
last-updated: '2026-08-14'
keywords:
- Jetpack Compose
- migration
@@ -1,7 +1,5 @@
## Set up the Compose Compiler Gradle plugin
For Gradle, use the Compose Compiler Gradle plugin to set
up and configure Compose.
For Gradle, use the Compose Compiler Gradle plugin to set up and configure
Compose.
> [!NOTE]
> **Note:** The Compose Compiler Gradle Plugin is only available from Kotlin 2.0+. For migration instructions, see ["Jetpack Compose compiler moving to the Kotlin
@@ -14,28 +12,29 @@ Set up the Compose Compiler Gradle plugin:
1. In the `libs.versions.toml` file, remove any reference to the Compose Compiler.
2. In the `versions` and `plugins` sections, add the new dependency:
[versions]
kotlin = "2.3.21"
[versions]
kotlin = "2.3.21"
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
// Add this line
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
// Add this line
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
1. In the project's root `build.gradle.kts` file, add the following to the `plugins` section.
3. In the project's root `build.gradle.kts` file, add the following to the
`plugins` section.
plugins {
plugins {
// Existing plugins
alias(libs.plugins.compose.compiler) apply false
}
}
1. In each module that uses Compose, apply the plugin:
4. In each module that uses Compose, apply the plugin:
plugins {
plugins {
// Existing plugins
alias(libs.plugins.compose.compiler)
}
}
The project should now build and compile if it was using the default set up. If
it had configured custom options on the Compose compiler, follow the next
@@ -75,10 +74,10 @@ For the full list of available options, see the [documentation](https://www.jetb
## Set up Compose dependencies
Always use the latest Compose BOM version: `2026.05.00`.
Always use the latest Compose BOM version: `2026.08.00`.
Set the `compose` flag to `true` inside the Android [`BuildFeatures`](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures)
to enable [Compose functionality](https://developer.android.com/develop/ui/compose/tooling) in Android Studio.
Set the `compose` flag to `true` inside the Android [`BuildFeatures`](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures) to
enable [Compose functionality](https://developer.android.com/develop/ui/compose/tooling) in Android Studio.
Add the following definition to your app's `build.gradle` file:
@@ -104,7 +103,7 @@ Add the Compose BOM and the subset of Compose library dependencies:
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2026.05.00')
def composeBom = platform('androidx.compose:compose-bom:2026.08.00')
implementation composeBom
androidTestImplementation composeBom
@@ -143,7 +142,7 @@ Add the Compose BOM and the subset of Compose library dependencies:
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.05.00")
val composeBom = platform("androidx.compose:compose-bom:2026.08.00")
implementation(composeBom)
androidTestImplementation(composeBom)
@@ -180,4 +179,19 @@ Add the Compose BOM and the subset of Compose library dependencies:
> [!NOTE]
> **Note:** Jetpack Compose is shipped using a Bill of Materials (BOM), to keep the versions of all library groups in sync. Read more about it in the [Bill of
> Materials page](https://developer.android.com/develop/ui/compose/bom/bom).
> Materials page](https://developer.android.com/develop/ui/compose/bom/bom).
## `compileSdk` and Android Gradle Plugin compatibility
Compose library releases continually adopt the latest `compileSdk` versions to
provide access to the latest Android features. Newer `compileSdk` versions
require newer versions of Android Gradle Plugin, so adopting new Compose
releases also requires projects to adopt new versions of the Android Gradle
Plugin. We recommend keeping your project's `compileSdk` up to date with the
latest released versions. `compileSdk` is unrelated from `targetSdk`.
For example, starting with Compose 1.12.0, projects are required to use
`compileSdk 37` and Android Gradle Plugin (AGP) 9.
To check which version of AGP is supported for different API levels, see the
[Android Gradle plugin API level support](https://developer.android.com/build/releases/about-agp#api-level-support) documentation.
+9 -3
View File
@@ -8,7 +8,7 @@ description: Use this skill to integrate the Jetpack Compose Styles API into an
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-06'
last-updated: '2026-09-03'
keywords:
- Jetpack Compose
- Styles
@@ -130,9 +130,15 @@ Refer to the official documentation to complete specific development tasks:
For each custom component (for example, `CustomButton`), complete the following
sequence:
1. If you are able to run an Android emulator, locate an existing screenshot test for the component. If none exists, create one using the existing project testing framework. If no framework exists, use UI Automator or Espresso to create a screenshot test with minimum required setup. Run the test and take a baseline screenshot of the Component. ELSE proceed to the next step without a screenshot test.
1. **Establish a visual baseline (If an emulator is available):**
- **If you CANNOT run an Android emulator:** Skip this step entirely and proceed to Step 2.
- **If you CAN run an Android emulator:** Perform the following to capture a baseline screenshot:
- **Option A:** Locate and run an existing screenshot test for the component.
- **Option B (If no test exists):** Create a test using the project's existing testing framework, then run it.
- **Option C (If no framework exists):** Create a minimal screenshot test using UI Automator or Espresso, then run it.
2. **Remove individual styling parameters** : Remove styling parameters such as `backgroundColor`, `shape`, `textStyle`, and `contentPadding` from the signature - anything that `StyleScope` supports.
3. **Add the style parameter** : Add `style: Style = Style` to the function signature.
3. **Add the style parameter** : Add `style: Style = Style` to the function signature. Always ensure the default value is exactly `Style` (e.g., `style:
Style = Style`) and not a specific style default like `ChipStyleDefault` or any other value.
4. **Declare state tracking** : If the component is interactable, create a `MutableStyleState` using the interaction source. Update state fields (such as `isEnabled`) inside the Composable to track the state correctly.
5. **Apply styleable modifier** : Replace specific layout modifiers on the root element with `Modifier.styleable()`.
6. **Move defaults to ComponentStyles** : Move hardcoded values from the component definition to a dedicated `Style` instance in `ComponentStyles.kt`.
@@ -42,8 +42,7 @@ BaseButton(
onClick = { },
style = { }
) {
BaseText("Click me")
}
BaseText("Click mnippets.kt
```
<br />
@@ -57,8 +56,7 @@ BaseButton(
onClick = { },
style = { background(Color.Blue) }
) {
BaseText("Click me")
}
BaseText("Click mnippets.kt
```
<br />
@@ -77,8 +75,7 @@ own custom components.
Row(
modifier = Modifier.styleable { }
) {
BaseText("Content")
}
BaseText("Contennippets.kt
```
<br />
@@ -93,8 +90,7 @@ Row(
background(Color.Blue)
}
) {
BaseText("Content")
}
BaseText("Contennippets.kt
```
<br />
@@ -145,8 +141,8 @@ val styleState = remember { MutableStyleState(null) }
Column(
Modifier.styleable(styleState, style)
) {
BaseText("Column content")
}
BaseText("Col)
}StylesSnippets.kt
```
<br />
@@ -172,10 +168,8 @@ Column(
}
val rowStyleState = remember { MutableStyleState(null) }
Row(
Modifier.styleable(rowStyleState, style)
) {
BaseText("Row")
}
Modifier.styleable(rowStyleState, stText("Row")
}StylesSnippets.kt
```
<br />
@@ -194,8 +188,7 @@ BaseButton(
contentPaddingStart(16.dp)
}
) {
BaseText("Button")
}
BaseText("Buttonippets.kt
```
<br />
@@ -225,8 +218,7 @@ BaseButton(
//
}
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -250,8 +242,7 @@ BaseButton(
},
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -283,8 +274,7 @@ BaseButton(
},
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -297,7 +287,7 @@ Styles](https://developer.android.com/static/develop/ui/compose/styles/images/bu
## Style inheritance
> [!NOTE]
> **Note:** Whilst the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`.
> **Note:** While the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`.
Certain style properties, such as `contentColor` and text style-related
properties, propagate to the child composables. A style set on a child
@@ -334,8 +324,8 @@ Column(
) {
BaseText("Children inherit", style = { width(60.dp) })
BaseText("certain properties")
BaseText("from their parents")
}
BaseText(&quents")
}StylesSnippets.kt
```
<br />
@@ -365,8 +355,8 @@ Column(
contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
})
BaseText("override properties")
BaseText("set by their parents")
}
BaseText("ents")
}StylesSnippets.kt
```
<br />
@@ -356,7 +356,7 @@ fun MediaPlayer(
<br />
Within the `style` lambda, you can apply state based styling for custom states,
Within the `style` lambda, you can apply state-based styling for custom states,
using the previously defined extension functions.
@@ -4,7 +4,7 @@ You can do everything Styles can do with modifiers, but not all functionality in
modifiers is available in Styles.
**Important:**
- **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define theme-wide set of properties for a component.
- **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define a theme-wide set of properties for a component.
- **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties.
The following is a comparison between Styles versus modifiers:
@@ -20,7 +20,7 @@ The following is a comparison between Styles versus modifiers:
## Limitations of modifiers
Modifiers have many benefits in the current Compose landscape. However, Styles
addresses some limitations of modifiers, which the following list describes:
address some limitations of modifiers, which the following list describes:
- Modifiers are typically created in the Composition phase. Updates can force a full rerun of Composition, Layout, and Draw, even for small visual changes like color, unless you create lambda-based modifiers.
- Conditional modifiers require disruptive if-else logic within fluent chains. Animating them requires manual state boilerplate and lacks a high-performance "auto-animate" mechanism.
@@ -9,6 +9,19 @@ depends on where your app sits in relation to its adoption of Material Design:
2. Using Material Design
- **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible.
## Android skills
[View on GitHub](https://github.com/android/skills/blob/main/jetpack-compose/theming/styles)
### Use the Jetpack Compose Styles API
Use the `styles` [Android skill](https://developer.android.com/tools/agents/android-skills) to create and customize components with the Styles API. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add styles
<br />
## The Style layer
In the traditional Compose model, customization often relies heavily on
@@ -75,9 +88,8 @@ val interactiveShadowAtomic = Style {
#### Composition using "then"
One of the powerful features of the new Styles API is the `then` operator, which
lets you merge multiple `Style` objects. This lets you build a component using
atomic utility classes.
The `then` operator in the Styles API lets you merge multiple `Style` objects.
This lets you build a component using atomic utility classes.
**Traditional (non-atomic)**:
@@ -250,8 +262,8 @@ access your base styles from anywhere in your project.
<br />
Beyond global theme adoption, there are alternative strategies for incorporating
`Styles` into your apps. You can leverage `Styles` inline for specific call
sites or use static definitions when full theming capabilities are unnecessary.
`Styles` into your apps. You can use `Styles` inline for specific call sites or
use static definitions when full theming capabilities aren't necessary.
`Styles` shouldn't be swapped conditionally unless the whole style is
fundamentally different. You should prefer accessing dynamic tokens inside a
visual definition rather than switching between distinct style objects.
fundamentally different. Prefer accessing dynamic tokens inside a visual
definition rather than switching between distinct style objects.
+233
View File
@@ -0,0 +1,233 @@
---
name: media3-cast-integration
description: Implements Google Cast support in Android apps using Jetpack Media3.
Handles adding build dependencies, updating manifest, configuring OptionsProvider,
and managing CastPlayer or RemoteCastPlayer for playback in both Compose and View-based
UIs. Use when adding Cast functionality or migrating from legacy Cast SDK to Media3
Cast.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-08-14'
keywords:
- Android
- Media3
- Cast
- Integration
- Migration
- ExoPlayer
- MediaSession
- Jetpack Media3
---
## Prerequisites
- Jetpack Media3 version must be `>= 1.9.0`. Cast isn't available in lower versions.
## Glossary
- **`CastPlayer`** : Media3 `Player` that controls playback on both local and remote Cast devices.
- **`RemoteCastPlayer`** : Media3 `Player` that communicates with a Cast receiver, only used for remote playback.
- **Google Cast SDK**: Legacy casting SDK in maintenance mode, superseded by Jetpack Media3.
- **`OptionsProvider`** : Interface providing configuration options to initialize GMS `CastContext`.
## Common guidelines
- Legacy [Google Cast SDK](https://developers.google.com/cast) is in maintenance mode.
- For new Cast setups:
- You must use [Jetpack Media3 Cast](references/android/media/media3/cast/index.md).
- You mustn't use legacy Cast SDK unless explicitly requested.
## Step 1: Set up dependencies
To complete this step, you **MUST** ensure the following:
- In the app-level build file, declare the `media3-cast` dependency version
1.9.0 or higher.
implementation("androidx.media3:media3-cast:1.11.0")
- Ensure required Media3 dependencies are present:
- `androidx.media3:media3-exoplayer`
- `androidx.media3:media3-session`
- `androidx.media3:media3-ui-compose`
- If the application uses legacy Views, add `media3-ui`.
- Enforce the same versions across all Media3 dependencies.
- Use configurations in "Add build dependencies" section of [Getting started
with CastPlayer](references/android/media/media3/cast/create-castplayer.md) as the source of
truth.
- **For apps without an existing Cast integration:**
- Verify legacy Cast SDK (`libs.play.services.cast.framework`) is absent.
- **If Migrating from Legacy Cast SDK:**
- Add Media3 Cast dependencies first.
- Keep existing legacy dependencies untouched at this stage to prevent compilation errors.
## Step 2: Update the manifest
To complete this step, you **MUST** ensure the following:
- Inside the manifest's `<application>` tag, declare the Cast options provider.
- Use `DefaultCastOptionsProvider` by default. See the "OptionsProvider" section in [Getting started with
CastPlayer](references/android/media/media3/cast/create-castplayer.md).
- Declare a custom `OptionsProvider` only if explicitly requested. See [Customize CastOptions](references/android/media/media3/cast/customize-castoptions.md).
- Ensure `INTERNET` permission is present. Don't add any unnecessary permissions.
- **If Migrating from Legacy Cast SDK:**
- Don't delete existing custom options provider files or manifest entries.
## Step 3: Implement the player and service
### Architecture baseline
Before integrating Media3 Cast, an existing app follows one of two setups:
- **Local-only playback:** Uses Media3 `ExoPlayer` only to support local playback.
- **Legacy Cast setup:** Uses `ExoPlayer` for local playback, alongside a `Player` wrapper over the legacy `RemoteMediaClient` for remote playback. The UI interfaces with a `MediaSession` interacting with a `ForwardingPlayer`, which finally routes controls to either local or remote playback.
To complete this step, you **MUST** ensure the following:
- Inside the application's `MediaSessionService` (or `MediaLibraryService`) `onCreate()` method, initialize `ExoPlayer` and `CastPlayer`.
- Use `CastPlayer` by default unless `RemoteCastPlayer` is explicitly requested. See the "Build a CastPlayer" section in [Getting started with
CastPlayer](references/android/media/media3/cast/create-castplayer.md).
- For `CastPlayer`, pass the instance directly to `MediaSession.Builder`.
- Replace all legacy forwarding player wrappers.
- Don't delete legacy class files yet to prevent compilation errors during migration.
### Advanced: `RemoteCastPlayer`
- Use `RemoteCastPlayer` only if explicitly requested by user.
- Initialize `MediaSession` with `localPlayer` and set a `SessionAvailabilityListener` on `RemoteCastPlayer` to transfer playback state on Cast session availability changes:
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
private lateinit var localPlayer: ExoPlayer
private lateinit var remotePlayer: RemoteCastPlayer
override fun onCreate() {
super.onCreate()
localPlayer = ExoPlayer.Builder(this).build()
remotePlayer = RemoteCastPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, localPlayer).build()
remotePlayer.setSessionAvailabilityListener(
object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
transferPlaybackState(localPlayer, remotePlayer)
}
override fun onCastSessionUnavailable() {
transferPlaybackState(remotePlayer, localPlayer)
}
}
)
}
private fun transferPlaybackState(previousPlayer: Player, newPlayer: Player) {
if (previousPlayer.mediaItemCount > 0) {
val transferStateBuilder = PlayerTransferState.builderFromPlayer(previousPlayer)
if (previousPlayer.playbackState == Player.STATE_ENDED ||
previousPlayer.currentPosition == C.TIME_END_OF_SOURCE) {
transferStateBuilder.setCurrentMediaItemIndex(0)
transferStateBuilder.setCurrentPosition(0)
}
transferStateBuilder.build().setToPlayer(newPlayer)
}
previousPlayer.stop()
previousPlayer.clearMediaItems()
newPlayer.prepare()
mediaSession?.setPlayer(newPlayer)
}
}
## Step 4: Set up the UI
### Compose-based UI
To complete this step, you **MUST** ensure the following:
- See the "Add a MediaRouteButton Composable to the Player" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md) for Compose integration guidelines.
- Use the [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) from `androidx.media3.cast` package.
- Don't use `AndroidView` in the Compose UI hierarchy.
- Place `MediaRouteButton` in an area next to playback controls. Don't hide it behind system UI.
- Don't use `PlayerSurface` for custom player UI. Use the Material3 [`Player`
composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable).
- Force recomposition on playback location shifts to ensure UI sync. Use key
constraints on `DeviceInfo` changes:
@OptIn(UnstableApi::class)
@Composable
fun MainScreen() {
val player = rememberMediaController()
val deviceInfo = rememberDeviceInfo(player)
player?.let { activePlayer -> key(deviceInfo) { PlayerScreen(player = activePlayer) } }
}
@Composable
private fun rememberMediaController(): Player? {
// Logic to connect MediaController to MediaSession and release it
}
@Composable
private fun rememberDeviceInfo(player: Player?): DeviceInfo? {
var deviceInfo by remember(player) { mutableStateOf(player?.deviceInfo) }
DisposableEffect(player) {
val activePlayer = player ?: return@DisposableEffect onDispose {}
deviceInfo = activePlayer.deviceInfo
val listener = object : Player.Listener {
override fun onDeviceInfoChanged(info: DeviceInfo) {
deviceInfo = info
}
}
activePlayer.addListener(listener)
onDispose { activePlayer.removeListener(listener) }
}
return deviceInfo
}
### View-based UI
To complete this step, you **MUST** ensure the following:
- For View-based UI setups, see the "Add UI elements" section in [Getting
started with CastPlayer](references/android/media/media3/cast/create-castplayer.md).
- Casting Activities must extend `AppCompatActivity` or `FragmentActivity` and use a `Theme.AppCompat` descendant.
- Ensure the `AppCompat` theme has a visible `ActionBar` if adding `MediaRouteButton` to the options menu.
- Replace all instances and imports of `CastButtonFactory` with `MediaRouteButtonFactory`.
- Rebind `PlayerView.player` references upon `onDeviceInfoChanged` events to
prevent black screens or UI freezes:
private val playerListener: Player.Listener =
object : Player.Listener {
override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
// Resetting to null bypasses PlayerView.setPlayer()'s instance equality check
// (this.player == player), forcing it to re-bind the video surface to the controller.
playerView.player = null
playerView.player = controller
}
}
- **Migration to Compose:**
- Don't use `AndroidView` to wrap the legacy `PlayerView`.
- Implement Material3 [`Player` composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable) and [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) as per [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md).
- Remove legacy XML layout declarations, menu files, and View component references.
## Step 5: Clean up legacy Cast SDK code
> [!WARNING]
> **Warning:** Don't perform cleanup directly. Remove legacy files and dependencies only when explicitly requested by the user.
To complete this step, you **MUST** ensure the following:
- Remove legacy GMS Cast SDK (`libs.play.services.cast.framework`) and MediaRouter (`libs.androidx.mediarouter`) dependencies.
- Delete custom `OptionsProvider` classes and manifest entries if `DefaultCastOptionsProvider` is adopted.
- Remove legacy `MediaTransferReceiver` manifest declarations if present.
- Remove all references to legacy Cast SDK components such as legacy helper wrappers, forwarding players, and `RemoteMediaClient` interfaces.
- Delete legacy View XML layouts, menu files, and references to `PlayerView` if the migration to Compose is complete.
@@ -0,0 +1,390 @@
The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) is a Jetpack Media3 [Player](https://developer.android.com/reference/kotlin/androidx/media3/common/Player) implementation that supports
both local playback and casting to a remote Cast-enabled device. [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer)
simplifies adding cast functionality to your app and provides rich features to
seamlessly switch between local and remote playback. This guide shows you how to
integrate [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) into your media app.
To integrate Cast with other platforms, see the [Cast SDK](https://developers.google.com/cast/docs/developers).
## Get a Cast-enabled device
To test `CastPlayer`, you need a [Cast-enabled device](https://store.google.com/gb/category/connected_home?hl=en-GB). Choices include Android
TV, Chromecast, smart speakers, and smart displays. Verify that your device is
set up and connected to the same Wi-Fi network as your development mobile for
discovery.
## Add build dependencies
To start using `CastPlayer`, add the AndroidX Media3 and `CastPlayer`
dependencies to the `build.gradle` file of your app module.
### Kotlin
implementation("androidx.media3:media3-exoplayer:1.11.0")
implementation("androidx.media3:media3-ui:1.11.0")
implementation("androidx.media3:media3-session:1.11.0")
implementation("androidx.media3:media3-cast:1.11.0")
### Groovy
implementation "androidx.media3:media3-exoplayer:1.11.0"
implementation "androidx.media3:media3-ui:1.11.0"
implementation "androidx.media3:media3-session:1.11.0"
implementation "androidx.media3:media3-cast:1.11.0"
## Configure your CastPlayer
To configure the [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer), update your `AndroidManifest.xml` file with an
options provider.
### Options provider
The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) requires an options provider to configure its behavior. For a
basic setup, you can use the [`DefaultCastOptionsProvider`](https://developer.android.com/reference/androidx/media3/cast/DefaultCastOptionsProvider) by adding it to your
`AndroidManifest.xml` file. This uses default settings, including the default
receiver application.
<application>
...
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
...
</application>
To customize the configuration, implement your own custom `OptionsProvider`. See
the [CastOptions](https://developer.android.com/media/media3/cast/customize-castoptions) guide to learn how.
### Add a receiver for media transfers
Adding a `MediaTransferReceiver` to your manifest enables the System UI to
discover Cast-enabled devices on the network and reroute media without opening
the app activity. For example, a user can change the device playing your app's
media from the [media notification](https://developer.android.com/media/implement/surfaces/mobile).
<application>
...
<receiver android:name="androidx.mediarouter.media.MediaTransferReceiver" />
...
</application>
## Build a CastPlayer
For remote playback with Cast, your app should be able to manage playback even
when the user isn't interacting with an Activity from your app, such as through
the system media notification. For this reason, you should create your
`ExoPlayer` (for local playback) and `CastPlayer` (for remote playback)
instances in a service, such as [MediaSessionService](https://developer.android.com/media/media3/session/background-playback#service-lifecycle) or [MediaLibraryService](https://developer.android.com/guide/topics/media/session/medialibraryservice).
First, create your `ExoPlayer` instance and then when building your
[`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) instance, set `ExoPlayer` as the local player instance. You can
then switch media playback between your mobile and the Cast-enabled device from
the media notification or the lock screen notification. Media3 uses the **Output
Switcher** feature to handle player transfers when the output route changes from
local to remote or from remote to local.
![Screenshot showing the Output Switcher UI in notifications.](https://developer.android.com/static/media/media3/cast/images/output_switcher.jpeg) Figure 1: (a) Device chip on Media notification (b) Cast-enabled devices shown on tapping the device chip (c) Device chip on Lock screen notification
### Kotlin
```kotlin
override fun onCreate() {
super.onCreate()
val exoPlayer = ExoPlayer.Builder(context).build()
val castPlayer = CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context, castPlayer).build()
}
```
### Java
```java
@Override
public void onCreate() {
super.onCreate();
ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
CastPlayer castPlayer = new CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build();
mediaSession =
new MediaSession.Builder(/* context= */ context, /* player= */ castPlayer).build();
}
```
<br />
> [!IMPORTANT]
> **Important:** The preceding code snippet shows the `onCreate` method of a `MediaSessionService`. In an `Activity`, players should be created in `onStart` or `onResume`.
## Add UI elements
Add a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your app's UI. Tapping the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton)
opens a dialog displaying a list of available Cast-enabled devices on the
network. When the user selects a device, the media playback is transferred from
the mobile to the selected receiver device. This section shows you how to add
the button and listen for events to update your UI when playback switches
between local and remote devices.
### Set the MediaRouteButton
There are four ways to add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your activity's UI. The
best choice depends on your app's design and requirements.
- **Compose UI**: Add a button composable.
- **Views UI** :
- Add the button to the app bar menu.
- Add the button inside `PlayerView`.
- Add the button as a standard `View`.
![Screenshot showing the MediaRouteButton in the UI.](https://developer.android.com/static/media/media3/cast/images/cast.jpeg) Figure 2: (a) MediaRouteButton in menu bar, (b) as a View, (c) in PlayerView, and (d) Dialog of Cast-enabled devices.
> [!IMPORTANT]
> **Important:** To use the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in Views UI, the containing activity must be a subclass of `FragmentActivity`.
> [!TIP]
> **Tip:** These screenshots are from the [demo-session](https://github.com/androidx/media/tree/release/demos/session) apps in Media3. You can checkout the app to see an example of `CastPlayer` implementation.
#### Add a Composable `MediaRouteButton` to the Player
You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) Composable to your player's UI. For more
information, see the [Compose](https://developer.android.com/media/media3/ui/compose) guide.
```kotlin
@Composable
fun PlayerComposeView(player: Player, modifier: Modifier = Modifier) {
var controlsVisible by remember { mutableStateOf(false) }
Box(
modifier = modifier.clickable { controlsVisible = true },
contentAlignment = Alignment.Center,
) {
PlayerSurface(player = player, modifier = modifier)
AnimatedVisibility(visible = controlsVisible, enter = fadeIn(), exit = fadeOut()) {
Box(modifier = Modifier.fillMaxSize()) {
MediaRouteButton(modifier = Modifier.align(Alignment.TopEnd))
PrimaryControls(player = player, modifier = Modifier.align(Alignment.Center))
}
}
}
}
@Composable
fun PrimaryControls(player: Player, modifier: Modifier = Modifier) {
// ...
}
```
<br />
#### Add the `MediaRouteButton` to the PlayerView
You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) directly within the [PlayerView](https://developer.android.com/guide/topics/media/ui/playerview)'s UI
controls. After setting the [MediaController](https://developer.android.com/guide/topics/media/session/mediacontroller) as the player for your
`PlayerView`, provide a `MediaRouteButtonViewProvider` to display the Cast
button on the Player.
### Kotlin
```kotlin
override fun onStart() {
super.onStart()
playerView.player = mediaController
playerView.setMediaRouteButtonViewProvider(MediaRouteButtonViewProvider())
}
```
### Java
```java
@Override
public void onStart() {
super.onStart();
playerView.setPlayer(mediaController);
playerView.setMediaRouteButtonViewProvider(new MediaRouteButtonViewProvider());
}
```
<br />
#### Add the `MediaRouteButton` to the app bar menu
To set up a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in the app bar menu, create an XML menu and
override `onCreateOptionsMenu` in your `Activity`.
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:showAsAction="always"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"/>
</menu>
### Kotlin
```kotlin
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// ...
menuInflater.inflate(R.menu.sample_media_route_button_menu, menu)
val menuItemFuture: ListenableFuture<MenuItem> =
MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item)
Futures.addCallback(
menuItemFuture,
object : FutureCallback<MenuItem> {
override fun onSuccess(menuItem: MenuItem?) {
// Do something with the menu item.
}
override fun onFailure(t: Throwable) {
// Handle the failure.
}
},
executor,
)
// ...
return true
}
```
### Java
```java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// ...
getMenuInflater().inflate(R.menu.sample_media_route_button_menu, menu);
ListenableFuture<MenuItem> menuItemFuture =
MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item);
Futures.addCallback(
menuItemFuture,
new FutureCallback<MenuItem>() {
@Override
public void onSuccess(MenuItem menuItem) {
// Do something with the menu item.
}
@Override
public void onFailure(Throwable t) {
// Handle the failure.
}
},
executor);
// ...
return true;
}
```
<br />
#### Add the `MediaRouteButton` as a View
You can set up a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in your activity layout.xml.
<androidx.mediarouter.app.MediaRouteButton
android:id="@+id/media_route_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mediaRouteButtonTint="@android:color/white" />
To complete the setup for the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton), use the Media3 Cast
`MediaRouteButtonFactory` in your `Activity` code.
### Kotlin
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findViewById<MediaRouteButton>(R.id.media_route_button)?.also {
val unused = MediaRouteButtonFactory.setUpMediaRouteButton(context, it)
}
}
```
### Java
```java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
MediaRouteButton button = findViewById(R.id.media_route_button);
ListenableFuture<Void> setUpFuture =
MediaRouteButtonFactory.setUpMediaRouteButton(context, button);
}
```
<br />
### Activity Listener
Create a `Player.Listener` in your `Activity` to listen for changes to media
playback location. When the `playbackType` changes between `PLAYBACK_TYPE_LOCAL`
and `PLAYBACK_TYPE_REMOTE`, you can adjust your UI as needed. To prevent memory
leaks and to confine listener activity to only when your app is visible,
register the listener in `onStart` and unregister it in `onStop`:
### Kotlin
```kotlin
private val playerListener: Player.Listener =
object : Player.Listener {
override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) {
// Add UI changes for local playback.
} else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
// Add UI changes for remote playback.
}
}
}
override fun onStart() {
super.onStart()
mediaController.addListener(playerListener)
}
override fun onStop() {
super.onStop()
mediaController.removeListener(playerListener)
}
```
### Java
```java
private final Player.Listener playerListener =
new Player.Listener() {
@Override
public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) {
// Add UI changes for local playback.
} else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
// Add UI changes for remote playback.
}
}
};
@Override
protected void onStart() {
super.onStart();
mediaController.addListener(playerListener);
}
@Override
protected void onStop() {
super.onStop();
mediaController.removeListener(playerListener);
}
```
<br />
For more information about listening and responding to playback events, see the
[player events](https://developer.android.com/media/media3/exoplayer/listening-to-player-events) guide.
@@ -0,0 +1,93 @@
To configure your app's Cast session, provide an [`OptionsProvider`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/OptionsProvider). Use the
[`CastOptions`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions) object built by the provider to set the receiver application ID,
manage session lifecycles, and customize media playback behavior.
## Use the default options provider
For a basic setup that uses the default Cast receiver application, add the
`DefaultCastOptionsProvider` to your app's `AndroidManifest.xml` file:
<application>
...
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
...
</application>
## Create a custom options provider
For more advanced configurations, such as setting a custom receiver application
ID, you need to create your own `OptionsProvider`.
### 1. Declare the provider in your manifest
First, declare your custom provider in `AndroidManifest.xml`. Make sure to use
the fully qualified class name.
<application>
...
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="path.to.your.class.MyCustomCastOptionsProvider" />
...
</application>
### 2. Implement the OptionsProvider interface
Next, create a class that implements the `OptionsProvider` interface. In this
class, you must override `getCastOptions()` to return a `CastOptions` instance.
The custom `OptionsProvider` class is where you configure your Cast session, for
example, by setting your custom receiver application ID.
For more information, see [CastOptions.Builder](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder).
### Kotlin
```kotlin
class MyCustomCastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
return CastOptions.Builder()
.setReceiverApplicationId(APP_ID)
.setRemoteToLocalEnabled(true)
.build()
}
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
return null
}
companion object {
// Add your receiver app ID in <APP_ID>.
private const val APP_ID = "<APP_ID>"
}
}
```
### Java
```java
public static final class MyCustomCastOptionsProvider implements OptionsProvider {
// Add your receiver app ID in <APP_ID>.
public static final String APP_ID = "<APP_ID>";
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
.setReceiverApplicationId(APP_ID)
.setRemoteToLocalEnabled(true)
.build();
}
@Override
@Nullable
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null;
}
}
```
<br />
@@ -0,0 +1,21 @@
The Media3 [`cast`](https://developer.android.com/reference/androidx/media3/cast/package-summary) module extends your media playback experience to
Cast devices. It lets your app cast content to [compatible devices](https://developer.android.com/media/media3/cast/create-castplayer#get-device),
such as TVs and speakers. The [sender](https://developers.google.com/cast/glossary#sender) app acts as a remote controller for media
playback on the [receiver](https://developers.google.com/cast/glossary#receiver) device. This module integrates with the [Jetpack
Media3](https://developer.android.com/guide/topics/media/media3) library, so you can use the same `Player` interface and UI
components for both local and remote playback.
## Media3 CastPlayer
The `CastPlayer` is a media player API included in Media3 that supports both
local and remote media playback. It implements the `Player` interface to manage
playback, simplifying the integration of casting into your media experience.
[Go to Media3 CastPlayer](https://developer.android.com/media/media3/cast/create-castplayer)
## Other Cast integrations
The Cast SDK overview explains how to integrate Cast with other platforms, such
as building a receiver app or integrating with the web sender SDK.
[Explore Cast SDK](https://developers.google.com/cast/docs/developers)
+14 -5
View File
@@ -3,12 +3,12 @@ name: navigation-3
description: Learn how to install and migrate to Jetpack Navigation 3, and how to
implement features and patterns such as deep links, multiple backstacks, scenes
(dialogs, bottom sheets, list-detail, two-pane, supporting pane), conditional navigation
(such as logged-in navigation vs anonymous), returning results from flows, integration
(such as logged-in navigation versus anonymous), returning results from flows, integration
with Hilt, ViewModel, Kotlin, and view interoperability.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-02'
last-updated: '2026-09-01'
keywords:
- recipe
- Android
@@ -33,6 +33,8 @@ metadata:
- View interop.
---
*** ** * ** ***
## Migration guide
- *[Navigation 2 to Navigation 3 migration guide](references/android/guide/navigation/navigation-3/migration-guide.md)*: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements.
@@ -43,7 +45,7 @@ metadata:
## Developer documentation
- \*[Navigation 3](references/android/guide/navigation/navigation-3/index.md). Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations.
- *[Navigation 3](references/android/guide/navigation/navigation-3/index.md)*. Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations.
## Recipes
@@ -61,8 +63,10 @@ Code examples showcasing common patterns.
### Deep links
- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key.
- *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior.
- *[Static URI](references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md)*: Shows how to handle simple static URI deep links.
- *[URI with Arguments](references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md)*: Shows how to parse path and query arguments from a deep link.
- *[Synthetic BackStack](references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md)*: Shows how to handle deep links with a synthetic back stack.
- *[Custom Matcher](references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md)*: Shows how to implement custom deep link matching logic.
### Scenes
@@ -84,6 +88,7 @@ Code examples showcasing common patterns.
### Animations
- *[Animations](references/android/guide/navigation/navigation-3/recipes/animations.md)*: Shows how to override the default animations for all destinations and a single destination.
- *[Conditional Transitions](references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md)*: Shows how to implement conditional transition animations.
### Common back stack behavior
@@ -93,6 +98,10 @@ Code examples showcasing common patterns.
- *[Conditional navigation](references/android/guide/navigation/navigation-3/recipes/conditional.md)*: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding.
### Lifecycle
- *[Lifecycle Owner](references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md)*: Shows how to use and observe Lifecycle in Navigation 3.
### Architecture
- *[Modularized navigation code (Hilt)](references/android/guide/navigation/navigation-3/recipes/modular-hilt.md)*: Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI.
@@ -1,3 +1,15 @@
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3)
### Jetpack Navigation 3
Use an Android skill to help you build using Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add navigation-3
<br />
Navigation 3 is a new navigation library designed to work with Compose. With
Navigation 3, you have full control over your back stack, and navigating to and
from destinations is as simple as adding and removing items from a list. It
@@ -1,3 +1,15 @@
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3)
### Jetpack Navigation 3
Use an Android skill to help you build and migrate to Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add navigation-3
<br />
To migrate your app from [Navigation 2](https://developer.android.com/guide/navigation) to Navigation 3, follow these steps:
1. Add the Navigation 3 dependencies.
@@ -8,32 +20,6 @@ To migrate your app from [Navigation 2](https://developer.android.com/guide/navi
6. Replace `NavHost` with `NavDisplay`.
7. Remove Navigation 2 dependencies.
> [!IMPORTANT]
> **Important:** We released an agent skill to help you install and migrate to Jetpack Navigation 3. Try out the skill from the [Android skills repository](https://github.com/android/skills).
<br />
## AI Prompt
### Migrate from Navigation 2 to Navigation 3
This prompt will use this guide to migrate to navigation 3.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.
### Using AI prompts
AI prompts are intended to be used within Gemini in Android Studio.
Learn more about Gemini in Studio here: [https://developer.android.com/studio/gemini/overview](https://developer.android.com/studio/gemini/overview)
<button class="devsite-dialog-close">Close</button> <button class="button icon-button android-ai-prompt-help-button" data-modal-dialog-id="ai-prompt_help_modal__migrate-from-navigation-2-to-navigation-3"> </button> <button class="button google-feedback" data-p="5207477" data-b="llm-prompts" data-context="migrate-from-navigation-2-to-navigation-3"> Share your thoughts </button>
<br />
If you run into problems [file an issue here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D).
## Preparation
The following sections describe the prerequisites for migration and assumptions
@@ -111,10 +97,10 @@ project. The core dependencies are provided for you to copy.
**lib.versions.toml**
[versions]
nav3Core = "1.0.0"
nav3Core = "1.1.7"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"
lifecycleViewmodelNav3 = "2.11.0"
[libraries]
# Core Navigation 3 libraries
@@ -145,11 +131,21 @@ navigation state](https://developer.android.com/guide/navigation/navigation-3/sa
Before:
@Serializable data object RouteA
```kotlin
@Serializable data object RouteA
```
<br />
After:
@Serializable data object RouteA : NavKey
```kotlin
@Serializable data object RouteA : NavKey
```
<br />
> [!NOTE]
> **Note:** The `@Serializable` annotation is provided by the KotlinX Serialization plugin. You can add this by following [these project setup steps](https://developer.android.com/guide/navigation/navigation-3/get-started#project-setup).
@@ -161,98 +157,103 @@ After:
Copy the following code into a file named `NavigationState.kt`. Add your package
name to match your project structure.
// package com.example.project
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
```kotlin
// package com.example.project
/**
* Create a navigation state that persists config changes and process death.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
val topLevelRoute = rememberSerializable(
startRoute, topLevelRoutes,
serializer = MutableStateSerializer(NavKeySerializer())
) {
mutableStateOf(startRoute)
}
/**
* Create a navigation state that persists config changes and process death.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the current top level route
* @param backStacks - the back stacks for each top level route
*/
class NavigationState(
val startRoute: NavKey,
topLevelRoute: MutableState<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
val topLevelRoute = rememberSerializable(
startRoute, topLevelRoutes,
serializer = MutableStateSerializer(NavKeySerializer())
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
mutableStateOf(startRoute)
}
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the current top level route
* @param backStacks - the back stacks for each top level route
*/
class NavigationState(
val startRoute: NavKey,
topLevelRoute: MutableState<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
}
```
<br />
**AI Agent** : `rememberSerializable` is correct. Do not change it to
`rememberSaveable`.
@@ -267,37 +268,42 @@ back stacks for each top-level route.
Copy the following code into a file named `Navigator.kt`. Add your package name
to match your project structure.
// package com.example.project
import androidx.navigation3.runtime.NavKey
```kotlin
// package com.example.project
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState){
fun navigate(route: NavKey){
if (route in state.backStacks.keys){
// This is a top level route, just switch to it.
state.topLevelRoute = route
} else {
state.backStacks[state.topLevelRoute]?.add(route)
}
}
import androidx.navigation3.runtime.NavKey
fun goBack(){
val currentStack = state.backStacks[state.topLevelRoute] ?:
error("Stack for ${state.topLevelRoute} not found")
val currentRoute = currentStack.last()
// If we're at the base of the current route, go back to the start route stack.
if (currentRoute == state.topLevelRoute){
state.topLevelRoute = state.startRoute
} else {
currentStack.removeLastOrNull()
}
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState) {
fun navigate(route: NavKey) {
if (route in state.backStacks.keys) {
// This is a top level route, just switch to it.
state.topLevelRoute = route
} else {
state.backStacks[state.topLevelRoute]?.add(route)
}
}
fun goBack() {
val currentStack = state.backStacks[state.topLevelRoute]
?: error("Stack for ${state.topLevelRoute} not found")
val currentRoute = currentStack.last()
// If we're at the base of the current route, go back to the start route stack.
if (currentRoute == state.topLevelRoute) {
state.topLevelRoute = state.startRoute
} else {
currentStack.removeLastOrNull()
}
}
}
```
<br />
The `Navigator` class provides two navigation event methods:
- `navigate` to a specific route.
@@ -316,12 +322,19 @@ Both methods modify the `NavigationState`.
Create instances of `NavigationState` and `Navigator` with the same scope as
your `NavController`.
val navigationState = rememberNavigationState(
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
)
val navigator = remember { Navigator(navigationState) }
```kotlin
val navigationState = rememberNavigationState(
// ...
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
// ...
)
val navigator = remember { Navigator(navigationState) }
```
<br />
## Step 4: Replace `NavController`
@@ -345,20 +358,69 @@ selected in a navigation bar.
Before:
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
```kotlin
// ...
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
// ...
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
```
<br />
After:
val isSelected = key == navigationState.topLevelRoute
```kotlin
val isSelected = key == navigationState.topLevelRoute
```
<br />
Verify that you have removed all references to `NavController`, including
any imports.
### Step 4.1 Migrate lifecycle-aware logic
In Navigation 2, `NavBackStackEntry` implements `LifecycleOwner`, letting you
listen to lifecycle events or collect flows in a lifecycle-aware manner using
`navController.currentBackStackEntry`.
In Navigation 3, `NavDisplay` provides an entry-scoped `LifecycleOwner`
through `LocalLifecycleOwner.current` to each destination's composable
content. See [Destination lifecycle](https://developer.android.com/guide/navigation/navigation-3/basics#destination-lifecycle) for more information.
You should perform lifecycle-aware operations directly inside your destination's
composable content by referencing `LocalLifecycleOwner.current`.
For example, if you collect a flow in a lifecycle-aware manner using the back
stack entry:
Before:
```kotlin
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry!!
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
```
<br />
After:
```kotlin
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()
```
<br />
## Step 5: Move your destinations from `NavHost`'s `NavGraph` into an `entryProvider`
In Navigation 2, you [define your destinations](https://developer.android.com/guide/navigation/design#compose)
@@ -379,16 +441,21 @@ as follows:
> [!NOTE]
> **Note:** If your app needs to navigate from an entry in one stack to another, you need to define the parent-child relationships for the routes and update the navigation logic in `Navigator` to support this.
## Step 5.1: Create an `entryProvider`
### Step 5.1: Create an `entryProvider`
Create an `entryProvider` [using the DSL](https://developer.android.com/guide/navigation/navigation-3/basics#entry-provider-DSL) at the same scope as the
`NavigationState`.
val entryProvider = entryProvider {
}
```kotlin
val entryProvider = entryProvider<NavKey> {
## Step 5.2: Move destinations into the `entryProvider`
}
```
<br />
### Step 5.2: Move destinations into the `entryProvider`
For each destination defined inside `NavHost`, do the following based on the
destination type:
@@ -413,60 +480,77 @@ Obtain navigation arguments using the key provided to `entry`'s trailing lambda.
For example:
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD
```kotlin
// ...
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
// ...
@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD
@Composable
fun NavHostSnippet(navController: NavHostController) {
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{
composable<RouteA>{ entry ->
val id = entry.toRoute<RouteA>().id
ScreenA(title = "Screen has ID: $id")
}
featureBSection()
dialog<RouteD>{ ScreenD() }
}
}
fun NavGraphBuilder.featureBSection() {
navigation<BaseRouteB>(startDestination = RouteB) {
composable<RouteB> { ScreenB() }
}
fun NavGraphBuilder.featureBSection() {
navigation<BaseRouteB>(startDestination = RouteB) {
composable<RouteB> { ScreenB() }
}
}
```
<br />
becomes:
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey
```kotlin
// ...
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
// ...
val entryProvider = entryProvider {
entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
featureBSection()
entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}
@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey
fun EntryProviderScope<NavKey>.featureBSection() {
entry<RouteB> { ScreenB() }
}
val entryProvider = entryProvider {
entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
featureBSection()
entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}
fun EntryProviderScope<NavKey>.featureBSection() {
entry<RouteB> { ScreenB() }
}
```
<br />
## Step 6: Replace `NavHost` with `NavDisplay`
@@ -479,13 +563,16 @@ Replace `NavHost` with `NavDisplay`.
For example:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
```kotlin
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
```
<br />
## Step 7: Remove Navigation 2 dependencies
@@ -0,0 +1,182 @@
# Conditional Transitions Recipe
This recipe demonstrates how to create route-dependent screen transitions in Navigation 3 using `transitionSpec` and `popTransitionSpec`. The slide directions (right, left, bottom, top) are conditionally selected based on pairs of `initialState` and `targetState` navigation keys.
## How it works
- **Route Definitions** : Navigation destinations (`Step1`, `Step2`, `Step3`, `Step4`) are defined using a sealed class hierarchy implementing `NavKey` and marked with `@Serializable`.
- **Conditional Forward Transitions (`transitionSpec`)** : Matches pairs of `(initialKey to targetKey)` to determine the direction of the slide animation:
- `Step1` $\\rightarrow$ `Step2`: Swipes to the left
- `Step2` $\\rightarrow$ `Step3`: Swipes to the up
- `Step3` $\\rightarrow$ `Step4`: Swipes to the right
- `Step4` $\\rightarrow$ `Step1`: Slides to the bottom (restarts flow)
- **Conditional Pop Transitions (`popTransitionSpec`)**: Handles reverse slide directions when navigating back or when clearing the backstack.
- **Backstack Control** : Demonstrates clearing the navigation stack on the final step (`backStack.clear()` \& `backStack.add(Step1)`) while executing a seamless top-slide transition.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/conditionaltransitions)
```
package com.example.nav3recipes.conditionaltransitions
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.content.ContentGreen
import com.example.nav3recipes.content.ContentOrange
import com.example.nav3recipes.content.ContentPurple
import com.example.nav3recipes.content.ContentRed
import com.example.nav3recipes.sharedviewmodel.toContentKey
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.serialization.Serializable
@Serializable
sealed class Step(val firstButtonTitle: String, val secondButtonTitle: String) : NavKey
@Serializable
data object Step1 : Step("Home", "Swipe left")
@Serializable
data object Step2 : Step("Swipe right", "Swipe up")
@Serializable
data object Step3 : Step("Swipe down", "Swipe right")
@Serializable
data object Step4 : Step("Swipe left", "Swipe down")
@AndroidEntryPoint
class ConditionalTransitionsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setEdgeToEdgeConfig()
setContent {
Scaffold { paddingValues ->
val backStack = rememberNavBackStack(Step1)
NavDisplay(
backStack = backStack,
modifier = Modifier.padding(paddingValues),
onBack = backStack::removeLastOrNull,
entryProvider = entryProvider {
entry<Step1> {
ContentGreen(title = "Screen 1") {
StepContent(
step = it,
onNext = { backStack += Step2 },
onBack = ::finish, // closing the activity
)
}
}
entry<Step2> {
ContentRed(title = "Screen 2") {
StepContent(
step = it,
onNext = { backStack += Step3 },
onBack = backStack::removeLastOrNull,
)
}
}
entry<Step3> {
ContentOrange(title = "Screen 3") {
StepContent(
step = it,
onNext = { backStack += Step4 },
onBack = backStack::removeLastOrNull,
)
}
}
entry<Step4> {
ContentPurple(title = "Screen 4") {
StepContent(
step = it,
onNext = {
backStack.clear()
backStack.add(Step1)
},
onBack = backStack::removeLastOrNull,
)
}
}
},
transitionSpec = {
val initialKey = initialState.entries.lastOrNull()?.contentKey
val targetKey = targetState.entries.lastOrNull()?.contentKey
when (initialKey to targetKey) {
Step1.toContentKey() to Step2.toContentKey() -> swipeLeft()
Step2.toContentKey() to Step3.toContentKey() -> swipeUp()
Step3.toContentKey() to Step4.toContentKey() -> swipeRight()
Step4.toContentKey() to Step1.toContentKey() -> swipeDown()
else -> swipeRight()
}
},
popTransitionSpec = {
val initialKey = initialState.entries.lastOrNull()?.contentKey
val targetKey = targetState.entries.lastOrNull()?.contentKey
when (initialKey to targetKey) {
Step4.toContentKey() to Step1.toContentKey() -> swipeDown() // via backstack clearing
Step2.toContentKey() to Step1.toContentKey() -> swipeRight()
Step3.toContentKey() to Step2.toContentKey() -> swipeDown()
Step4.toContentKey() to Step3.toContentKey() -> swipeLeft()
else -> swipeLeft()
}
},
)
}
}
}
}
@Composable
fun StepContent(step: Step, onNext: () -> Unit, onBack: () -> Unit) {
Column(
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) {
ElevatedButton(onBack) { Text(text = step.firstButtonTitle) }
ElevatedButton(onNext) { Text(text = step.secondButtonTitle) }
}
}
}
private fun swipeLeft(): ContentTransform =
slideInHorizontally(initialOffsetX = { it }) togetherWith slideOutHorizontally(targetOffsetX = { -it })
private fun swipeUp(): ContentTransform =
slideInVertically(initialOffsetY = { it }) togetherWith slideOutVertically(targetOffsetY = { -it })
private fun swipeRight(): ContentTransform =
slideInHorizontally(initialOffsetX = { -it }) togetherWith slideOutHorizontally(targetOffsetX = { it })
private fun swipeDown(): ContentTransform =
slideInVertically(initialOffsetY = { -it }) togetherWith slideOutVertically(targetOffsetY = { it })
```
@@ -1,744 +0,0 @@
# Deep Link Basic Recipe
This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key.
## How it works
It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request.
## Demonstrated forms of deeplink
The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks:
1. `HomeKey` - deeplink with an exact url (no deeplink arguments)
2. `UsersKey` - deeplink with path arguments
3. `SearchKey` - deeplink with query arguments
See `MainActivity.deepLinkPatterns` for the actual url pattern of each.
## Recipe structure
This recipe consists of three main packages:
1. `basic.deeplink` - Contains the two activities
2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc
3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/basic)
```
package com.example.nav3recipes.deeplink.basic
import androidx.navigation3.runtime.NavKey
import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER
import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME
import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH
import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS
import kotlinx.serialization.Serializable
internal interface NavRecipeKey: NavKey {
val name: String
}
@Serializable
internal object HomeKey: NavRecipeKey {
override val name: String = STRING_LITERAL_HOME
}
@Serializable
internal data class UsersKey(
val filter: String,
): NavRecipeKey {
override val name: String = STRING_LITERAL_USERS
companion object {
const val FILTER_KEY = STRING_LITERAL_FILTER
const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded"
const val FILTER_OPTION_ALL = "all"
}
}
@Serializable
internal data class SearchKey(
val firstName: String? = null,
val ageMin: Int? = null,
val ageMax: Int? = null,
val location: String? = null,
): NavRecipeKey {
override val name: String = STRING_LITERAL_SEARCH
}
```
```
package com.example.nav3recipes.deeplink.basic
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.net.toUri
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.FriendsList
import com.example.nav3recipes.common.deeplink.LIST_USERS
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT
import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH
import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER
import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult
import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher
import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern
import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest
import com.example.nav3recipes.deeplink.basic.util.KeyDecoder
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
/**
* Parses a target deeplink into a NavKey. There are several crucial steps involved:
*
* STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable
* format (see [DeepLinkPattern])
* STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest])
* **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each
* other to facilitate comparison and finding a match
* STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match
* (see [DeepLinkMatchResult]). The match result's format should enable conversion from result
* to backstack key, regardless of what the conversion method may be.
* STEP 4. Associate the match results with the correct backstack key
*
* This recipes provides an example for each of the above steps by way of kotlinx.serialization.
*
* **This recipe is designed to focus on parsing an intent into a key, and therefore these additional
* deeplink considerations are not included in this scope**
* - Create synthetic backStack
* - Multi-modular setup
* - DI
* - Managing TaskStack
* - Up button ves Back Button
*
*/
class MainActivity : ComponentActivity() {
/** STEP 1. Parse supported deeplinks */
// internal so that landing activity can link to this in the kdocs
internal val deepLinkPatterns: List<DeepLinkPattern<out NavKey>> = listOf(
// "https://www.nav3recipes.com/home"
DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()),
// "https://www.nav3recipes.com/users/with/{filter}"
DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()),
// "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}"
DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())),
)
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
// retrieve the target Uri
val uri: Uri? = intent.data
// associate the target with the correct backstack key
val key: NavKey = uri?.let {
/** STEP 2. Parse requested deeplink */
val request = DeepLinkRequest(uri)
/** STEP 3. Compared requested with supported deeplink to find match*/
val match = deepLinkPatterns.firstNotNullOfOrNull { pattern ->
DeepLinkMatcher(request, pattern).match()
}
/** STEP 4. If match is found, associate match to the correct key*/
match?.let {
//leverage kotlinx.serialization's Decoder to decode
// match result into a backstack key
KeyDecoder(match.args)
.decodeSerializableValue(match.serializer)
}
} ?: HomeKey // fallback if intent.uri is null or match is not found
/**
* Then pass starting key to backstack
*/
setContent {
val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<HomeKey> { key ->
EntryScreen(key.name) {
TextContent("<matches exact url>")
}
}
entry<UsersKey> { key ->
EntryScreen("${key.name} : ${key.filter}") {
TextContent("<matches path argument>")
val list = when {
key.filter.isEmpty() -> LIST_USERS
key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS
else -> LIST_USERS.take(5)
}
FriendsList(list)
}
}
entry<SearchKey> { search ->
EntryScreen(search.name) {
TextContent("<matches query parameters, if any>")
val matchingUsers = LIST_USERS.filter { user ->
(search.firstName == null || user.firstName == search.firstName) &&
(search.location == null || user.location == search.location) &&
(search.ageMin == null || user.age >= search.ageMin) &&
(search.ageMax == null || user.age <= search.ageMax)
}
FriendsList(matchingUsers)
}
}
}
)
}
}
}
```
```
package com.example.nav3recipes.deeplink.basic
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.compose.dropUnlessResumed
import com.example.nav3recipes.common.deeplink.EMPTY
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN
import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE
import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY
import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM
import com.example.nav3recipes.common.deeplink.LOCATION_BC
import com.example.nav3recipes.common.deeplink.LOCATION_BR
import com.example.nav3recipes.common.deeplink.LOCATION_CA
import com.example.nav3recipes.common.deeplink.LOCATION_US
import com.example.nav3recipes.common.deeplink.MenuDropDown
import com.example.nav3recipes.common.deeplink.MenuTextInput
import com.example.nav3recipes.common.deeplink.PaddedButton
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE
import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE
import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH
import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
/**
* This activity allows the user to create a deep link and make a request with it.
*
* **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct
* and trigger the deeplink request, and the [MainActivity] to show how an app can handle
* that request.
*
* **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to
* demonstrate different types of supported deeplinks:
* 1. [HomeKey] - deeplink with an exact url (no deeplink arguments)
* 2. [UsersKey] - deeplink with path arguments
* 3. [SearchKey] - deeplink with query arguments
* See [MainActivity.deepLinkPatterns] for the actual url pattern of each.
*
* **RECIPE STRUCTURE** This recipe consists of three main packages:
* 1. basic.deeplink - Contains the two activities
* 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc
* 3. basic.deeplink.util - Contains the classes and helper methods to parse and match
* the deeplinks
*
* See [MainActivity] for how the requested deeplink is handled.
*/
class CreateDeepLinkActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
/**
* UI for deeplink sandbox
*/
EntryScreen("Sandbox - Build Your Deeplink") {
TextContent("Base url:\n${PATH_BASE}/")
var showFilterOptions by remember { mutableStateOf(false) }
val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) }
var showQueryOptions by remember { mutableStateOf(false) }
var selectedFilter by remember { mutableStateOf("") }
val selectedSearchQuery = remember { mutableStateMapOf<String, String>() }
// manage path options
MenuDropDown(
menuOptions = MENU_OPTIONS_PATH,
) { _, selection ->
selectedPath.value = selection
when (selection) {
PATH_SEARCH -> {
showQueryOptions = true
showFilterOptions = false
}
PATH_INCLUDE -> {
showQueryOptions = false
showFilterOptions = true
}
else -> {
showQueryOptions = false
showFilterOptions = false
}
}
}
// manage path filter options, reset state if menu is closed
LaunchedEffect(showFilterOptions) {
selectedFilter = if (showFilterOptions) {
MENU_OPTIONS_FILTER.values.first().first()
} else {
""
}
}
if (showFilterOptions) {
MenuDropDown(
menuOptions = MENU_OPTIONS_FILTER,
) { _, selected ->
selectedFilter = selected
}
}
// manage query options, reset state if menu is closed
LaunchedEffect(showQueryOptions) {
if (showQueryOptions) {
val initEntry = MENU_OPTIONS_SEARCH.entries.first()
selectedSearchQuery[initEntry.key] = initEntry.value.first()
} else {
selectedSearchQuery.clear()
}
}
if (showQueryOptions) {
MenuTextInput(
menuLabels = MENU_LABELS_SEARCH,
) { label, selected ->
selectedSearchQuery[label] = selected
}
MenuDropDown(
menuOptions = MENU_OPTIONS_SEARCH,
) { label, selected ->
selectedSearchQuery[label] = selected
}
}
// form final deeplink url
val arguments = when (selectedPath.value) {
PATH_INCLUDE -> "/${selectedFilter}"
PATH_SEARCH -> {
buildString {
selectedSearchQuery.forEach { entry ->
if (entry.value.isNotEmpty()) {
val prefix = if (isEmpty()) "?" else "&"
append("$prefix${entry.key}=${entry.value}")
}
}
}
}
else -> ""
}
val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments"
TextContent("Final url:\n$finalUrl")
// deeplink to target
PaddedButton("Deeplink Away!", onClick = dropUnlessResumed {
val intent = Intent(
this@CreateDeepLinkActivity,
MainActivity::class.java
)
// start activity with the url
intent.data = finalUrl.toUri()
startActivity(intent)
})
}
}
}
}
private const val KEY_PATH = "path"
private val MENU_OPTIONS_PATH = mapOf(
KEY_PATH to listOf(
STRING_LITERAL_HOME,
PATH_INCLUDE,
PATH_SEARCH,
),
)
private val MENU_OPTIONS_FILTER = mapOf(
UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL),
)
private val MENU_OPTIONS_SEARCH = mapOf(
SearchKey::firstName.name to listOf(
EMPTY,
FIRST_NAME_JOHN,
FIRST_NAME_TOM,
FIRST_NAME_MARY,
FIRST_NAME_JULIE
),
SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US)
)
private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name)
```
```
package com.example.nav3recipes.deeplink.basic.util
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
/**
* Decodes the list of arguments into a a back stack key
*
* **IMPORTANT** This decoder assumes that all argument types are Primitives.
*/
@OptIn(ExperimentalSerializationApi::class)
internal class KeyDecoder(
private val arguments: Map<String, Any>,
) : AbstractDecoder() {
override val serializersModule: SerializersModule = EmptySerializersModule()
private var elementIndex: Int = -1
private var elementName: String = ""
/**
* Decodes the index of the next element to be decoded. Index represents a position of the
* current element in the [descriptor] that can be found with [descriptor].getElementIndex.
*
* The returned index will trigger deserializer to call [decodeValue] on the argument at that
* index.
*
* The decoder continually calls this method to process the next available argument until this
* method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more
* arguments to decode.
*
* This method should sequentially return the element index for every element that has its value
* available within [arguments].
*/
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
var currentIndex = elementIndex
while (true) {
// proceed to next element
currentIndex++
// if we have reached the end, let decoder know there are not more arguments to decode
if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
val currentName = descriptor.getElementName(currentIndex)
// Check if bundle has argument value. If so, we tell decoder to process
// currentIndex. Otherwise, we skip this index and proceed to next index.
if (arguments.contains(currentName)) {
elementIndex = currentIndex
elementName = currentName
return elementIndex
}
}
}
/**
* Returns argument value from the [arguments] for the argument at the index returned by
* [decodeElementIndex]
*/
override fun decodeValue(): Any {
val arg = arguments[elementName]
checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" }
return arg
}
override fun decodeNull(): Nothing? = null
// we want to know if it is not null, so its !isNull
override fun decodeNotNullMark(): Boolean = arguments[elementName] != null
}
```
```
package com.example.nav3recipes.deeplink.basic.util
import android.net.Uri
/**
* Parse the requested Uri and store it in a easily readable format
*
* @param uri the target deeplink uri to link to
*/
internal class DeepLinkRequest(
val uri: Uri
) {
/**
* A list of path segments
*/
val pathSegments: List<String> = uri.pathSegments
/**
* A map of query name to query value
*/
val queries = buildMap {
uri.queryParameterNames.forEach { argName ->
this[argName] = uri.getQueryParameter(argName)!!
}
}
// TODO add parsing for other Uri components, i.e. fragments, mimeType, action
}
```
````
package com.example.nav3recipes.deeplink.basic.util
import android.net.Uri
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.encoding.CompositeDecoder
import java.io.Serializable
/**
* Parse a supported deeplink and stores its metadata as a easily readable format
*
* The following notes applies specifically to this particular sample implementation:
*
* The supported deeplink is expected to be built from a serializable backstack key [T] that
* supports deeplink. This means that if this deeplink contains any arguments (path or query),
* the argument name must match any of [T] member field name.
*
* One [DeepLinkPattern] should be created for each supported deeplink. This means if [T]
* supports two deeplink patterns:
* ```
* val deeplink1 = www.nav3recipes.com/home
* val deeplink2 = www.nav3recipes.com/profile/{userId}
* ```
* Then two [DeepLinkPattern] should be created
* ```
* val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1)
* val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2)
* ```
*
* This implementation assumes a few things:
* 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match
* 2. all query arguments are optional by way of nullable/has default value
*
* @param T the backstack key type that supports the deeplinking of [uriPattern]
* @param serializer the serializer of [T]
* @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}"
*/
internal class DeepLinkPattern<T : NavKey>(
val serializer: KSerializer<T>,
val uriPattern: Uri
) {
/**
* Help differentiate if a path segment is an argument or a static value
*/
private val regexPatternFillIn = Regex("\\{(.+?)\\}")
// TODO make these lazy
/**
* parse the path into a list of [PathSegment]
*
* order matters here - path segments need to match in value and order when matching
* requested deeplink to supported deeplink
*/
val pathSegments: List<PathSegment> = buildList {
uriPattern.pathSegments.forEach { segment ->
// first, check if it is a path arg
var result = regexPatternFillIn.find(segment)
if (result != null) {
// if so, extract the path arg name (the string value within the curly braces)
val argName = result.groups[1]!!.value
// from [T], read the primitive type of this argument to get the correct type parser
val elementIndex = serializer.descriptor.getElementIndex(argName)
if (elementIndex == CompositeDecoder.UNKNOWN_NAME) {
throw IllegalArgumentException(
"Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'."
)
}
val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
// finally, add the arg name and its respective type parser to the map
add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind)))
} else {
// if its not a path arg, then its just a static string path segment
add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING)))
}
}
}
/**
* Parse supported queries into a map of queryParameterNames to [TypeParser]
*
* This will be used later on to parse a provided query value into the correct KType
*/
val queryValueParsers: Map<String, TypeParser> = buildMap {
uriPattern.queryParameterNames.forEach { paramName ->
val elementIndex = serializer.descriptor.getElementIndex(paramName)
// Ignore static query parameters that are not in the Serializable class
if (elementIndex != CompositeDecoder.UNKNOWN_NAME) {
val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
this[paramName] = getTypeParser(elementDescriptor.kind)
}
}
}
/**
* Metadata about a supported path segment
*/
class PathSegment(
val stringValue: String,
val isParamArg: Boolean,
val typeParser: TypeParser
)
}
/**
* Parses a String into a Serializable Primitive
*/
private typealias TypeParser = (String) -> Serializable
private fun getTypeParser(kind: SerialKind): TypeParser {
return when (kind) {
PrimitiveKind.STRING -> Any::toString
PrimitiveKind.INT -> String::toInt
PrimitiveKind.BOOLEAN -> String::toBoolean
PrimitiveKind.BYTE -> String::toByte
PrimitiveKind.CHAR -> String::toCharArray
PrimitiveKind.DOUBLE -> String::toDouble
PrimitiveKind.FLOAT -> String::toFloat
PrimitiveKind.LONG -> String::toLong
PrimitiveKind.SHORT -> String::toShort
else -> throw IllegalArgumentException(
"Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive."
)
}
}
````
```
package com.example.nav3recipes.deeplink.basic.util
import android.util.Log
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.KSerializer
internal class DeepLinkMatcher<T : NavKey>(
val request: DeepLinkRequest,
val deepLinkPattern: DeepLinkPattern<T>
) {
/**
* Match a [DeepLinkRequest] to a [DeepLinkPattern].
*
* Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise
*/
fun match(): DeepLinkMatchResult<T>? {
if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null
if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null
if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null
// exact match (url does not contain any arguments)
if (request.uri == deepLinkPattern.uriPattern)
return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf())
val args = mutableMapOf<String, Any>()
// match the path
request.pathSegments
.asSequence()
// zip to compare the two objects side by side, order matters here so we
// need to make sure the compared segments are at the same position within the url
.zip(deepLinkPattern.pathSegments.asSequence())
.forEach { it ->
// retrieve the two path segments to compare
val requestedSegment = it.first
val candidateSegment = it.second
// if the potential match expects a path arg for this segment, try to parse the
// requested segment into the expected type
if (candidateSegment.isParamArg) {
val parsedValue = try {
candidateSegment.typeParser.invoke(requestedSegment)
} catch (e: IllegalArgumentException) {
Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e)
return null
}
args[candidateSegment.stringValue] = parsedValue
} else if(requestedSegment != candidateSegment.stringValue){
// if it's path arg is not the expected type, its not a match
return null
}
}
// match queries (if any)
request.queries.forEach { query ->
val name = query.key
// If the pattern does not define this query parameter, ignore it.
// This prevents a NullPointerException.
val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach
val queryParsedValue = try {
queryStringParser.invoke(query.value)
} catch (e: IllegalArgumentException) {
Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e)
return null
}
args[name] = queryParsedValue
}
// provide the serializer of the matching key and map of arg names to parsed arg values
return DeepLinkMatchResult(deepLinkPattern.serializer, args)
}
}
/**
* Created when a requested deeplink matches with a supported deeplink
*
* @param [T] the backstack key associated with the deeplink that matched with the requested deeplink
* @param serializer serializer for [T]
* @param args The map of argument name to argument value. The value is expected to have already
* been parsed from the raw url string back into its proper KType as declared in [T].
* Includes arguments for all parts of the uri - path, query, etc.
* */
internal data class DeepLinkMatchResult<T : NavKey>(
val serializer: KSerializer<T>,
val args: Map<String, Any>
)
const val TAG_LOG_ERROR = "Nav3RecipesDeepLink"
```
```
package com.example.nav3recipes.deeplink.basic.ui
import com.example.nav3recipes.deeplink.basic.SearchKey
/**
* String resources
*/
internal const val STRING_LITERAL_FILTER = "filter"
internal const val STRING_LITERAL_HOME = "home"
internal const val STRING_LITERAL_USERS = "users"
internal const val STRING_LITERAL_SEARCH = "search"
internal const val STRING_LITERAL_INCLUDE = "include"
internal const val PATH_BASE = "https://www.nav3recipes.com"
internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE"
internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH"
internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME"
internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}"
internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" +
"?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" +
"&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" +
"&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" +
"&${SearchKey::location.name}={${SearchKey::location.name}}"
```
@@ -0,0 +1,176 @@
# Custom DeepLinkMatcher Recipe
This recipe demonstrates how to create a custom `DeepLinkMatcher` in Navigation 3 using custom request extras and Kotlinx Serialization.
## How it works
This recipe consists of two activities:
- `CustomDeepLinkMatcherActivity`: Accepts user input, serializes a `HomeKey` instance into JSON, attaches it to an `Intent` extra via a `RequestExtrasKey`, and launches `MainActivity`.
- `MainActivity`: Constructs a `DeepLinkRequest(intent)`, evaluates it with `JsonDeepLinkMatcher`, decodes the `HomeKey`, and sets it as the starting route in `NavDisplay`.
## Key Concepts
1. **Custom `RequestExtrasKey`** :
`JsonDeepLinkMatcherKey` defines a custom extra key implementing `RequestExtrasKey<String>` to type-safely store and read serialized JSON payloads in `DeepLinkRequest.extras`.
2. **Custom `DeepLinkMatcher`** :
`JsonDeepLinkMatcher<T>` extends `DeepLinkMatcher<T, MatchResult<T>>` and implements `matchRequest(request)` to extract `request.extras[JsonDeepLinkMatcherKey]` and decode it into a strongly typed `NavKey` using Kotlinx Serialization.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/usecases/matcher)
```
package com.example.nav3recipes.deeplink.usecases.matcher
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.deeplink.DeepLinkMatcher
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
import androidx.navigation3.runtime.deeplink.RequestExtrasKey
import androidx.navigation3.runtime.deeplink.get
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
class CustomDeepLinkMatcherActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
EntryScreen {
Column(
verticalArrangement = Arrangement.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
placeholder = { Text("Your name...", color = Color.Black.copy(alpha = 0.5f)) },
value = text,
singleLine = true,
onValueChange = { text = it },
)
ElevatedButton(
onClick =
dropUnlessResumed {
val intent = Intent(
this@CustomDeepLinkMatcherActivity,
MainActivity::class.java
)
val json = Json.encodeToString(HomeKey.serializer(), HomeKey(text))
intent.putExtra(JsonDeepLinkMatcherKey.toString(), json)
startActivity(intent)
}
) {
Text("Sign up")
}
}
}
}
}
}
internal data object JsonDeepLinkMatcherKey: RequestExtrasKey<String>
internal class JsonDeepLinkMatcher<T: NavKey>(val serializer: KSerializer<T>): DeepLinkMatcher<T, DeepLinkMatcher.MatchResult<T>>() {
override fun matchRequest(request: DeepLinkRequest): MatchResult<T>? {
val json = request.extras[JsonDeepLinkMatcherKey] ?: return null
return try {
val result = Json.decodeFromString(serializer, json)
MatchResult(result)
} catch (e: SerializationException) {
Log.v("DeepLinkMatcher", "Failed to decode json", e)
return null
}
}
}
```
```
package com.example.nav3recipes.deeplink.usecases.matcher
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
import androidx.navigation3.runtime.deeplink.invoke
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
@Serializable
internal data class HomeKey(val name: String): NavKey
@Serializable
internal object FallbackKey: NavKey
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
val request = DeepLinkRequest(intent)
val deepLinkMatcher = createJsonDeepLinkMatcher<HomeKey>()
val matchResult = deepLinkMatcher.match(request)
val key = matchResult?.key ?: FallbackKey
setContent {
val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
entryProvider = entryProvider {
entry<HomeKey> { key ->
EntryScreen("Welcome") {
TextContent(key.name)
}
}
entry<FallbackKey> { key ->
EntryScreen("Fallback Key") {
TextContent(
"Failed to deep link - DeepLinkRequest " +
"did not match with any DeepLinkMatcher"
)
}
}
}
)
}
}
}
// Optional JsonDeepLinkMatcher factory function that automatically captures KSerializer for T.
private inline fun <reified T : NavKey> createJsonDeepLinkMatcher(): JsonDeepLinkMatcher<T> {
val serializer = serializer<T>()
return JsonDeepLinkMatcher(serializer)
}
```
@@ -0,0 +1,140 @@
# Deep Link Static URI Recipe
This recipe demonstrates how deep link with a static Uri.
## Recipe components
The recipe contains two activities:
1. `StaticUriDeepLinkActivity` to construct and start an Intent with the deep link uri
2. `MainActivity` is the target Activity of the deep link, represents an app that users can deep link to.
## How the demonstrated deep link works
1. The deep link source (`StaticUriDeepLinkActivity`) defines the uri and creates an Intent to deep link with.
2. The app (`MainActivity`) declares a navigation key (`HomeKey`). To indicate that `HomeKey` supports deep linking, the app declares a `UriDeepLinkMatcher` with the `HomeKey` serializer along with the uri pattern that `HomeKey` supports.
3. `MainActivity` onCreate instantiates a `DeepLinkRequest` with the intent and matches it with the `UriDeepLinkMatcher` to get a `MatchResult`. If the `MatchResult` is non-null, the app navigates to the key returned by the result. Otherwise, the deep link is not supported and the app navigates to a `Fallback` screen.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/staticuri)
```
package com.example.nav3recipes.deeplink.handlerequests.staticuri
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.net.toUri
import androidx.lifecycle.compose.dropUnlessResumed
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.PaddedButton
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_BASE
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
const val HOME_URI = "$PATH_BASE/home"
class StaticUriDeepLinkActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
EntryScreen("Deep link url:") {
TextContent(HOME_URI)
PaddedButton("Deeplink Away!", onClick = dropUnlessResumed {
val intent = Intent(
this@StaticUriDeepLinkActivity,
MainActivity::class.java
)
// the uri to deep link with
intent.data = HOME_URI.toUri()
startActivity(intent)
})
}
}
}
}
```
```
package com.example.nav3recipes.deeplink.handlerequests.staticuri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.HomeKey
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.NavRecipeKey
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
import androidx.navigation3.runtime.deeplink.DeepLinkUri
import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher
import androidx.navigation3.runtime.deeplink.invoke
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
@Serializable
internal object FallbackKey: NavRecipeKey {
override val name: String = "Fallback Key"
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
// create a DeepLinkRequest with the intent
val request = DeepLinkRequest(intent)
// try to match DeepLinkRequest to a DeepLinkMatcher
val matchResult = HOME_MATCHER.match(request)
val key = matchResult?.key ?: FallbackKey
/**
* Then pass starting key to backstack
*/
setContent {
val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<HomeKey> { key ->
EntryScreen(key.name) {
TextContent("Deep linked to Home")
}
}
entry<FallbackKey> { key ->
EntryScreen("${key.name} ") {
TextContent(
"Failed to deep link - DeepLinkRequest " +
"did not match with any DeepLinkMatcher"
)
}
}
}
)
}
}
}
/**
* Each matcher is associated with a navigation key that supports this deep link.
*
* A navigation key can be associated with multiple DeepLinkMatchers if it supports more than one deep link.
*/
private val HOME_MATCHER = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(HOME_URI),
serializer = serializer<HomeKey>(),
)
```
@@ -1,34 +1,63 @@
# Deep Link Advanced Recipe
# Deep Link Synthetic BackStack Recipe
This recipe demonstrates how to apply the principles of navigation in the context of deep links by
managing a synthetic backStack and Task stacks.
# Recipe Structure
This recipe simulates a real-world scenario where "App A" deeplinks
This recipe simulates a real-world scenario where "App A" deep links
into "App B".
"App A" is simulated by the module [com.example.nav3recipes.deeplink.advanced](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/advanced), which
contains the `CreateAdvancedDeepLinkActivity` that allows you to create a deeplink intent and
"App A" is simulated by the module [syntheticbackstack](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/syntheticbackstack), which
contains the `SyntheticBackStackDeepLinkActivity` that allows you to create a deeplink intent and
trigger that in either the existing Task, or in a new Task.
"App B" is simulated by the module [advanceddeeplinkapp](https://developer.android.com/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced), which contains
the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack
"App B" is simulated by the module [syntheticbackstackapp](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack), which contains
the `SyntheticBackStackAppActivity` that you deeplink into. That module shows you how to build a synthetic backStack
and how to manage the Task stack properly in order to support both Back and Up buttons.
# How to Use
Ensure both the main `app` and `syntheticbackstackapp` are installed on the emulator or connected device. Ensure that the installed `syntheticbackstackapp` supports
the `"www.nav3deeplink.com"` link.
On the recipe's landing page, choose the filters and click the button to deep link. It should bring you to the Activity of `syntheticbackstackapp`.
# How it Works
The recipe follows the deep link guideline summarized [here](https://developer.android.com/docs/deeplink-guide#summary).
To see behavior of `Existing Task`:
1. Open deep link using current task
2. On the device, swipe up to see all recent apps
3. Notice that the new Activity is opened within the Nav3Recipes app
4. Click back button to go back to the original Activity
5. Repeat step 1
6. Click the up button to go to parent screen
7. On the device, swipe up to see all recent apps
8. Notice that the new Activity is now opened within the Nav3SyntheticBackStack app
To see behavior of `New Task`:
1. Open deep link using new task
2. On the device, swipe up to see all recent apps
3. Notice that the new Activity is opened within the Nav3SyntheticBackStack app
4. Click Up or Back button to go to parent screen
# Core implementation
The core helper functions for navigateUp and building synthetic backStack can be
found [here](https://developer.android.com/static/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt)
found [here](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack/util)
# Further Read
Check out the [deep link guide](https://developer.android.com/docs/deeplink-guide) for a
comprehensive guide on Deep linking principles and how to apply them in Navigation 3.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/advanced)
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/syntheticbackstack)
```
package com.example.nav3recipes.deeplink.advanced
package com.example.nav3recipes.deeplink.handlerequests.syntheticbackstack
import android.content.Intent
import android.os.Bundle
@@ -53,20 +82,20 @@ internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com"
/**
* The recipe entry point that allows users to create a deep link and make a request with it.
*
* **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deeplinks
* **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deep links
* into "App B".
*
* "App A" is simulated by this current module [com.example.nav3recipes.deeplink.advanced], which
* contains the [AdvancedCreateDeepLinkActivity] that allows you to create a deeplink intent and
* "App A" is simulated by this current module `syntheticbackstack`, which
* contains the [SyntheticBackStackDeepLinkActivity] that allows you to create a deeplink intent and
* trigger that in either the existing Task, or in a new Task.
*
* "App B" is simulated by the module [com.example.nav3recipes.deeplink.advanced], which contains
* the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack
* "App B" is simulated by the module `syntheticbackstackapp`, which contains
* the SyntheticBackStackAppActivity that you deeplink into. That module shows you how to build a synthetic backStack
* and how to manage the Task stack properly in order to support both Back and Up buttons.
*
* See the [README](README.md) file of current module for more info on advanced deep linking.
*/
class AdvancedCreateDeepLinkActivity: ComponentActivity() {
class SyntheticBackStackDeepLinkActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
@@ -0,0 +1,390 @@
# Deep Link URI Arguments Recipe
This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key.
## Recipe components
It consists of two activities
1. `UriWithArgumentsDeepLinkActivity` constructs and triggers the deeplink request
2. `MainActivity` parses the intent into the target navigation key.
## How it works
The `MainActivity` handles the request with these steps
1. Declare a `UriDeepLinkMatcher` for each url pattern that can be deep linked into. Each matcher accepts a uri pattern and the KSerializer of the NavKey that supports this deep link.
2. Create a `DeepLinkRequest` with the incoming intent.
3. Match all candidate `UriDeepLinkMatchers` with the request and compare the resulting `UriMatchResults` for the best match.
4. Read the matching key from `UriMatchResult.key` or use default key if no match.
This recipe focuses on handing an intent and does not include these considerations:
- Create synthetic backStack
- Multi-modular setup
- DI
- Managing TaskStack
- Up button vs Back Button
## Demonstrated forms of deeplink
The `MainActivity` has several backStack keys to demonstrate different types of supported deep links:
1. `HomeKey` - deeplink with an exact url (no deeplink arguments)
2. `UsersKey` - deeplink with path arguments
3. `SearchKey` - deeplink with query arguments
See `MainActivity.deepLinkMatchers` for the actual url pattern of each.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/uriarguments)
```
package com.example.nav3recipes.deeplink.handlerequests.uriarguments
import androidx.navigation3.runtime.NavKey
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_FILTER
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_SEARCH
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_USERS
import kotlinx.serialization.Serializable
internal interface NavRecipeKey: NavKey {
val name: String
}
@Serializable
internal object HomeKey: NavRecipeKey {
override val name: String = STRING_LITERAL_HOME
}
@Serializable
internal data class UsersKey(
val filter: String,
): NavRecipeKey {
override val name: String = STRING_LITERAL_USERS
companion object {
const val FILTER_KEY = STRING_LITERAL_FILTER
const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded"
const val FILTER_OPTION_ALL = "all"
}
}
@Serializable
internal data class SearchKey(
val firstName: String? = null,
val ageMin: Int? = null,
val ageMax: Int? = null,
val location: String? = null,
): NavRecipeKey {
override val name: String = STRING_LITERAL_SEARCH
}
```
```
package com.example.nav3recipes.deeplink.handlerequests.uriarguments
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.net.toUri
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher
import androidx.navigation3.runtime.deeplink.invoke
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.FriendsList
import com.example.nav3recipes.common.deeplink.LIST_USERS
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_HOME_EXACT
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_SEARCH
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_USERS_WITH_FILTER
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.serializer
/**
* See README.md for how this recipe works.
*/
class MainActivity : ComponentActivity() {
/** STEP 1. Declare supported deep links */
internal val deepLinkMatchers: List<UriDeepLinkMatcher<NavKey>> = listOf(
// "https://www.nav3recipes.com/home"
UriDeepLinkMatcher(URL_HOME_EXACT.toUri(), serializer<HomeKey>()),
// "https://www.nav3recipes.com/users/with/{filter}"
UriDeepLinkMatcher(URL_USERS_WITH_FILTER.toUri(), serializer<UsersKey>()),
// "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}"
UriDeepLinkMatcher(URL_SEARCH.toUri(), serializer<SearchKey>()),
)
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
/** STEP 2. Create a [DeepLinkRequest] from the intent */
val request = DeepLinkRequest(intent)
/** STEP 3. Match the request to the DeepLinkMatchers*/
// First get all the possible matching UriMatchResult
val matches = deepLinkMatchers.mapNotNull {
// returns null if no match
it.match(request)
}
// compare all matches to find best match
val bestMatch = matches.maxOrNull()
/** STEP 4. Get the key from the match or use default key if no match*/
val key = bestMatch?.key ?: HomeKey
/**
* STEP 5. pass the initial key to backstack
*/
setContent {
val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<HomeKey> { key ->
EntryScreen(key.name) {
TextContent("<matches exact url>")
}
}
entry<UsersKey> { key ->
EntryScreen("${key.name} : ${key.filter}") {
TextContent("<matches path argument>")
val list = when {
key.filter.isEmpty() -> LIST_USERS
key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS
else -> LIST_USERS.take(5)
}
FriendsList(list)
}
}
entry<SearchKey> { search ->
EntryScreen(search.name) {
TextContent("<matches query parameters, if any>")
val matchingUsers = LIST_USERS.filter { user ->
(search.firstName == null || user.firstName == search.firstName) &&
(search.location == null || user.location == search.location) &&
(search.ageMin == null || user.age >= search.ageMin) &&
(search.ageMax == null || user.age <= search.ageMax)
}
FriendsList(matchingUsers)
}
}
}
)
}
}
}
```
```
package com.example.nav3recipes.deeplink.handlerequests.uriarguments
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.compose.dropUnlessResumed
import com.example.nav3recipes.common.deeplink.EMPTY
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN
import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE
import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY
import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM
import com.example.nav3recipes.common.deeplink.LOCATION_BC
import com.example.nav3recipes.common.deeplink.LOCATION_BR
import com.example.nav3recipes.common.deeplink.LOCATION_CA
import com.example.nav3recipes.common.deeplink.LOCATION_US
import com.example.nav3recipes.common.deeplink.MenuDropDown
import com.example.nav3recipes.common.deeplink.MenuTextInput
import com.example.nav3recipes.common.deeplink.PaddedButton
import com.example.nav3recipes.common.deeplink.TextContent
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_BASE
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_INCLUDE
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_SEARCH
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
/**
* See README.md for how this recipe works.
*
* See [MainActivity] for how the requested deeplink is handled.
*/
class UriWithArgumentsDeepLinkActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
/**
* UI for deeplink sandbox
*/
EntryScreen("Sandbox - Build Your Deeplink") {
TextContent("Base url:\n${PATH_BASE}/")
var showFilterOptions by remember { mutableStateOf(false) }
val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) }
var showQueryOptions by remember { mutableStateOf(false) }
var selectedFilter by remember { mutableStateOf("") }
val selectedSearchQuery = remember { mutableStateMapOf<String, String>() }
// manage path options
MenuDropDown(
menuOptions = MENU_OPTIONS_PATH,
) { _, selection ->
selectedPath.value = selection
when (selection) {
PATH_SEARCH -> {
showQueryOptions = true
showFilterOptions = false
}
PATH_INCLUDE -> {
showQueryOptions = false
showFilterOptions = true
}
else -> {
showQueryOptions = false
showFilterOptions = false
}
}
}
// manage path filter options, reset state if menu is closed
LaunchedEffect(showFilterOptions) {
selectedFilter = if (showFilterOptions) {
MENU_OPTIONS_FILTER.values.first().first()
} else {
""
}
}
if (showFilterOptions) {
MenuDropDown(
menuOptions = MENU_OPTIONS_FILTER,
) { _, selected ->
selectedFilter = selected
}
}
// manage query options, reset state if menu is closed
LaunchedEffect(showQueryOptions) {
if (showQueryOptions) {
val initEntry = MENU_OPTIONS_SEARCH.entries.first()
selectedSearchQuery[initEntry.key] = initEntry.value.first()
} else {
selectedSearchQuery.clear()
}
}
if (showQueryOptions) {
MenuTextInput(
menuLabels = MENU_LABELS_SEARCH,
) { label, selected ->
selectedSearchQuery[label] = selected
}
MenuDropDown(
menuOptions = MENU_OPTIONS_SEARCH,
) { label, selected ->
selectedSearchQuery[label] = selected
}
}
// form final deeplink url
val arguments = when (selectedPath.value) {
PATH_INCLUDE -> "/${selectedFilter}"
PATH_SEARCH -> {
buildString {
selectedSearchQuery.forEach { entry ->
if (entry.value.isNotEmpty()) {
val prefix = if (isEmpty()) "?" else "&"
append("$prefix${entry.key}=${entry.value}")
}
}
}
}
else -> ""
}
val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments"
TextContent("Final url:\n$finalUrl")
// deeplink to target
PaddedButton("Deeplink Away!", onClick = dropUnlessResumed {
val intent = Intent(
this@UriWithArgumentsDeepLinkActivity,
MainActivity::class.java
)
// start activity with the url
intent.data = finalUrl.toUri()
startActivity(intent)
})
}
}
}
}
private const val KEY_PATH = "path"
private val MENU_OPTIONS_PATH = mapOf(
KEY_PATH to listOf(
STRING_LITERAL_HOME,
PATH_INCLUDE,
PATH_SEARCH,
),
)
private val MENU_OPTIONS_FILTER = mapOf(
UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL),
)
private val MENU_OPTIONS_SEARCH = mapOf(
SearchKey::firstName.name to listOf(
EMPTY,
FIRST_NAME_JOHN,
FIRST_NAME_TOM,
FIRST_NAME_MARY,
FIRST_NAME_JULIE
),
SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US)
)
private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name)
```
```
package com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui
import com.example.nav3recipes.deeplink.handlerequests.uriarguments.SearchKey
/**
* String resources
*/
internal const val STRING_LITERAL_FILTER = "filter"
internal const val STRING_LITERAL_HOME = "home"
internal const val STRING_LITERAL_USERS = "users"
internal const val STRING_LITERAL_SEARCH = "search"
internal const val STRING_LITERAL_INCLUDE = "include"
internal const val PATH_BASE = "https://www.nav3recipes.com"
internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE"
internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH"
internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME"
internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}"
internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" +
"?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" +
"&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" +
"&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" +
"&${SearchKey::location.name}={${SearchKey::location.name}}"
```
@@ -0,0 +1,185 @@
# LocalLifecycleOwner Recipe
This recipe demonstrates how to use `LifecycleResumeEffect` in Navigation 3 entries to pause and resume work based on the entry's lifecycle state.
## How it works
In Navigation 3, by default each `NavEntry` is provided its own `LifecycleOwner` via `LocalLifecycleOwner.current`. This means that any lifecycle-aware components inside the entry is automatically scoped to the `NavEntry`.
### `LifecycleResumeEffect` with Dialog Scenes
1. **RouteA (Screen)**:
- Uses `LifecycleResumeEffect(Unit)` scoped to the `NavEntry`'s `LocalLifecycleOwner.current` to advance the`LinearProgressIndicator` while in the `RESUMED` state.
- Automatically resets `progressValue` back to `0f` whenever it hits `1f`.
2. **RouteB (Dialog)**:
- Configured as a dialog using `DialogSceneStrategy.dialog()`.
- When the user opens the RouteB dialog, RouteA remains visible behind the dialog in the `STARTED` state (leaving `RESUMED`).
- `LifecycleResumeEffect` calls `onPauseOrDispose`, pausing the progress indicator.
- When the dialog is dismissed, RouteA returns to `RESUMED`, and `LifecycleResumeEffect` resumes the progress indicator automatically.
[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/lifecycleowner)
```
/*
* 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
*
* http://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.nav3recipes.lifecycleowner
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.scene.DialogSceneStrategy
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.content.ContentGreen
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.milliseconds
@Serializable
private data object RouteA : NavKey
@Serializable
private data object RouteB : NavKey
class LifecycleOwnerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
val backStack = rememberNavBackStack(RouteA)
val dialogStrategy = remember { DialogSceneStrategy<NavKey>() }
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
sceneStrategies = listOf(dialogStrategy),
entryProvider = entryProvider {
entry<RouteA> {
LogLifecycleEffect("ScreenA")
ContentGreen("Screen A") {
var progressValue by remember { mutableFloatStateOf(0f) }
val coroutineScope = rememberCoroutineScope()
val animatedProgress by animateFloatAsState(
targetValue = progressValue,
animationSpec = if (progressValue == 0f) snap() else tween(durationMillis = 100, easing = LinearEasing),
label = "ProgressAnimation"
)
// LifecycleResumeEffect runs only while ScreenA is in the RESUMED state.
// When the dialog opens, ScreenA transitions to PAUSED, which calls onPauseOrDispose.
LifecycleResumeEffect(Unit) {
val job = coroutineScope.launch {
while (true) {
delay(100.milliseconds)
progressValue += 0.01f
if (progressValue >= 1f) {
progressValue = 0f
}
}
}
onPauseOrDispose {
job.cancel()
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
LinearProgressIndicator(progress = { animatedProgress })
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = dropUnlessResumed { backStack.add(RouteB) }) {
Text("Open Dialog")
}
}
}
}
entry<RouteB>(metadata = DialogSceneStrategy.dialog()) {
LogLifecycleEffect("Dialog")
AlertDialog(
onDismissRequest = backStack::removeLastOrNull,
title = { Text("Dialog") },
text = {
Text("Screen A is no longer in resumed state, pausing the progress indicator.")
},
confirmButton = {
Button(onClick = dropUnlessResumed { backStack.removeLastOrNull() }) {
Text("Dismiss Dialog")
}
}
)
}
}
)
}
}
}
@Composable
private fun LogLifecycleEffect(screenName: String) {
LifecycleEventEffect(Lifecycle.Event.ON_CREATE) {
Log.d("LifecycleOwner", "$screenName: ON_CREATE")
}
LifecycleEventEffect(Lifecycle.Event.ON_START) {
Log.d("LifecycleOwner", "$screenName: ON_START")
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
Log.d("LifecycleOwner", "$screenName: ON_RESUME")
}
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
Log.d("LifecycleOwner", "$screenName: ON_PAUSE")
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
Log.d("LifecycleOwner", "$screenName: ON_STOP")
}
// Note that ON_DESTROY event is not observable from composables.
}
```
@@ -2,7 +2,7 @@
This recipe demonstrates how to create multiple back stacks.
The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. The content for the sub routes is a counter that can be used to verify state retention through configuration changes and process death.
The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. `RouteA1` contains a 100-item scrollable list, while `RouteB1` and `RouteC1` contain counters used to verify state retention through configuration changes and process death.
The app's navigation state is held in the `NavigationState` class. The state itself is created using `rememberNavigationState`.
@@ -14,6 +14,7 @@ Key behaviors:
- This app follows the "exit through home" pattern where the user always exits through the starting back stack. This means that `RouteA`'s entries are *always* in the list of entries.
- Navigating to a top level route that is not the starting route *replaces* the other entries. For example, navigating A-\>B-\>C would result in entries for A+C, B's entries are removed.
- When a top-level route is reselected, for example if the user is on `RouteA` and taps `RouteA` on the navigation bar again, the `NavigationBar` signals the reselected key to `Navigator`, which emits it to a `Flow<NavKey>`. `RouteA1` observes this flow and, when it receives `RouteA`, resets its list scroll position to index 0.
Important implementation details:
@@ -41,11 +42,16 @@ Important implementation details:
package com.example.nav3recipes.multiplestacks
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState){
private val _reselectEvents = MutableSharedFlow<NavKey>(extraBufferCapacity = 1)
val reselectEvents = _reselectEvents.asSharedFlow()
fun navigate(route: NavKey){
if (route in state.backStacks.keys){
// This is a top level route, just switch to it
@@ -55,6 +61,10 @@ class Navigator(val state: NavigationState){
}
}
fun onReselect(route: NavKey) {
_reselectEvents.tryEmit(route)
}
fun goBack(){
val currentStack = state.backStacks[state.topLevelRoute] ?:
error("Stack for ${state.topLevelRoute} not found")
@@ -225,10 +235,11 @@ class NavigationState(
package com.example.nav3recipes.multiplestacks
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Face
@@ -239,7 +250,9 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
@@ -277,7 +290,6 @@ data class NavBarItem(
)
class MultipleStacksActivity : ComponentActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
@@ -290,18 +302,26 @@ class MultipleStacksActivity : ComponentActivity() {
val navigator = remember { Navigator(navigationState) }
val entryProvider = entryProvider {
featureASection(onSubRouteClick = { navigator.navigate(RouteA1) })
featureASection(
reselectEvents = navigator.reselectEvents,
onSubRouteClick = { navigator.navigate(RouteA1) }
)
featureBSection(onSubRouteClick = { navigator.navigate(RouteB1) })
featureCSection(onSubRouteClick = { navigator.navigate(RouteC1) })
}
Scaffold(bottomBar = {
Scaffold(contentWindowInsets = WindowInsets(0.dp), bottomBar = {
NavigationBar {
TOP_LEVEL_ROUTES.forEach { (key, value) ->
val isSelected = key == navigationState.topLevelRoute
NavigationBarItem(
selected = isSelected,
onClick = { navigator.navigate(key) },
onClick = {
navigator.navigate(key)
if (isSelected) {
navigator.onReselect(key)
}
},
icon = {
Icon(
imageVector = value.icon,
@@ -312,10 +332,11 @@ class MultipleStacksActivity : ComponentActivity() {
)
}
}
}) {
}) { innerPadding ->
NavDisplay(
entries = navigationState.toDecoratedEntries(entryProvider),
onBack = { navigator.goBack() }
onBack = { navigator.goBack() },
modifier = Modifier.padding(innerPadding)
)
}
}
@@ -343,13 +364,17 @@ class MultipleStacksActivity : ComponentActivity() {
package com.example.nav3recipes.multiplestacks
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
@@ -359,27 +384,34 @@ import com.example.nav3recipes.content.ContentOrange
import com.example.nav3recipes.content.ContentPink
import com.example.nav3recipes.content.ContentPurple
import com.example.nav3recipes.content.ContentRed
import kotlinx.coroutines.flow.Flow
fun EntryProviderScope<NavKey>.featureASection(
reselectEvents: Flow<NavKey>,
onSubRouteClick: () -> Unit,
) {
entry<RouteA> {
ContentRed("Route A") {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = dropUnlessResumed(block = onSubRouteClick)) {
Text("Go to A1")
}
Button(onClick = dropUnlessResumed(block = onSubRouteClick)) {
Text("Go to A1")
}
}
}
entry<RouteA1> {
ContentPink("Route A1") {
var count by rememberSaveable {
mutableIntStateOf(0)
val scrollState = rememberLazyListState()
LaunchedEffect(reselectEvents) {
reselectEvents.collect { route ->
if (route == RouteA) {
scrollState.scrollToItem(0)
}
}
}
Button(onClick = { count++ }) {
Text("Value: $count")
ContentPink("Route A1") {
LazyColumn(state = scrollState) {
items(100) { index ->
Text("Route A item ${index + 1}", fontSize = 24.sp)
}
}
}
}
@@ -91,11 +91,10 @@ class PersonDetailsForm : NavKey
package com.example.nav3recipes.results.common
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
data class Person(val name: String, val favoriteColor: String) : Parcelable
@Serializable
data class Person(val name: String, val favoriteColor: String)
```
```
@@ -207,14 +206,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.result.LocalResultEventBus
import androidx.navigation3.runtime.result.ResultEffect
import androidx.navigation3.runtime.result.ResultEventBus
import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.results.common.Home
@@ -240,7 +238,10 @@ class ResultEventActivity : ComponentActivity() {
backStack = backStack,
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()),
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberResultEventBusNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Home> {
val viewModel = viewModel<HomeViewModel>(key = Home.toString())
@@ -91,11 +91,10 @@ class PersonDetailsForm : NavKey
package com.example.nav3recipes.results.common
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
data class Person(val name: String, val favoriteColor: String) : Parcelable
@Serializable
data class Person(val name: String, val favoriteColor: String)
```
```
@@ -208,16 +207,14 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.result.LocalResultEventBus
import androidx.navigation3.runtime.result.ResultEffect
import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.results.common.Home
import com.example.nav3recipes.results.common.HomeScreen
import com.example.nav3recipes.results.common.HomeViewModel
import com.example.nav3recipes.results.common.Person
import com.example.nav3recipes.results.common.PersonDetailsForm
import com.example.nav3recipes.results.common.PersonDetailsScreen
@@ -236,7 +233,10 @@ class ResultStateActivity : ComponentActivity() {
backStack = backStack,
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()),
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberResultEventBusNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Home> {
val resultState = LocalResultEventBus
+309
View File
@@ -0,0 +1,309 @@
---
name: navigation-event
description: Intercept back gestures and run Predictive Back animations using the
NavigationEvent (androidx.navigationevent) library in Compose Android. Handles Activity
setup, parent-child dispatcher scoping in `ViewPagers` or tabs, Compose `NavigationBackHandler`,
and migration from legacy `BackHandler` on SDK 36+.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-09-01'
keywords:
- Android
- Navigation Event
- Jetpack Compose
- Back Navigation
- Dispatcher
- Guidelines
- Troubleshooting
- ComponentActivity
- Dialog
- ViewPager
---
## Common guidelines
- **For architecture concepts** : To understand the foundational architecture, continuous gesture event lifecycles, or class definitions of the Navigation Event library, read [Navigation Event overview](references/android/guide/navigation/navigation-event/index.md).
- **For Android target** : If compile SDK is lower than 36, set it to `36` or higher in `build.gradle.kts`.
- **For Compose Android target**: The project must use Jetpack Compose for Compose-specific APIs. This skill is scoped exclusively to Compose Android (Android Views and non-Compose implementations are excluded).
- **For activity dispatchers** : `ComponentActivity` automatically implements `NavigationEventDispatcherOwner` out-of-the-box. You must use the built-in `navigationEventDispatcher` without creating anonymous delegate owners or overriding member properties.
- **For dialog scoping** : Floating windows (Compose `Dialog`, `ModalBottomSheet`, `ComponentDialog`) automatically provide a `NavigationEventDispatcherOwner`. You don't need manual `CompositionLocalProvider` propagation for dialogs.
- **For parent-child dispatcher hierarchies** : When scoping navigation handling to `ViewPagers`, tabbed interfaces, or nested navigation containers in Compose, use `rememberNavigationEventDispatcherOwner()` to create a child owner linked to the parent. Disabling the owner (`enabled = false`) automatically cascades to disable all child handlers.
- **For Compose handlers** : A one-to-one relationship between `NavigationEventState` and handlers is strictly enforced. Never bind the same `NavigationEventState` to multiple active `NavigationBackHandler` instances (`IllegalArgumentException`).
## Step 1: Plan
To complete this step, you **MUST** ensure the following:
1. **Identify the target platform** : Verify the app is targeting Compose Android. If `compileSdk` is lower than 36, set it to `36` or higher in `build.gradle.kts`.
2. **Navigation check**: Check if Navigation 3 is in use. If it is in use, use Navigation 3's built-in back navigation support rather than manually implementing low-level dispatchers from this skill.
3. **Hierarchy check** : Identify host Activities, `ViewPagers`, tabbed interfaces, or nested navigation hosts that require back gesture interception or parent-child dispatcher linking.
4. **Migration check** : Check if the project is migrating from back handling (`OnBackPressedCallback`, `BackHandler`, `onBackPresser`) to `NavigationEvent` and `NavigationBackHandler`.
5. **Input interception** : Detect where the app is intercepting navigation events from gestures or hardware button presses requiring translation to `NavigationEvent`.
## Step 2: Set up dependencies
To complete this step, you **MUST** ensure the following:
- For setting up compile SDKs, declaring catalog versions, and adding dependencies, follow [setup guide](references/android/guide/navigation/navigation-event/setup.md).
## Step 3: Configure dispatcher and inputs
To complete this step, you **MUST** ensure the following:
- To configure your dispatcher, leverage automatic `ComponentActivity` or `ComponentDialog` owner resolution.
- Link parent-child dispatchers in Compose following [dispatcher guide](references/android/guide/navigation/navigation-event/dispatcher.md).
## Step 4: Handle back navigation and UI transitions
To complete this step, you **MUST** ensure the following:
- To create navigation event handlers, integrate back gesture interception in Compose, animate UI components during swipes, and migrate from legacy back handlers, follow [handle back guide](references/android/guide/navigation/navigation-event/handle-back.md).
## Step 5: Clean up resources
> [!WARNING]
> **Warning:** Compose APIs perform teardown automatically. When using Compose APIs such as `NavigationBackHandler` and `rememberNavigationEventDispatcherOwner()`, handler removal and dispatcher disposal occur automatically when the composable leaves the composition.
You **MUST** perform explicit manual cleanup only when managing custom
dispatchers or non-Compose handlers:
- Call `remove()` on active handlers during teardown.
- Call `isEnabled = false` to temporarily disable navigation subtrees.
- Call `dispose()` on dispatcher instances when hosting components are destroyed. Disposing a parent dispatcher automatically cascades to all child dispatchers.
## Core troubleshooting guidelines
### 1. Activity dispatcher setup (StackOverflowError recursion)
`ComponentActivity` implements `NavigationEventDispatcherOwner` automatically
out-of-the-box. Don't override `navigationEventDispatcher` or wrap it in an
anonymous delegate owner.
#### RIGHT
**Why this is RIGHT** : Compose apps use `ComponentActivity` as the host.
`LocalNavigationEventDispatcherOwner.current` automatically resolves the
Activity's built-in dispatcher.
```kotlin
// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
```
<br />
#### WRONG
**Why this is WRONG** : Implementing `NavigationEventDispatcherOwner` directly on
`MainActivity` and overriding `navigationEventDispatcher` with a new instance
shadows the library's extension property, causing a recursive infinite loop
crash on launch (`StackOverflowError`). Creating redundant anonymous delegate
owners (`object : NavigationEventDispatcherOwner`) is unnecessary.
```kotlin
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
```
<br />
### 2. Floating window and dialog scoping (automatic ComponentDialog owner)
Floating windows (Compose `Dialog`, `ModalBottomSheet`, and any window backed by
`ComponentDialog`) automatically provide a `NavigationEventDispatcherOwner`.
Don't manually re-provide `LocalNavigationEventDispatcherOwner` using
`CompositionLocalProvider` inside dialogs.
#### RIGHT
**Why this is RIGHT** : `ComponentDialog` handles navigation dispatchers
automatically. Compose `Dialog` components resolve their dispatcher owner
out-of-the-box without manual propagation.
```kotlin
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
```
<br />
#### WRONG
**Why this is WRONG** : Wrapping dialog content in a manual
`CompositionLocalProvider` creates redundant boilerplate and obscures the
automatic dispatcher resolution provided by `ComponentDialog`.
```kotlin
// WRONG
@Composable
fun MyDialog(onDismiss: () -> Unit) {
val dispatcherOwner = LocalNavigationEventDispatcherOwner.current!!
Dialog(onDismissRequest = onDismiss) {
// Redundant: ComponentDialog provides NavigationEventDispatcherOwner automatically
CompositionLocalProvider( LocalNavigationEventDispatcherOwner provides dispatcherOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
}
```
<br />
### 3. Parent-child dispatcher hierarchy (`ViewPagers` and nested navigation)
When managing nested UI hierarchies such as `ViewPagers`, tabbed
interfaces, or custom navigation containers in Compose, use
`rememberNavigationEventDispatcherOwner()` to create a child owner linked to the
composition hierarchy. Setting `enabled = false` on the child owner
automatically disables its dispatcher and all registered child handlers.
#### RIGHT
**Why this is RIGHT** : Using `rememberNavigationEventDispatcherOwner(enabled =
isSelected)` creates a scoped child dispatcher linked to the parent from
`LocalNavigationEventDispatcherOwner.current`. Providing it using
`CompositionLocalProvider` ensures non-visible tabs or pages automatically stop
intercepting back gestures without leaking handlers.
```kotlin
// RIGHT: Scoping child navigation in a ViewPager or Tab interface
@Composable
fun TabPage(isSelected: Boolean) {
val childOwner = rememberNavigationEventDispatcherOwner(enabled = isSelected)
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides childOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = { /* Handle page back navigation */ }
)
// Page content
}
}
```
<br />
#### WRONG
**Why this is WRONG** : Creating unlinked standalone dispatchers, instantiating
raw dispatchers without remembering them across recompositions, or attempting to
use non-existent methods like `.addChild()` breaks hierarchy routing and leaves
child handlers active even when the page is inactive.
```kotlin
// WRONG
@Composable
fun TabPage(isSelected: Boolean) {
val parentDispatcher = LocalNavigationEventDispatcherOwner.current?.navigationEventDispatcher
val childDispatcher = NavigationEventDispatcher() // Unlinked and not remembered across recompositions
// WRONG: Method does not exist
parentDispatcher?.addChild(childDispatcher)
}
```
<br />
### 4. Compose multi-handler registration (IllegalArgumentException)
You must not bind the same `NavigationEventState` to multiple active
`NavigationBackHandler` instances, as this throws an `IllegalArgumentException`
at runtime. To handle conditional workflows (such as checking for unsaved
changes versus navigating back immediately), you must register a single unified
handler and branch logic inside `onBackCompleted`.
#### RIGHT
**Why this is RIGHT** : Using a single `NavigationBackHandler` with internal
branching logic inside `onBackCompleted` maintains a strict 1:1 mapping between
`NavigationEventState` and the handler, preventing state collisions.
```kotlin
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)
```
<br />
#### WRONG
**Why this is WRONG** : Attaching multiple `NavigationBackHandler` composables to
the same `navigationState` instance attempts to bind duplicate handlers to a
single state object, which throws an `IllegalArgumentException` at runtime.
```kotlin
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)
```
<br />
## Checklist
**For Compose Android targets:**
- \[ \] Is compile SDK set to `36` or higher? (If compile SDK is lower than 36, set it to `36` or higher in `build.gradle.kts`).
- \[ \] Is `android:enableOnBackInvokedCallback` NOT explicitly set to `"false"` in `AndroidManifest.xml`? (On API 36+, it defaults to `"true"`; on API 33--35, ensure it is set to `"true"`).
- \[ \] Does the Activity rely on the built-in `ComponentActivity` dispatcher owner without redundant anonymous delegate wrapping?
- \[ \] Do dialogs or sheets rely on automatic `ComponentDialog` dispatcher resolution without redundant `CompositionLocalProvider` wrapping?
- \[ \] Are parent-child dispatcher relationships in Compose scoped using `rememberNavigationEventDispatcherOwner()` when managing nested hierarchies?
- \[ \] Is conditional back logic handled within a single unified `NavigationBackHandler` to avoid duplicate registration (`IllegalArgumentException`)?
- \[ \] Are legacy `BackHandler` usages migrated to `NavigationBackHandler` with predictive progress support?
- \[ \] Does the project build and pass tests successfully?
@@ -0,0 +1,134 @@
To implement a robust navigation system, your app needs a centralized way to
handle back gestures and other navigation signals. This page describes how to
use [`NavigationEventDispatcher`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher) to coordinate and distribute these
navigation events across your application.
## Declare a `NavigationEventDispatcher`
The `NavigationEventDispatcher` is the central component of the
`NavigationEvent` library. It acts as an event hub that dispatches
navigation-related events, such as back gestures and navigation transitions, to
registered listeners within your app. Components can subscribe to these events
to react to navigation changes or other system-driven navigation actions.
You should provide `NavigationEventDispatcher` instances through a
[`NavigationEventDispatcherOwner`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventDispatcherOwner). This ensures that different parts of your
app can access the same dispatcher and observe navigation events in a consistent
and coordinated way.
```kotlin
class MyComponent: NavigationEventDispatcherOwner {
override val navigationEventDispatcher: NavigationEventDispatcher =
NavigationEventDispatcher()
}
```
<br />
If you are inside of a `ComponentActivity`, instead of implementing your own
dispatcher, you can retrieve the one provided for you.
```kotlin
class MyCustomActivity : ComponentActivity() {
fun addMyHandler() {
// navigationEventDispatcher provided by the ComponentActivity
navigationEventDispatcher.addHandler(myNavigationEventHandler)
}
}
```
<br />
## Add a `NavigationEventInput`
Now that you've registered the handler, you are set up to receive events.
However, you need to provide a source from which the events are generated with
`NavigationEventInput`.
`NavigationEventInput` is the platform-specific component that receives
raw system input and translates it into a standard `NavigationEvent` to be sent
to the `NavigationEventDispatcher`.
The following example is a custom implementation of a `NavigationEventInput`:
```kotlin
public class MyInput : NavigationEventInput() {
@MainThread
public fun backStarted(event: NavigationEvent) {
dispatchOnBackStarted(event)
}
@MainThread
public fun backProgressed(event: NavigationEvent) {
dispatchOnBackProgressed(event)
}
@MainThread
public fun backCancelled() {
dispatchOnBackCancelled()
}
@MainThread
public fun backCompleted() {
dispatchOnBackCompleted()
}
}
```
<br />
Next, provide that input to your dispatcher:
```kotlin
navigationEventDispatcher.addInput(MyInput())
```
<br />
> [!NOTE]
> **Note:** To provide a simple input, use the [`DirectNavigationEventInput`](https://developer.android.com/reference/androidx/navigationevent/DirectNavigationEventInput) class.
## Clean up resources with `dispose()`
To prevent memory leaks in a dynamic UI, every created
`NavigationEventDispatcher` instance must be explicitly removed from the
hierarchy using the `dispose()` method when the component it is tied to is
destroyed:
```kotlin
navigationEventDispatcher.dispose()
```
<br />
The `dispose()` method ensures a *cascading cleanup* by iteratively removing
the dispatcher and all of its descendants (children and grandchildren),
guaranteeing that all associated handlers are unregistered from the shared
system.
### Dispatcher hierarchy and control
The `NavigationEventDispatcher` supports a parent-child hierarchy, enabling
components nested deep within a UI (such as nested `NavHost`s or dialogs) to
participate in navigation event handling.
#### Create a child dispatcher
A child dispatcher is created by passing a reference to its parent dispatcher
during construction. All dispatchers in a hierarchy share the same
`NavigationEventProcessor` to maintain a global **Last-In, First-Out (LIFO)**
event ordering based on priority.
#### Hierarchical enabling
The dispatcher includes an `isEnabled` property that allows developers to enable
or disable an entire subtree of handlers at once.
When a parent dispatcher is disabled (`isEnabled = false`), all handlers
associated with that parent and any of its children will be ignored, regardless
of their individual enabled state.
@@ -0,0 +1,217 @@
You can extend the abstract class `NavigationEventHandler` to handle navigation
events across platforms. This class provides methods corresponding to the
lifecycle of a navigation gesture.
```kotlin
val myHandler = object: NavigationEventHandler<NavigationEventInfo>(
initialInfo = NavigationEventInfo.None,
isBackEnabled = true
) {
override fun onBackStarted(event: NavigationEvent) {
// Prepare for the back event
}
override fun onBackProgressed(event: NavigationEvent) {
// Use event.progress for predictive animations
}
// This is the required method for final event handling
override fun onBackCompleted() {
// Complete the back event
}
override fun onBackCancelled() {
// Cancel the back event
}
}
```
<br />
The `addHandler` function connects the handler to the dispatcher:
```kotlin
navigationEventDispatcher.addHandler(myHandler)
```
<br />
Call `myHandler.remove()` to remove the handler from the dispatcher:
```kotlin
myHandler.remove()
```
<br />
Handlers are invoked based on priority, and then by recency. All
[`PRIORITY_OVERLAY`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher#PRIORITY_OVERLAY()) handlers are called before any [`PRIORITY_DEFAULT`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher#PRIORITY_DEFAULT())
handlers. Within each priority group, handlers are invoked in a Last-In,
First-Out (LIFO) order --- the most recently added handler is called first.
## Intercept back with Jetpack Compose
For Jetpack Compose, the library provides a utility composable to manage the
dispatcher hierarchy.
The `NavigationBackHandler` composable creates a `NavigationEventHandler` for
its content and links it to the `LocalNavigationEventDispatcherOwner`. It uses
Compose's `DisposableEffect` to automatically call the dispatcher's `dispose()`
method when the composable leaves the screen, safely managing resources.
```kotlin
@Composable
public fun NavigationBackHandler(
state: NavigationEventState<out NavigationEventInfo>,
isBackEnabled: Boolean = true,
onBackCancelled: () -> Unit = {},
onBackCompleted: () -> Unit,
){
}
```
<br />
This function lets you control event handling precisely within localized UI
subtrees.
```kotlin
@Composable
fun HandlingBackWithTransitionState(
onNavigateUp: () -> Unit
) {
val navigationState = rememberNavigationEventState(
currentInfo = NavigationEventInfo.None
)
val transitionState = navigationState.transitionState
// React to predictive back transition updates
when (transitionState) {
is NavigationEventTransitionState.InProgress -> {
val progress = transitionState.latestEvent.progress
// Use progress (0f..1f) to update UI during the gesture
}
is NavigationEventTransitionState.Idle -> {
// Reset any temporary UI state if the gesture is cancelled
}
}
NavigationBackHandler(
state = navigationState,
onBackCancelled = {
// Called if the back gesture is cancelled
},
onBackCompleted = {
// Called when the back gesture fully completes
onNavigateUp()
}
)
}
```
<br />
This example shows how to observe predictive back gesture updates using
[`NavigationEventTransitionState`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventTransitionState). The `progress` value can be used to
update UI elements in response to the back gesture, while handling completion
and cancellation through `NavigationBackHandler`.
### Access the back gesture or swipe edge in Compose
> [!NOTE]
> **Note:** For Android, if you're already using a navigation library with built-in Predictive Back support, like [Navigation 3](https://developer.android.com/guide/navigation/navigation-3/animate-destinations), use that instead of implementing the guidance here. The following section shows how to create a Predictive Back animation using only `NavigationEvent` and Compose.
**Figure 1** . A predictive back animation built with `NavigationEvent` and Compose.
To animate the screen while the user swipes back, you'll need to (a) check if
the `NavigationEventTransitionState` is `InProgress`, and (b) observe the
progress and swipe edge state with `rememberNavigationEventState`:
- `progress`: A Float from `0.0` to `1.0` indicating how far the user has swiped.
- `swipeEdge`: An integer constant (`EDGE_LEFT` or `EDGE_RIGHT`) indicating where the gesture started.
The following snippet is a simplified example of how to implement a scale and
shift animation:
```kotlin
object Routes {
const val SCREEN_A = "Screen A"
const val SCREEN_B = "Screen B"
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var state by remember { mutableStateOf(Routes.SCREEN_A) }
val backEventState = rememberNavigationEventState<NavigationEventInfo>(currentInfo = NavigationEventInfo.None)
when (state) {
Routes.SCREEN_A -> {
ScreenA(onNavigate = { state = Routes.SCREEN_B })
}
else -> {
if (backEventState.transitionState is NavigationEventTransitionState.InProgress) {
ScreenA(onNavigate = { })
}
ScreenB(
backEventState = backEventState,
onBackCompleted = { state = Routes.SCREEN_A }
)
}
}
}
}
}
@Composable
fun ScreenB(
backEventState: NavigationEventState<NavigationEventInfo>,
onBackCompleted: () -> Unit = {},
) {
val transitionState = backEventState.transitionState
val latestEvent =
(transitionState as? NavigationEventTransitionState.InProgress)
?.latestEvent
val backProgress = latestEvent?.progress ?: 0f
val swipeEdge = latestEvent?.swipeEdge ?: NavigationEvent.EDGE_LEFT
if (transitionState is NavigationEventTransitionState.InProgress) {
Log.d("BackGesture", "Progress: ${transitionState.latestEvent.progress}")
} else if (transitionState is NavigationEventTransitionState.Idle) {
Log.d("BackGesture", "Idle")
}
val animatedScale by animateFloatAsState(
targetValue = 1f - (backProgress * 0.1f),
label = "ScaleAnimation"
)
val windowInfo = LocalWindowInfo.current
val density = LocalDensity.current
val maxShift = remember(windowInfo, density) {
val widthDp = with(density) { windowInfo.containerSize.width.toDp() }
(widthDp.value / 20f) - 8
}
val offsetX = when (swipeEdge) {
NavigationEvent.EDGE_LEFT -> (backProgress * maxShift).dp
NavigationEvent.EDGE_RIGHT -> (-backProgress * maxShift).dp
else -> 0.dp
}
NavigationBackHandler(
state = backEventState,
onBackCompleted = onBackCompleted,
isBackEnabled = true
)
Box(
modifier = Modifier
.offset(x = offsetX)
.scale(animatedScale)
){
// Rest of UI
}
}
```
<br />
@@ -0,0 +1,83 @@
Navigation Event is a library that provides a Kotlin Multiplatform (KMP)
solution for integrating system-level navigation events into your application.
It is designed to be the foundational layer for handling navigation directions
across various [supported platforms](https://developer.android.com/kotlin/multiplatform#kotlin-multiplatform-and-jetpack-libraries).
## Key concepts
The Navigation Event system is built around a centralized dispatcher-handler
model, often used in a parent-child hierarchy to map to complex UI structures,
such as those found in Jetpack Compose.
### `NavigationEventDispatcher`
The [`NavigationEventDispatcher`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher) is the central class responsible for
managing all registered navigation event consumers
([`NavigationEventHandler`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventHandler))) and orchestrating the flow of events.
In a hierarchical setup, all dispatchers within the same chain share a single
`NavigationEventProcessor`, which manages the global state and ensures a single,
unified dispatching order across the entire tree.
### `NavigationEventHandler`
`NavigationEventHandler` is an abstract class that receives and handles
navigation events dispatched by a `NavigationEventDispatcher`. It defines
callback methods that correspond to different stages of a navigation gesture
lifecycle, such as when a gesture starts, progresses, completes, or is
cancelled.
Handlers can respond to these events to update UI or application state in
response to user navigation actions. Multiple handlers can be registered with a
dispatcher and are invoked based on priority and registration order.
### `NavigationEvent`
[`NavigationEvent`](https://developer.android.com/reference/androidx/navigationevent/NavigationEvent) is a data class that carries the details of the
navigation gesture.
### `NavigationEventInfo`
[`NavigationEventInfo`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventInfo) is an abstract class that provides contextual
information about a navigation state.
### `NavigationEventInput`
[`NavigationEventInput`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventInput) is an abstract class for components that generate
and dispatch navigation events. It acts as the "input" side of the navigation
system, translating platform-specific events (like system back gestures or
button clicks) into standardized events that can be sent to a
`NavigationEventDispatcher`.
## Supported navigation directions and triggers
The Navigation Event system is designed to encompass more than just the system
back button, with designs supporting multiple navigation directions and input
methods across platforms.
### Supported directions
Different platforms support varying navigation directions:
|---|---|---|---|---|
| **Platform** | **Back** | **Up** | **Forward** | **Home** |
| **Android phone** | ✅ | ✅ | 🚫 | ✅ |
| **Android tablet** | ✅ | ✅ | 🚫 | ✅ |
| **Web (Browser)** | ✅ | ✅ | ✅ | 🚫 |
| **iOS (iPhone/iPad)** | ✅ | 🚫 | ✅ | ✅ |
### Supported triggers
Input handling is achieved through various mechanisms on each platform:
|---|---|---|---|
| **Trigger** | **Android Phone** | **Web (Browser)** | **iOS (iPhone/iPad)** |
| **Keyboard back button** | ✅ Back | ❓ | ✅ Back |
| **Software back button** | 🚫 | ✅ Back | ✅ Back |
| **Software up button** | ✅ Up | 🚫 | 🚫 |
| **Gesture from left** | ✅ Back | ❓ | ✅ Back |
| **Gesture from right** | ✅ Back | ❓ | ✅ Forward |
| **Gesture from bottom** | ✅ Home | 🚫 | ✅ Home |
> [!NOTE]
> **Note:** The **Web** platform has unique navigation handling, where the browser controls the back stack state. This requires synchronization between the browser window and the application's navigation stack. The question mark represents behavior that is inconsistent because web browsers don't have a single, unified "back" button or gesture.
@@ -0,0 +1,32 @@
To set up your development environment for `NavigationEvent`, follow these
steps.
## Declare dependencies
1. Add the `navigationevent` artifact to your project. This is the core library
containing the shared `NavigationEventDispatcher` and `NavigationEventHandler`
classes.
For Jetpack Compose integration, you also need to add the corresponding
Compose artifact:
[versions]
navigationevent = "1.0.0"
[libraries]
# NavigationEvent libraries
androidx-navigationevent = { module = "androidx.navigationevent:navigationevent", version.ref = "navigationevent" }
androidx-navigationevent-compose = { module = "androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" }
2. Update your compile SDK to 36 or above:
[versions]
compileSdk = "36"
3. Add the following to your app build file, `app/build.gradle.kts`:
dependencies {
...
implementation(libs.androidx.navigationevent)
implementation(libs.androidx.navigationevent.compose)
}
+22 -15
View File
@@ -7,7 +7,7 @@ description: Analyzes Android build files and R8 keep rules to identify redundan
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-09'
last-updated: '2026-08-28'
keywords:
- R8
- proguard
@@ -20,33 +20,40 @@ metadata:
- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`.
- Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations.
- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement
performance](references/android/topic/performance/app-optimization/enable-app-optimization.md)
- **AGP** : If version is lower than 9.0, suggest migration to 9.0 for [build-time performance improvement](references/android/topic/performance/app-optimization/enable-app-optimization.md).
- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties.
## Step 2. Analysis path selection
- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and
`libs.versions.toml` to get the R8 version
`libs.versions.toml` to get the AGP and R8 versions.
- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**.
- **If AGP \>= 9.3.0** : Proceed to **Path A (Standalone Task)**.
- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**.
- **If AGP \< 9.3.0 and R8 \>= 9.3.7-dev** : Proceed to **Path B (Quantitative)**.
### Path A: Quantitative data generation (R8 \>= 9.3.7-dev)
- If none of the conditions are met, proceed to **Path C (Heuristic)**.
- **Check requirements** : Python and `protobuf` package are mandatory.
- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result.
- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3.
### Path A: Standalone Gradle task (AGP \>= 9.3.0)
### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev)
- **Step 1: Run standalone task** : Run `./gradlew :app:analyzeReleaseR8Config` to evaluate the R8 configuration. You MUST wait for this command to finish before proceeding.
- **Step 2: Convert to JSON** : The report is generated at `app/build/reports/r8/r8-config-analyzer-release.pb`. You MUST explicitly run the conversion script by executing: `python3 .agents/skills/r8-analyzer/scripts/convert_pb_to_json.py`. Wait for this command to finish.
- **Step 3: Analyze** : You MUST explicitly run the analysis script by executing: `python3 .agents/skills/r8-analyzer/scripts/analyze.py`. This outputs `tmp/keepradius/analysis_result.txt`. Wait for this command to finish.
### Path B: Quantitative data generation (R8 \>= 9.3.7-dev and AGP \< 9.3.0)
- **Step 1: Check requirements** : Python and `protobuf` package are mandatory.
- **Step 2: Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to JSON and analyze the result.
- **Step 3: Analyze** : You MUST ensure the analysis produces `tmp/keepradius/analysis_result.txt` for scores and rule impact metrics.
### Path C: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev)
*(Use ONLY if quantitative data generation is not possible)*
- **Manual evaluation** : Inspect `proguard-rules.pro`.
- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules.
- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide).
- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3.
- **Step 1: Manual evaluation** : Inspect `proguard-rules.pro`.
- **Step 2: Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules.
- **Step 3: Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide).
- **Step 4: Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3.
## Step 3. Report generation
@@ -1,3 +1,15 @@
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/performance/r8-analyzer)
### R8 Analyzer
Use the R8 Analyzer [Android skill](https://developer.android.com/tools/agents/android-skills) to analyze build files and identify redundant, broad, or subsumed [keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview). To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add r8-analyzer
<br />
For the best user experience, you should optimize your app to make it as small
and fast as possible. Our app optimizer, called R8, streamlines your app by
removing unused code and resources, rewriting code to optimize runtime
@@ -12,9 +24,6 @@ performance, and more. To your users, this means:
> **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the
> optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization).
> [!IMPORTANT]
> **Important:** We released an agent skill that you can use to improve your app performance with R8. Try out the skill from the [Android skills repository](https://github.com/android/skills).
## R8 optimization overview
R8 uses a multi-phase process to optimize your app for size and speed. Key
@@ -47,62 +56,97 @@ resource shrinking](https://developer.android.com/topic/performance/app-optimiza
## Enable optimization
To enable app optimization, set `isMinifyEnabled = true` (for code optimization)
and `isShrinkResources = true` (for resource optimization) in your [release
build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script as shown in the following code. We recommend
that you always enable both settings. We also recommend enabling app
optimization only in the final version of your app that you test before
publishing---usually your release build---because the optimizations increase the
build time of your project and can make debugging harder due to the way it
modifies code.
To enable app optimization for your app use the appropriate DSL based on your
project's AGP version. We recommend enabling app optimization only in the final
version of your app that you test before publishing---usually your release
build---because the optimizations increase the build time of your project and can
make debugging harder due to the way it modifies code.
### For AGP versions 9.3 and higher
To enable app optimization for AGP 9.3 and higher:
- Set `enable = true` in the `optimization` block in your release build's app-level build script.
- Add your [keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to a file with the suffix `.keep` in the `src/<variant>/keepRules` source set. For example, `src/main/keepRules/custom-rules.keep`.
> [!NOTE]
> **Note:** The updated DSL enables both code and resource optimization. It also includes a set of default keep rules for the Android platform, equivalent to "proguard-android-optimize.txt". To omit the default rules, see [Omit default
> rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview#omit-default).
### Kotlin
```kotlin
android {
buildTypes {
release {
// Enables code-related app optimization.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default file with automatically generated optimization rules.
getDefaultProguardFile("proguard-android-optimize.txt"),
...
)
...
android {
buildTypes {
release {
optimization {
enable = true // Enables code and resource optimizations.
}
}
}
}
...
}
```
### Groovy
```groovy
android {
buildTypes {
release {
// Enables code-related app optimization.
minifyEnabled = true
// Enables resource shrinking.
shrinkResources = true
// Default file with automatically generated optimization rules.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
...
android {
buildTypes {
release {
optimization {
enable = true // Enables code and resource optimizations.
}
}
}
}
### Legacy DSL for AGP versions lower than 9.3
To enable app optimization, set `isMinifyEnabled = true` (for code optimization)
and `isShrinkResources = true` (for resource optimization) in your [release
build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script. We recommend that you always enable both
settings.
### Legacy DSL (Kotlin)
android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default file with automatically generated optimization rules.
getDefaultProguardFile("proguard-android-optimize.txt"),
...
)
...
}
}
...
}
### Legacy DSL (Groovy)
android {
buildTypes {
release {
// Enables code optimizations.
minifyEnabled = true
// Enables resource shrinking.
shrinkResources = true
// Default file with automatically generated optimization rules.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
...
}
}
}
}
```
## Improve R8 optimization
@@ -128,10 +172,8 @@ R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https
## Optimize resource shrinking for even smaller apps
The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource
shrinking, which aims to integrate resource and code optimization to create even
smaller and faster apps.
The 8.12.0 version of AGP introduces optimized resource shrinking, which aims to
integrate resource and code optimization to create even smaller and faster apps.
Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2)
generated keep rules that effectively treating resource shrinking separately
from code, often retaining inaccessible code or resources that referenced each
@@ -159,7 +201,7 @@ configuration.
To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the
following line from your project's `gradle.properties` file, if it exists:
android.enableR8.fullMode=false # Remove this line from your codebase.
android.enableR8.fullMode=false
Note that enabling app optimization makes stack traces difficult to understand,
especially if R8 renames class or method names. To get stack traces that
@@ -190,9 +232,21 @@ the Android Gradle Plugin (AGP) and the R8 compiler.
| AGP version | Features introduced |
|---|---|
| 9.3 | **Simplified DSL:** New \`optimization {}\` block enables requires less configuration- optimized resource shrinking is always enabled, default Android keep rules are opt-out. <br /> **\`keepRules\` source set:** If you use the new DSL, you must place your keep rules in the `src/<variant>/keepRules` source set, in files with the suffix `.keep`. The source sets are also compatible with the legacy DSL. <br /> **KMP (Kotlin MultiPlatform) consumer rules:** KMP no longer requires setting `publish = true`, and supports the `src/<variant>/keepRules` source set with the `.keep` extension. <br /> Note that the legacy DSL is still supported. |
| 9.1 | **Classes repackaged by default:** R8 repackages classes (moving them to the unnamed package, at the top level) to compact DEX further, eliminating the need to specify `-repackageclasses` option. For information about how this works and how to opt out, see [global options](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). |
| 9.0 | **Optimized resource shrinking:** Enabled by default (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline, leading to smaller, faster apps. By optimizing both code and resource references simultaneously, it identifies and removes resources referenced exclusively from unused code. This is a significant improvement over the previous separate optimization processes. This is especially useful for apps that share substantial resources and code across different form factor verticals, with measured improvements of over 50% in app size. The resulting size reduction leads to smaller downloads, faster installations, and a better user experience with faster startup, improved rendering, and fewer ANRs. **Library rule filtering:** Support for global options (for example, `-dontobfuscate`) in library consumer rules has been dropped, and apps will filter them out. For more information, see [Add global options](https://developer.android.com/topic/performance/app-optimization/global-options). **Kotlin null checks:** Optimized by default (controlled using `-processkotlinnullchecks`). This version also introduced significant improvements in build speed. For more information, see [Global options for additional optimization](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). **Optimize specific packages:** You can use `packageScope` to optimize specific packages. This is in experimental support. For more information, see [Optimize specified packages with `packageScope`](https://developer.android.com/topic/performance/app-optimization/optimize-specified-packages). **Optimized by default:** Support for `getDefaultProguardFile("proguard-android.txt")` has been dropped, because it includes `-dontoptimize`, which should be avoided. Instead, use `"proguard-android-optimize.txt"`. If you need to globally disable optimization in your app, [add the flag manually to a proguard file](https://developer.android.com/topic/performance/app-optimization/global-options#global-options-2). |
| 8.12 | **Optimized resource shrinking:** Initial support added (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline. You must manually enable it in this version of AGP. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). |
| 8.6 | **Improved retracing:** Includes filename and line number retracing by default for all `minSdk` levels (previously required `minSdk` 26+ in version 8.2). Updating R8 helps ensure that stack traces from obfuscated builds are readily and clearly readable. This version improves how line numbers and source files are mapped, making it easier for tools like the Android Studio Logcat to automatically retrace crashes to the original source code. |
| 8.0 | **Full mode by default:** [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) provides significantly more powerful optimization. It is enabled by default. You can opt out using `android.enableR8.fullMode=false`. |
| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. |
| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. |
## Use R8 with other build systems
While AGP is the recommended and officially supported build system for Android
apps, you might use an alternative build system like [Bazel](https://bazel.build/). If
you're using Bazel, you can integrate R8 into your build pipeline to shrink,
obfuscate, and optimize your app.
For information about optimizing an Android app using Bazel, see Bazel's
[`rules_android` documentation](https://github.com/bazelbuild/rules_android/tree/main/docs/r8-optimization.md). Note that Bazel isn't
[officially supported](https://developer.android.com/build#other-build-systems) for Android app development.
+48 -50
View File
@@ -6,7 +6,7 @@ description: Helps developers integrate, debug, and resolve Play Engage SDK impl
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
last-updated: '2026-06-09'
last-updated: '2026-09-01'
keywords:
- android
- engage
@@ -23,85 +23,83 @@ required Engage entities for each vertical.
Follow these steps to assist the developer:
1. **Identify Vertical and Cluster:**
1. **Identify vertical and cluster:**
- Ask the developer which vertical their app belongs to based on **[references/schemas/](references/schemas)**.
- Check if the integration is for TV or Mobile. Read the TV-specific sections in [patterns.md](references/patterns.md) as well if the integration is for TV.
- Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. (e.g., `com.google.android.engage.food.service.AppEngageFoodClient`).
- **Note:** Initializing the client class requires a `Context` parameter (e.g., `AppEngageFoodClient(context)`).
- Check if the integration is for TV or mobile. If the integration is for TV, read the TV-specific sections in [patterns.md](references/patterns.md) as well.
- Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. For example, `com.google.android.engage.food.service.AppEngageFoodClient`.
- **Note:** Initializing the client class requires a `Context` parameter. For example, `AppEngageFoodClient(context)`.
- Always refer to [common.md](references/common.md) for common entities.
- Ask which cluster type they want to publish from the supported cluster types for that vertical.
- Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects.
- Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters.
2. **Generate Structured Boilerplate Code:**
2. **Generate structured boilerplate code:**
- Create a new directory for all Engage-related code. Name the directory to match the naming convention of the existing codebase.
- Generate the following classes using templates in [patterns.md](references/patterns.md):
- `Constants`: Holds constant values like attempt counts, publish types.
- `ItemToEntityConverter`: Converts app's local models to Engage's Entity models.
- `ClusterRequestFactory`: Constructs the publish requests.
- `EngageWorker`: Handles the actual publishing and publish errors using WorkManager.
- `EngagePublisher`: Orchestrates periodic and one-time jobs.
- `EngageBroadcastReceiver`: Listens for AppEngageService intents and starts a one-time publish job from `EngagePublisher`. **Important** : Implement both **static registration** and **dynamic registration** patterns, including the companion object `register` method inside the `EngageBroadcastReceiver` class.
3. **Suggest Entity Mapping:**
- `Constants`: holds constant values such as attempt counts and publish types.
- `ItemToEntityConverter`: converts the app's local models to Engage's Entity models.
- `ClusterRequestFactory`: constructs the publish requests.
- `EngageWorker`: handles the actual publishing and publish errors using WorkManager.
- `EngagePublisher`: orchestrates periodic and one-time jobs.
- `EngageBroadcastReceiver`: listens for AppEngageService intents and starts a one-time publish job from `EngagePublisher`. **Important** : Implement both **static registration** and **dynamic registration** patterns, including the companion object `register` method inside the `EngageBroadcastReceiver` class.
3. **Suggest entity mapping:**
- Ask the developer to provide their local model schema(e.g., a data class or a JSON snippet).
- Ask the developer to provide their local model schema (for example, a data class or a JSON snippet).
- If they haven't provided one, share entities from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory as a guide.
- Once the local model is identified, suggest a mapping to the corresponding Engage entity.
- Generate the conversion logic using the `ItemToEntityConverter` pattern in [patterns.md](references/patterns.md) and add it to the generated `{ENGAGE_CODE_DIR}/ItemToEntityConverter`
4. **Suggest Data Source:**
- Generate the conversion logic using the `ItemToEntityConverter` pattern in [patterns.md](references/patterns.md) and add it to the generated `{ENGAGE_CODE_DIR}/ItemToEntityConverter`.
4. **Suggest data source:**
- Ask the developer to provide the source of actual data you'll publish.
- Once the source of data is identified, use the source of data to fetch data in app's local model schema.
- Use `{ENGAGE_CODE_DIR}/ItemToEntityConverter` to convert this data to Engage entity.
- Once you identify the data source, use it to fetch the data in the app's local model schema.
- Use `{ENGAGE_CODE_DIR}/ItemToEntityConverter` to convert this data to an Engage entity.
- Use obtained Engage entity model data with `{ENGAGE_CODE_DIR}/
ClusterRequestFactory` to get cluster requests.
- Call corresponding cluster publishing method obtained from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory with the obtained request in previous step in `{ENGAGE_CODE_DIR}/EngageWorker`.
5. **Gradle and Manifest Updates:**
5. **Gradle and manifest updates:**
- Suggest updates to `build.gradle` and `AndroidManifest.xml`.
- For mobile apps, use [patterns.md](references/patterns.md).
- For TV apps, use the TV-specific sections in [patterns.md](references/patterns.md).
- Provide the necessary `implementation` dependencies for `build.gradle` or `build.gradle.kts` from [patterns.md](references/patterns.md).
- Provide the `<receiver>` and `<service>` declarations for `AndroidManifest.xml`.
- Note: There's no separate import according to vertical except TV. For each vertical other than TV 'com.google.android.engage:engage-core:1.5.12' is enough.
6. **Debugging:**
- Note: Except for TV, there aren't any vertical-specific imports. For all other verticals, `com.google.android.engage:engage-core:1.6.0` is sufficient.
6. **Debugging and verification**:
- **Self-verification checklist** : Before considering your work complete, you must verify that you've implemented all of the following:
- \[ \] Registered `EngageBroadcastReceiver` **statically** in `AndroidManifest.xml` (inside the `<application>` tag).
- \[ \] Registered `EngageBroadcastReceiver` **dynamically** by calling `EngageBroadcastReceiver.register(context)` in the `Application` class or main `Activity` class.
- \[ \] Implemented the `register` method in `EngageBroadcastReceiver`'s `companion object` to handle dynamic registration.
- \[ \] Handled empty data lists in `EngageWorker` (for example, by deleting the cluster instead of publishing empty data).
- \[ \] Used `--no-daemon` for all Gradle compilations.
- Perform a Gradle sync.
- If errors occur, follow this resolution order:
- Fix import errors. For package `com.google.android.engage` or classes starting with `AppEngage`, verify the package name in the `{VERTICAL}.md` in **[references/schemas/](references/schemas)** directory or [common.md](references/common.md).
- Fix any other errors.
- Execute a full Gradle build and resolve any remaining compilation issues. Repeat this step until the Gradle build is successful.
7. **User Checklist:**
At the end of code generation, notify the user to go through this checklist
to verify that the integration is complete and as intended:
\[ \] Verify that all the engage related files are created in
`{ENGAGE_CODE_DIR}/`:
- `Constants`
- `ItemToEntityConverter`
- `ClusterRequestFactory`
- `EngageWorker`
- `{cluster_type}Publisher`
- `EngageBroadcastReceiver`
\[ \] Verify that app's local model is converted to Engage entity by populating
the fields correctly in the model in `{ENGAGE_CODE_DIR}/
ItemToEntityConverter`.
\[ \] Verify that `{ENGAGE_CODE_DIR}/EngageWorker` uses the data source
identified in Step 4.
\[ \] Verify that `EngageBroadcastReceiver.register(context)` is called within
the `Application` class or `MainActivity` to register the receiver
dynamically.
\[ \] Verify that `AndroidManifest.xml` contains the static `<receiver>`
declaration for `EngageBroadcastReceiver` with the necessary intent actions.
- If errors occur (such as import failures, namespace conflicts, or compile errors), read **[references/troubleshooting.md](references/troubleshooting.md)** to resolve them.
- Execute a Gradle compilation. You must run `./gradlew compileDebugUnitTestSources --no-daemon` or `./gradlew assembleDebug --no-daemon`. See **[references/troubleshooting.md](references/troubleshooting.md)** for compile rules and warnings about fast-compilation shortcuts. Repeat this step until compilation is successful.
7. **User checklist:** At the end of code generation, notify the user to go
through this checklist to verify that the integration is complete and as
intended:
- \[ \] Verify that all the Engage-related files are created in `{ENGAGE_CODE_DIR}/`:
- `Constants`
- `ItemToEntityConverter`
- `ClusterRequestFactory`
- `EngageWorker`
- `{cluster_type}Publisher`
- `EngageBroadcastReceiver`
- \[ \] Verify that app's local model is converted to Engage entity by populating the fields correctly in the model in `{ENGAGE_CODE_DIR}/ItemToEntityConverter`.
- \[ \] Verify that all image URIs in `ItemToEntityConverter` point to images matching the strict aspect ratio requirements of the vertical (for example, 16:9, 1:1, 2:3).
- \[ \] Verify that `{ENGAGE_CODE_DIR}/EngageWorker` uses the data source identified in Step 4.
- \[ \] Verify that `EngageBroadcastReceiver.register(context)` is called within the `Application` class or `MainActivity` to register the receiver dynamically.
- \[ \] Verify that `AndroidManifest.xml` contains the static `<receiver>` declaration for `EngageBroadcastReceiver` with the necessary intent actions.
- **Important** : Explicitly instruct the developer to call `EngageBroadcastReceiver.register(context)` inside their custom `Application` class `onCreate()` (or their main activity `onCreate()`) to dynamically register the receiver. Stress that **both** static and dynamic registrations are required for the integration to function.
## Reference Materials
## Reference materials
- **FAQ:** [Engage FAQ](references/android/guide/playcore/engage/faq.md) - Refer to this document for answers to frequently
asked questions from developers.
- **Vertical-Specific Guides:**
- **Vertical-specific guides:**
- [Food Vertical](references/android/guide/playcore/engage/food.md)
- [Watch Vertical](references/android/guide/playcore/engage/watch.md)
@@ -110,13 +108,13 @@ Follow these steps to assist the developer:
- [Shopping Vertical](references/android/guide/playcore/engage/shopping.md)
- [Social Vertical](references/android/guide/playcore/engage/social.md)
- [Travel Vertical](references/android/guide/playcore/engage/travel.md)
- [Health \& Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md)
- [Health and Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md)
- [Other Verticals](references/android/guide/playcore/engage/otherverticals.md)
- [TV Getting Started](references/android/guide/playcore/engage/tv/getting-started.md)
- [TV Recommendations](references/android/guide/playcore/engage/tv/recommendations.md)
- [TV Continue Watching](references/android/guide/playcore/engage/tv/continue-watching/index.md)
- [TV Entitlements](references/android/guide/playcore/engage/tv/entitlements.md)
- **Vertical-Specific Schemas:**
- **Vertical-specific schemas:**
- [Food Schema](references/schemas/food.md)
- [Watch Schema](references/schemas/watch.md)
@@ -68,7 +68,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
### Summary
@@ -294,6 +294,113 @@ There are following APIs to publish clusters in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| Food Reorder Cluster | `TYPE_FOOD_REORDER` | 5 |
| Food Shopping Cart Cluster | `TYPE_FOOD_SHOPPING_CART` | 6 |
| Food Shopping List Cluster | `TYPE_FOOD_SHOPPING_LIST` | 7 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Food Shopping
Cart, Food Shopping List, and Reorder) will be still enabled by default
configuration, and the rest of the cluster types will be enabled and disabled
intermittently. If intermittent ingestion applies to you but this default
configuration is not suitable for your needs, please contact
engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
@@ -89,7 +89,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
### Summary
@@ -343,6 +343,110 @@ There are following APIs to publish clusters in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Continue
Reading and Event Reservations) will be still enabled by default
configuration, and the rest of the cluster types will be enabled and disabled
intermittently. If intermittent ingestion applies to you but this default
configuration is not suitable for your needs, please contact
engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
@@ -53,7 +53,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
### Summary
@@ -363,14 +363,117 @@ APIs are available in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Continue
Listening) will be still enabled by default configuration, and the rest of
the cluster types will be enabled and disabled intermittently. If
intermittent ingestion applies to you but this default configuration is not
suitable for your needs, please contact engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Handle IPC call success
if(task.result) {
// Service is available on the device, proceed with content
// publish calls.
// Service is available on the device, proceed with content publish
// calls.
} else {
// Service is not available, no further action is needed.
}
@@ -129,7 +129,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
### Summary
@@ -581,6 +581,119 @@ There are following APIs to publish clusters in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| Shopping Cart Cluster | `TYPE_SHOPPING_CART` | 4 |
| Food Reorder Cluster | `TYPE_FOOD_REORDER` | 5 |
| Food Shopping Cart Cluster | `TYPE_FOOD_SHOPPING_CART` | 6 |
| Food Shopping List Cluster | `TYPE_FOOD_SHOPPING_LIST` | 7 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Shopping List Cluster | `TYPE_SHOPPING_LIST` | 9 |
| Shopping Reorder Cluster | `TYPE_SHOPPING_REORDER` | 10 |
| Shopping Order Tracking Cluster | `TYPE_SHOPPING_ORDER_TRACKING` | 11 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
| Continue Search Cluster | `TYPE_CONTINUE_SEARCH` | 13 |
| Reservation Cluster | `TYPE_RESERVATION` | 14 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Continue
Reading and Reservations) will be still enabled by default configuration,
and the rest of the cluster types will be enabled and disabled
intermittently. If intermittent ingestion applies to you but this default
configuration is not suitable for your needs, please contact
engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
@@ -55,7 +55,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
### Summary
@@ -225,14 +225,117 @@ APIs are available in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Continue
Reading) will be still enabled by default
configuration, and the rest of the cluster types will be enabled and disabled
intermittently. If intermittent ingestion applies to you but this default
configuration is not suitable for your needs, please contact
engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Handle IPC call success
if(task.result) {
// Service is available on the device, proceed with content
// publish calls.
// Service is available on the device, proceed with content publish
// calls.
} else {
// Service is not available, no further action is needed.
}
@@ -89,7 +89,7 @@ Add the `com.google.android.engage:engage-core` library to your app:
dependencies {
// Make sure you also include that repository in your project's build.gradle file.
implementation 'com.google.android.engage:engage-core:1.5.12'
implementation 'com.google.android.engage:engage-core:1.6.0'
}
For more information, see [Package visibility in Android
@@ -263,6 +263,114 @@ Following APIs are exposed to publish clusters in the client:
This API is used to check if the service is available for integration and
whether the content can be presented on the device.
##### For Engage SDK v1.6.0 and higher (Recommended)
## Android skills
[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration)
### Engage SDK integration
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types.
<br />
You can check the service availability for every cluster type that you intend to
publish. The `isServiceAvailable` API accepts a request object,
`ServiceAvailabilityRequest`, which contains the cluster types for which service
availability needs to be checked. You can find the `ClusterType` enum values
required for `ServiceAvailabilityRequest` from the following table.
| Cluster Type | Cluster Type Constant | Integer Value |
|---|---|---|
| Unknown | `TYPE_UNKNOWN` | 0 |
| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 |
| Featured Cluster | `TYPE_FEATURED` | 2 |
| Continuation Cluster | `TYPE_CONTINUATION` | 3 |
| Shopping Cart Cluster | `TYPE_SHOPPING_CART` | 4 |
| User Management Cluster | `TYPE_ENGAGEMENT` | 8 |
| Shopping List Cluster | `TYPE_SHOPPING_LIST` | 9 |
| Shopping Reorder Cluster | `TYPE_SHOPPING_REORDER` | 10 |
| Shopping Order Tracking Cluster | `TYPE_SHOPPING_ORDER_TRACKING` | 11 |
| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 |
### Kotlin
val request = ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build()
client.isServiceAvailable(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
val availabilityMap = task.result
if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) {
// Proceed with publishing continuation content
}
if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
}
### Java
ServiceAvailabilityRequest request =
new ServiceAvailabilityRequest.Builder()
.addIntendedClusterType(ClusterType.TYPE_CONTINUATION)
.addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION)
.build();
client.isServiceAvailable(request).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Map<Integer, Boolean> availabilityMap = task.getResult();
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) {
// Proceed with publishing continuation content
}
if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) {
// Proceed with publishing recommendation content
}
} else {
// The IPC call itself fails, proceed with error handling logic here,
// such as retry.
}
});
###### Conditional Service Availability Feature
Some integrated apps request a special configuration that enables and disables
the Engage service intermittently in order to reduce their serving cost. This
intermittent content ingestion strategy, although possible, negatively affects
the user and the product -- stale content will not be presented and some surfaces
will not be served at all.
Starting with v1.6.0, the Engage SDK allows checking availability for specific
cluster types. This provides more flexibility so that if the intermittent
content strategy was adopted by a given application, some cluster types can
follow that intermittent strategy while other cluster types are always enabled
(i.e. continuation clusters).
If the Engage service should not be 'continuously' enabled on all supported
devices for whatever reason, and is configured for intermittent ingestion for
any set of devices, all continuation cluster publications (e.g. Shopping
Cart, Shopping List, Reorder, and Shopping Order Tracking) will be still
enabled by default configuration, and the rest of the cluster types will be
enabled and disabled intermittently. If intermittent ingestion applies to
you but this default configuration is not suitable for your needs, please
contact engage-developers@google.com.
##### For SDK versions prior to v1.6.0 (Deprecated)
### Kotlin
client.isServiceAvailable.addOnCompleteListener { task ->
@@ -286,8 +394,8 @@ whether the content can be presented on the device.
if (task.isSuccessful()) {
// Handle success
if(task.getResult()) {
// Service is available on the device, proceed with content
// publish calls.
// Service is available on the device, proceed with content publish
// calls.
} else {
// Service is not available, no further action is needed.
}

Some files were not shown because too many files have changed in this diff Show More