mirror of
https://github.com/android/skills.git
synced 2026-09-14 20:07:09 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bac232fd02 | |||
| ac4238481a | |||
| 725364add9 | |||
| ea05a53683 | |||
| aaca635061 | |||
| a8e8d000ef | |||
| f87c432be4 | |||
| 1c807685ca | |||
| eb1c09b18c | |||
| 6685cac292 | |||
| 86932856ef | |||
| 1e5e7ae613 | |||
| 7311f4905d | |||
| 28822b2306 | |||
| 63123be36f | |||
| 9f80a958c4 | |||
| 6fd7e4d545 | |||
| ba0042c08b | |||
| 23d9eae21a | |||
| 4e1674995b | |||
| bc6cd7246a | |||
| 47e1dff74a | |||
| 57ff3c7d02 | |||
| aaf42b970f | |||
| 79bee216c4 | |||
| 07302ca15e |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
+5
-7
File diff suppressed because one or more lines are too long
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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 />
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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!\"}'"`
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+832
@@ -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**
|
||||
.
|
||||
|
||||
`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).
|
||||
|
||||
 **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).
|
||||
 **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)
|
||||
+219
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+45
-28
@@ -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
|
||||
|
||||
|
||||
+22
-9
@@ -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).
|
||||
 **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:
|
||||
 **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:
|
||||
 **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:
|
||||
 **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
|
||||
 **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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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)`
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
 **Figure 2.** Second item placed in the `FlexBox` container after the first item.
|
||||
 **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
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
+182
-5
@@ -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 />
|
||||
|
||||
 **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 />
|
||||
|
||||
 **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 />
|
||||
|
||||
 **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 />
|
||||
|
||||
 **Figure 4**. Intrinsic sizes specified in the columns.
|
||||
 **Figure 5**. Intrinsic sizes specified in the columns.
|
||||
|
||||
## Set gaps between rows and columns
|
||||
|
||||
@@ -206,7 +288,7 @@ Grid(
|
||||
|
||||
<br />
|
||||
|
||||
 **Figure 5**. Gaps between rows and columns.
|
||||
 **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.
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
+1
-1
@@ -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 |
|
||||
|
||||
|
||||
+4
-2
@@ -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 />
|
||||
|
||||
 **Figure 4** . Card **#3** is placed next to card **#1**, as it is an auto-placement.
|
||||
 **Figure 4** . Card **#3** is placed next to Card **#1**, as it is an auto-placement.
|
||||
+4
@@ -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
|
||||
|
||||
+36
-22
@@ -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.
|
||||
@@ -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`.
|
||||
|
||||
+17
-27
@@ -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 />
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+19
-7
@@ -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.
|
||||
@@ -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.
|
||||
+390
@@ -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.
|
||||
 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`.
|
||||
|
||||
 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.
|
||||
+93
@@ -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)
|
||||
@@ -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
|
||||
|
||||
+280
-193
@@ -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
|
||||
|
||||
|
||||
+182
@@ -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.
|
||||
|
||||
[ 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 })
|
||||
```
|
||||
-744
@@ -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
|
||||
|
||||
[ 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}}"
|
||||
```
|
||||
+176
@@ -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.
|
||||
|
||||
[ 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)
|
||||
}
|
||||
```
|
||||
+140
@@ -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.
|
||||
|
||||
[ 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>(),
|
||||
)
|
||||
```
|
||||
+44
-15
@@ -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.
|
||||
[ Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/advanced)
|
||||
[ 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()
|
||||
+390
@@ -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.
|
||||
[ 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}}"
|
||||
```
|
||||
+185
@@ -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.
|
||||
|
||||
[ 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.
|
||||
}
|
||||
```
|
||||
+49
-17
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -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())
|
||||
|
||||
+8
-8
@@ -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
|
||||
|
||||
@@ -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?
|
||||
+134
@@ -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.
|
||||
+217
@@ -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 />
|
||||
+83
@@ -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.
|
||||
+32
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
+109
-55
@@ -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.
|
||||
@@ -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 ->
|
||||
|
||||
+105
-1
@@ -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.
|
||||
}
|
||||
|
||||
+114
-1
@@ -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
Reference in New Issue
Block a user