mirror of
https://github.com/android/skills.git
synced 2026-09-14 20:07:09 +08:00
Updates skills (2026-08-07 14:24)
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"./jetpack-compose/adaptive",
|
||||
"./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose",
|
||||
"./jetpack-compose/theming/styles",
|
||||
"./media/media3-cast-integration",
|
||||
"./navigation/navigation-3",
|
||||
"./performance/r8-analyzer",
|
||||
"./play/engage-sdk-integration",
|
||||
@@ -32,6 +33,7 @@
|
||||
"./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"
|
||||
],
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
"path": "./jetpack-compose/theming/styles"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"path": "./media/media3-cast-integration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"path": "./navigation/navigation-3"
|
||||
@@ -105,6 +110,11 @@
|
||||
"path": "./testing/testing-setup"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"path": "./tv/leanback-to-compose-tv-migration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"path": "./wear/wear-compose-m3"
|
||||
|
||||
@@ -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-07-23'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- Android Gradle Plugin 9
|
||||
- AGP 9
|
||||
@@ -18,23 +18,13 @@ metadata:
|
||||
|
||||
## Migration guide
|
||||
|
||||
See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for the major changes, many
|
||||
breaking, in AGP 9 compared to AGP 8.
|
||||
See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for the major changes, many breaking, in AGP 9 compared to AGP 8.
|
||||
|
||||
## Requirements
|
||||
|
||||
If the user requests to update or migrate to AGP 9, first check the AGP version
|
||||
used in the project. If it is lower than 9, stop and ask the user to run the AGP
|
||||
Upgrade Assistant in Android Studio to update to the latest stable version of
|
||||
AGP, and confirm when done. The user may also request that this requirement be
|
||||
skipped; if this is the case, you should update the version of AGP to the latest
|
||||
stable version as part of the AGP 9 migration. See the
|
||||
[AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this.
|
||||
If the user requests to update or migrate to AGP 9, first check the AGP version used in the project. If it is lower than 9, stop and ask the user to run the AGP Upgrade Assistant in Android Studio to update to the latest stable version of AGP, and confirm when done. The user may also request that this requirement be skipped; if this is the case, you should update the version of AGP to the latest stable version as part of the AGP 9 migration. See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this.
|
||||
|
||||
Each version of AGP has its own set of compatibilities with other tools, such as
|
||||
Gradle, JDK, and Kotlin. The release notes for each of these versions will
|
||||
include a **Compatibility** table indicating the minimum versions for these
|
||||
tools.
|
||||
Each version of AGP has its own set of compatibilities with other tools, such as Gradle, JDK, and Kotlin. The release notes for each of these versions will include a **Compatibility** table indicating the minimum versions for these tools.
|
||||
|
||||
Do not use this skill for KMP projects, as they are unsupported.
|
||||
|
||||
@@ -44,8 +34,7 @@ If AGP is already at 9 or higher, then do the following:
|
||||
|
||||
### Step 1: Update dependencies
|
||||
|
||||
If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on
|
||||
version 2.3.6 or higher.
|
||||
If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on version 2.3.6 or higher.
|
||||
|
||||
If Hilt is used in the project, ensure it is on version 2.59.2 or higher.
|
||||
|
||||
@@ -57,19 +46,15 @@ See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for deta
|
||||
|
||||
See [the guide](references/android/build/releases/agp-9-0-0-release-notes.md) for detailed information.
|
||||
|
||||
See also [gradle-recipes](references/recipes.md) for examples on how to migrate old code to code
|
||||
that is compatible with AGP 9 and the new DSL.
|
||||
See also [gradle-recipes](references/recipes.md) for examples on how to migrate old code to code that is compatible with AGP 9 and the new DSL.
|
||||
|
||||
### Step 4. Migrate kapt to KSP or legacy-kapt
|
||||
|
||||
If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are
|
||||
used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration
|
||||
steps.
|
||||
If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration steps.
|
||||
|
||||
### Step 5. BuildConfig
|
||||
|
||||
If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md)
|
||||
for detailed information.
|
||||
If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md) for detailed information.
|
||||
|
||||
### Step 6. Update gradle.properties
|
||||
|
||||
@@ -99,5 +84,4 @@ After migration, verify the following:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Paparazzi v2.0.0-alpha04 and lower versions have issues with AGP 9. See
|
||||
[references/paparazzi-gradle-9.md](references/paparazzi-gradle-9.md) for details.
|
||||
Paparazzi v2.0.0-alpha04 and lower versions have issues with AGP 9. See [references/paparazzi-gradle-9.md](references/paparazzi-gradle-9.md) for details.
|
||||
|
||||
+25
-79
@@ -1,33 +1,20 @@
|
||||
Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it
|
||||
by default. That means you no longer have to apply the
|
||||
`org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files
|
||||
to compile Kotlin source files.
|
||||
With built-in Kotlin, your build files are simpler and you can avoid
|
||||
compatibility issues between AGP and the `kotlin-android` plugin.
|
||||
Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it by default. That means you no longer have to apply the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files to compile Kotlin source files. With built-in Kotlin, your build files are simpler and you can avoid compatibility issues between AGP and the `kotlin-android` plugin.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Built-in Kotlin replaces the `kotlin-android` plugin only. If you are writing a Kotlin Multiplatform (KMP) library module, you still need to apply the `org.jetbrains.kotlin.multiplatform` plugin and the [`com.android.kotlin.multiplatform.library`](https://developer.android.com/kotlin/multiplatform/plugin) plugin. Also, using the `org.jetbrains.kotlin.multiplatform` plugin together with the `com.android.library` or `com.android.application` plugin is no longer allowed when built-in Kotlin is enabled.
|
||||
|
||||
## Enable built-in Kotlin
|
||||
|
||||
You need AGP 9.0 or higher to have built-in Kotlin support.
|
||||
AGP 9.0 already enables built-in Kotlin for all your modules where you apply
|
||||
AGP, so you don't need to do anything to enable it. However, if you previously
|
||||
[opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin) by setting `android.builtInKotlin=false`
|
||||
in the `gradle.properties` file, you need to remove that setting or set it to
|
||||
`true`.
|
||||
You need AGP 9.0 or higher to have built-in Kotlin support. AGP 9.0 already enables built-in Kotlin for all your modules where you apply AGP, so you don't need to do anything to enable it. However, if you previously [opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin) by setting `android.builtInKotlin=false` in the `gradle.properties` file, you need to remove that setting or set it to `true`.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** You can also enable built-in Kotlin for [one module at a time](https://developer.android.com/build/migrate-to-built-in-kotlin#module-by-module-migration).
|
||||
|
||||
Built-in Kotlin requires some changes to your project, so after you
|
||||
have built-in Kotlin enabled, follow the next steps to migrate your project.
|
||||
Built-in Kotlin requires some changes to your project, so after you have built-in Kotlin enabled, follow the next steps to migrate your project.
|
||||
|
||||
## Migration steps
|
||||
|
||||
After you upgrade your project from an older AGP version to AGP 9.0 or after
|
||||
you manually [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin), you might see the following error
|
||||
message:
|
||||
After you upgrade your project from an older AGP version to AGP 9.0 or after you manually [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin), you might see the following error message:
|
||||
|
||||
Failed to apply plugin 'org.jetbrains.kotlin.android'.
|
||||
> Cannot add extension with name 'kotlin', as there is an extension already registered with that name.
|
||||
@@ -37,8 +24,7 @@ message:
|
||||
Failed to apply plugin 'com.jetbrains.kotlin.android'
|
||||
> The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
|
||||
|
||||
This error occurs because built-in Kotlin requires some changes to your project.
|
||||
To resolve this error, follow these steps:
|
||||
This error occurs because built-in Kotlin requires some changes to your project. To resolve this error, follow these steps:
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** If you're not yet ready to migrate your project, you can also [opt out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin).
|
||||
@@ -50,10 +36,7 @@ To resolve this error, follow these steps:
|
||||
|
||||
### 1. Remove the `kotlin-android` plugin
|
||||
|
||||
Remove the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin from
|
||||
the module-level build files where you apply it.
|
||||
The exact code to remove depends on
|
||||
whether you use [version catalogs](https://docs.gradle.org/current/userguide/version_catalogs.html) to declare plugins.
|
||||
Remove the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin from the module-level build files where you apply it. The exact code to remove depends on whether you use [version catalogs](https://docs.gradle.org/current/userguide/version_catalogs.html) to declare plugins.
|
||||
|
||||
### With version catalogs
|
||||
|
||||
@@ -137,8 +120,7 @@ plugins {
|
||||
}
|
||||
```
|
||||
|
||||
If you use version catalogs, also remove the plugin definition from the
|
||||
version catalog TOML file (usually `gradle/libs.versions.toml`):
|
||||
If you use version catalogs, also remove the plugin definition from the version catalog TOML file (usually `gradle/libs.versions.toml`):
|
||||
|
||||
```toml
|
||||
[plugins]
|
||||
@@ -147,16 +129,11 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "KOTLIN_VE
|
||||
|
||||
### 2. Migrate the `kotlin-kapt` plugin if necessary
|
||||
|
||||
The `org.jetbrains.kotlin.kapt` (or `kotlin-kapt`) plugin is incompatible with
|
||||
built-in Kotlin. If you use `kapt`, we recommend that you
|
||||
[migrate your project to KSP](https://developer.android.com/build/migrate-to-ksp).
|
||||
The `org.jetbrains.kotlin.kapt` (or `kotlin-kapt`) plugin is incompatible with built-in Kotlin. If you use `kapt`, we recommend that you [migrate your project to KSP](https://developer.android.com/build/migrate-to-ksp).
|
||||
|
||||
If you can't migrate to KSP yet, replace the `kotlin-kapt` plugin with the
|
||||
`com.android.legacy-kapt` plugin, using the same version as your Android Gradle
|
||||
plugin.
|
||||
If you can't migrate to KSP yet, replace the `kotlin-kapt` plugin with the `com.android.legacy-kapt` plugin, using the same version as your Android Gradle plugin.
|
||||
|
||||
For example, with version catalogs, update your version catalog TOML
|
||||
file as follows:
|
||||
For example, with version catalogs, update your version catalog TOML file as follows:
|
||||
|
||||
```toml
|
||||
[plugins]
|
||||
@@ -216,8 +193,7 @@ plugins {
|
||||
|
||||
### 3. Migrate the `android.kotlinOptions{}` DSL if necessary
|
||||
|
||||
If you use the `android.kotlinOptions{}` DSL, you need to
|
||||
migrate it to the [`kotlin.compilerOptions{}`](https://kotlinlang.org/docs/gradle-compiler-options.html#migrate-from-kotlinoptions-to-compileroptions) DSL.
|
||||
If you use the `android.kotlinOptions{}` DSL, you need to migrate it to the [`kotlin.compilerOptions{}`](https://kotlinlang.org/docs/gradle-compiler-options.html#migrate-from-kotlinoptions-to-compileroptions) DSL.
|
||||
|
||||
For example, update this code:
|
||||
|
||||
@@ -274,15 +250,9 @@ kotlin {
|
||||
|
||||
### 4. Migrate the `kotlin.sourceSets{}` DSL if necessary
|
||||
|
||||
When you use the `kotlin-android` plugin, AGP lets you add additional Kotlin
|
||||
source directories using either the [`android.sourceSets{}`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/AndroidSourceSet) DSL or the
|
||||
[`kotlin.sourceSets{}`](https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.plugin/-kotlin-source-set/) DSL.
|
||||
With the `android.sourceSets{}` DSL, you can add the directories to either the
|
||||
`AndroidSourceSet.kotlin` set or the `AndroidSourceSet.java` set.
|
||||
When you use the `kotlin-android` plugin, AGP lets you add additional Kotlin source directories using either the [`android.sourceSets{}`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/AndroidSourceSet) DSL or the [`kotlin.sourceSets{}`](https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.plugin/-kotlin-source-set/) DSL. With the `android.sourceSets{}` DSL, you can add the directories to either the `AndroidSourceSet.kotlin` set or the `AndroidSourceSet.java` set.
|
||||
|
||||
With built-in Kotlin, the only supported option is to add the directories to the
|
||||
`AndroidSourceSet.kotlin` set using the `android.sourceSets{}` DSL.
|
||||
If you use unsupported options, migrate them as follows:
|
||||
With built-in Kotlin, the only supported option is to add the directories to the `AndroidSourceSet.kotlin` set using the `android.sourceSets{}` DSL. If you use unsupported options, migrate them as follows:
|
||||
|
||||
### Kotlin
|
||||
|
||||
@@ -322,10 +292,7 @@ android.sourceSets.named("main") {
|
||||
}
|
||||
```
|
||||
|
||||
If you want to add a Kotlin source directory to a specific variant or if the
|
||||
directory is generated by a task, you can use the
|
||||
[`addStaticSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addStaticSourceDirectory(kotlin.String)) or [`addGeneratedSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addGeneratedSourceDirectory(org.gradle.api.tasks.TaskProvider,kotlin.Function1)) methods
|
||||
in the [variant API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks):
|
||||
If you want to add a Kotlin source directory to a specific variant or if the directory is generated by a task, you can use the [`addStaticSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addStaticSourceDirectory(kotlin.String)) or [`addGeneratedSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addGeneratedSourceDirectory(org.gradle.api.tasks.TaskProvider,kotlin.Function1)) methods in the [variant API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks):
|
||||
|
||||
### Kotlin
|
||||
|
||||
@@ -347,57 +314,36 @@ androidComponents.onVariants { variant ->
|
||||
|
||||
## Report issues
|
||||
|
||||
If you encounter issues after completing the previous steps,
|
||||
review the known issues in [issue #438678642](https://issuetracker.google.com/438678642) and give us
|
||||
feedback if needed.
|
||||
If you encounter issues after completing the previous steps, review the known issues in [issue #438678642](https://issuetracker.google.com/438678642) and give us feedback if needed.
|
||||
|
||||
## Opt out of built-in Kotlin
|
||||
|
||||
If you are unable to migrate your project to use built-in Kotlin, set
|
||||
`android.builtInKotlin=false` in the `gradle.properties` file to temporarily
|
||||
disable it.
|
||||
When you do that, the build shows a warning reminding you to migrate to built-in
|
||||
Kotlin as you won't be able to disable built-in Kotlin in AGP 10.0.
|
||||
If you are unable to migrate your project to use built-in Kotlin, set `android.builtInKotlin=false` in the `gradle.properties` file to temporarily disable it. When you do that, the build shows a warning reminding you to migrate to built-in Kotlin as you won't be able to disable built-in Kotlin in AGP 10.0.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** You also need to set `android.newDsl=false` to opt out of the [new DSL](https://developer.android.com/r/tools/new-dsl) because the `kotlin-android` plugin is not compatible with it.
|
||||
|
||||
Once you're ready to migrate your project, [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin)
|
||||
and follow the [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps).
|
||||
Once you're ready to migrate your project, [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin) and follow the [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps).
|
||||
|
||||
## Module-by-module migration
|
||||
|
||||
The `android.builtInKotlin` Gradle property lets you enable or disable built-in
|
||||
Kotlin for all your modules where you apply AGP.
|
||||
The `android.builtInKotlin` Gradle property lets you enable or disable built-in Kotlin for all your modules where you apply AGP.
|
||||
|
||||
If migrating all your modules at once is challenging, you can migrate one module
|
||||
at a time:
|
||||
If migrating all your modules at once is challenging, you can migrate one module at a time:
|
||||
|
||||
1. Set `android.builtInKotlin=false` in the `gradle.properties` file to
|
||||
disable built-in Kotlin for all modules.
|
||||
1. Set `android.builtInKotlin=false` in the `gradle.properties` file to disable built-in Kotlin for all modules.
|
||||
|
||||
2. Apply the `com.android.built-in-kotlin` plugin to the module
|
||||
you want to enable built-in Kotlin, using the same version as your
|
||||
Android Gradle plugin.
|
||||
2. Apply the `com.android.built-in-kotlin` plugin to the module you want to enable built-in Kotlin, using the same version as your Android Gradle plugin.
|
||||
|
||||
3. Follow the previous [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps) to migrate this module to
|
||||
built-in Kotlin.
|
||||
3. Follow the previous [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps) to migrate this module to built-in Kotlin.
|
||||
|
||||
4. Once you've migrated all your modules, remove the
|
||||
`android.builtInKotlin=false` setting in `gradle.properties`
|
||||
and the `com.android.built-in-kotlin` plugin in your build files.
|
||||
4. Once you've migrated all your modules, remove the `android.builtInKotlin=false` setting in `gradle.properties` and the `com.android.built-in-kotlin` plugin in your build files.
|
||||
|
||||
## Option to selectively disable built-in Kotlin
|
||||
|
||||
Android Gradle plugin 9.0 enables built-in Kotlin for all modules where it is
|
||||
applied.
|
||||
We recommend disabling built-in Kotlin selectively for modules that don't have
|
||||
Kotlin sources in large projects.
|
||||
This removes both the Kotlin compilation task, which has a small build
|
||||
performance cost, and the automatic dependency on the Kotlin standard library.
|
||||
Android Gradle plugin 9.0 enables built-in Kotlin for all modules where it is applied. We recommend disabling built-in Kotlin selectively for modules that don't have Kotlin sources in large projects. This removes both the Kotlin compilation task, which has a small build performance cost, and the automatic dependency on the Kotlin standard library.
|
||||
|
||||
To disable built-in Kotlin for a module,
|
||||
set `enableKotlin = false` in that module's build file:
|
||||
To disable built-in Kotlin for a module, set `enableKotlin = false` in that module's build file:
|
||||
|
||||
### Kotlin
|
||||
|
||||
|
||||
+76
-240
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
||||
When an Android module contains custom BuildConfig fields, the following steps
|
||||
are necessary to ensure a correct build.
|
||||
When an Android module contains custom BuildConfig fields, the following steps are necessary to ensure a correct build.
|
||||
|
||||
### Step 1: Enable the buildConfig build feature
|
||||
|
||||
@@ -37,11 +36,9 @@ 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-system/agp/agp-9-upgrade/references/recipes)
|
||||
repository.
|
||||
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
|
||||
*must* include quotation marks as part of the String. For example:
|
||||
**IMPORTANT:** For `BuildConfigField`s with a type of `String`, the `value` field *must* include quotation marks as part of the String. For example:
|
||||
|
||||
BuildConfigField(
|
||||
type = "String",
|
||||
@@ -49,6 +46,4 @@ repository.
|
||||
comment = "Optional comment",
|
||||
)
|
||||
|
||||
It is an **error** if the `value` field doesn't include quotation marks as
|
||||
part of the String. For example, `value = "Some value"` **is an error** . This is
|
||||
because the `value` is written out literally.
|
||||
It is an **error** if the `value` field doesn't include quotation marks as part of the String. For example, `value = "Some value"` **is an error** . This is because the `value` is written out literally.
|
||||
@@ -1,39 +1,23 @@
|
||||
When migrating to built-in Kotlin, it is important to consider usage of `kapt`
|
||||
and the `org.jetbrains.kotlin.kapt` (also known as the `kotlin("kapt")`) plugin.
|
||||
The goal is to migrate as many `kapt` usages to `ksp` as possible.
|
||||
When migrating to built-in Kotlin, it is important to consider usage of `kapt` and the `org.jetbrains.kotlin.kapt` (also known as the `kotlin("kapt")`) plugin. The goal is to migrate as many `kapt` usages to `ksp` as possible.
|
||||
|
||||
Follow these steps when migrating `kapt`:
|
||||
|
||||
## 1. Remove all references to the `org.jetbrains.kotlin.kapt` plugin
|
||||
|
||||
The `org.jetbrains.kotlin.kapt` (also known as `kotlin("kapt")`) plugin is
|
||||
incompatible with built-in Kotlin. Remove it when migrating to built-in Kotlin.
|
||||
The `org.jetbrains.kotlin.kapt` (also known as `kotlin("kapt")`) plugin is incompatible with built-in Kotlin. Remove it when migrating to built-in Kotlin.
|
||||
|
||||
## 2. Check each usage of `kapt`
|
||||
|
||||
Check each usage of `kapt` to see if it is compatible with `ksp`. To check if a
|
||||
dependency is compatible with `ksp`, inspect the dependency's jar. For it to be
|
||||
compatible with `ksp`, the jar must have a file,
|
||||
`services/com.google.devtools.ksp.processing.SymbolProcessorProvider`. If it
|
||||
does not, it is **incompatible** with `ksp`.
|
||||
Check each usage of `kapt` to see if it is compatible with `ksp`. To check if a dependency is compatible with `ksp`, inspect the dependency's jar. For it to be compatible with `ksp`, the jar must have a file, `services/com.google.devtools.ksp.processing.SymbolProcessorProvider`. If it does not, it is **incompatible** with `ksp`.
|
||||
|
||||
For example, the `androidx.room:room-compiler` library is compatible with KSP
|
||||
since version 2.3.0-beta02. We can verify this by finding the jar file in the
|
||||
Gradle caches directory, which is typically located at
|
||||
`~/.gradle/caches/modules-2/files-2.1/` on Linux and Mac. In this specific case,
|
||||
the `androidx.room:room-compiler` dependency is located at
|
||||
`~/.gradle/caches/modules-2/files-2.1/androidx.room/room-compiler/`.
|
||||
For example, the `androidx.room:room-compiler` library is compatible with KSP since version 2.3.0-beta02. We can verify this by finding the jar file in the Gradle caches directory, which is typically located at `~/.gradle/caches/modules-2/files-2.1/` on Linux and Mac. In this specific case, the `androidx.room:room-compiler` dependency is located at `~/.gradle/caches/modules-2/files-2.1/androidx.room/room-compiler/`.
|
||||
|
||||
More generally, you can find a dependency by looking in
|
||||
`~/.gradle/caches/modules-2/files-2.1/group-name/artifact-name/`.
|
||||
More generally, you can find a dependency by looking in `~/.gradle/caches/modules-2/files-2.1/group-name/artifact-name/`.
|
||||
|
||||
## 3. Migrate to KSP where possible
|
||||
|
||||
For each usage of `kapt` that is compatible with `ksp`, use `ksp`. The prior
|
||||
step explains how to check compatibility.
|
||||
For each usage of `kapt` that is compatible with `ksp`, use `ksp`. The prior step explains how to check compatibility.
|
||||
|
||||
## 4. Apply legacy-kapt
|
||||
|
||||
If a Gradle module has `kapt` dependencies that cannot be migrated to `ksp`
|
||||
because they are incompatible (see step 2), then leave that dependency alone and
|
||||
apply the `com.android.legacy-kapt` plugin.
|
||||
If a Gradle module has `kapt` dependencies that cannot be migrated to `ksp` because they are incompatible (see step 2), then leave that dependency alone and apply the `com.android.legacy-kapt` plugin.
|
||||
@@ -1,19 +1,10 @@
|
||||
If Paparazzi is used in the project, update it to version 2.0.0-alpha04 or
|
||||
higher.
|
||||
If Paparazzi is used in the project, update it to version 2.0.0-alpha04 or higher.
|
||||
|
||||
Paparazzi version 2.0.0-alpha04 and lower is not fully compatible with Gradle
|
||||
9, and Gradle 9 is required by AGP 9. This means that, without workarounds,
|
||||
projects that use Paparazzi v2.0.0-alpha04 and lower cannot migrate to AGP 9.
|
||||
Paparazzi version 2.0.0-alpha04 and lower is not fully compatible with Gradle 9, and Gradle 9 is required by AGP 9. This means that, without workarounds, projects that use Paparazzi v2.0.0-alpha04 and lower cannot migrate to AGP 9.
|
||||
|
||||
At time of writing, there are no higher versions of Paparazzi. That is,
|
||||
v2.0.0-alpha04 is the latest release.
|
||||
At time of writing, there are no higher versions of Paparazzi. That is, v2.0.0-alpha04 is the latest release.
|
||||
|
||||
The issue is due to Paparazzi using internal classes from Gradle that tend to
|
||||
move in breaking ways without warning. This specific issue is related to HTML
|
||||
test reports. To work around it, disable those HTML test reports. Here
|
||||
are two examples of how to do this, one for Kotlin DSL and the other for Groovy
|
||||
DSL. Any module that has the paparazzi plugin (`app.cash.paparazzi`) applied
|
||||
must apply one of these two workarounds.
|
||||
The issue is due to Paparazzi using internal classes from Gradle that tend to move in breaking ways without warning. This specific issue is related to HTML test reports. To work around it, disable those HTML test reports. Here are two examples of how to do this, one for Kotlin DSL and the other for Groovy DSL. Any module that has the paparazzi plugin (`app.cash.paparazzi`) applied must apply one of these two workarounds.
|
||||
|
||||
Kotlin DSL:
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
When migrating to AGP's new DSL, any Gradle code (plugins or logic in build
|
||||
scripts) that relied on the old DSL will stop working. Such code must be
|
||||
migrated.
|
||||
When migrating to AGP's new DSL, any Gradle code (plugins or logic in build scripts) that relied on the old DSL will stop working. Such code must be migrated.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -8,14 +6,9 @@ migrated.
|
||||
- **DO NOT** use AGP internals in migrated code.
|
||||
- **DO** use only public APIs in migrated code.
|
||||
|
||||
In some cases, there is a one-to-one replacement for the old code. Some examples
|
||||
are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes).
|
||||
In some cases, there is a one-to-one replacement for the old code. Some examples are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes).
|
||||
|
||||
In other cases, there is no direct one-to-one replacement. For these situations,
|
||||
the [gradle-recipes repo](https://github.com/android/gradle-recipes) is a great resource. You can checkout one of its
|
||||
AGP 9.x branches, such as `agp-9.0`, `agp-9.1`, or `agp-9.2`. These branches
|
||||
contain recipes for common situations in Android projects. The following table
|
||||
lists the compatibility for recipes for each version of AGP.
|
||||
In other cases, there is no direct one-to-one replacement. For these situations, the [gradle-recipes repo](https://github.com/android/gradle-recipes) is a great resource. You can checkout one of its AGP 9.x branches, such as `agp-9.0`, `agp-9.1`, or `agp-9.2`. These branches contain recipes for common situations in Android projects. The following table lists the compatibility for recipes for each version of AGP.
|
||||
|
||||
## Compatibility table
|
||||
|
||||
@@ -56,7 +49,4 @@ In the old DSL, an APK could be renamed very simply. Here's an example:
|
||||
}
|
||||
}
|
||||
|
||||
However, with AGP 9 and the new DSL, `applicationVariants` is no longer
|
||||
available. You must instead react to artifact creation using the
|
||||
`androidComponents.onVariants` API. A complete example of this is available in
|
||||
the **gradle-recipes** repository in the `listenToArtifacts` recipe.
|
||||
However, with AGP 9 and the new DSL, `applicationVariants` is no longer available. You must instead react to artifact creation using the `androidComponents.onVariants` API. A complete example of this is available in the **gradle-recipes** repository in the `listenToArtifacts` recipe.
|
||||
+9
-17
@@ -7,7 +7,7 @@ description: Provide technical guidance for Android camera development with Came
|
||||
license: Complete terms in LICENSE.txt
|
||||
metadata:
|
||||
author: Google LLC
|
||||
last-updated: '2026-07-07'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- recipe
|
||||
- Android
|
||||
@@ -26,20 +26,17 @@ metadata:
|
||||
- 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.
|
||||
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.
|
||||
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**
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// WRONG
|
||||
@@ -62,6 +59,7 @@ run {
|
||||
pending = pending.withAudioEnabled() // Reassignment
|
||||
val active = pending.start(exec, listener)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -77,9 +75,7 @@ When migrating legacy camera codebases to the CameraX Jetpack library:
|
||||
|
||||
### 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:
|
||||
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.
|
||||
@@ -106,20 +102,16 @@ 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:
|
||||
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.
|
||||
- **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.
|
||||
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.
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
## Initialize `ProcessCameraProvider`
|
||||
|
||||
Request the `ProcessCameraProvider` and bind use cases to the Activity or
|
||||
Fragment lifecycle.
|
||||
Request the `ProcessCameraProvider` and bind use cases to the Activity or Fragment lifecycle.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val context = LocalContext.current
|
||||
@@ -36,6 +36,7 @@ LaunchedEffect(context, lifecycleOwner) {
|
||||
)
|
||||
val cameraControl = camera.cameraControl
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -50,21 +51,25 @@ Use `androidx.camera.view.PreviewView`.
|
||||
|
||||
1. **Set up preview**:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
2. **Handle tap-to-focus**:
|
||||
|
||||
<br />
|
||||
|
||||
```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 />
|
||||
@@ -75,6 +80,7 @@ Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
|
||||
1. **Set up preview and SurfaceRequest**:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
|
||||
@@ -83,12 +89,14 @@ Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
setSurfaceProvider { request -> surfaceRequest = request }
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
2. **Render viewfinder**:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
surfaceRequest?.let { request ->
|
||||
@@ -98,12 +106,14 @@ Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
3. **Handle tap-to-focus in Compose**:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Inside your tap gesture handler...
|
||||
@@ -115,12 +125,14 @@ Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
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**:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
LaunchedEffect(configuration) {
|
||||
@@ -130,15 +142,16 @@ Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
preview.targetRotation = rotation
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Capture a photo
|
||||
|
||||
Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles
|
||||
rotation directly.
|
||||
Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles rotation directly.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
imageCapture.takePicture(
|
||||
@@ -170,15 +183,16 @@ imageCapture.takePicture(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Switch cameras
|
||||
|
||||
To flip between front and rear cameras, change the `CameraSelector` and
|
||||
retrigger the `ProcessCameraProvider` logic.
|
||||
To flip between front and rear cameras, change the `CameraSelector` and retrigger the `ProcessCameraProvider` logic.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
|
||||
@@ -186,6 +200,7 @@ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
|
||||
} else {
|
||||
CameraSelector.LENS_FACING_BACK
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
Camera2 offers granular control but introduces boilerplate: managing
|
||||
`CameraDevice` states, `CameraCaptureSession` lifecycles, background threads,
|
||||
`HandlerThread`, and manual orientation calculations.
|
||||
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.
|
||||
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.
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
@@ -21,10 +17,9 @@ Migrating to CameraX removes manual setup code:
|
||||
|
||||
## Initialize `ProcessCameraProvider`
|
||||
|
||||
Request the `ProcessCameraProvider` and bind your use cases to the
|
||||
`LifecycleOwner` activity or fragment. This replaces the
|
||||
`CameraManager.openCamera` flow.
|
||||
Request the `ProcessCameraProvider` and bind your use cases to the `LifecycleOwner` activity or fragment. This replaces the `CameraManager.openCamera` flow.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val context = LocalContext.current
|
||||
@@ -56,6 +51,7 @@ LaunchedEffect(context, lifecycleOwner) {
|
||||
imageAnalysis
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -64,17 +60,17 @@ LaunchedEffect(context, lifecycleOwner) {
|
||||
|
||||
## Implement the preview and tap-to-focus
|
||||
|
||||
CameraX handles surface configuration automatically. Choose based on your UI
|
||||
toolkit:
|
||||
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.
|
||||
Use `androidx.camera.view.PreviewView` in your layout, and bind it to the `Preview` use case.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -83,6 +79,7 @@ preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||
|
||||
Use `androidx.camera.compose.CameraXViewfinder`.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
|
||||
@@ -91,6 +88,7 @@ val preview = remember {
|
||||
setSurfaceProvider { request -> surfaceRequest = request }
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -99,10 +97,9 @@ val preview = remember {
|
||||
|
||||
## 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`.
|
||||
Replace `ImageReader` capture flows and `CaptureRequest.Builder.TEMPLATE_STILL_CAPTURE` with the `ImageCapture` use case. CameraX handles the rotation natively using the returned `ImageProxy`.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
imageCapture.takePicture(
|
||||
@@ -134,6 +131,7 @@ imageCapture.takePicture(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -142,9 +140,9 @@ imageCapture.takePicture(
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
||||
@@ -157,6 +155,7 @@ imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -165,10 +164,9 @@ imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Use Camera2Interop to set Camera2-specific capture options
|
||||
@@ -180,6 +178,7 @@ extender.setCaptureRequestOption(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_TORCH
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -54,5 +52,4 @@ phases.
|
||||
|
||||
## 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).
|
||||
For guidance on Night Mode Extensions and Low Light Boost, [low-light.md](https://developer.android.com/agents/skills/camera/camerax/references/low-light).
|
||||
@@ -1,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -16,9 +14,9 @@ postures, such as tabletop and book modes.
|
||||
|
||||
### Detect posture changes
|
||||
|
||||
Use the Jetpack WindowManager library to observe the device's hinge state and
|
||||
fold layout.
|
||||
Use the Jetpack WindowManager library to observe the device's hinge state and fold layout.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
@@ -33,15 +31,14 @@ lifecycleScope.launch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<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.
|
||||
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.
|
||||
@@ -49,9 +46,9 @@ seeing a "bent" image.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val viewport = ViewPort.Builder(Rational(viewfinder.width, viewfinder.height), display.rotation)
|
||||
@@ -62,14 +59,14 @@ 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.
|
||||
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.
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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.
|
||||
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.
|
||||
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 |
|
||||
|---|---|---|
|
||||
@@ -20,6 +17,7 @@ reassign the variable.
|
||||
|
||||
To set up video recording, use the following code:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// WRONG
|
||||
@@ -42,6 +40,7 @@ run {
|
||||
pending = pending.withAudioEnabled() // Reassignment
|
||||
val active = pending.start(exec, listener)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -50,11 +49,13 @@ run {
|
||||
|
||||
To set up the viewport, use the following code:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val viewport = ViewPort.Builder(Rational(width, height), displayRotation)
|
||||
.setScaleType(ViewPort.FILL_CENTER)
|
||||
.build()
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -1,5 +1,4 @@
|
||||
This guide covers implementing low-light features using **Night mode
|
||||
extensions** and **Low Light Boost (LLB)**.
|
||||
This guide covers implementing low-light features using **Night mode extensions** and **Low Light Boost (LLB)**.
|
||||
|
||||
## Choosing the right tool
|
||||
|
||||
@@ -13,13 +12,13 @@ extensions** and **Low Light Boost (LLB)**.
|
||||
|
||||
## Night mode extension
|
||||
|
||||
CameraX Extensions provide access to the device's built-in computational
|
||||
photography pipeline.
|
||||
CameraX Extensions provide access to the device's built-in computational photography pipeline.
|
||||
|
||||
### Basic setup
|
||||
|
||||
To set up the extension, initialize the extension manager:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Use ListenableFuture.await() extension function for coroutine support
|
||||
@@ -30,6 +29,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT))
|
||||
)
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, nightSelector, imageCapture, preview)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -42,6 +42,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT))
|
||||
val imageCapture = ImageCapture.Builder()
|
||||
.setPostviewEnabled(true)
|
||||
.build()
|
||||
|
||||
```
|
||||
- **Extension strength** : Let users control the intensity of the night effect.
|
||||
|
||||
@@ -50,6 +51,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT))
|
||||
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.
|
||||
|
||||
@@ -61,6 +63,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT))
|
||||
} catch (e: ImageCaptureException) {
|
||||
// Handle capture failure
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
*** ** * ** ***
|
||||
@@ -71,8 +74,7 @@ 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.
|
||||
The built-in CameraX way to prioritize brightness. It modifies the hardware's auto-exposure algorithm.
|
||||
|
||||
- **Activation** : Use `CameraControl.enableLowLightBoostAsync`.
|
||||
- **Implementation** :
|
||||
@@ -80,6 +82,7 @@ auto-exposure algorithm.
|
||||
```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.
|
||||
|
||||
@@ -112,6 +115,7 @@ To implement Google Play services LLB, follow these core steps:
|
||||
.addUseCase(videoCapture)
|
||||
.addEffect(effect)
|
||||
.build()
|
||||
|
||||
```
|
||||
4. **Scene detection** : Use `session.setSceneDetectorCallback` to receive `boostStrength` updates for real-time UI indicators.
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -16,25 +14,23 @@ analysis image and the viewfinder UI.
|
||||
|
||||
### Coordinate transformation matrix
|
||||
|
||||
Android provides the `Viewport` and `UseCaseGroup` APIs to calculate the
|
||||
transformation matrix automatically. **Don't** calculate aspect ratio scaling
|
||||
manually.
|
||||
Android provides the `Viewport` and `UseCaseGroup` APIs to calculate the transformation matrix automatically. **Don't** calculate aspect ratio scaling manually.
|
||||
|
||||
<br />
|
||||
|
||||
```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.
|
||||
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:
|
||||
|
||||
@@ -44,20 +40,20 @@ To map the coordinates, use the following workflow:
|
||||
|
||||
### 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**.
|
||||
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**.
|
||||
|
||||
<br />
|
||||
|
||||
```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`.
|
||||
**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
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Always prefer these various abstractions over legacy Camera2 or early CameraX
|
||||
implementations.
|
||||
Always prefer these various abstractions over legacy Camera2 or early CameraX implementations.
|
||||
|
||||
## Compare APIs
|
||||
|
||||
@@ -38,5 +37,4 @@ Add the following dependencies to your `libs.versions.toml` file:
|
||||
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.
|
||||
Refer to the official [CameraX Release Notes](https://developer.android.com/jetpack/androidx/releases/camera) for the stable versions.
|
||||
@@ -1,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -17,42 +15,41 @@ patterns for reliable, hermetic camera tests.
|
||||
|
||||
### 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.
|
||||
**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.
|
||||
|
||||
<br />
|
||||
|
||||
```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.
|
||||
Use `FakeAppConfig` from `androidx.camera:camera-testing` to simulate specific hardware constraints in tests, such as a device without a flash.
|
||||
|
||||
<br />
|
||||
|
||||
```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.
|
||||
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.
|
||||
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,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -16,9 +14,9 @@ force-close the camera app.
|
||||
|
||||
### 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.
|
||||
Android 13 (API level 33) introduced `StreamUseCase`. This is the **single most effective** way to tell the hardware how to balance quality versus power.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// In CameraX: Set the hint on your Use Case
|
||||
@@ -30,6 +28,7 @@ val preview = Preview.Builder()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -43,9 +42,9 @@ Review the following key use cases for stream optimization:
|
||||
|
||||
### Monitor thermal status
|
||||
|
||||
Don't wait for a crash. Monitor the `PowerManager` status and react before
|
||||
`THERMAL_STATUS_CRITICAL`.
|
||||
Don't wait for a crash. Monitor the `PowerManager` status and react before `THERMAL_STATUS_CRITICAL`.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
@@ -63,6 +62,7 @@ powerManager.addThermalStatusListener { status ->
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -17,8 +15,7 @@ control the phone's camera.
|
||||
|
||||
### The circular UI challenge
|
||||
|
||||
Wear OS devices are often round. Standard rectangular layouts clip corner
|
||||
buttons.
|
||||
Wear OS devices are often round. Standard rectangular layouts clip corner buttons.
|
||||
|
||||
Follow these blueprint recommendations:
|
||||
|
||||
@@ -30,6 +27,7 @@ Follow these blueprint recommendations:
|
||||
|
||||
You can't send a raw 60 fps stream over Bluetooth. compress and throttle.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Example: Sending a viewfinder frame to the watch
|
||||
@@ -41,30 +39,31 @@ if (bitmap != null) {
|
||||
}
|
||||
Wearable.getDataClient(context).putDataItem(request.asPutDataRequest())
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
**Optimization** : Cap the watch preview at **10-15 fps** to preserve battery and
|
||||
bandwidth.
|
||||
**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."
|
||||
Use the `MessageClient` for low-latency commands like "Take Photo" or "Switch Camera."
|
||||
|
||||
<br />
|
||||
|
||||
```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**.
|
||||
On devices that support it, use the physical crown, Rotary Input, to control **Zoom** or **Exposure**.
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Developing camera features for XR devices, headsets, and AR glasses requires a
|
||||
shift from 2D pixel-pushing to 3D spatial awareness.
|
||||
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
|
||||
|
||||
@@ -15,8 +14,7 @@ shift from 2D pixel-pushing to 3D spatial awareness.
|
||||
|
||||
### API selection
|
||||
|
||||
On XR devices, standard `CameraX` implementations are often restricted or
|
||||
insufficient. Always use spatial software development kits (SDKs):
|
||||
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.
|
||||
@@ -26,13 +24,13 @@ insufficient. Always use spatial software development kits (SDKs):
|
||||
|
||||
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.
|
||||
**\[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 />
|
||||
|
||||
@@ -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-07-28'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- AppFunctions
|
||||
- Kotlin
|
||||
@@ -20,29 +20,19 @@ metadata:
|
||||
- MCP
|
||||
---
|
||||
|
||||
Analyzes Android apps to identify key user workflows for AppFunctions such as
|
||||
creating a note, playing media, or sending an automated or AI agent triggered
|
||||
message, voice commands, or system shortcuts, without needing to open the app
|
||||
UI.
|
||||
Analyzes Android apps to identify key user workflows for AppFunctions such as creating a note, playing media, or sending an automated or AI agent triggered message, voice commands, or system shortcuts, without needing to open the app UI.
|
||||
|
||||
Generates Kotlin code to expose these workflows to the Android system,
|
||||
allowing agents to discover and execute them on-device.
|
||||
Generates Kotlin code to expose these workflows to the Android system, allowing agents to discover and execute them on-device.
|
||||
|
||||
Also refines KDoc documentation to ensure AI agents correctly
|
||||
understand and use the provided functionality.
|
||||
Also refines KDoc documentation to ensure AI agents correctly understand and use the provided functionality.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
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 because it handles backward compatibility.
|
||||
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 because it handles backward compatibility.
|
||||
|
||||
## Workflows
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
@@ -55,7 +45,7 @@ If users request a subset of steps, you must encourage them to use all steps.
|
||||
|
||||
If they apply, you must load the following references:
|
||||
|
||||
- *[Context and terminology](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.
|
||||
- *[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
|
||||
@@ -67,5 +57,5 @@ If they apply, you must load the following references:
|
||||
|
||||
- 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](context.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).
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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.
|
||||
@@ -1,16 +1,13 @@
|
||||
Provides commands to interact with AppFunctions on a connected device or
|
||||
emulator using ADB for AppFunction testing and debugging.
|
||||
Provides commands to interact with AppFunctions on a connected device or emulator using ADB for AppFunction testing and debugging.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Scenario 1: List app functions
|
||||
|
||||
Use this scenario when you want 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** : 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>`.
|
||||
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: Invoke app functions
|
||||
|
||||
@@ -19,38 +16,28 @@ 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**: 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>'`.
|
||||
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: Manage function state
|
||||
|
||||
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 <SERVICE_CLASS_NAME#FUNCTION_NAME>
|
||||
--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
|
||||
|
||||
### Follow metadata descriptions
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
### JSON escaping
|
||||
|
||||
**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"}'`.
|
||||
**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
|
||||
|
||||
The `app_function` service must be available on the device. If `cmd: Can't find
|
||||
service: app_function` is returned, the device doesn't support this feature.
|
||||
The `app_function` service must be available on the device. If `cmd: Can't find service: app_function` is returned, the device doesn't support this feature.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -58,8 +45,7 @@ service: app_function` is returned, the device doesn't support this feature.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -91,5 +77,4 @@ parameters provided to guide the ADB interaction testing tool interactions.
|
||||
|
||||
**Cause**: The function ID or package name is incorrect.
|
||||
|
||||
**Solution** : Run `list-app-functions` and search for the relevant identifiers
|
||||
in the JSON output.
|
||||
**Solution** : Run `list-app-functions` and search for the relevant identifiers in the JSON output.
|
||||
@@ -0,0 +1,11 @@
|
||||
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.
|
||||
@@ -13,15 +13,11 @@ Analyzes Android codebases to identify and recommend high-value AppFunctions.
|
||||
|
||||
### 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).
|
||||
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 and privacy
|
||||
|
||||
Don't recommend exposing functions that handle raw credentials, financial
|
||||
secrets, or irreversible destructive actions without explicit user confirmation
|
||||
steps.
|
||||
Don't recommend exposing functions that handle raw credentials, financial secrets, or irreversible destructive actions without explicit user confirmation steps.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -29,9 +25,7 @@ steps.
|
||||
|
||||
**Recommended AppFunction** : `playArtistRadio`
|
||||
|
||||
**Rationale**: Lets you 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** : `artistName` as a `String`.
|
||||
|
||||
@@ -39,33 +33,22 @@ menus.
|
||||
|
||||
**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.
|
||||
**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"`.
|
||||
**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.
|
||||
**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.
|
||||
**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.
|
||||
**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`.
|
||||
@@ -1,19 +1,19 @@
|
||||
Specialized instructions for generating Kotlin implementations of AppFunctions,
|
||||
handling system-wide configuration, and managing build dependencies.
|
||||
Specialized instructions for generating Kotlin implementations of AppFunctions, handling system-wide configuration, and managing build dependencies.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1: Configure Gradle dependencies and KSP
|
||||
|
||||
Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin
|
||||
Symbol Processing) plugin.
|
||||
Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin Symbol Processing) plugin.
|
||||
|
||||
1. **Version check** : Use library version `1.0.0-alpha10` or later from maven.google.com.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
implementation(libs.androidx.appfunctions)
|
||||
ksp(libs.androidx.appfunctions.compiler)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -24,9 +24,9 @@ ksp(libs.androidx.appfunctions.compiler)
|
||||
|
||||
### Step 2: Set up app metadata XML
|
||||
|
||||
Describe the app's capabilities to the LLM by defining
|
||||
`res/xml/app_metadata.xml`.
|
||||
Describe the app's capabilities to the LLM by defining `res/xml/app_metadata.xml`.
|
||||
|
||||
<br />
|
||||
|
||||
```xml
|
||||
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
|
||||
@@ -36,13 +36,14 @@ Describe the app's capabilities to the LLM by defining
|
||||
Constraints:
|
||||
- Title or content must be non-null when creating a task."
|
||||
appfn:displayDescription="@string/user_visible_description" />
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Register the service and reference the app metadata in `AndroidManifest.xml`
|
||||
within the `<application>` tag:
|
||||
Register the service and reference the app metadata in `AndroidManifest.xml` within the `<application>` tag:
|
||||
|
||||
<br />
|
||||
|
||||
```xml
|
||||
<service
|
||||
@@ -63,6 +64,7 @@ within the `<application>` tag:
|
||||
<property
|
||||
android:name="android.app.appfunctions.app_metadata"
|
||||
android:resource="@xml/app_metadata" />
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -96,16 +98,13 @@ When generating Kotlin code for AppFunctions, you MUST adhere to these rules:
|
||||
|
||||
### Step 4: Set up dependency injection and service entry points
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
#### Recommended approach with Hilt
|
||||
|
||||
Annotate your service with `@AndroidEntryPoint` and inject your data
|
||||
repositories or use cases using standard `@Inject internal lateinit var`:
|
||||
Annotate your service with `@AndroidEntryPoint` and inject your data repositories or use cases using standard `@Inject internal lateinit var`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@RequiresApi(36)
|
||||
@@ -126,19 +125,16 @@ abstract class BaseAppFunctionServiceHeader : AppFunctionService() {
|
||||
return messageRepository.send(name, endpointValue, messageBody)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Framework-agnostic approach with alternative dependency injection or service locators
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@RequiresApi(36)
|
||||
@@ -163,16 +159,14 @@ abstract class BaseAppFunctionServiceLocator : AppFunctionService() {
|
||||
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.
|
||||
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 />
|
||||
|
||||
@@ -180,19 +174,17 @@ than creating redundant abstraction layers around the OS service.
|
||||
|
||||
### 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.
|
||||
**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`.
|
||||
Configuration APIs and the `@AppFunction` annotation are located in `androidx.appfunctions`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example: Serializable with inline KDoc
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
/** The parameter to create the task. */
|
||||
@@ -214,6 +206,7 @@ data class Task(
|
||||
/** The content of the task. */
|
||||
val content: String,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -222,6 +215,7 @@ data class Task(
|
||||
|
||||
### Example: Implementation detail
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@RequiresApi(36)
|
||||
@@ -262,6 +256,7 @@ abstract class BaseTaskAppFunctionService : AppFunctionService() {
|
||||
// Maps internal TaskEntity
|
||||
private fun TaskEntity.toTask() = Task(id = id, title = title, content = description)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
@@ -5,8 +5,7 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol.
|
||||
### Workflow: Agent-centric documentation
|
||||
|
||||
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"**.
|
||||
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 declaration. KSP **won't** extract documentation from class-level tags.
|
||||
@@ -14,8 +13,7 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol.
|
||||
|
||||
### Workflow: Global app description for server instructions
|
||||
|
||||
When writing the `appfn:description` for `app_metadata.xml`, follow these
|
||||
instructions:
|
||||
When writing the `appfn:description` for `app_metadata.xml`, follow these instructions:
|
||||
|
||||
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."
|
||||
@@ -29,19 +27,15 @@ instructions:
|
||||
|
||||
### 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...".
|
||||
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"
|
||||
|
||||
Remove conversational padding like "This method is used to..." or "Helpful
|
||||
for...". Be concise and technical.
|
||||
Remove conversational padding like "This method is used to..." or "Helpful for...". Be concise and technical.
|
||||
|
||||
### Inline KDoc for serializables
|
||||
|
||||
**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be
|
||||
inline for each property. KSP ignores class-level `@param` or `@property` tags
|
||||
for these classes.
|
||||
**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be inline for each property. KSP ignores class-level `@param` or `@property` tags for these classes.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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`.
|
||||
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`.
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
@@ -23,9 +20,7 @@ In version `1.0.0-alpha10` featuring `@AppFunctionServiceEntryPoint`:
|
||||
|
||||
### 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:
|
||||
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`.
|
||||
@@ -38,10 +33,7 @@ strict requirements that you must complete:
|
||||
|
||||
### 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.
|
||||
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 {
|
||||
@@ -66,16 +58,13 @@ within the main `appfunctions` artifact.
|
||||
|
||||
### 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`.
|
||||
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`:
|
||||
Annotate your service with `@AndroidEntryPoint` and inject your data repositories or use cases using standard `@Inject internal lateinit var`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@RequiresApi(36)
|
||||
@@ -96,19 +85,16 @@ abstract class BaseAppFunctionService : AppFunctionService() {
|
||||
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:
|
||||
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:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@RequiresApi(36)
|
||||
@@ -133,6 +119,7 @@ abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() {
|
||||
return messageRepository.send(name, endpointValue, messageBody)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -144,10 +131,7 @@ abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() {
|
||||
|
||||
### 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`.
|
||||
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
|
||||
@@ -156,8 +140,7 @@ because the wrapper inherently extends `android.content.Context`.
|
||||
|
||||
### Remove legacy configuration provider
|
||||
|
||||
Update your `Application` class by removing
|
||||
`AppFunctionConfiguration.Provider` and its associated builder entry points:
|
||||
Update your `Application` class by removing `AppFunctionConfiguration.Provider` and its associated builder entry points:
|
||||
|
||||
- abstract class BaseChatApplication : Application(), AppFunctionConfiguration.Provider { ... }
|
||||
+ abstract class BaseChatApplication : Application()
|
||||
@@ -166,21 +149,15 @@ Update your `Application` class by removing
|
||||
|
||||
### 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.
|
||||
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:
|
||||
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:
|
||||
|
||||
<br />
|
||||
|
||||
```xml
|
||||
<service
|
||||
@@ -201,6 +178,7 @@ within the `<application>` tag:
|
||||
<property
|
||||
android:name="android.app.appfunctions.app_metadata"
|
||||
android:resource="@xml/app_metadata" />
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -209,18 +187,7 @@ within the `<application>` tag:
|
||||
|
||||
## 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/`.*
|
||||
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!\"}'"`
|
||||
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!\"}'"`
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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-07-02'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- implementation
|
||||
- Android
|
||||
@@ -57,15 +57,11 @@ Email verification is applicable for the following use cases:
|
||||
|
||||
### Scope \& Pre-requisites
|
||||
|
||||
**Crucial** : This skill focuses exclusively on the **Android client-side
|
||||
integration** . It does **not** implement the app's server-side cryptographic
|
||||
validation logic. Server-side validation of the returned credential is required
|
||||
for security and must be implemented in your backend.
|
||||
**Crucial** : This skill focuses exclusively on the **Android client-side integration** . It does **not** implement the app's server-side cryptographic validation logic. Server-side validation of the returned credential is required for security and must be implemented in your backend.
|
||||
|
||||
## Codebase exploration for Use Cases
|
||||
|
||||
Get started with the following queries in project source code to find relevant
|
||||
screens with different use cases to implement verified email:
|
||||
Get started with the following queries in project source code to find relevant screens with different use cases to implement verified email:
|
||||
|
||||
- `SignUpScreen`
|
||||
- `"Email address"`
|
||||
@@ -76,14 +72,11 @@ screens with different use cases to implement verified email:
|
||||
|
||||
## Identifying Integration Points
|
||||
|
||||
To implement this feature effectively, you must first locate the relevant
|
||||
flows in your codebase. To initiate, start with the following strategies to
|
||||
cater to different use cases using verified email:
|
||||
To implement this feature effectively, you must first locate the relevant flows in your codebase. To initiate, start with the following strategies to cater to different use cases using verified email:
|
||||
|
||||
### 1. Search for Navigation Routes
|
||||
|
||||
If your app uses Navigation, search for routes or destinations related
|
||||
to authentication:
|
||||
If your app uses Navigation, search for routes or destinations related to authentication:
|
||||
|
||||
Look for:
|
||||
|
||||
@@ -92,16 +85,14 @@ Look for:
|
||||
|
||||
### 2. Locate Authentication ViewModels
|
||||
|
||||
Find the business logic handling user attributes and account creation, account
|
||||
recovery:
|
||||
Find the business logic handling user attributes and account creation, account recovery:
|
||||
|
||||
- **Keywords** : `SignUpViewModel`, `AuthViewModel`, `RegistrationRepository`.
|
||||
- **Code Pattern** : Look for methods like `onCreateAccount`, `onRecoverAccount`, or `validateEmail`.
|
||||
|
||||
### 3. Find instances of reauthentication for sensitive actions
|
||||
|
||||
For reauthentication use cases, find areas where users perform sensitive
|
||||
actions:
|
||||
For reauthentication use cases, find areas where users perform sensitive actions:
|
||||
|
||||
- **Keywords** : `ChangePassword`, `UpdatePayment`, `DeleteAccount`, `UpdateDetails`, `EditUserDetails`
|
||||
|
||||
@@ -117,21 +108,20 @@ 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.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
|
||||
## Add dependencies
|
||||
|
||||
In your app's `build.gradle` file, add the following dependencies for Credential
|
||||
Manager:
|
||||
In your app's `build.gradle` file, add the following dependencies for Credential Manager:
|
||||
|
||||
### Kotlin
|
||||
|
||||
```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 +129,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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -154,14 +144,9 @@ Use your app or activity context to create a `CredentialManager` object.
|
||||
|
||||
## Construct the Digital Credential request
|
||||
|
||||
To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest)
|
||||
containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a
|
||||
`requestJson` string formatted as an OpenID for Verifiable Presentations
|
||||
(OpenID4VP) request.
|
||||
To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a `requestJson` string formatted as an OpenID for Verifiable Presentations (OpenID4VP) request.
|
||||
|
||||
The OpenID4VP request JSON must follow a specific structure. The current
|
||||
providers support a JSON structure with an outer `"digital": {"requests":
|
||||
[...]}` wrapper.
|
||||
The OpenID4VP request JSON must follow a specific structure. The current providers support a JSON structure with an outer `"digital": {"requests": [...]}` wrapper.
|
||||
|
||||
val nonce = generateSecureRandomNonce()
|
||||
|
||||
@@ -206,26 +191,19 @@ providers support a JSON structure with an outer `"digital": {"requests":
|
||||
|
||||
The request contains the following key information:
|
||||
|
||||
- **DCQL query** : The `dcql_query` specifies the credential type and the
|
||||
claims being requested (`email_verified`). You can request other claims to
|
||||
determine the level of verification. A few possible claims are as follows:
|
||||
- **DCQL query** : The `dcql_query` specifies the credential type and the claims being requested (`email_verified`). You can request other claims to determine the level of verification. A few possible claims are as follows:
|
||||
|
||||
- `email_verified`: In the response, this is a Boolean that indicates whether the email is verified.
|
||||
- `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. 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.
|
||||
- **nonce**: A unique, cryptographically secure random value is generated for each request. This is critical for security, as it prevents replay attacks.
|
||||
|
||||
- `UserInfoCredential`: This value implies a specific type of digital
|
||||
credential that contains user attributes. Including this in the request is
|
||||
pivotal to distinguish the email verification use case.
|
||||
- `UserInfoCredential`: This value implies a specific type of digital credential that contains user attributes. Including this in the request is pivotal to distinguish the email verification use case.
|
||||
|
||||
Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create
|
||||
a `GetCredentialRequest`, and call `getCredential()`.
|
||||
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).
|
||||
@@ -261,15 +239,12 @@ Present the user with the request, using the Credential Manager built-in UI.
|
||||
|
||||
## Parse the response on the client
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Important:** This step is not for validation. Full cryptographic verification must be performed on your server.
|
||||
|
||||
The following code extracts the raw [Selective Disclosure JWT
|
||||
(SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims.
|
||||
The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims.
|
||||
|
||||
// 1. Parse the outer JSON wrapper to get the `vp_token`
|
||||
val responseData = JSONObject(responseJsonString)
|
||||
@@ -294,12 +269,9 @@ The following code extracts the raw [Selective Disclosure JWT
|
||||
|
||||
## Handle the response
|
||||
|
||||
The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential)
|
||||
response.
|
||||
The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) response.
|
||||
|
||||
The following is an example of what the raw `responseJsonString` looks like, and
|
||||
what the claims look like after parsing the inner SD-JWT where you get
|
||||
additional metadata as well along with verified email:
|
||||
The following is an example of what the raw `responseJsonString` looks like, and what the claims look like after parsing the inner SD-JWT where you get additional metadata as well along with verified email:
|
||||
|
||||
/*
|
||||
// Example of the raw JSON response from credential.credentialJson:
|
||||
@@ -337,18 +309,11 @@ additional metadata as well along with verified email:
|
||||
|
||||
## Server-side validation for account creation
|
||||
|
||||
Since the retrieved email is cryptographically verified, you can omit the email
|
||||
OTP verification step, significantly reducing sign-up friction and potentially
|
||||
increasing conversion. This process is best handled on your server. The client
|
||||
sends the raw response (containing the `vp_token`) and the original nonce to a
|
||||
new server endpoint.
|
||||
Since the retrieved email is cryptographically verified, you can omit the email OTP verification step, significantly reducing sign-up friction and potentially increasing conversion. This process is best handled on your server. The client sends the raw response (containing the `vp_token`) and the original nonce to a new server endpoint.
|
||||
|
||||
For verification, your application must send the full `responseJsonString` to
|
||||
your server for cryptographic validation before creating an account or logging
|
||||
the user in.
|
||||
For verification, your application must send the full `responseJsonString` to your server for cryptographic validation before creating an account or logging the user in.
|
||||
|
||||
The digital credential provides two critical levels of verification for your
|
||||
server:
|
||||
The digital credential provides two critical levels of verification for your server:
|
||||
|
||||
- **Authenticity of the data** : Verifying the issuer (`iss`) URL and the `SD-JWT` signature proves that a trusted authority issued this data.
|
||||
- **Identity of the presenter** : Verifying the `cnf` field and the Key Binding (`kb`) signature confirms that the credential is being shared by the same device it was originally issued to, preventing it from being intercepted or used on another device.
|
||||
@@ -359,15 +324,11 @@ The validation on the server must achieve the following:
|
||||
- **Verify signature**: Check the signature of the SD-JWT using the public keys (JWKs) available at https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable
|
||||
> Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
|
||||
For full security, make sure that you also validate the `nonce` to prevent
|
||||
replay attacks.
|
||||
For full security, make sure that you also validate the `nonce` to prevent replay attacks.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
try {
|
||||
// Send the raw credential response and the original nonce to your server.
|
||||
@@ -390,17 +351,11 @@ intercepted or spoofed before provisioning the new account.
|
||||
|
||||
## Passkey creation
|
||||
|
||||
An optional but highly recommended next step after provisioning an account is to
|
||||
immediately [create a passkey](references/android/identity/passkeys/create-passkeys.md) for that account. This provides a secure,
|
||||
passwordless method for the user to sign in. This flow is identical to a
|
||||
standard passkey registration.
|
||||
An optional but highly recommended next step after provisioning an account is to immediately [create a passkey](references/android/identity/passkeys/create-passkeys.md) for that account. This provides a secure, passwordless method for the user to sign in. This flow is identical to a standard passkey registration.
|
||||
|
||||
## WebView support
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -409,8 +364,7 @@ actual call to the Credential Manager API.
|
||||
|
||||
## Critical Security Guidelines
|
||||
|
||||
To maintain the integrity of the email verification flow, the following security
|
||||
requirements are mandatory:
|
||||
To maintain the integrity of the email verification flow, the following security requirements are mandatory:
|
||||
|
||||
- **Server-side Validation** : Never trust claims parsed on the client for security-sensitive operations like account creation. Send the complete, raw `responseJsonString` and the original `nonce` to the app's server for full verification.
|
||||
- **Nonce Integrity** : Generate a unique, cryptographically secure nonce for every request and **never** reuse a nonce across multiple requests to prevent replay attacks.
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
[Credential Manager](https://developer.android.com/reference/kotlin/androidx/credentials/package-summary) is the recommended Jetpack API for credential exchange
|
||||
in Android apps. The Credential Manager API streamlines credential exchange
|
||||
across form factors for use cases spanning authentication and authorization. You
|
||||
can also use Credential Manager to handle digital credentials and restore user
|
||||
credentials on new Android devices.
|
||||
[Credential Manager](https://developer.android.com/reference/kotlin/androidx/credentials/package-summary) is the recommended Jetpack API for credential exchange in Android apps. The Credential Manager API streamlines credential exchange across form factors for use cases spanning authentication and authorization. You can also use Credential Manager to handle digital credentials and restore user credentials on new Android devices.
|
||||
|
||||
## Credential Manager features
|
||||
|
||||
Credential Manager streamlines the sign-in process and enhances security by
|
||||
offering a robust set of capabilities, including:
|
||||
Credential Manager streamlines the sign-in process and enhances security by offering a robust set of capabilities, including:
|
||||
|
||||
- **Support for various authentication mechanisms** : Enables users to sign up or sign in to your app using:
|
||||
- [Passkeys](https://developer.android.com/identity/passkeys)
|
||||
- [Sign-in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) and other federated sign-in mechanisms
|
||||
- [Passwords](https://developer.android.com/identity/passwords)
|
||||
- [Digital credentials](https://developer.android.com/identity/digital-credentials)
|
||||
- **Cross-device credential restore** : Provides integration with [Restore
|
||||
Credentials](https://developer.android.com/identity/sign-in/restore-credentials), allowing users to seamlessly start using your app on a new device.
|
||||
- **Cross-device credential restore** : Provides integration with [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials), allowing users to seamlessly start using your app on a new device.
|
||||
- **Seamless integration with credential providers** : Offers support for [credential providers](https://developer.android.com/identity/sign-in/credential-provider), including password managers such as Google Password Manager.
|
||||
- **Credential management**: Enables updating the metadata for user credentials, helping to keep credentials consistent across your app and credential providers.
|
||||
- **WebView compatibility** : Works with apps that use [WebView](https://developer.android.com/identity/sign-in/credential-manager-webview).
|
||||
@@ -23,12 +17,10 @@ offering a robust set of capabilities, including:
|
||||
|
||||
## Benefits of using Credential Manager
|
||||
|
||||
Adopting Credential Manager provides several key advantages for both your
|
||||
application and your users:
|
||||
Adopting Credential Manager provides several key advantages for both your application and your users:
|
||||
|
||||
- **Enhanced security**: Passkeys offer enhanced security and protect users from phishing attempts.
|
||||
- **Support for various form factors** : The APIs work across Android [form
|
||||
factors](https://developer.android.com/identity/form-factors), including mobile, Android XR, and Wear OS devices.
|
||||
- **Support for various form factors** : The APIs work across Android [form factors](https://developer.android.com/identity/form-factors), including mobile, Android XR, and Wear OS devices.
|
||||
- **Simplified authentication across use cases** : [Digital credentials](https://developer.android.com/identity/digital-credentials), such as digital driver's licenses, corporate IDs, and national ID cards, enable authentication across various use cases, including those requiring [phone number verification](https://developer.android.com/identity/digital-credentials/phone-number-verification).
|
||||
- **Improved user experience**: Credential Manager's unified interface gives users a familiar and consistent experience and improves registration and sign-in speeds. The bottom sheet UI appears inline to your app's content, so your users remain within your app's context during sign-in. The following image shows Credential Manager's built-in UI:
|
||||
|
||||
@@ -36,8 +28,7 @@ application and your users:
|
||||
|
||||
## Authentication terminology
|
||||
|
||||
An entity that requires authentication for its users is known as a **relying
|
||||
party**. An authentication workflow typically includes the following components:
|
||||
An entity that requires authentication for its users is known as a **relying party**. An authentication workflow typically includes the following components:
|
||||
|
||||
- **Relying party client app**: The client---in this case, your Android app---that handles the user interface to create and use passkeys.
|
||||
- **Relying party server**: An app server that helps with the creation, storage, and verification of passkeys.
|
||||
@@ -45,21 +36,13 @@ party**. An authentication workflow typically includes the following components:
|
||||
|
||||
## Credential storage
|
||||
|
||||
Credential providers, such as Google Password Manager, provide a centralized and
|
||||
secure location for users to manage credentials across their devices, further
|
||||
simplifying the authentication process. To integrate with Credential Manager as
|
||||
a credential provider, see
|
||||
[Implement authentication as a credential provider](https://developer.android.com/identity/sign-in/credential-provider).
|
||||
Credential providers, such as Google Password Manager, provide a centralized and secure location for users to manage credentials across their devices, further simplifying the authentication process. To integrate with Credential Manager as a credential provider, see [Implement authentication as a credential provider](https://developer.android.com/identity/sign-in/credential-provider).
|
||||
|
||||
For more information about how Google Password Manager keeps your credentials
|
||||
secure, see
|
||||
[Security of Passkeys in the Google Password Manager](https://security.googleblog.com/2022/10/SecurityofPasskeysintheGooglePasswordManager.html).
|
||||
For more information about how Google Password Manager keeps your credentials secure, see [Security of Passkeys in the Google Password Manager](https://security.googleblog.com/2022/10/SecurityofPasskeysintheGooglePasswordManager.html).
|
||||
|
||||
## Migrate to Credential Manager
|
||||
|
||||
Credential Manager is intended to replace legacy Android authentication APIs and
|
||||
local FIDO2 credentials. For more information about migrating to Credential
|
||||
Manager, see the following guides:
|
||||
Credential Manager is intended to replace legacy Android authentication APIs and local FIDO2 credentials. For more information about migrating to Credential Manager, see the following guides:
|
||||
|
||||
- [Google Sign-In](https://developer.android.com/identity/sign-in/legacy-gsi-migration)
|
||||
- [One Tap](https://developer.android.com/identity/legacy/one-tap)
|
||||
|
||||
+6
-21
@@ -1,14 +1,6 @@
|
||||
Digital credential verification within Android apps can be used to authenticate
|
||||
and authorize a user's identity (such as a government ID), properties about that
|
||||
user (such as a driver's license, academic degree, or attributes such as age or
|
||||
address), or other scenarios where a credential needs to be issued and verified
|
||||
to assert the authenticity of an entity.
|
||||
Digital credential verification within Android apps can be used to authenticate and authorize a user's identity (such as a government ID), properties about that user (such as a driver's license, academic degree, or attributes such as age or address), or other scenarios where a credential needs to be issued and verified to assert the authenticity of an entity.
|
||||
|
||||
Digital Credentials is a public W3C standard that specifies how to access a
|
||||
user's verifiable digital credentials from a digital wallet, and is implemented
|
||||
for web use cases with the [W3C Credential Management API](https://www.w3.org/TR/credential-management-1/). On
|
||||
Android, Credential Manager's [`DigitalCredential`](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential) API is used for
|
||||
verifying digital credentials.
|
||||
Digital Credentials is a public W3C standard that specifies how to access a user's verifiable digital credentials from a digital wallet, and is implemented for web use cases with the [W3C Credential Management API](https://www.w3.org/TR/credential-management-1/). On Android, Credential Manager's [`DigitalCredential`](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential) API is used for verifying digital credentials.
|
||||
|
||||
### Android version compatibility
|
||||
|
||||
@@ -37,8 +29,7 @@ Next, Initialize an instance of the `CredentialManager` class.
|
||||
|
||||
#### Construct a digital credential request
|
||||
|
||||
Construct a digital credential request and use it to initialize a
|
||||
`DigitalCredentialOption`.
|
||||
Construct a digital credential request and use it to initialize a `DigitalCredentialOption`.
|
||||
|
||||
// The request in the JSON format to conform with
|
||||
// the JSON-ified Credential Manager - Verifier API request definition.
|
||||
@@ -51,8 +42,7 @@ Construct a digital credential request and use it to initialize a
|
||||
listOf(digitalCredentialOption)
|
||||
)
|
||||
|
||||
Here is an example of an OpenId4Vp request. A full reference can be found at
|
||||
this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
Here is an example of an OpenId4Vp request. A full reference can be found at this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
|
||||
{
|
||||
"requests": [
|
||||
@@ -100,14 +90,9 @@ this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.ht
|
||||
|
||||
#### Get the credential
|
||||
|
||||
Launch the `getCredential` flow with the constructed request. You will receive
|
||||
either a successful `GetCredentialResponse`, or a `GetCredentialException` if
|
||||
the request fails.
|
||||
Launch the `getCredential` flow with the constructed request. You will receive either a successful `GetCredentialResponse`, or a `GetCredentialException` if the request fails.
|
||||
|
||||
The `getCredential` flow triggers Android system dialogs to present the user's
|
||||
available credential options and collect their selection. Next, the wallet app
|
||||
that contains the chosen credential option will display UIs to collect consent
|
||||
and perform actions needed to generate a digital credential response.
|
||||
The `getCredential` flow triggers Android system dialogs to present the user's available credential options and collect their selection. Next, the wallet app that contains the chosen credential option will display UIs to collect consent and perform actions needed to generate a digital credential response.
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
|
||||
+40
-66
@@ -1,22 +1,31 @@
|
||||
> [!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).
|
||||
<br />
|
||||
|
||||
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
|
||||
Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request.
|
||||
## Android skills
|
||||
|
||||
[View on GitHub](https://github.com/android/skills/tree/main/identity/verified-email)
|
||||
|
||||
### Retrieve verified email
|
||||
|
||||
Use an Android skill 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 --skill 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 Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request.
|
||||
|
||||
## Add dependencies
|
||||
|
||||
In your app's `build.gradle` file, add the following dependencies for Credential
|
||||
Manager:
|
||||
In your app's `build.gradle` file, add the following dependencies for Credential Manager:
|
||||
|
||||
### Kotlin
|
||||
|
||||
```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 +33,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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,14 +48,9 @@ Use your app or activity context to create a `CredentialManager` object.
|
||||
|
||||
## Construct the Digital Credential request
|
||||
|
||||
To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest)
|
||||
containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a
|
||||
`requestJson` string formatted as an OpenID for Verifiable Presentations
|
||||
(OpenID4VP) request.
|
||||
To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a `requestJson` string formatted as an OpenID for Verifiable Presentations (OpenID4VP) request.
|
||||
|
||||
The OpenID4VP request JSON must follow a specific structure. The current
|
||||
providers support a JSON structure with an outer `"digital": {"requests":
|
||||
[...]}` wrapper.
|
||||
The OpenID4VP request JSON must follow a specific structure. The current providers support a JSON structure with an outer `"digital": {"requests": [...]}` wrapper.
|
||||
|
||||
val nonce = generateSecureRandomNonce()
|
||||
|
||||
@@ -91,26 +95,19 @@ providers support a JSON structure with an outer `"digital": {"requests":
|
||||
|
||||
The request contains the following key information:
|
||||
|
||||
- **DCQL query** : The `dcql_query` specifies the credential type and the
|
||||
claims being requested (`email_verified`). You can request other claims to
|
||||
determine the level of verification. A few possible claims are as follows:
|
||||
- **DCQL query** : The `dcql_query` specifies the credential type and the claims being requested (`email_verified`). You can request other claims to determine the level of verification. A few possible claims are as follows:
|
||||
|
||||
- `email_verified`: In the response, this is a Boolean that indicates whether the email is verified.
|
||||
- `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. 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.
|
||||
- **nonce**: A unique, cryptographically secure random value is generated for each request. This is critical for security, as it prevents replay attacks.
|
||||
|
||||
- `UserInfoCredential`: This value implies a specific type of digital
|
||||
credential that contains user attributes. Including this in the request is
|
||||
pivotal to distinguish the email verification use case.
|
||||
- `UserInfoCredential`: This value implies a specific type of digital credential that contains user attributes. Including this in the request is pivotal to distinguish the email verification use case.
|
||||
|
||||
Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create
|
||||
a `GetCredentialRequest`, and call `getCredential()`.
|
||||
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).
|
||||
@@ -146,15 +143,12 @@ Present the user with the request, using the Credential Manager built-in UI.
|
||||
|
||||
## Parse the response on the client
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Important:** This step is not for validation. Full cryptographic verification must be performed on your server.
|
||||
|
||||
The following code extracts the raw [Selective Disclosure JWT
|
||||
(SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims.
|
||||
The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims.
|
||||
|
||||
// 1. Parse the outer JSON wrapper to get the `vp_token`
|
||||
val responseData = JSONObject(responseJsonString)
|
||||
@@ -179,12 +173,9 @@ The following code extracts the raw [Selective Disclosure JWT
|
||||
|
||||
## Handle the response
|
||||
|
||||
The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential)
|
||||
response.
|
||||
The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) response.
|
||||
|
||||
The following is an example of what the raw `responseJsonString` looks like, and
|
||||
what the claims look like after parsing the inner SD-JWT where you get
|
||||
additional metadata as well along with verified email:
|
||||
The following is an example of what the raw `responseJsonString` looks like, and what the claims look like after parsing the inner SD-JWT where you get additional metadata as well along with verified email:
|
||||
|
||||
/*
|
||||
// Example of the raw JSON response from credential.credentialJson:
|
||||
@@ -222,18 +213,11 @@ additional metadata as well along with verified email:
|
||||
|
||||
## Server-side validation for account creation
|
||||
|
||||
Since the retrieved email is cryptographically verified, you can omit the email
|
||||
OTP verification step, significantly reducing sign-up friction and potentially
|
||||
increasing conversion. This process is best handled on your server. The client
|
||||
sends the raw response (containing the `vp_token`) and the original nonce to a
|
||||
new server endpoint.
|
||||
Since the retrieved email is cryptographically verified, you can omit the email OTP verification step, significantly reducing sign-up friction and potentially increasing conversion. This process is best handled on your server. The client sends the raw response (containing the `vp_token`) and the original nonce to a new server endpoint.
|
||||
|
||||
For verification, your application must send the full `responseJsonString` to
|
||||
your server for cryptographic validation before creating an account or logging
|
||||
the user in.
|
||||
For verification, your application must send the full `responseJsonString` to your server for cryptographic validation before creating an account or logging the user in.
|
||||
|
||||
The digital credential provides two critical levels of verification for your
|
||||
server:
|
||||
The digital credential provides two critical levels of verification for your server:
|
||||
|
||||
- **Authenticity of the data** : Verifying the issuer (`iss`) URL and the `SD-JWT` signature proves that a trusted authority issued this data.
|
||||
- **Identity of the presenter** : Verifying the `cnf` field and the Key Binding (`kb`) signature confirms that the credential is being shared by the same device it was originally issued to, preventing it from being intercepted or used on another device.
|
||||
@@ -244,15 +228,11 @@ The validation on the server must achieve the following:
|
||||
- **Verify signature**: Check the signature of the SD-JWT using the public keys (JWKs) available at https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable
|
||||
> Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
|
||||
|
||||
For full security, make sure that you also validate the `nonce` to prevent
|
||||
replay attacks.
|
||||
For full security, make sure that you also validate the `nonce` to prevent replay attacks.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
try {
|
||||
// Send the raw credential response and the original nonce to your server.
|
||||
@@ -275,17 +255,11 @@ intercepted or spoofed before provisioning the new account.
|
||||
|
||||
## Passkey creation
|
||||
|
||||
An optional but highly recommended next step after provisioning an account is to
|
||||
immediately [create a passkey](https://developer.android.com/identity/passkeys/create-passkeys) for that account. This provides a secure,
|
||||
passwordless method for the user to sign in. This flow is identical to a
|
||||
standard passkey registration.
|
||||
An optional but highly recommended next step after provisioning an account is to immediately [create a passkey](https://developer.android.com/identity/passkeys/create-passkeys) for that account. This provides a secure, passwordless method for the user to sign in. This flow is identical to a standard passkey registration.
|
||||
|
||||
## WebView support
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
+20
-58
@@ -1,7 +1,4 @@
|
||||
This document describes using Credential Manager to get a cryptographically
|
||||
verified email address from a user's device. This process removes the need for
|
||||
your app users to verify their email with one-time passwords (OTPs) or magic
|
||||
links.
|
||||
This document describes using Credential Manager to get a cryptographically verified email address from a user's device. This process removes the need for your app users to verify their email with one-time passwords (OTPs) or magic links.
|
||||
|
||||
This document explains the following areas:
|
||||
|
||||
@@ -19,90 +16,66 @@ This guide assumes you are familiar with the following concepts:
|
||||
|
||||
## Android compatibility
|
||||
|
||||
This feature is supported on mobiles, tablets, and foldable devices running
|
||||
Android 9 (API level 28) and higher. The minimum version of Google Play services
|
||||
(GMS) required is 25.49.x.
|
||||
This feature is supported on mobiles, tablets, and foldable devices running Android 9 (API level 28) and higher. The minimum version of Google Play services (GMS) required is 25.49.x.
|
||||
|
||||
## User experience
|
||||
|
||||
The following sections describe the user experience during the verification
|
||||
flow, the need to include fallback verification methods, as well as the
|
||||
recommended user experience for various use cases.
|
||||
The following sections describe the user experience during the verification flow, the need to include fallback verification methods, as well as the recommended user experience for various use cases.
|
||||
|
||||
### The verification flow
|
||||
|
||||
The user experience for sharing a verified email is as follows:
|
||||
|
||||
1. The user either focuses on an input field or taps a button that calls the
|
||||
Credential Manager API. Depending on the design of the screen, you can also
|
||||
call the API on your app's screen load.
|
||||
1. The user either focuses on an input field or taps a button that calls the Credential Manager API. Depending on the design of the screen, you can also call the API on your app's screen load.
|
||||
|
||||
2. A bottom sheet appears, showing the information that will be shared with the
|
||||
app. If no information is available on that device, the user sees a generic
|
||||
error message.
|
||||
2. A bottom sheet appears, showing the information that will be shared with the app. If no information is available on that device, the user sees a generic error message.
|
||||
|
||||
3. After the user taps **Agree and Continue**, display a success or failure
|
||||
message.
|
||||
3. After the user taps **Agree and Continue**, display a success or failure 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.
|
||||
|
||||
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, 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.
|
||||
|
||||
> [!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.
|
||||
|
||||
### Include primary and fallback flows
|
||||
|
||||
To ensure a streamlined user experience, include the following options on
|
||||
screens that require email verification:
|
||||
To ensure a streamlined user experience, include the following options on screens that require email verification:
|
||||
|
||||
- **Primary verification option**: An email field or button to trigger the Credential Manager API flow for quick verification.
|
||||
- **Alternate verification options**: A link or button for users to "Verify another way" or with "Other options" for manual email entry in case of failures, such as no information available on the device, or a mismatch between the retrieved and expected email. This should allow users to try verification with a different credential or by providing a manual OTP.
|
||||
|
||||
### Use cases
|
||||
|
||||
The following sections describe the recommended use cases, as well as the
|
||||
suggested user experience, for email verification.
|
||||
The following sections describe the recommended use cases, as well as the suggested user experience, for email verification.
|
||||
|
||||
#### Sign up
|
||||
|
||||
Users can immediately create an account with a verified email without a separate
|
||||
verification step. Optionally, prompt the user to add a passkey. If they opt to
|
||||
add a passkey, trigger the [passkey creation](https://developer.android.com/identity/passkeys/create-passkeys) flow.
|
||||
Users can immediately create an account with a verified email without a separate verification step. Optionally, prompt the user to add a passkey. If they opt to add a passkey, trigger the [passkey creation](https://developer.android.com/identity/passkeys/create-passkeys) flow.
|
||||
 Email verification during sign up
|
||||
|
||||
#### Account recovery
|
||||
|
||||
To eliminate the frustration of users searching for recovery codes in their spam
|
||||
folders, allow them to recover their account using the verified email securely
|
||||
stored on their device. Additionally, suggest that they create a passkey for
|
||||
future use.
|
||||
To eliminate the frustration of users searching for recovery codes in their spam folders, allow them to recover their account using the verified email securely stored on their device. Additionally, suggest that they create a passkey for future use.
|
||||
 Email verification during account recovery
|
||||
|
||||
#### Reauthentication for sensitive actions
|
||||
|
||||
Protect sensitive user actions, such as changing settings or updating profile
|
||||
details, by requiring a quick reauthentication step.
|
||||
Protect sensitive user actions, such as changing settings or updating profile details, by requiring a quick reauthentication step.
|
||||
 Email verification during reauthentication
|
||||
|
||||
## Accounts supported
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
A consumer Google Account can be created with an email address from any
|
||||
provider, not necessarily @gmail.com. However, Google verifies these accounts
|
||||
differently:
|
||||
A consumer Google Account can be created with an email address from any provider, not necessarily @gmail.com. However, Google verifies these accounts differently:
|
||||
|
||||
- For @gmail.com accounts: Google is the authoritative source, and the email is known to be verified.
|
||||
- For non-@gmail.com accounts: Google is not the authoritative source for these email addresses in the long term. While Google verifies the email when the account is created, the ownership of that email address might change over time. Therefore, for non-@gmail.com addresses, you should consider an additional verification step, such as sending an OTP, to ensure that the user still has access to the email account.
|
||||
|
||||
For more information about what verification implies, see [Digital
|
||||
Credentials](https://developer.android.com/identity/digital-credentials#verified).
|
||||
For more information about what verification implies, see [Digital Credentials](https://developer.android.com/identity/digital-credentials#verified).
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Apart from a user's email information, you can request other unverified fields, such as the user's given name, family name, name, and the profile picture of their Google Account. However, only the email is verified by Google.
|
||||
@@ -110,30 +83,19 @@ Credentials](https://developer.android.com/identity/digital-credentials#verified
|
||||
|
||||
## Validity and freshness
|
||||
|
||||
The system issues [verifiable credentials](https://developer.android.com/identity/digital-credentials#verified) (VCs) based on the user's current
|
||||
email from the active Google Accounts on the device. These credentials are
|
||||
issued to the device in advance, typically while the device is idle. While these
|
||||
credentials might remain valid for multiple days, the system performs a check at
|
||||
the moment of sharing the credentials to ensure that the account still exists,
|
||||
is on the device, and that the email address is valid---effectively prioritizing
|
||||
the account's immediate status over the credential's validity window.
|
||||
The system issues [verifiable credentials](https://developer.android.com/identity/digital-credentials#verified) (VCs) based on the user's current email from the active Google Accounts on the device. These credentials are issued to the device in advance, typically while the device is idle. While these credentials might remain valid for multiple days, the system performs a check at the moment of sharing the credentials to ensure that the account still exists, is on the device, and that the email address is valid---effectively prioritizing the account's immediate status over the credential's validity window.
|
||||
|
||||
To help ensure authenticity, a Key Binding (kb) signature is generated at the
|
||||
time of sharing, incorporating the nonce.
|
||||
To help ensure authenticity, a Key Binding (kb) signature is generated at the time of sharing, incorporating the nonce.
|
||||
|
||||
If a device is offline or the account is removed, the process fails rather than
|
||||
providing an expired VC or a VC for an inactive Google Account.
|
||||
If a device is offline or the account is removed, the process fails rather than 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
|
||||
delivery (for instance, the email might be diverted to spam). An OTP remains the
|
||||
definitive method for confirming email deliverability.
|
||||
While the process confirms the account's legitimacy, it does not guarantee inbox delivery (for instance, the email might be diverted to spam). An OTP remains the definitive method for confirming email deliverability.
|
||||
|
||||
## Comparison with Sign in with Google
|
||||
|
||||
While both Digital Credentials and [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) solutions provide a
|
||||
verified email, the user flows and use cases are different:
|
||||
While both Digital Credentials and [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) solutions provide a 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.
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
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 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).
|
||||
|
||||
## Understand digital credentials
|
||||
|
||||
In the physical world, a person might keep their identity in their wallet, and
|
||||
present it to a requesting party when asked:
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Digital credentials have the following differences from credentials in the
|
||||
physical world:
|
||||
Digital credentials have the following differences from credentials in the physical world:
|
||||
|
||||
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.
|
||||
|
||||
As such, Credential Manager takes on several roles that were formerly handled by
|
||||
the user:
|
||||
As such, Credential Manager takes on several roles that were formerly handled by the user:
|
||||
|
||||
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.
|
||||
@@ -37,52 +25,32 @@ This flow is shown here:
|
||||
|
||||
## Verifiable 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.
|
||||
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.
|
||||
|
||||
Not all digital credentials are verifiable credentials, but all verifiable
|
||||
credentials are digital credentials.
|
||||
Not all digital credentials are verifiable credentials, but all verifiable credentials are digital credentials.
|
||||
|
||||
## What it means for a claim to be verified
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### User experience
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
### Standards
|
||||
|
||||
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/).
|
||||
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/).
|
||||
|
||||
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 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).
|
||||
|
||||
Custom protocols are also feasible, though we recommend using one of the
|
||||
standard protocols in your application.
|
||||
Custom protocols are also feasible, though we recommend using one of the standard protocols in your application.
|
||||
|
||||
### Try it out
|
||||
|
||||
You can test out the digital credentials flow across platforms with an Android
|
||||
wallet and web-based verifier:
|
||||
You can test out the digital credentials flow across platforms with an Android wallet and web-based verifier:
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
Before your users can authenticate with passkeys, your app must first register
|
||||
or create the passkey for their account.
|
||||
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.
|
||||
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).
|
||||
@@ -14,15 +9,11 @@ app server.
|
||||
|
||||
## 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.
|
||||
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).
|
||||
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.
|
||||
@@ -33,8 +24,7 @@ server](https://developer.android.com/identity/credential-manager#authentication
|
||||
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).
|
||||
> **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
|
||||
|
||||
@@ -44,8 +34,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 +43,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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -71,26 +61,16 @@ Use your app or activity context to create a `CredentialManager` object.
|
||||
|
||||
## 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.
|
||||
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).
|
||||
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.
|
||||
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:
|
||||
The following snippet shows the structure the public key creation options sent by the app server:
|
||||
|
||||
{
|
||||
"challenge": "<base64url-encoded challenge>",
|
||||
@@ -149,9 +129,7 @@ Key fields in the public key creation options include:
|
||||
|
||||
## 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()`.
|
||||
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:
|
||||
|
||||
@@ -160,25 +138,14 @@ The `createPublicKeyCredentialRequest` includes the following:
|
||||
- `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).
|
||||
- `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.
|
||||
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).
|
||||
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]
|
||||
@@ -186,11 +153,9 @@ sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/mai
|
||||
|
||||
## 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.
|
||||
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 response after you successfully call `createCredential()` is a [PublicKeyCredential](https://developer.android.com/jetpack/androidx/releases/credentials) object.
|
||||
|
||||
The `PublicKeyCredential` looks as follows:
|
||||
|
||||
@@ -241,79 +206,55 @@ Add code to handle failures as shown in the following snippet:
|
||||
|
||||
## 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).
|
||||
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.
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
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"
|
||||
|
||||
+7
-20
@@ -1,11 +1,8 @@
|
||||
This document describes how to integrate the Credential Manager API with an
|
||||
Android app that uses WebView. Credential Manager is supported natively in the
|
||||
`android.webkit.WebView` library in [version 1.12.0](https://developer.android.com/jetpack/androidx/releases/webkit#1.12.0) and later.
|
||||
This document describes how to integrate the Credential Manager API with an Android app that uses WebView. Credential Manager is supported natively in the `android.webkit.WebView` library in [version 1.12.0](https://developer.android.com/jetpack/androidx/releases/webkit#1.12.0) and later.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To use Credential Manager in WebView, add the following dependencies to your app
|
||||
module's build script:
|
||||
To use Credential Manager in WebView, add the following dependencies to your app module's build script:
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.credentials:credentials:1.6.0-beta02")
|
||||
@@ -13,14 +10,11 @@ module's build script:
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
}
|
||||
|
||||
You will also need to associate your app with a website that your app owns using
|
||||
digital asset linking. For more information, see
|
||||
[adding digital asset linking](https://developer.android.com/identity/credential-manager/prerequisites).
|
||||
You will also need to associate your app with a website that your app owns using digital asset linking. For more information, see [adding digital asset linking](https://developer.android.com/identity/credential-manager/prerequisites).
|
||||
|
||||
## Use the WebKit library
|
||||
|
||||
To use the WebKit library, check for feature support, and then enable support by
|
||||
calling `setWebAuthenticationSupport()`:
|
||||
To use the WebKit library, check for feature support, and then enable support by calling `setWebAuthenticationSupport()`:
|
||||
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -66,19 +60,12 @@ calling `setWebAuthenticationSupport()`:
|
||||
|
||||
## Web integration
|
||||
|
||||
To learn how to build Web integration see
|
||||
[Create a passkey for passwordless logins](https://web.dev/passkey-registration/).
|
||||
You can also reference the [demo site source](https://github.com/deephand/webauthn-in-webview).
|
||||
To learn how to build Web integration see [Create a passkey for passwordless logins](https://web.dev/passkey-registration/). You can also reference the [demo site source](https://github.com/deephand/webauthn-in-webview).
|
||||
|
||||
## Testing and deployment
|
||||
|
||||
Test the entire flow thoroughly in a controlled environment to verify proper
|
||||
communication between the Android app, the web page, and the backend.
|
||||
Test the entire flow thoroughly in a controlled environment to verify proper communication between the Android app, the web page, and the backend.
|
||||
|
||||
Deploy the integrated solution to production, verifying that the backend can
|
||||
handle incoming registration and authentication requests. The backend code
|
||||
should generate initial JSON for registration (create) and authentication (get)
|
||||
processes. It should also handle validation and verification of the responses
|
||||
received from the web page.
|
||||
Deploy the integrated solution to production, verifying that the backend can handle incoming registration and authentication requests. The backend code should generate initial JSON for registration (create) and authentication (get) processes. It should also handle validation and verification of the responses received from the web page.
|
||||
|
||||
Verify the implementation corresponds to the [UX recommendations](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys).
|
||||
@@ -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-07-02'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- android
|
||||
- ui
|
||||
@@ -30,8 +30,7 @@ The app must:
|
||||
|
||||
## Workflow to make an app adaptive
|
||||
|
||||
To make an app adaptive, follow these steps or a subset of them adapting to the
|
||||
task.
|
||||
To make an app adaptive, follow these steps or a subset of them adapting to the task.
|
||||
|
||||
- Step 1: Verify current UI
|
||||
- Step 2: Make the navigation bar adaptive
|
||||
@@ -41,11 +40,9 @@ task.
|
||||
|
||||
## Step 1. Verify current UI
|
||||
|
||||
Ensure that screenshot tests exist to verify the current UI on different form
|
||||
factors. If they don't exist, add the [Compose Preview Screenshot Testing
|
||||
tool](references/android/develop/ui/compose/tooling/debug.md). Use the following annotation to create previews for all the major form
|
||||
factors. For example:
|
||||
Ensure that screenshot tests exist to verify the current UI on different form factors. If they don't exist, add the [Compose Preview Screenshot Testing tool](references/android/develop/ui/compose/tooling/debug.md). Use the following annotation to create previews for all the major form factors. For example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Preview(name = "Phone", device = Devices.PHONE, showBackground = true)
|
||||
@@ -64,25 +61,21 @@ fun FeedScreenPreview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Step 2. Make the navigation bar adaptive
|
||||
|
||||
Bottom navigation bars are optimized for touch input when the user is holding a
|
||||
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).
|
||||
Bottom navigation bars are optimized for touch input when the user is holding a 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 space for the content, hide the
|
||||
navigation area. Examples of this include:
|
||||
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 displaying a full-screen photo.
|
||||
|
||||
When the detail screen is displayed full-screen on mobile, full-screen mode must
|
||||
be deactivated on larger screens.
|
||||
When the detail screen is displayed full-screen on mobile, full-screen mode must be deactivated on larger screens.
|
||||
|
||||
Steps to migrate:
|
||||
|
||||
@@ -94,10 +87,7 @@ Steps to migrate:
|
||||
|
||||
### Step 2.1. Control navigation area visibility
|
||||
|
||||
If the navigation bar's visibility changes - it is hidden under certain
|
||||
scenarios or on certain screens - this behavior must be maintained with the
|
||||
adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state`
|
||||
parameter.
|
||||
If the navigation bar's visibility changes - it is hidden under certain scenarios or on certain screens - this behavior must be maintained with the adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state` parameter.
|
||||
|
||||
Steps to migrate:
|
||||
|
||||
@@ -107,6 +97,7 @@ Steps to migrate:
|
||||
|
||||
For example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Pass this variable to any composable that needs to control the navigation area visibility
|
||||
@@ -127,33 +118,26 @@ LaunchedEffect(isNavBarVisible){
|
||||
scaffoldVisibilityState.hide()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Step 3. Add multi-pane layouts using Navigation 3 Scenes
|
||||
|
||||
Analyze the codebase looking for related screens - tapping on something in one
|
||||
screen opens another screen that shows information related to the first. There
|
||||
are two canonical screen relationships: list-detail and supporting pane.
|
||||
Analyze the codebase looking for related screens - tapping on something in one screen opens another screen that shows information related to the first. There are two canonical screen relationships: list-detail and supporting pane.
|
||||
|
||||
IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement
|
||||
multi-pane layouts. Do not use `ListDetailPaneScaffold` or
|
||||
`SupportingPaneScaffold`.
|
||||
IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement multi-pane layouts. Do not use `ListDetailPaneScaffold` or `SupportingPaneScaffold`.
|
||||
|
||||
### Step 3.1. List-detail
|
||||
|
||||
#### Identify the list and detail screens
|
||||
|
||||
List-detail layouts display a list of items (this is the list screen) and
|
||||
clicking on an item opens a new screen that shows more details about that item
|
||||
(the detail screen).
|
||||
List-detail layouts display a list of items (this is the list screen) and clicking on an item opens a new screen that shows more details about that item (the detail screen).
|
||||
|
||||
Typical usage includes productivity apps like email, notes, and messaging.
|
||||
|
||||
Unless requested explicitly, avoid this pattern when the detail content requires
|
||||
substantial screen space (e.g., images or media that benefits from a full-screen
|
||||
presentation).
|
||||
Unless requested explicitly, avoid this pattern when the detail content requires substantial screen space (e.g., images or media that benefits from a full-screen presentation).
|
||||
|
||||
#### Add a Material list-detail SceneStrategy
|
||||
|
||||
@@ -163,8 +147,7 @@ presentation).
|
||||
|
||||
#### Use metadata to identify the list and detail screens
|
||||
|
||||
- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = {
|
||||
<placeholder composable> })`.
|
||||
- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = { <placeholder composable> })`.
|
||||
- Use the `detailPlaceholder` parameter to add a placeholder on the detail screen when no list items are selected.
|
||||
- Add metadata to the detail entry using `ListDetailSceneStrategy.detailPane()`.
|
||||
|
||||
@@ -173,14 +156,11 @@ presentation).
|
||||
- When a detail screen displays its content full-screen on mobile (content fills the entire screen, bars or rails are hidden), full-screen mode must be deactivated if it's part of a list-detail layout.
|
||||
- Detail screens must not show a back arrow when on a list-detail layout.
|
||||
|
||||
For a reference implementation, check the [Nav3 **Material** List Detail
|
||||
recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md).
|
||||
For a reference implementation, check the [Nav3 **Material** List Detail recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md).
|
||||
|
||||
### Step 3.2. Supporting pane
|
||||
|
||||
Identify supporting pane screens where a main screen displays a single item, and
|
||||
selecting it opens a "supporting screen" with more details. The supporting
|
||||
screen complements the main screen and is shown in a supporting pane.
|
||||
Identify supporting pane screens where a main screen displays a single item, and selecting it opens a "supporting screen" with more details. The supporting screen complements the main screen and is shown in a supporting pane.
|
||||
|
||||
#### Add a Material supporting pane `SceneStrategy`
|
||||
|
||||
@@ -195,15 +175,13 @@ screen complements the main screen and is shown in a supporting pane.
|
||||
|
||||
### Step 3.3. Run screenshot tests
|
||||
|
||||
If you have made changes, record new reference files. Ask the user to visually
|
||||
verify that the new layouts are correct.
|
||||
If you have made changes, record new reference files. Ask the user to visually verify that the new layouts are correct.
|
||||
|
||||
## Step 4. Make vertical lists adaptive by changing the number of columns
|
||||
|
||||
### Step 4.1. Make lazy lists adaptive
|
||||
|
||||
Look for the following vertical list composables: `LazyColumn`,
|
||||
`LazyVerticalGrid`, `LazyVerticalStaggeredGrid`.
|
||||
Look for the following vertical list composables: `LazyColumn`, `LazyVerticalGrid`, `LazyVerticalStaggeredGrid`.
|
||||
|
||||
Steps to migrate:
|
||||
|
||||
@@ -214,24 +192,16 @@ Steps to migrate:
|
||||
|
||||
### Step 4.2. Migrate non-lazy lists to Grid
|
||||
|
||||
WARNING: Grid is an experimental API available from Compose 1.11.0-beta01.
|
||||
Confirm with the user that they are happy to use an experimental API in their
|
||||
codebase.
|
||||
WARNING: Grid is an experimental API available from Compose 1.11.0-beta01. Confirm with the user that they are happy to use an experimental API in their codebase.
|
||||
|
||||
Look for any `Column` that contains multiple items of the same type and replace
|
||||
it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy
|
||||
layout. Do not place `Grid` inside the existing `Column`. Completely replace it.
|
||||
Look for any `Column` that contains multiple items of the same type and replace it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy layout. Do not place `Grid` inside the existing `Column`. Completely replace it.
|
||||
|
||||
`Grid` is configured by supplying a lambda (an extension function on
|
||||
`GridConfigurationScope`) to its `config` parameter. Inside the lambda,
|
||||
`constraints` provides the minimum and maximum dimensions of the grid container
|
||||
and can be used to change the number of rows and columns based on the available
|
||||
size. For example, the following code configures `Grid` such that when the
|
||||
available width is:
|
||||
`Grid` is configured by supplying a lambda (an extension function on `GridConfigurationScope`) to its `config` parameter. Inside the lambda, `constraints` provides the minimum and maximum dimensions of the grid container and can be used to change the number of rows and columns based on the available size. For example, the following code configures `Grid` such that when the available width is:
|
||||
|
||||
- less than 800dp, a 2x4 grid is used
|
||||
- 800dp or more, a 4x2 grid is used
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -250,26 +220,23 @@ Grid(
|
||||
gap(gapSizeDp)
|
||||
}
|
||||
) { /** items **/ }
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)`
|
||||
annotation to any function that uses it.
|
||||
`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)` annotation to any function that uses it.
|
||||
|
||||
## Step 5: Hide App Bars when scrolling
|
||||
|
||||
In an app with multiple top-level destinations, each screen must manage its own
|
||||
app bar state independently. There are two main scroll behaviors:
|
||||
In an app with multiple top-level destinations, each screen must manage its own app bar state independently. There are two main scroll behaviors:
|
||||
|
||||
- `exitUntilCollapsedScrollBehavior`: Hides on scroll down, stays hidden while you scroll up until you reach the very top (0 offset).
|
||||
- `enterAlwaysScrollBehavior`: Hides on scroll down, shows immediately on scroll up.
|
||||
|
||||
## Final step: Build and test
|
||||
|
||||
Build the app and run the local tests. If the project has screenshot tests, run
|
||||
them but DO NOT update the reference images. Prompt the user to do this after
|
||||
they have viewed the screenshot diffs.
|
||||
Build the app and run the local tests. If the project has screenshot tests, run them but DO NOT update the reference images. Prompt the user to do this after they have viewed the screenshot diffs.
|
||||
|
||||
## Additional documentation for experimental adaptive APIs
|
||||
|
||||
@@ -286,14 +253,11 @@ Check the FlexBox documentation:
|
||||
|
||||
## MediaQuery
|
||||
|
||||
Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's
|
||||
screen size, pointer precision, keyboard type, whether it has cameras or
|
||||
microphones, and other device capabilities.
|
||||
Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's screen size, pointer precision, keyboard type, whether it has cameras or microphones, and other device capabilities.
|
||||
|
||||
## Grid
|
||||
|
||||
Check the Grid documentation when you need to display a fixed number of items in
|
||||
a grid layout:
|
||||
Check the Grid documentation when you need to display a fixed number of items in a grid layout:
|
||||
|
||||
- [Overview](references/android/develop/ui/compose/layouts/adaptive/grid/index.md)
|
||||
- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md)
|
||||
|
||||
+15
-34
@@ -1,6 +1,6 @@
|
||||
To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig`
|
||||
block and supply it using the `config` parameter.
|
||||
To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` block and supply it using the `config` parameter.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox(
|
||||
@@ -14,17 +14,16 @@ FlexBox(
|
||||
}
|
||||
) { // child items
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Use `FlexBoxConfig` to define the layout direction, wrapping behavior,
|
||||
alignment, and gaps between items.
|
||||
Use `FlexBoxConfig` to define the layout direction, wrapping behavior, alignment, and gaps between items.
|
||||
|
||||
## Layout direction
|
||||
|
||||
The `direction` function sets the main axis, which dictates the direction
|
||||
items are laid out in. It accepts the following values:
|
||||
The `direction` function sets the main axis, which dictates the direction items are laid out in. It accepts the following values:
|
||||
|
||||
- `Row` (default): Sets the main axis to be horizontal. In left-to-right locales this will be left-to-right, with the opposite in right-to-left.
|
||||
- `RowReverse`: Reverses the direction of `Row`.
|
||||
@@ -33,13 +32,11 @@ items are laid out in. It accepts the following values:
|
||||
|
||||
## Align items and distribute extra space
|
||||
|
||||
The following sections describe how to align items and distribute extra space
|
||||
along the main and cross axes.
|
||||
The following sections describe how to align items and distribute extra space along the main and cross axes.
|
||||
|
||||
### Along the main axis
|
||||
|
||||
Use `justifyContent` to distribute items along the main axis. The following
|
||||
table shows the behavior when the direction is `Row`.
|
||||
Use `justifyContent` to distribute items along the main axis. The following table shows the behavior when the direction is `Row`.
|
||||
|
||||
|---|---|
|
||||
| |  |
|
||||
@@ -52,9 +49,7 @@ table shows the behavior when the direction is `Row`.
|
||||
|
||||
### Along the cross axis
|
||||
|
||||
Use `alignItems` to align items along the cross axis within a single line. This
|
||||
behavior can be overridden by individual items using the
|
||||
[`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment).
|
||||
Use `alignItems` to align items along the cross axis within a single line. This behavior can be overridden by individual items using the [`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment).
|
||||
|
||||
The following images show the behavior when the direction is `Row`:
|
||||
|
||||
@@ -62,10 +57,7 @@ The following images show the behavior when the direction is `Row`:
|
||||
|  |  |  |  |  |  |
|
||||
| | `Start` | `End` | `Center` | `Stretch` | `Baseline` |
|
||||
|
||||
Use `alignContent` to align lines to the cross axis and to distribute extra
|
||||
space between lines. This property only applies when there are multiple lines
|
||||
(wrapping is enabled). The following images show the behavior when the direction
|
||||
is `Row`:
|
||||
Use `alignContent` to align lines to the cross axis and to distribute extra space between lines. This property only applies when there are multiple lines (wrapping is enabled). The following images show the behavior when the direction is `Row`:
|
||||
|
||||
|---|---|---|---|---|---|---|
|
||||
|  |  |  |  |  |  |  |
|
||||
@@ -73,9 +65,7 @@ is `Row`:
|
||||
|
||||
## Wrap items
|
||||
|
||||
Wrapping lets a `FlexBox` container become multi-line, moving items that don't
|
||||
fit onto a new row or column along the cross-axis. Configure wrapping behavior
|
||||
using `wrap`.
|
||||
Wrapping lets a `FlexBox` container become multi-line, moving items that don't fit onto a new row or column along the cross-axis. Configure wrapping behavior using `wrap`.
|
||||
|
||||
|---|---|
|
||||
| **`FlexWrap` value** | **Example using direction `Row`** |
|
||||
@@ -83,29 +73,20 @@ using `wrap`.
|
||||
| `Wrap`: When there is insufficient space for an item (plus any [gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps)), a new line is created in the direction of the cross axis. For example, if the direction is `Row`, a new line is added **below**. |  |
|
||||
| `WrapReverse`: The same as `Wrap`, except the new line is added in the opposite direction to the cross axis. For example, if the direction is `Row`, a new line is added **above**. |  |
|
||||
|
||||
The following example shows how the `FlexBox` wrapping algorithm works. The
|
||||
`FlexBox` container has a main size of `100dp`, with `wrap` set to
|
||||
`FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`,
|
||||
`40dp`, and `50dp`, respectively.
|
||||
The following example shows how the `FlexBox` wrapping algorithm works. The `FlexBox` container has a main size of `100dp`, with `wrap` set to `FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`, `40dp`, and `50dp`, respectively.
|
||||
|
||||
There is `100dp` available space in the line. Child 1 is `20dp`.
|
||||
There is space, so Child 1 is placed into the line.
|
||||
There is `100dp` available space in the line. Child 1 is `20dp`. There is space, so Child 1 is placed into the line.
|
||||
 **Figure 1.** First item placed in the `FlexBox` container.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
line, so Child 3 is placed in a new line.
|
||||
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 line, so Child 3 is placed in a new line.
|
||||
 **Figure 3.** Third item placed on a new line because it doesn't fit on the first line.
|
||||
|
||||
## Add gaps between items
|
||||
|
||||
Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful
|
||||
to avoid adding spacing modifiers to children.
|
||||
Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful to avoid adding spacing modifiers to children.
|
||||
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
|
||||
+9
-10
@@ -2,11 +2,10 @@ This page describes how to implement basic `FlexBox` layouts.
|
||||
|
||||
## Set up project
|
||||
|
||||
1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's
|
||||
`lib.versions.toml`.
|
||||
1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's `lib.versions.toml`.
|
||||
|
||||
[versions]
|
||||
compose = "1.12.0-beta02"
|
||||
compose = "1.12.0-rc01"
|
||||
|
||||
[libraries]
|
||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
|
||||
@@ -19,9 +18,9 @@ This page describes how to implement basic `FlexBox` layouts.
|
||||
|
||||
## Create basic FlexBox layouts
|
||||
|
||||
**Example 1** : `FlexBox` lays out two `Text` elements that are centrally
|
||||
aligned.
|
||||
**Example 1** : `FlexBox` lays out two `Text` elements that are centrally aligned.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox(
|
||||
@@ -33,16 +32,16 @@ FlexBox(
|
||||
Text(text = "Hello", fontSize = 48.sp)
|
||||
Text(text = "World!", fontSize = 48.sp)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||

|
||||
|
||||
**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally
|
||||
to fill the available space on each row. There is an `8.dp`
|
||||
gap, both vertically and horizontally, between the items.
|
||||
**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally to fill the available space on each row. There is an `8.dp` gap, both vertically and horizontally, between the items.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox(
|
||||
@@ -59,11 +58,11 @@ FlexBox(
|
||||
OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) })
|
||||
PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||

|
||||
|
||||
To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set
|
||||
item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior).
|
||||
To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior).
|
||||
+11
-36
@@ -1,10 +1,7 @@
|
||||
> [!NOTE]
|
||||
> **Note:** FlexBox is an experimental API and is likely to change in the future. To use it, annotate your code with `@ExperimentalFlexBoxApi`. Please file any issues or feedback on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&title=%5BFlexBox%5D).
|
||||
|
||||
[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can
|
||||
resize, wrap, align, and distribute space among items to optimally fill the
|
||||
available space. It's a useful layout for different sized items and for resizing
|
||||
items when the available space changes.
|
||||
[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can resize, wrap, align, and distribute space among items to optimally fill the available space. It's a useful layout for different sized items and for resizing items when the available space changes.
|
||||
|
||||
With `FlexBox`, you can:
|
||||
|
||||
@@ -14,23 +11,16 @@ With `FlexBox`, you can:
|
||||
|
||||
## When to use FlexBox
|
||||
|
||||
`FlexBox` is usually used to display a small number of items *within* an
|
||||
overall screen layout. For an overall screen layout,
|
||||
`Grid` is usually a better choice. `FlexBox` does not support lazy-loading of
|
||||
items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you
|
||||
need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`.
|
||||
`FlexBox` is usually used to display a small number of items *within* an overall screen layout. For an overall screen layout, `Grid` is usually a better choice. `FlexBox` does not support lazy-loading of items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`.
|
||||
|
||||
## Terminology and concepts
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Key Point:** `FlexBox` is heavily influenced by the [CSS Flexible Box Layout specification](https://www.w3.org/TR/css-flexbox-1/) and has almost identical concepts, terminology, and behavior. If you're familiar with `display: flex`, you'll find `FlexBox`'s properties and behavior almost identical.
|
||||
|
||||
`FlexBox` lays out its items in either horizontal or vertical *lines* . This
|
||||
direction of these lines establishes the *main axis* . 90 degrees to the main
|
||||
axis is the *cross axis* . The length of the `FlexBox` along the main axis is
|
||||
known as the *main size* . The corresponding cross axis length is known as the
|
||||
*cross size* . These sizes and axes form the basis of `FlexBox`'s behavior.
|
||||
`FlexBox` lays out its items in either horizontal or vertical *lines* . This direction of these lines establishes the *main axis* . 90 degrees to the main axis is the *cross axis* . The length of the `FlexBox` along the main axis is known as the *main size* . The corresponding cross axis length is known as the *cross size* . These sizes and axes form the basis of `FlexBox`'s behavior.
|
||||
|
||||
<br />
|
||||
|
||||
 **Figure 1.** Axes and sizes when the `FlexBox` direction is `Row`.  **Figure 2.** Axes and sizes when the `FlexBox` direction is `Column`.
|
||||
|
||||
@@ -49,33 +39,18 @@ You can apply `FlexBox` properties in two ways:
|
||||
|
||||
### Understand the `FlexBox` layout algorithm
|
||||
|
||||
One of `FlexBox`'s most powerful features is its ability to resize its children
|
||||
to best fit the space available to it. Understanding how `FlexBox` does this can
|
||||
help you set `FlexBox` properties to optimize your UI for all possible sizes.
|
||||
One of `FlexBox`'s most powerful features is its ability to resize its children to best fit the space available to it. Understanding how `FlexBox` does this can help you set `FlexBox` properties to optimize your UI for all possible sizes.
|
||||
|
||||
`FlexBox`'s layout algorithm works in the following way:
|
||||
|
||||
1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size)
|
||||
to calculate its initial size along the main axis before any extra space is
|
||||
distributed.
|
||||
1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size) to calculate its initial size along the main axis before any extra space is distributed.
|
||||
|
||||
2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if
|
||||
present.
|
||||
2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if present.
|
||||
|
||||
3. **Build lines** : For each child, check if its initial size plus
|
||||
[`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line.
|
||||
If so, place this child into the line. If not, place it onto a new line if
|
||||
[wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line
|
||||
where it will overflow (it will be partially obscured by the edge of the
|
||||
container).
|
||||
3. **Build lines** : For each child, check if its initial size plus [`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line. If so, place this child into the line. If not, place it onto a new line if [wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line where it will overflow (it will be partially obscured by the edge of the container).
|
||||
|
||||
4. **Align or resize items in the main axis** : For each line, distribute extra
|
||||
space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or
|
||||
[aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them.
|
||||
4. **Align or resize items in the main axis** : For each line, distribute extra space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or [aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them.
|
||||
|
||||
5. **Align or resize items in the cross axis** : For each line, distribute extra
|
||||
space to or between items and lines by [stretching or aligning
|
||||
them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis).
|
||||
5. **Align or resize items in the cross axis** : For each line, distribute extra space to or between items and lines by [stretching or aligning them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis).
|
||||
|
||||
Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to
|
||||
create a basic `FlexBox`.
|
||||
Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to create a basic `FlexBox`.
|
||||
+21
-44
@@ -1,10 +1,10 @@
|
||||
Use `Modifier.flex` to control how an item changes size, order, and is aligned
|
||||
inside a `FlexBox`.
|
||||
Use `Modifier.flex` to control how an item changes size, order, and is aligned inside a `FlexBox`.
|
||||
|
||||
## Item size
|
||||
|
||||
Use the `basis`, `grow`, and `shrink` functions to control an item's size.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox {
|
||||
@@ -16,14 +16,14 @@ FlexBox {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Set initial size
|
||||
|
||||
Use `basis` to specify the item's initial size before any extra space is
|
||||
distributed. You can think of this as the item's *preferred* size.
|
||||
Use `basis` to specify the item's initial size before any extra space is distributed. You can think of this as the item's *preferred* size.
|
||||
|
||||
|---|---|---|---|
|
||||
| **Value type** | **Behavior** | **Code snippet** Note: The boxes have a maximum intrinsic size of `100dp` | **Example using container width `600dp`** |
|
||||
@@ -31,48 +31,31 @@ distributed. You can think of this as the item's *preferred* size.
|
||||
| Fixed `dp` | A fixed size in Dp. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(200.dp) } ) BlueRoundedBox( Modifier.flex { basis(100.dp) } ) } ``` |  |
|
||||
| Percentage | A percentage of the container size. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(0.7f) } ) BlueRoundedBox( Modifier.flex { basis(0.3f) } ) } ``` |  |
|
||||
|
||||
If the basis value is less than the item's intrinsic minimum size, the intrinsic
|
||||
minimum size is used instead. For example, if a `Text` item that contains a word
|
||||
requires `50dp` to display, but also has `basis = 10.dp`, a
|
||||
value of `50dp` is used.
|
||||
If the basis value is less than the item's intrinsic minimum size, the intrinsic minimum size is used instead. For example, if a `Text` item that contains a word requires `50dp` to display, but also has `basis = 10.dp`, a value of `50dp` is used.
|
||||
|
||||
### Grow items when there's space
|
||||
|
||||
Use `grow` to specify how much an item grows when there is extra space. This is
|
||||
space remaining in the `FlexBox` container after all the items' `basis` values
|
||||
have been added up. The `grow` value indicates *how much* of the extra space a
|
||||
given child will receive, relative to its siblings. By default, items won't
|
||||
grow.
|
||||
Use `grow` to specify how much an item grows when there is extra space. This is space remaining in the `FlexBox` container after all the items' `basis` values have been added up. The `grow` value indicates *how much* of the extra space a given child will receive, relative to its siblings. By default, items won't grow.
|
||||
|
||||
The following example shows a `FlexBox` with three child items. Each has a basis
|
||||
value of `100dp`. The first child has a positive `grow` value. Since there is
|
||||
only one child with a `grow` value, the actual value is irrelevant - as long as
|
||||
it's positive, the child receives all the extra space.
|
||||
The following example shows a `FlexBox` with three child items. Each has a basis value of `100dp`. The first child has a positive `grow` value. Since there is only one child with a `grow` value, the actual value is irrelevant - as long as it's positive, the child receives all the extra space.
|
||||
|
||||
The images show the `FlexBox` behavior when its container size is `600dp`.
|
||||
|
||||
|---|---|
|
||||
| ```kotlin FlexBox { RedRoundedBox( title = "400dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox(title = "100dp") GreenRoundedBox(title = "100dp") } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space.  Child 1 grows by `300dp` to fill the extra space.  |
|
||||
|
||||
In the following example, the container size and `basis` size are the same. The
|
||||
difference is that each child has a different `grow` value.
|
||||
In the following example, the container size and `basis` size are the same. The difference is that each child has a different `grow` value.
|
||||
|
||||
|---|---|
|
||||
| ```kotlin FlexBox { RedRoundedBox( title = "150dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox( title = "200dp", modifier = Modifier.flex { grow(2f) } ) GreenRoundedBox( title = "250dp", modifier = Modifier.flex { grow(3f) } ) } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space.  The total grow value is 6. Child 1 grows by (1 / 6) \* 300 = `50dp` Child 2 grows by (2 / 6) \* 300 = `100dp` Child 3 grows by (3 / 6) \* 300 = `150dp`  |
|
||||
|
||||
### Shrink items when there's insufficient space
|
||||
|
||||
Use `shrink` to specify how much an item shrinks when the `FlexBox` container
|
||||
has insufficient space for all the items. `shrink` works the same way as `grow`
|
||||
except that, instead of distributing *extra space* to items, the *space deficit*
|
||||
is distributed to items. The `shrink` value specifies how much of the space
|
||||
deficit the item receives, or rather, how much the item will shrink by. By
|
||||
default, items have a `shrink` value of `1f`, meaning they shrink equally.
|
||||
Use `shrink` to specify how much an item shrinks when the `FlexBox` container has insufficient space for all the items. `shrink` works the same way as `grow` except that, instead of distributing *extra space* to items, the *space deficit* is distributed to items. The `shrink` value specifies how much of the space deficit the item receives, or rather, how much the item will shrink by. By default, items have a `shrink` value of `1f`, meaning they shrink equally.
|
||||
|
||||
The following example shows two `Text` composables with the same text. The first
|
||||
child has a shrink value of `1f`, meaning it shrinks to absorb all the space
|
||||
deficit.
|
||||
The following example shows two `Text` composables with the same text. The first child has a shrink value of `1f`, meaning it shrinks to absorb all the space deficit.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox {
|
||||
@@ -91,6 +74,7 @@ FlexBox {
|
||||
.flex { shrink(0f) }
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -105,14 +89,11 @@ As the container size shrinks, Child 1 shrinks.
|
||||
|
||||
## Item alignment
|
||||
|
||||
Use `alignSelf` to control how an item is aligned to the cross axis. This
|
||||
overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It
|
||||
has all the same possible values, with the addition of `Auto` which inherits the
|
||||
behavior of the `FlexBox` container.
|
||||
Use `alignSelf` to control how an item is aligned to the cross axis. This overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It has all the same possible values, with the addition of `Auto` which inherits the behavior of the `FlexBox` container.
|
||||
|
||||
For example, this `FlexBox` has `alignItems` set to `Start` and five children
|
||||
which override the cross axis alignment.
|
||||
For example, this `FlexBox` has `alignItems` set to `Start` and five children which override the cross axis alignment.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox(
|
||||
@@ -126,6 +107,7 @@ FlexBox(
|
||||
PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) })
|
||||
OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -134,19 +116,13 @@ FlexBox(
|
||||
|
||||
## Item order
|
||||
|
||||
By default, `FlexBox` lays out items in the order that they are declared in
|
||||
code. Override this behavior using `order`.
|
||||
By default, `FlexBox` lays out items in the order that they are declared in code. Override this behavior using `order`.
|
||||
|
||||
The default value for `order` is zero, and `FlexBox` sorts items based on this
|
||||
value in ascending order. Any items that have the same `order` value are
|
||||
laid out in the same order they are declared in. Use negative and positive
|
||||
`order` values to move items to the start or end of a layout without changing
|
||||
where they are declared.
|
||||
The default value for `order` is zero, and `FlexBox` sorts items based on this value in ascending order. Any items that have the same `order` value are laid out in the same order they are declared in. Use negative and positive `order` values to move items to the start or end of a layout without changing where they are declared.
|
||||
|
||||
The following example shows two child items. The first has the default `order`
|
||||
of zero, and the second has an order of `-1`. After sorting, Child 1 appears
|
||||
after Child 2.
|
||||
The following example shows two child items. The first has the default `order` of zero, and the second has an order of `-1`. After sorting, Child 1 appears after Child 2.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
FlexBox {
|
||||
@@ -163,6 +139,7 @@ FlexBox {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
+33
-61
@@ -1,6 +1,4 @@
|
||||
You can define a Grid container configuration to create flexible layouts
|
||||
that respond to different screen sizes and content types.
|
||||
This page describes how to do the following:
|
||||
You can define a Grid container configuration to create flexible layouts that respond to different screen sizes and content types. This page describes how to do the following:
|
||||
|
||||
- [Define a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-definition): Set up the basic structure of rows and columns.
|
||||
- [Place items in a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#item-placement): Understand how items are placed into grid cells and how to change flow direction.
|
||||
@@ -9,13 +7,9 @@ This page describes how to do the following:
|
||||
|
||||
## Define a grid
|
||||
|
||||
A grid consists of columns and rows.
|
||||
The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter
|
||||
that accepts a lambda to define the columns and rows
|
||||
within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope).
|
||||
The following example defines a grid that has three rows and two columns,
|
||||
each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp):
|
||||
A grid consists of columns and rows. The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter that accepts a lambda to define the columns and rows within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope). The following example defines a grid that has three rows and two columns, each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp):
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -29,26 +23,18 @@ Grid(
|
||||
}
|
||||
) {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Place items in a grid
|
||||
|
||||
`Grid` takes the UI elements
|
||||
in the `content` lambda and places them into grid cells.
|
||||
The grid lays out items regardless of
|
||||
whether you have explicitly defined the rows and columns.
|
||||
By default,
|
||||
`Grid` tries to place a UI element in the available grid cell in the row;
|
||||
if it can't, it places it in an available grid cell in the next row.
|
||||
If there are no empty cells, `Grid` creates a new row.
|
||||
`Grid` takes the UI elements in the `content` lambda and places them into grid cells. The grid lays out items regardless of whether you have explicitly defined the rows and columns. By default, `Grid` tries to place a UI element in the available grid cell in the row; if it can't, it places it in an available grid cell in the next row. If there are no empty cells, `Grid` creates a new row.
|
||||
|
||||
In the following example, the grid has six grid cells
|
||||
and places a card into each one (Figure 1).
|
||||
Each grid cell is `160dp` x `90dp`,
|
||||
making the total grid size `320dp` x `270dp`.
|
||||
In the following example, the grid has six grid cells and places a card into each one (Figure 1). Each grid cell is `160dp` x `90dp`, making the total grid size `320dp` x `270dp`.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -68,15 +54,16 @@ Grid(
|
||||
Card5()
|
||||
Card6()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
 **Figure 1**. Six cards are placed in a grid that has three rows and two columns.
|
||||
|
||||
To change this default behavior to filling by column,
|
||||
set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()).
|
||||
To change this default behavior to filling by column, set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -98,6 +85,7 @@ Grid(
|
||||
Card5()
|
||||
Card6()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -106,17 +94,16 @@ Grid(
|
||||
|
||||
## Manage track sizing
|
||||
|
||||
Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track).
|
||||
You can specify the size of a grid track using one of the following methods:
|
||||
Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track). You can specify the size of a grid track using one of the following methods:
|
||||
|
||||
- **Fixed** (`Dp`): Allocates a specific size (e.g., `column(180.dp)`).
|
||||
- **Percentage** (`Float`): Allocates a percentage of the total available space from `0.0f` to `1.0f` (e.g., `row(0.5f)` for 50%).
|
||||
- **Flexible** ([`Fr`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Fr)): Distributes remaining space proportionally after fixed and percentage tracks are calculated. For example, if two rows are set to `1.fr` and `3.fr`, the latter receives 75% of the remaining height.
|
||||
- **Intrinsic** : Sizes the track based on the content inside it. For more information, see [Determine grid track size intrinsically](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#intrinsic-grid-track-size).
|
||||
|
||||
The following example uses the different track sizing options
|
||||
to define the row heights:
|
||||
The following example uses the different track sizing options to define the row heights:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -136,6 +123,7 @@ Grid(
|
||||
PastelYellowCard("Auto")
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -144,14 +132,11 @@ 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.
|
||||
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:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -171,6 +156,7 @@ Grid(
|
||||
PastelGreenCard("Flex(1.fr)")
|
||||
PastelBlueCard("Fixed(200.dp)")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -179,18 +165,11 @@ Grid(
|
||||
|
||||
### 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).
|
||||
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.
|
||||
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.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -205,7 +184,7 @@ Grid(
|
||||
},
|
||||
modifier = Modifier.size(width = 170.dp, height = 240.dp)
|
||||
) {
|
||||
Text("Lazy column in a Grid")
|
||||
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 ->
|
||||
@@ -213,6 +192,7 @@ Grid(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -221,20 +201,15 @@ Grid(
|
||||
|
||||
### Determine grid track size intrinsically
|
||||
|
||||
You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid`
|
||||
when you want the layout to adapt to the content,
|
||||
rather than forcing it into a fixed container.
|
||||
The grid track size is determined with the following values:
|
||||
You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` when you want the layout to adapt to the content, rather than forcing it into a fixed container. The grid track size is determined with the following values:
|
||||
|
||||
- [`GridTrackSize.MaxContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MaxContent()): Use the content's maximum intrinsic size (e.g., the width is determined by the full length of the text in a text block with no wrapping).
|
||||
- [`GridTrackSize.MinContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinContent()): Use the content's minimum intrinsic size (e.g., the width is determined by the longest single word in a text block).
|
||||
- [`GridTrackSize.Auto`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#Auto()): Use a flexible size for a track that adapts based on available space. It behaves like `MaxContent` by default, but shrinks and wraps its content to fit within the parent container.
|
||||
|
||||
The following example places two texts side by side.
|
||||
The column size for the first text is determined
|
||||
by the required minimum width to display the text,
|
||||
and the second column width depends on the required maximum width of the text.
|
||||
The following example places two texts side by side. The column size for the first text is determined by the required minimum width to display the text, and the second column width depends on the required maximum width of the text.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -248,6 +223,7 @@ Grid(
|
||||
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.")
|
||||
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -256,13 +232,9 @@ Grid(
|
||||
|
||||
## Set gaps between rows and columns
|
||||
|
||||
Once your grid tracks are sized,
|
||||
you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks.
|
||||
You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function,
|
||||
and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example,
|
||||
there is a `16dp` gap between each row,
|
||||
and an `8dp` gap between each column (Figure 5).
|
||||
Once your grid tracks are sized, you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks. You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function, and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example, there is a `16dp` gap between each row, and an `8dp` gap between each column (Figure 5).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -284,17 +256,16 @@ Grid(
|
||||
Card5()
|
||||
Card6()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
 **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,
|
||||
and to define column and gap sizes separately using a single function.
|
||||
The following code adds `8dp` gaps to the grid:
|
||||
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, and to define column and gap sizes separately using a single function. The following code adds `8dp` gaps to the grid:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -315,6 +286,7 @@ Grid(
|
||||
Card5()
|
||||
Card6()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
+6
-7
@@ -2,11 +2,10 @@ This page describes how to implement basic [`Grid`](https://developer.android.co
|
||||
|
||||
## Set up project
|
||||
|
||||
1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's
|
||||
`lib.versions.toml`.
|
||||
1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's `lib.versions.toml`.
|
||||
|
||||
[versions]
|
||||
compose = "1.12.0-beta02"
|
||||
compose = "1.12.0-rc01"
|
||||
|
||||
[libraries]
|
||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
|
||||
@@ -19,9 +18,9 @@ This page describes how to implement basic [`Grid`](https://developer.android.co
|
||||
|
||||
## Create a basic grid
|
||||
|
||||
The following example creates a basic 2x3 grid,
|
||||
with the columns and rows having a fixed size of `100.dp`.
|
||||
The following example creates a basic 2x3 grid, with the columns and rows having a fixed size of `100.dp`.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -41,11 +40,11 @@ Grid(
|
||||
Card5(containerColor = PastelOrange)
|
||||
Card6(containerColor = PastelYellow)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
 **Figure 1**. A basic grid consists of rows and columns with fixed size.
|
||||
|
||||
To learn how to implement more advanced grids,
|
||||
see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties).
|
||||
To learn how to implement more advanced grids, see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties).
|
||||
+9
-31
@@ -1,27 +1,16 @@
|
||||
> [!NOTE]
|
||||
> **Note:** `Grid` is an experimental API and is subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&template=1424126).
|
||||
|
||||
[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API
|
||||
that lets you flexibly implement a two-dimensional layout.
|
||||
With this API, you can display items in multi-column
|
||||
or multi-row layouts that adapt to the available container size.
|
||||
[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API that lets you flexibly implement a two-dimensional layout. With this API, you can display items in multi-column or multi-row layouts that adapt to the available container size.
|
||||
 **Figure 1.** A flexible and adaptive two-dimensional layout with `Grid`.
|
||||
|
||||
## How is Grid different from similar composables?
|
||||
|
||||
Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)).
|
||||
These components are mainly for visualization of large, homogeneous data sets---
|
||||
for example, displaying a content catalog in a video streaming app.
|
||||
These components are NOT designed
|
||||
for the structural layout of a screen or complex component.
|
||||
Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)). These components are mainly for visualization of large, homogeneous data sets--- for example, displaying a content catalog in a video streaming app. These components are NOT designed for the structural layout of a screen or complex component.
|
||||
|
||||
You can also implement a two-dimensional layout
|
||||
by combining multiple `Row` and `Column` composables.
|
||||
However, this approach has some downsides,
|
||||
such as deep hierarchies and difficulties in adaptability.
|
||||
You can also implement a two-dimensional layout by combining multiple `Row` and `Column` composables. However, this approach has some downsides, such as deep hierarchies and difficulties in adaptability.
|
||||
|
||||
The following table provides an overview
|
||||
of which layouts are suitable for each API:
|
||||
The following table provides an overview of which layouts are suitable for each API:
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
@@ -34,24 +23,16 @@ of which layouts are suitable for each API:
|
||||
|
||||
## Terminology
|
||||
|
||||
Familiarize yourself with the following terminology
|
||||
to understand how `Grid` works.
|
||||
Familiarize yourself with the following terminology to understand how `Grid` works.
|
||||
|
||||
### Grid line
|
||||
|
||||
A grid is made up of lines, which run horizontally and vertically.
|
||||
If your grid has three rows, it has four horizontal lines,
|
||||
including the one after the last row.
|
||||
In the following image, each dotted line represents a grid line:
|
||||
A grid is made up of lines, which run horizontally and vertically. If your grid has three rows, it has four horizontal lines, including the one after the last row. In the following image, each dotted line represents a grid line:
|
||||
 **Figure 2**. The grid consists of four horizontal lines and three vertical lines.
|
||||
|
||||
### Grid track
|
||||
|
||||
A grid track is the space between two grid lines.
|
||||
A row track is between two horizontal lines,
|
||||
and a column track is between two vertical lines.
|
||||
To define the size of these tracks,
|
||||
assign a size to them when you create the grid.
|
||||
A grid track is the space between two grid lines. A row track is between two horizontal lines, and a column track is between two vertical lines. To define the size of these tracks, assign a size to them when you create the grid.
|
||||
 **Figure 3**. A grid track for the first row.
|
||||
|
||||
### Grid cell
|
||||
@@ -61,13 +42,10 @@ A grid cell is the intersection of a row and column track.
|
||||
|
||||
### Grid area
|
||||
|
||||
A grid area consists of several grid cells.
|
||||
You can define a grid area by making an item span multiple tracks.
|
||||
A grid area consists of several grid cells. You can define a grid area by making an item span multiple tracks.
|
||||
 **Figure 5**. A grid area that consists of four grid cells.
|
||||
|
||||
### Grid gap
|
||||
|
||||
A grid gap is the gutter between grid tracks.
|
||||
You can't place a UI element into a gap,
|
||||
but you can span a UI element across it.
|
||||
A grid gap is the gutter between grid tracks. You can't place a UI element into a gap, but you can span a UI element across it.
|
||||
 **Figure 6**. A grid gap between the first column and the second column.
|
||||
+17
-41
@@ -1,33 +1,18 @@
|
||||
While the `Grid` config defines the overall structure,
|
||||
you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning,
|
||||
and alignment of items within that structure.
|
||||
While the `Grid` config defines the overall structure, you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning, and alignment of items within that structure.
|
||||
|
||||
## Set the item position
|
||||
|
||||
Place an item into a specific track or cell
|
||||
with the `row` and `column` parameters.
|
||||
Place an item into a specific track or cell with the `row` and `column` parameters.
|
||||
|
||||
The `row` and `column` parameters specify the row and column track indexes
|
||||
that the item is placed in.
|
||||
Track indexes are 1-based---they start at one.
|
||||
Specifying only `row` or `column` (not both) places the item
|
||||
in the next available space in that track.
|
||||
Specifying both places the item into that cell.
|
||||
The `row` and `column` parameters specify the row and column track indexes that the item is placed in. Track indexes are 1-based---they start at one. Specifying only `row` or `column` (not both) places the item in the next available space in that track. Specifying both places the item into that cell.
|
||||
|
||||
Use a positive integer to specify the track index from the start.
|
||||
For example, to place an item in the first row and column,
|
||||
use `gridItem(row = 1, column = 1)`.
|
||||
Use a positive integer to specify the track index from the start. For example, to place an item in the first row and column, use `gridItem(row = 1, column = 1)`.
|
||||
|
||||
Use a negative integer to specify the track relative to the end.
|
||||
For example, to place an item in the second-to-last row and column, use
|
||||
`gridItem(row = -2, column = -2)`.
|
||||
Use a negative integer to specify the track relative to the end. For example, to place an item in the second-to-last row and column, use `gridItem(row = -2, column = -2)`.
|
||||
|
||||
In the following example, Card **#2** is placed
|
||||
in the second row and the second column.
|
||||
Card **#3** is assigned to the last row (indexed by -1),
|
||||
where it automatically occupies
|
||||
the first available column in that track (Figure 1).
|
||||
In the following example, Card **#2** is placed in the second row and the second column. Card **#3** is assigned to the last row (indexed by -1), where it automatically occupies the first available column in that track (Figure 1).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -45,6 +30,7 @@ Grid(
|
||||
Card2(modifier = Modifier.gridItem(row = 2, column = 2))
|
||||
Card3(modifier = Modifier.gridItem(row = -1, column = -2))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -55,16 +41,9 @@ and Card #3 is placed in the first column in the third row.](https://developer.a
|
||||
|
||||
## Span rows and columns
|
||||
|
||||
Use the `rowSpan` and `columnSpan` parameters
|
||||
to span an item over multiple cells.
|
||||
You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area),
|
||||
which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell).
|
||||
The `gridItem` modifier lets you specify the grid area
|
||||
with the `rowSpan` and `columnSpan` parameters.
|
||||
In the following example,
|
||||
Card **#1** is placed in the area consisting of two rows and two columns
|
||||
(Figure 2).
|
||||
Use the `rowSpan` and `columnSpan` parameters to span an item over multiple cells. You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area), which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell). The `gridItem` modifier lets you specify the grid area with the `rowSpan` and `columnSpan` parameters. In the following example, Card **#1** is placed in the area consisting of two rows and two columns (Figure 2).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -84,6 +63,7 @@ Grid(
|
||||
Card3()
|
||||
Card4(modifier = Modifier.gridItem(columnSpan = 3))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -92,11 +72,9 @@ Grid(
|
||||
|
||||
## Set the alignment in a grid area
|
||||
|
||||
You can set the alignment of the UI element in a grid area
|
||||
by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier.
|
||||
In the following example, **#1** is placed in the center of the grid area
|
||||
consisting of two columns and two rows.
|
||||
You can set the alignment of the UI element in a grid area by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier. In the following example, **#1** is placed in the center of the grid area consisting of two columns and two rows.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -124,6 +102,7 @@ Grid(
|
||||
Card3()
|
||||
Card4(modifier = Modifier.gridItem(columnSpan = 3))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -133,13 +112,9 @@ consisting of two rows and two columns.](https://developer.android.com/static/de
|
||||
|
||||
## Auto-placement mixed with placed items
|
||||
|
||||
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 placed in specified grid cells,
|
||||
and the other items are auto-placed.
|
||||
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 placed in specified grid cells, and the other items are auto-placed.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Grid(
|
||||
@@ -161,6 +136,7 @@ Grid(
|
||||
Card5()
|
||||
Card6()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
+40
-98
@@ -1,10 +1,7 @@
|
||||
> [!NOTE]
|
||||
> **Note:** The `mediaQuery` function and the related data types are experimental and subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues?q=componentid:1876021).
|
||||
|
||||
You need various types of information, such as device capability
|
||||
and app status, to update your app layout.
|
||||
Window width and height are the most commonly used information.
|
||||
In addition to that, you can refer to the following information:
|
||||
You need various types of information, such as device capability and app status, to update your app layout. Window width and height are the most commonly used information. In addition to that, you can refer to the following information:
|
||||
|
||||
- Window posture
|
||||
- Pointing devices precision
|
||||
@@ -12,13 +9,9 @@ In addition to that, you can refer to the following information:
|
||||
- Whether the camera and microphone are supported by the device
|
||||
- The distance between a user and the device display
|
||||
|
||||
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.
|
||||
The following example switches the layout to `TabletopLayout`
|
||||
when the foldable posture is tabletop:
|
||||
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. The following example switches the layout to `TabletopLayout` when the foldable posture is tabletop:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -33,16 +26,16 @@ fun VideoPlayer(
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Enable the `mediaQuery` function
|
||||
|
||||
To enable the `mediaQuery` function,
|
||||
set the `isMediaQueryIntegrationEnabled` attribute of
|
||||
the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`:
|
||||
To enable the `mediaQuery` function, set the `isMediaQueryIntegrationEnabled` attribute of the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class MyApplication : Application() {
|
||||
@@ -51,20 +44,14 @@ class MyApplication : Application() {
|
||||
super.onCreate()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Define a condition with parameters
|
||||
|
||||
You can define a condition as a lambda
|
||||
that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope).
|
||||
The `mediaQuery` function evaluates the condition according to
|
||||
the current status and the device capabilities.
|
||||
The function returns a boolean value,
|
||||
so you can determine the layout with conditional branches
|
||||
like an `if` expression.
|
||||
Table 1 describes the parameters available in `UiMediaScope`.
|
||||
You can define a condition as a lambda that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope). The `mediaQuery` function evaluates the condition according to the current status and the device capabilities. The function returns a boolean value, so you can determine the layout with conditional branches like an `if` expression. Table 1 describes the parameters available in `UiMediaScope`.
|
||||
|
||||
| Parameter | Value type | Description |
|
||||
|---|---|---|
|
||||
@@ -77,17 +64,9 @@ Table 1 describes the parameters available in `UiMediaScope`.
|
||||
| `hasMicrophone` | `Boolean` | Whether the microphone is supported on the device. |
|
||||
| `viewingDistance` | [`UiMediaScope.ViewingDistance`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance) | The typical distance between the user and the device screen. |
|
||||
|
||||
A `UiMediaScope` object resolves the values of the parameters.
|
||||
The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope())
|
||||
to access the `UiMediaScope` object,
|
||||
which represents the current device capabilities and context.
|
||||
This object is dynamically updated when any changes are made,
|
||||
such as when the user changes the device posture.
|
||||
The `mediaQuery` function then evaluates the `query` lambda
|
||||
with the updated `UiMediaScope` object and returns a boolean value.
|
||||
For example, the following snippet chooses between `TabletopLayout`
|
||||
and `FlatLayout` based on the `windowPosture` parameter value.
|
||||
A `UiMediaScope` object resolves the values of the parameters. The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope()) to access the `UiMediaScope` object, which represents the current device capabilities and context. This object is dynamically updated when any changes are made, such as when the user changes the device posture. The `mediaQuery` function then evaluates the `query` lambda with the updated `UiMediaScope` object and returns a boolean value. For example, the following snippet chooses between `TabletopLayout` and `FlatLayout` based on the `windowPosture` parameter value.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -102,26 +81,18 @@ fun VideoPlayer(
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Make a decision based on the window size
|
||||
|
||||
[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints
|
||||
that help you design, develop, and test adaptive layouts.
|
||||
You can compare the two parameters representing the current window size
|
||||
with the threshold defined in the window size classes.
|
||||
The following example changes the number of panes according to the window width.
|
||||
[`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds
|
||||
of window size classes (Figure 1).
|
||||
[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints that help you design, develop, and test adaptive layouts. You can compare the two parameters representing the current window size with the threshold defined in the window size classes. The following example changes the number of panes according to the window width. [`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds of window size classes (Figure 1).
|
||||
|
||||
The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda
|
||||
and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof).
|
||||
Because `windowWidth` and `windowHeight` can update frequently,
|
||||
call the `derivedMediaQuery` function instead of the `mediaQuery` function
|
||||
when you refer to those parameters in the `query` lambda.
|
||||
The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof). Because `windowWidth` and `windowHeight` can update frequently, call the `derivedMediaQuery` function instead of the `mediaQuery` function when you refer to those parameters in the `query` lambda.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val narrowerThanMedium by derivedMediaQuery {
|
||||
@@ -135,6 +106,7 @@ when {
|
||||
narrowerThanExpanded -> TwoPaneLayout()
|
||||
else -> ThreePaneLayout()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -143,12 +115,9 @@ when {
|
||||
|
||||
### Update layout according to the window posture
|
||||
|
||||
The `windowPosture` parameter describes the current window posture
|
||||
as a `UiMediaScope.Posture` object.
|
||||
You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter
|
||||
with the values defined in the `UiMediaScope.Posture` class.
|
||||
The following example switches layout according to the window posture:
|
||||
The `windowPosture` parameter describes the current window posture as a `UiMediaScope.Posture` object. You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter with the values defined in the `UiMediaScope.Posture` class. The following example switches layout according to the window posture:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
when {
|
||||
@@ -156,32 +125,22 @@ when {
|
||||
mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout()
|
||||
mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Check the precision of the available pointing device
|
||||
|
||||
A high precision pointing device helps users to point a UI element precisely.
|
||||
The precision of a pointing device depends on the device type.
|
||||
A high precision pointing device helps users to point a UI element precisely. The precision of a pointing device depends on the device type.
|
||||
|
||||
The `pointerPrecision` parameter describes the precision
|
||||
of the available pointing devices, such as a mouse and touchscreen.
|
||||
There are four values defined in the `UiMediaScope.PointerPrecision` class:
|
||||
[`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()).
|
||||
`None` means that no pointing device is available.
|
||||
The precision ranges from highest to lowest in this order:
|
||||
`Fine`, `Coarse`, and `Blunt`.
|
||||
The `pointerPrecision` parameter describes the precision of the available pointing devices, such as a mouse and touchscreen. There are four values defined in the `UiMediaScope.PointerPrecision` class: [`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()). `None` means that no pointing device is available. The precision ranges from highest to lowest in this order: `Fine`, `Coarse`, and `Blunt`.
|
||||
|
||||
If multiple pointing devices are available and their precisions are different,
|
||||
the parameter is resolved with the highest one.
|
||||
For example, if there are two pointing devices --- a `Fine` precision device and
|
||||
a `Blunt` precision device ---
|
||||
`Fine` is the value of the `pointerPrecision` parameter.
|
||||
If multiple pointing devices are available and their precisions are different, the parameter is resolved with the highest one. For example, if there are two pointing devices --- a `Fine` precision device and a `Blunt` precision device --- `Fine` is the value of the `pointerPrecision` parameter.
|
||||
|
||||
The following example shows a larger button
|
||||
when the user is using a pointing device with low precision:
|
||||
The following example shows a larger button when the user is using a pointing device with low precision:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) {
|
||||
@@ -189,38 +148,31 @@ if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) {
|
||||
} else {
|
||||
NormalSizeButton()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Check the available keyboard type
|
||||
|
||||
The `keyboardKind` parameter represents the type of the available keyboards:
|
||||
[`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()).
|
||||
If an on-screen keyboard is displayed and
|
||||
a hardware keyboard is available at the same time,
|
||||
the parameter is resolved as `Physical`.
|
||||
If neither is detected, `None` is the value of the parameter.
|
||||
The following example shows a message suggesting that users connect a keyboard
|
||||
when no keyboard is detected:
|
||||
The `keyboardKind` parameter represents the type of the available keyboards: [`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()). If an on-screen keyboard is displayed and a hardware keyboard is available at the same time, the parameter is resolved as `Physical`. If neither is detected, `None` is the value of the parameter. The following example shows a message suggesting that users connect a keyboard when no keyboard is detected:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
if (mediaQuery { keyboardKind == UiMediaScope.KeyboardKind.None }) {
|
||||
SuggestKeyboardConnect()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Check if the device supports camera and microphone
|
||||
|
||||
Some devices don't support cameras or microphones.
|
||||
You can check if the device supports a camera and a microphone
|
||||
with the `hasCamera` parameter and the `hasMicrophone` parameter.
|
||||
The following example shows buttons to use with camera and microphone
|
||||
when the device supports them:
|
||||
Some devices don't support cameras or microphones. You can check if the device supports a camera and a microphone with the `hasCamera` parameter and the `hasMicrophone` parameter. The following example shows buttons to use with camera and microphone when the device supports them:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Row {
|
||||
@@ -234,25 +186,18 @@ Row {
|
||||
CameraButton()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Adjust UI with the estimated viewing distance
|
||||
|
||||
Viewing distance is a factor that helps determine layout.
|
||||
If the user is using the app from a distance,
|
||||
they would expect the text and UI elements to be bigger.
|
||||
The `viewingDistance` parameter provides an estimate of the viewing distance
|
||||
based on the device type and its typical usage context.
|
||||
Viewing distance is a factor that helps determine layout. If the user is using the app from a distance, they would expect the text and UI elements to be bigger. The `viewingDistance` parameter provides an estimate of the viewing distance based on the device type and its typical usage context.
|
||||
|
||||
There are three values defined in the `UiMediaScope.ViewingDistance` class:
|
||||
[`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()).
|
||||
`Near` means that the screen is in close range,
|
||||
and `Far` means that the device is viewed from a distance.
|
||||
The following example increases the font size when the viewing distance is
|
||||
`Far` or `Medium`:
|
||||
There are three values defined in the `UiMediaScope.ViewingDistance` class: [`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()). `Near` means that the screen is in close range, and `Far` means that the device is viewed from a distance. The following example increases the font size when the viewing distance is `Far` or `Medium`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val fontSize = when {
|
||||
@@ -260,19 +205,16 @@ val fontSize = when {
|
||||
mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Medium } -> 18.sp
|
||||
else -> 16.sp
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Preview a UI component
|
||||
|
||||
You can call the `mediaQuery` and `derivedMediaQuery` functions in the
|
||||
composable functions to preview UI components.
|
||||
The following snippet chooses between `TabletopLayout`
|
||||
and `FlatLayout` based on the `windowPosture` parameter value.
|
||||
To preview the `TabletopLayout`, the `windowPosture` parameter should be
|
||||
[`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()).
|
||||
You can call the `mediaQuery` and `derivedMediaQuery` functions in the composable functions to preview UI components. The following snippet chooses between `TabletopLayout` and `FlatLayout` based on the `windowPosture` parameter value. To preview the `TabletopLayout`, the `windowPosture` parameter should be [`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
when {
|
||||
@@ -280,14 +222,12 @@ when {
|
||||
mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout()
|
||||
mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
The `mediaQuery` and `derivedMediaQuery` functions evaluate
|
||||
the given `query` lambda within a `UiMediaScope` object,
|
||||
which is provided as `LocalUiMediaScope.current`.
|
||||
You can override it with the following steps:
|
||||
The `mediaQuery` and `derivedMediaQuery` functions evaluate the given `query` lambda within a `UiMediaScope` object, which is provided as `LocalUiMediaScope.current`. You can override it with the following steps:
|
||||
|
||||
1. Enable the `mediaQuery` function.
|
||||
2. Define a custom object that implements the `UiMediaScope` interface.
|
||||
@@ -296,6 +236,7 @@ You can override it with the following steps:
|
||||
|
||||
You can preview the `TabletopLayout` with the following example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Preview
|
||||
@@ -324,6 +265,7 @@ fun PreviewLayoutForTabletop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -2,60 +2,29 @@ Tools for debugging your Compose UI are available in Android Studio.
|
||||
|
||||
## Layout Inspector
|
||||
|
||||
Layout Inspector lets you inspect a Compose layout inside a running app in an
|
||||
emulator or physical device. You can use the Layout Inspector to check how often
|
||||
a composable is recomposed or skipped, which can help identify issues with your
|
||||
app. For example, some coding errors might force your UI to recompose
|
||||
excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance).
|
||||
Some coding errors can prevent your UI from recomposing and, therefore,
|
||||
prevent your UI changes from showing up on the screen. If you're new to
|
||||
Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to
|
||||
run it.
|
||||
Layout Inspector lets you inspect a Compose layout inside a running app in an emulator or physical device. You can use the Layout Inspector to check how often a composable is recomposed or skipped, which can help identify issues with your app. For example, some coding errors might force your UI to recompose excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance). Some coding errors can prevent your UI from recomposing and, therefore, prevent your UI changes from showing up on the screen. If you're new to Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to run it.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** If you're not seeing Compose components in layout inspector, make sure you are not removing `META-INF/androidx.compose.*.version` files from the APK. These are required for layout inspector to work.
|
||||
|
||||
### Get recomposition counts
|
||||
|
||||
When debugging your Compose layouts, knowing when composables
|
||||
[recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in
|
||||
understanding whether your UI is implemented properly. For example, if it's
|
||||
recomposing too many times, your app might be doing more work than is necessary.
|
||||
On the other hand, components that don't recompose when you anticipate them to
|
||||
can lead to unexpected behaviors.
|
||||
When debugging your Compose layouts, knowing when composables [recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in understanding whether your UI is implemented properly. For example, if it's recomposing too many times, your app might be doing more work than is necessary. On the other hand, components that don't recompose when you anticipate them to can lead to unexpected behaviors.
|
||||
|
||||
The Layout Inspector shows you when discrete composables in your layout
|
||||
hierarchy have either recomposed or skipped, as you interact with your app. In
|
||||
Android Studio, your recompositions are highlighted to help you determine
|
||||
where in the UI your composables are recomposing.
|
||||
The Layout Inspector shows you when discrete composables in your layout hierarchy have either recomposed or skipped, as you interact with your app. In Android Studio, your recompositions are highlighted to help you determine where in the UI your composables are recomposing.
|
||||
|
||||
**Figure 1.** Recompositions are highlighted in Layout Inspector.
|
||||
|
||||
The highlighted portion shows a gradient overlay of the composable in the image
|
||||
section of the Layout Inspector, and gradually disappears so that you can get an
|
||||
idea of where in the UI the composable with the highest recompositions can be
|
||||
found. If one composable is recomposing at a higher rate than another
|
||||
composable, then the first composable receives a stronger gradient overlay
|
||||
color. If you double-click a composable in the layout inspector, you're taken to
|
||||
the corresponding code for analysis.
|
||||
The highlighted portion shows a gradient overlay of the composable in the image section of the Layout Inspector, and gradually disappears so that you can get an idea of where in the UI the composable with the highest recompositions can be found. If one composable is recomposing at a higher rate than another composable, then the first composable receives a stronger gradient overlay color. If you double-click a composable in the layout inspector, you're taken to the corresponding code for analysis.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** To view recomposition counts, make sure your app is using an API level of 29 or higher, and `Compose 1.2.0` or higher. Then, deploy your app as you normally would.
|
||||
|
||||
 **Figure 2.**The composition and skip counter in Layout Inspector.
|
||||
|
||||
Open the **Layout Inspector** window and connect to your app process. In the
|
||||
**Component Tree** , there are two columns that appear next to the layout
|
||||
hierarchy. The first column shows the number of compositions for each node and
|
||||
the second column displays the number of skips for each node. Selecting a
|
||||
composable node shows the dimensions and parameters of the composable, unless
|
||||
it's an inline function, in which case the parameters can't be shown. You can
|
||||
also see similar information in the **Attributes** pane when you select a
|
||||
composable from the **Component Tree** or the **Layout Display**.
|
||||
Open the **Layout Inspector** window and connect to your app process. In the **Component Tree** , there are two columns that appear next to the layout hierarchy. The first column shows the number of compositions for each node and the second column displays the number of skips for each node. Selecting a composable node shows the dimensions and parameters of the composable, unless it's an inline function, in which case the parameters can't be shown. You can also see similar information in the **Attributes** pane when you select a composable from the **Component Tree** or the **Layout Display**.
|
||||
|
||||
Resetting the count can help you understand recompositions or skips during a
|
||||
specific interaction with your app. If you want to reset the count, click
|
||||
**Reset** near the top of the **Component Tree** pane.
|
||||
Resetting the count can help you understand recompositions or skips during a specific interaction with your app. If you want to reset the count, click **Reset** near the top of the **Component Tree** pane.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** If you don't see the new columns in the **Component Tree** pane, you can view them by selecting **Show Recomposition Counts** from the **View Options** menu  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
|
||||
to inspect semantic information in your Compose layouts.
|
||||
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 to inspect semantic information in your Compose layouts.
|
||||
 **Figure 4.** Semantic information displayed using the Layout Inspector.
|
||||
|
||||
When selecting a Compose node, use the **Attributes** pane to check whether it
|
||||
declares semantic information directly, merges semantics from its children, or
|
||||
both. To quickly identify which nodes include semantics, either declared or
|
||||
merged, use select the **View options** drop-down in the **Component Tree** pane
|
||||
and select **Highlight Semantics Layers**. This highlights only the nodes in the
|
||||
tree that include semantics, and you can use your keyboard to quickly navigate
|
||||
between them.
|
||||
When selecting a Compose node, use the **Attributes** pane to check whether it declares semantic information directly, merges semantics from its children, or both. To quickly identify which nodes include semantics, either declared or merged, use select the **View options** drop-down in the **Component Tree** pane and select **Highlight Semantics Layers**. This highlights only the nodes in the tree that include semantics, and you can use your keyboard to quickly navigate between them.
|
||||
|
||||
## Compose UI Check
|
||||
|
||||
To help you build more adaptive and accessible UIs in Jetpack Compose, Android
|
||||
Studio provides a UI Check mode in Compose Preview. This feature is similar
|
||||
to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner)
|
||||
for views.
|
||||
To help you build more adaptive and accessible UIs in Jetpack Compose, Android Studio provides a UI Check mode in Compose Preview. This feature is similar to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner) for views.
|
||||
|
||||
When you activate Compose UI check mode on a Compose Preview, Android Studio
|
||||
automatically audits your Compose UI and suggests improvements to make your UI
|
||||
more accessible and adaptive. Android Studio checks that your UI works across
|
||||
different screen sizes. In the **Problems** panel, the tool shows the issues
|
||||
that it detects, such as text stretched on large screens or low color contrast.
|
||||
When you activate Compose UI check mode on a Compose Preview, Android Studio automatically audits your Compose UI and suggests improvements to make your UI more accessible and adaptive. Android Studio checks that your UI works across different screen sizes. In the **Problems** panel, the tool shows the issues that it detects, such as text stretched on large screens or low color contrast.
|
||||
|
||||
To access this feature, click the UI Check icon on Compose Preview:
|
||||
 **Figure 5.** Entry point to UI check mode.
|
||||
|
||||
UI check automatically previews your UI in different configurations and
|
||||
highlights issues found in different configurations. In the **Problems** panel,
|
||||
when you click an issue, you can see the details of the issue, suggested fixes,
|
||||
and the renderings that highlight the area of the issue.
|
||||
UI check automatically previews your UI in different configurations and highlights issues found in different configurations. In the **Problems** panel, when you click an issue, you can see the details of the issue, suggested fixes, and the renderings that highlight the area of the issue.
|
||||
 **Figure 6.** UI check mode in action.
|
||||
|
||||
### Fix with AI
|
||||
|
||||
For issues detected in UI Check mode, you can use the AI agent to propose and
|
||||
apply code fixes. Click the **Fix with AI** button on an issue in the
|
||||
**Problems** panel. The agent analyzes the problem and your code to suggest
|
||||
changes that resolve the accessibility or adaptive issue.
|
||||
For issues detected in UI Check mode, you can use the AI agent to propose and apply code fixes. Click the **Fix with AI** button on an issue in the **Problems** panel. The agent analyzes the problem and your code to suggest changes that resolve the accessibility or adaptive issue.
|
||||
 **Figure 7.** The agent fixes UI issues in UI Check mode.
|
||||
+2
@@ -138,4 +138,6 @@ class MaterialListDetailActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
@@ -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-07-02'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- Jetpack Compose
|
||||
- migration
|
||||
@@ -20,16 +20,11 @@ metadata:
|
||||
- UI development
|
||||
---
|
||||
|
||||
This skill guides through the process of migrating an existing Android XML View
|
||||
to Jetpack Compose. It performs a stable, safe and visually consistent
|
||||
transition by following a structured, 10-step methodology. This skill migrates
|
||||
UI (XML to Jetpack Compose) only.
|
||||
This skill guides through the process of migrating an existing Android XML View to Jetpack Compose. It performs a stable, safe and visually consistent transition by following a structured, 10-step methodology. This skill migrates UI (XML to Jetpack Compose) only.
|
||||
|
||||
## Objective
|
||||
|
||||
To systematically convert a single legacy XML layout into modern, declarative
|
||||
Jetpack Compose UI while maintaining pixel-perfect visual parity and functional
|
||||
integrity.
|
||||
To systematically convert a single legacy XML layout into modern, declarative Jetpack Compose UI while maintaining pixel-perfect visual parity and functional integrity.
|
||||
|
||||
## Summary of the 10-step migration process
|
||||
|
||||
@@ -48,61 +43,31 @@ integrity.
|
||||
|
||||
### Step 1: Identify the optimal XML candidate for migration
|
||||
|
||||
If the user has explicitly specified a target XML layout, proceed to Step 2.
|
||||
Otherwise, analyze the codebase to identify the best candidate for migration by
|
||||
following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md).
|
||||
If the user has explicitly specified a target XML layout, proceed to Step 2. Otherwise, analyze the codebase to identify the best candidate for migration by following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md).
|
||||
|
||||
### Step 2: Analyze the project and layout
|
||||
|
||||
Analyze the identified XML View's structure, hierarchy, and implementation
|
||||
details.
|
||||
Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to
|
||||
guide your technical audit of the layout and surrounding project context.
|
||||
Analyze the identified XML View's structure, hierarchy, and implementation details. Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to guide your technical audit of the layout and surrounding project context.
|
||||
|
||||
### Step 3: Create a plan
|
||||
|
||||
Using the outputs and analysis done in the Step 1 and 2, generate a
|
||||
step-by-step plan for the migration. If you support user interaction, present
|
||||
to the user and ask for approval before proceeding. If user interaction is not
|
||||
supported, proceed to Step 4 following the generated plan.
|
||||
Using the outputs and analysis done in the Step 1 and 2, generate a step-by-step plan for the migration. If you support user interaction, present to the user and ask for approval before proceeding. If user interaction is not supported, proceed to Step 4 following the generated plan.
|
||||
|
||||
### Step 4: Capture the XML View UI
|
||||
|
||||
**IF** you support user interaction, ask the user to upload a screenshot of the
|
||||
XML View UI or provide an absolute path to a file. Use this image as a visual
|
||||
reference for the layout migration in Step 7.
|
||||
**ELSE IF** you are able to run an Android emulator, locate an existing
|
||||
screenshot test for the XML candidate. 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 XML UI.
|
||||
**ELSE** proceed to Step 5.
|
||||
**IF** you support user interaction, ask the user to upload a screenshot of the XML View UI or provide an absolute path to a file. Use this image as a visual reference for the layout migration in Step 7. **ELSE IF** you are able to run an Android emulator, locate an existing screenshot test for the XML candidate. 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 XML UI. **ELSE** proceed to Step 5.
|
||||
|
||||
### Step 5: Set up Compose dependencies and compiler
|
||||
|
||||
Check `build.gradle` or `libs.versions.toml` for Compose dependencies and
|
||||
compiler setup. If missing, use
|
||||
[Setup Compose Dependencies and Compiler](references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md).
|
||||
Run a sync to ensure dependencies resolve without errors.
|
||||
Check `build.gradle` or `libs.versions.toml` for Compose dependencies and compiler setup. If missing, use [Setup Compose Dependencies and Compiler](references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md). Run a sync to ensure dependencies resolve without errors.
|
||||
|
||||
### Step 6: Set up Compose theming
|
||||
|
||||
If the project already has Compose theming set up, proceed to Step 7. If Compose
|
||||
theming is missing, initialize it. For Material-based projects, follow
|
||||
[Material 3 migration guidelines](references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md).
|
||||
For custom design systems, apply expert judgment to migrate XML theming and
|
||||
match existing styles.
|
||||
**Constraints:** Do not migrate the entire theme. Implement only the minimum
|
||||
theming required for the specific XML candidate. Maintain original XML themes
|
||||
for interoperability. Maintain existing project code conventions, patterns,
|
||||
names and values.
|
||||
If the project already has Compose theming set up, proceed to Step 7. If Compose theming is missing, initialize it. For Material-based projects, follow [Material 3 migration guidelines](references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md). For custom design systems, apply expert judgment to migrate XML theming and match existing styles. **Constraints:** Do not migrate the entire theme. Implement only the minimum theming required for the specific XML candidate. Maintain original XML themes for interoperability. Maintain existing project code conventions, patterns, names and values.
|
||||
|
||||
### Step 7: Migrate the XML View to Compose
|
||||
|
||||
Convert the XML candidate to Jetpack Compose code, referencing
|
||||
[references/xml-layout-migration.md](references/xml-layout-migration.md) and the image from Step 4.
|
||||
You must include a **Compose Preview** for the newly created composable to
|
||||
facilitate visual verification.
|
||||
Convert the XML candidate to Jetpack Compose code, referencing [references/xml-layout-migration.md](references/xml-layout-migration.md) and the image from Step 4. You must include a **Compose Preview** for the newly created composable to facilitate visual verification.
|
||||
|
||||
### Step 8: Replace usages
|
||||
|
||||
@@ -113,12 +78,8 @@ Replace the usages of the migrated XML layout to use the new Compose component.
|
||||
|
||||
### Step 9: Validate the migration
|
||||
|
||||
Compare the baseline screenshot image from Step 4 with the rendered Compose
|
||||
Preview of the new composable. Ignore string content; focus on layout and
|
||||
styling. Iterate on the Compose code until visual parity is achieved. Once
|
||||
verified, write a Compose UI test for the new composable.
|
||||
Compare the baseline screenshot image from Step 4 with the rendered Compose Preview of the new composable. Ignore string content; focus on layout and styling. Iterate on the Compose code until visual parity is achieved. Once verified, write a Compose UI test for the new composable.
|
||||
|
||||
### Step 10: XML code removal
|
||||
|
||||
Delete the migrated XML file and its associated legacy tests. **Caution:** Only
|
||||
remove code and resources that are not referenced by other parts of the project.
|
||||
Delete the migrated XML file and its associated legacy tests. **Caution:** Only remove code and resources that are not referenced by other parts of the project.
|
||||
|
||||
+5
-24
@@ -1,41 +1,22 @@
|
||||
## 1. Project health \& build validation
|
||||
|
||||
Before performing any analysis, you must confirm the project is in a functional state.
|
||||
\* **Integrity check:** Verify the project syncs (Gradle) and builds successfully.
|
||||
\* **Error resolution:** If there are pre-existing build errors or sync failures, you must report these immediately and attempt to fix. **Do not proceed** with migration until a stable baseline is established.
|
||||
Before performing any analysis, you must confirm the project is in a functional state. \* **Integrity check:** Verify the project syncs (Gradle) and builds successfully. \* **Error resolution:** If there are pre-existing build errors or sync failures, you must report these immediately and attempt to fix. **Do not proceed** with migration until a stable baseline is established.
|
||||
|
||||
## 2. Compose pattern \& consistency analysis
|
||||
|
||||
If Jetpack Compose is already present, you must align with the established implementation style.
|
||||
\* **Pattern identification:** Scan the codebase for `@Composable` functions. Identify the project's "Best Practices" regarding state hoisting, composable construction and naming conventions, and file organization.
|
||||
\* **Theming review:** Determine how `MaterialTheme` or custom theme systems are implemented.
|
||||
\* Identify if the project uses a custom design system theme.
|
||||
\* Map how attributes, styles, and other theme components are accessed in Compose.
|
||||
If Jetpack Compose is already present, you must align with the established implementation style. \* **Pattern identification:** Scan the codebase for `@Composable` functions. Identify the project's "Best Practices" regarding state hoisting, composable construction and naming conventions, and file organization. \* **Theming review:** Determine how `MaterialTheme` or custom theme systems are implemented. \* Identify if the project uses a custom design system theme. \* Map how attributes, styles, and other theme components are accessed in Compose.
|
||||
|
||||
## 3. Design system \& infrastructure audit
|
||||
|
||||
Understand the design system classification (e.g. Material 2, Material 3, or custom design system).
|
||||
\* **Resource mapping:** Locate central XML definitions:
|
||||
\* `colors.xml` (Light/Dark variants)
|
||||
\* `dimens.xml`
|
||||
\* `styles.xml` / `themes.xml`
|
||||
\* **Hybrid analysis:** Determine if the project is **XML-only** , **Compose-only** , or **Hybrid** .
|
||||
\* **Reuse constraint:** If a Compose theming layer (e.g., `AppTheme.kt`) already exists, **DO NOT** generate a new one. You must reuse the existing infrastructure and contribute to it by following its existing implementation pattern.
|
||||
Understand the design system classification (e.g. Material 2, Material 3, or custom design system). \* **Resource mapping:** Locate central XML definitions: \* `colors.xml` (Light/Dark variants) \* `dimens.xml` \* `styles.xml` / `themes.xml` \* **Hybrid analysis:** Determine if the project is **XML-only** , **Compose-only** , or **Hybrid** . \* **Reuse constraint:** If a Compose theming layer (e.g., `AppTheme.kt`) already exists, **DO NOT** generate a new one. You must reuse the existing infrastructure and contribute to it by following its existing implementation pattern.
|
||||
|
||||
## 4. Candidate layout decomposition
|
||||
|
||||
Analyze the specific XML layout targeted for migration. You must extract and document the following requirements for the new composable:
|
||||
\* **Inputs:** UI State objects, primitive parameters, and click listeners.
|
||||
\* **Styling:** Specific color constants, typography styles, and shape definitions referenced in the XML.
|
||||
\* **Resources:** Identifying string resources, drawables, and dimensions.
|
||||
\* **Layout logic:** Modifiers required to replicate the XML constraints (padding, alignment, weight).
|
||||
Analyze the specific XML layout targeted for migration. You must extract and document the following requirements for the new composable: \* **Inputs:** UI State objects, primitive parameters, and click listeners. \* **Styling:** Specific color constants, typography styles, and shape definitions referenced in the XML. \* **Resources:** Identifying string resources, drawables, and dimensions. \* **Layout logic:** Modifiers required to replicate the XML constraints (padding, alignment, weight).
|
||||
|
||||
## 5. Architectural \& non-UI analysis
|
||||
|
||||
Understand the environment in which the UI resides to ensure proper integration.
|
||||
\* **State management:** Identify the usage of `ViewModel`, `Flow`, or `LiveData`.
|
||||
\* **Dependency Injection:** Check for Hilt, Koin, or manual DI to understand how dependencies are provided to the UI layer.
|
||||
\* **Testing \& architecture:** Note the architectural pattern (MVI, MVVM, or custom architecture setup.) and existing UI testing frameworks to ensure the migrated code remains testable. Unless the user explicitly requests, **DO NOT** make any changes to any non-UI code that aren't strictly required for the migration of the XML View.
|
||||
Understand the environment in which the UI resides to ensure proper integration. \* **State management:** Identify the usage of `ViewModel`, `Flow`, or `LiveData`. \* **Dependency Injection:** Check for Hilt, Koin, or manual DI to understand how dependencies are provided to the UI layer. \* **Testing \& architecture:** Note the architectural pattern (MVI, MVVM, or custom architecture setup.) and existing UI testing frameworks to ensure the migrated code remains testable. Unless the user explicitly requests, **DO NOT** make any changes to any non-UI code that aren't strictly required for the migration of the XML View.
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
|
||||
+13
-37
@@ -1,14 +1,8 @@
|
||||
When you introduce Compose in an existing app, you need to migrate your Material
|
||||
XML themes to use `MaterialTheme` for Compose components. This means your app's
|
||||
theming will have two sources of truth: the View-based theme and the Compose
|
||||
theme. Any changes to your styling need to be made in multiple places. Once
|
||||
your app is fully migrated to Compose, remove your XML theming.
|
||||
When you introduce Compose in an existing app, you need to migrate your Material XML themes to use `MaterialTheme` for Compose components. This means your app's theming will have two sources of truth: the View-based theme and the Compose theme. Any changes to your styling need to be made in multiple places. Once your app is fully migrated to Compose, remove your XML theming.
|
||||
|
||||
You can use the [Material Theme Builder](https://m3.material.io/theme-builder)
|
||||
tool for migrating colors.
|
||||
You can use the [Material Theme Builder](https://m3.material.io/theme-builder) tool for migrating colors.
|
||||
|
||||
When you start the migration from XML to Compose, migrate the theming to
|
||||
Material 3 Compose theming.
|
||||
When you start the migration from XML to Compose, migrate the theming to Material 3 Compose theming.
|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -29,26 +23,19 @@ Before migrating, be aware of the following limitations:
|
||||
|
||||
## Step 1: Evaluate the design system
|
||||
|
||||
Identify which design system is used in the XML View project.
|
||||
Analyze the migration path and necessary steps to migrate the existing design
|
||||
system to Material 3 in Compose.
|
||||
Identify which design system is used in the XML View project. Analyze the migration path and necessary steps to migrate the existing design system to Material 3 in Compose.
|
||||
|
||||
## Step 2: Identify theme source files
|
||||
|
||||
In XML you write `?attr/colorPrimary`. In Compose, you access theme values
|
||||
with `MaterialTheme.*`:
|
||||
In XML you write `?attr/colorPrimary`. In Compose, you access theme values with `MaterialTheme.*`:
|
||||
|
||||
Identify and locate all XML resources and files necessary for theming:
|
||||
light and dark color schemes and qualifiers, themes, shapes, dimensions,
|
||||
typography, styles and other relevant files.
|
||||
Identify and locate all XML resources and files necessary for theming: light and dark color schemes and qualifiers, themes, shapes, dimensions, typography, styles and other relevant files.
|
||||
|
||||
Resources such as strings can be reused as is and don't need to be migrated.
|
||||
|
||||
## Step 3: Migrate colors
|
||||
|
||||
**Key principle:** XML uses named hex colors.
|
||||
Material 3 uses *semantic roles* (e.g., `primary`, `onPrimary`, `surface`).
|
||||
Stop naming colors by their hex; name them by their role.
|
||||
**Key principle:** XML uses named hex colors. Material 3 uses *semantic roles* (e.g., `primary`, `onPrimary`, `surface`). Stop naming colors by their hex; name them by their role.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -69,8 +56,7 @@ Examples:
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
Migrate the dark and light color schemes from XML to their equivalents in
|
||||
Material 3 Compose.
|
||||
Migrate the dark and light color schemes from XML to their equivalents in Material 3 Compose.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Material 3 naming differs from Material 2 color naming.
|
||||
@@ -117,20 +103,14 @@ XML styles (styles.xml) system defines styles and appearance of:
|
||||
3. Themes and overlays
|
||||
4. Shapes
|
||||
|
||||
XML Views and components combine multiple attributes to create a style.
|
||||
They set their styles from styles.xml in two different ways:
|
||||
XML Views and components combine multiple attributes to create a style. They set their styles from styles.xml in two different ways:
|
||||
|
||||
1. Setting "style="@style/..." directly and explicitly in the XML View
|
||||
2. Setting the style indirectly and implicitly for a component as part of a larger Theme (theme.xml)
|
||||
|
||||
Styles have no **direct** equivalent in Compose - instead styles are passed as:
|
||||
parameters or modifiers to composables, using the
|
||||
[new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating
|
||||
layered, reusable composable variations with the defined style.
|
||||
Styles have no **direct** equivalent in Compose - instead styles are passed as: parameters or modifiers to composables, using the [new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating layered, reusable composable variations with the defined style.
|
||||
|
||||
Provide separate @Composable functions named according to the style and the
|
||||
base component, to signify the difference in styling and use cases for those
|
||||
components.
|
||||
Provide separate @Composable functions named according to the style and the base component, to signify the difference in styling and use cases for those components.
|
||||
|
||||
- **Pattern:** If an XML element uses a custom style (e.g., `style="@style/MyPrimaryButton"`), don't try to replicate the style inline. Instead, suggest creating a specific composable.
|
||||
- **Example:**
|
||||
@@ -162,10 +142,6 @@ components.
|
||||
|
||||
## Step 6: Validate the theme migration
|
||||
|
||||
Always use the existing theme values from the original XML theme as the source
|
||||
of truth for the new Material Theme in Compose.
|
||||
Never invent new theme values during migration, to maintain brand consistency
|
||||
and avoid visual regressions.
|
||||
Always use the existing theme values from the original XML theme as the source of truth for the new Material Theme in Compose. Never invent new theme values during migration, to maintain brand consistency and avoid visual regressions.
|
||||
|
||||
Verify all new Compose theme values match the existing XML values.
|
||||
Don't hardcode any migrated values.
|
||||
Verify all new Compose theme values match the existing XML values. Don't hardcode any migrated values.
|
||||
+30
-49
@@ -3,10 +3,9 @@
|
||||
|
||||
You can add Compose-based UI into an existing app that uses a View-based design.
|
||||
|
||||
To create a new, entirely Compose-based screen, have your
|
||||
activity call the `setContent()` method, and pass whatever composable functions
|
||||
you like.
|
||||
To create a new, entirely Compose-based screen, have your activity call the `setContent()` method, and pass whatever composable functions you like.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class ExampleActivity : ComponentActivity() {
|
||||
@@ -25,6 +24,7 @@ class ExampleActivity : ComponentActivity() {
|
||||
fun Greeting(name: String) {
|
||||
Text(text = "Hello $name!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -32,30 +32,17 @@ fun Greeting(name: String) {
|
||||
This code looks just like what you'd find in a Compose-only app.
|
||||
|
||||
> [!CAUTION]
|
||||
> **Caution:** To use the `ComponentActivity.setContent`
|
||||
> method, add the `androidx.activity:activity-compose:$latestVersion`
|
||||
> dependency to your `build.gradle` file.
|
||||
> **Caution:** To use the `ComponentActivity.setContent` method, add the `androidx.activity:activity-compose:$latestVersion` dependency to your `build.gradle` file.
|
||||
>
|
||||
> See the [Activity releases page](https://developer.android.com/jetpack/androidx/releases/activity)
|
||||
> to find out the latest version.
|
||||
> See the [Activity releases page](https://developer.android.com/jetpack/androidx/releases/activity) to find out the latest version.
|
||||
|
||||
## `ViewCompositionStrategy` for `ComposeView`
|
||||
|
||||
[`ViewCompositionStrategy`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy)
|
||||
defines when the Composition should be disposed. The default,
|
||||
[`ViewCompositionStrategy.Default`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy#Default()),
|
||||
disposes the Composition when the underlying
|
||||
[`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView)
|
||||
detaches from the window, unless it is part of a pooling container such as a
|
||||
`RecyclerView`. In a single-Activity Compose-only app, this default behavior is
|
||||
what you would want, however, if you are incrementally adding Compose in your
|
||||
codebase, this behavior may cause state loss in some scenarios.
|
||||
[`ViewCompositionStrategy`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy) defines when the Composition should be disposed. The default, [`ViewCompositionStrategy.Default`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy#Default()), disposes the Composition when the underlying [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) detaches from the window, unless it is part of a pooling container such as a `RecyclerView`. In a single-Activity Compose-only app, this default behavior is what you would want, however, if you are incrementally adding Compose in your codebase, this behavior may cause state loss in some scenarios.
|
||||
|
||||
To change the `ViewCompositionStrategy`, call the [`setViewCompositionStrategy()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractComposeView#setViewCompositionStrategy(androidx.compose.ui.platform.ViewCompositionStrategy))
|
||||
method and provide a different strategy.
|
||||
To change the `ViewCompositionStrategy`, call the [`setViewCompositionStrategy()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractComposeView#setViewCompositionStrategy(androidx.compose.ui.platform.ViewCompositionStrategy)) method and provide a different strategy.
|
||||
|
||||
The table below summarizes the different scenarios you can use
|
||||
`ViewCompositionStrategy` in:
|
||||
The table below summarizes the different scenarios you can use `ViewCompositionStrategy` in:
|
||||
|
||||
| `ViewCompositionStrategy` | Description and Interop Scenario |
|
||||
|---|---|
|
||||
@@ -69,11 +56,7 @@ The table below summarizes the different scenarios you can use
|
||||
> [!NOTE]
|
||||
> **Note:** For hybrid apps with Views and Compose, that use Fragments, use `ComposeView` to wrap composables content and add to a Fragment during migration. For Compose-only apps, do not use Fragments and instead use the recommended Compose-only architecture with a single Activity and latest navigation libraries, like Navigation 3.
|
||||
|
||||
If you want to incorporate Compose UI content in a fragment or an existing View
|
||||
layout, use [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView)
|
||||
and call its
|
||||
[`setContent()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView#setContent(kotlin.Function0))
|
||||
method. `ComposeView` is an Android [`View`](https://developer.android.com/reference/android/view/View).
|
||||
If you want to incorporate Compose UI content in a fragment or an existing View layout, use [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) and call its [`setContent()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView#setContent(kotlin.Function0)) method. `ComposeView` is an Android [`View`](https://developer.android.com/reference/android/view/View).
|
||||
|
||||
You can put the `ComposeView` in your XML layout just like any other `View`:
|
||||
|
||||
@@ -93,13 +76,12 @@ You can put the `ComposeView` in your XML layout just like any other `View`:
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
|
||||
```
|
||||
|
||||
In the Kotlin source code, inflate the layout from the [layout
|
||||
resource](https://developer.android.com/guide/topics/resources/layout-resource) defined in XML. Then get the
|
||||
`ComposeView` using the XML ID, set a Composition strategy that works best for
|
||||
the host `View`, and call `setContent()` to use Compose.
|
||||
In the Kotlin source code, inflate the layout from the [layout resource](https://developer.android.com/guide/topics/resources/layout-resource) defined in XML. Then get the `ComposeView` using the XML ID, set a Composition strategy that works best for the host `View`, and call `setContent()` to use Compose.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class ExampleFragmentXml : Fragment() {
|
||||
@@ -125,13 +107,14 @@ class ExampleFragmentXml : Fragment() {
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Alternatively, you can also use view binding to obtain references to the
|
||||
`ComposeView` by referencing the generated binding class for your XML layout file:
|
||||
Alternatively, you can also use view binding to obtain references to the `ComposeView` by referencing the generated binding class for your XML layout file:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class ExampleFragment : Fragment() {
|
||||
@@ -167,20 +150,18 @@ class ExampleFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||

|
||||
|
||||
**Figure 1.** This shows the output of the code that adds Compose elements in a
|
||||
View UI hierarchy. The "Hello Android!" text is displayed by a
|
||||
`TextView` widget. The "Hello Compose!" text is displayed by a
|
||||
Compose text element.
|
||||
**Figure 1.** This shows the output of the code that adds Compose elements in a View UI hierarchy. The "Hello Android!" text is displayed by a `TextView` widget. The "Hello Compose!" text is displayed by a Compose text element.
|
||||
|
||||
You can also include a `ComposeView` directly in a fragment if your full screen
|
||||
is built with Compose, which lets you avoid using an XML layout file entirely.
|
||||
You can also include a `ComposeView` directly in a fragment if your full screen is built with Compose, which lets you avoid using an XML layout file entirely.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class ExampleFragmentNoXml : Fragment() {
|
||||
@@ -203,15 +184,16 @@ class ExampleFragmentNoXml : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Multiple `ComposeView` instances in the same layout
|
||||
|
||||
If there are multiple `ComposeView` elements in the same layout, each one must
|
||||
have a unique ID for `savedInstanceState` to work.
|
||||
If there are multiple `ComposeView` elements in the same layout, each one must have a unique ID for `savedInstanceState` to work.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class ExampleFragmentMultipleComposeView : Fragment() {
|
||||
@@ -242,6 +224,7 @@ class ExampleFragmentMultipleComposeView : Fragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -253,18 +236,16 @@ The `ComposeView` IDs are defined in the `res/values/ids.xml` file:
|
||||
<item name="compose_view_x" type="id" />
|
||||
<item name="compose_view_y" type="id" />
|
||||
</resources>
|
||||
|
||||
```
|
||||
|
||||
## Preview composables in Layout Editor
|
||||
|
||||
You can also preview composables within the Layout Editor for your XML layout
|
||||
containing a `ComposeView`. Doing so lets you see how your composables look
|
||||
within a mixed Views and Compose layout.
|
||||
You can also preview composables within the Layout Editor for your XML layout containing a `ComposeView`. Doing so lets you see how your composables look within a mixed Views and Compose layout.
|
||||
|
||||
Say you want to display the following composable in the Layout Editor. Note
|
||||
that composables annotated with `@Preview` are good candidates to preview in the
|
||||
Layout Editor.
|
||||
Say you want to display the following composable in the Layout Editor. Note that composables annotated with `@Preview` are good candidates to preview in the Layout Editor.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Preview
|
||||
@@ -272,13 +253,12 @@ Layout Editor.
|
||||
fun GreetingPreview() {
|
||||
Greeting(name = "Android")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
To display this composable, use the `tools:composableName` tools attribute and
|
||||
set its value to the fully qualified name of the composable to preview in the
|
||||
layout.
|
||||
To display this composable, use the `tools:composableName` tools attribute and set its value to the fully qualified name of the composable to preview in the layout.
|
||||
|
||||
```xml
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
@@ -294,6 +274,7 @@ layout.
|
||||
android:layout_width="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
```
|
||||
|
||||

|
||||
+28
-64
@@ -1,22 +1,13 @@
|
||||
You can include an Android View hierarchy in a Compose UI. This approach is
|
||||
particularly useful if you want to use UI elements that are not yet available in
|
||||
Compose, like
|
||||
[`AdView`](https://developers.google.com/android/reference/com/google/android/gms/ads/AdView).
|
||||
This approach also lets you reuse custom views you may have designed.
|
||||
You can include an Android View hierarchy in a Compose UI. This approach is particularly useful if you want to use UI elements that are not yet available in Compose, like [`AdView`](https://developers.google.com/android/reference/com/google/android/gms/ads/AdView). This approach also lets you reuse custom views you may have designed.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Use `AndroidView` as a wrapper only for missing SDK components without Compose support. Rewrite your custom Views in Compose wherever possible, starting the migration with the simplest custom Views and scaling to more complex ones.
|
||||
|
||||
<br />
|
||||
|
||||
To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView.composable#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1))
|
||||
composable. `AndroidView` is passed a lambda that returns a
|
||||
[`View`](https://developer.android.com/reference/android/view/View). `AndroidView` also provides an `update`
|
||||
callback that is called when the view is inflated. The `AndroidView` recomposes
|
||||
whenever a `State` read within the callback changes. `AndroidView`, like many
|
||||
other built-in composables, takes a `Modifier` parameter that can be used, for
|
||||
example, to set its position in the parent composable.
|
||||
To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView.composable#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable. `AndroidView` is passed a lambda that returns a [`View`](https://developer.android.com/reference/android/view/View). `AndroidView` also provides an `update` callback that is called when the view is inflated. The `AndroidView` recomposes whenever a `State` read within the callback changes. `AndroidView`, like many other built-in composables, takes a `Modifier` parameter that can be used, for example, to set its position in the parent composable.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -54,6 +45,8 @@ fun ContentExample() {
|
||||
CustomView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -63,14 +56,12 @@ fun ContentExample() {
|
||||
|
||||
## `AndroidView` with view binding
|
||||
|
||||
To embed an XML layout, use the
|
||||
[`AndroidViewBinding`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidViewBinding(kotlin.Function3,%20androidx.compose.ui.Modifier,%20kotlin.Function1))
|
||||
API, which is provided by the `androidx.compose.ui:ui-viewbinding` library. To
|
||||
do this, your project must enable [view binding](https://developer.android.com/topic/libraries/view-binding#setup).
|
||||
To embed an XML layout, use the [`AndroidViewBinding`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidViewBinding(kotlin.Function3,%20androidx.compose.ui.Modifier,%20kotlin.Function1)) API, which is provided by the `androidx.compose.ui:ui-viewbinding` library. To do this, your project must enable [view binding](https://developer.android.com/topic/libraries/view-binding#setup).
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** For Compose-only apps, don't use `AndroidViewBinding` to inflate full screen-level XML layouts, and instead use it only for smaller, legacy XML layouts during the incremental migration process.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -79,23 +70,21 @@ fun AndroidViewBindingExample() {
|
||||
exampleView.setBackgroundColor(Color.GRAY)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## `AndroidView` in Lazy lists
|
||||
|
||||
If you are using an `AndroidView` in a Lazy list (`LazyColumn`, `LazyRow`,
|
||||
`Pager`, etc.), consider using the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1))
|
||||
overload introduced in version 1.4.0-rc01. This overload allows Compose to reuse
|
||||
the underlying `View` instance when the containing composition is reused as is
|
||||
the case for Lazy lists.
|
||||
If you are using an `AndroidView` in a Lazy list (`LazyColumn`, `LazyRow`, `Pager`, etc.), consider using the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1)) overload introduced in version 1.4.0-rc01. This overload allows Compose to reuse the underlying `View` instance when the containing composition is reused as is the case for Lazy lists.
|
||||
|
||||
This overload of `AndroidView` adds 2 additional parameters:
|
||||
|
||||
- `onReset` - A callback invoked to signal that the `View` is about to be reused. This must be non-null to enable View reuse.
|
||||
- `onRelease` (optional) - A callback invoked to signal that the `View` has exited the composition and will not be reused again.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -117,72 +106,50 @@ fun AndroidViewInLazyList() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Fragments in Compose (Transitionary Step)
|
||||
|
||||
Use the `AndroidFragment` composable to add a `Fragment` in Compose.
|
||||
`AndroidFragment` has fragment-specific handling such as removing the
|
||||
fragment when the composable leaves the composition.
|
||||
Use the `AndroidFragment` composable to add a `Fragment` in Compose. `AndroidFragment` has fragment-specific handling such as removing the fragment when the composable leaves the composition.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Wrap existing Fragments in Compose only during incremental migration. For Compose-only apps, do not use Fragments and instead use the recommended Compose-only architecture with a single Activity and latest navigation libraries, like Navigation 3.
|
||||
|
||||
To include a fragment, use the [`AndroidFragment`](https://developer.android.com/reference/kotlin/androidx/fragment/compose/package-summary#AndroidFragment)
|
||||
composable. You pass a `Fragment` class to `AndroidFragment`, which then adds
|
||||
an instance of that class directly into the composition. `AndroidFragment` also
|
||||
provides a `fragmentState` object to create the `AndroidFragment` with a given
|
||||
state, `arguments` to pass into the new fragment, and an `onUpdate` callback
|
||||
that provides the fragment from the composition. Like many
|
||||
other built-in composables, `AndroidFragment` accepts a `Modifier` parameter
|
||||
that you can use, for
|
||||
example, to set its position in the parent composable.
|
||||
To include a fragment, use the [`AndroidFragment`](https://developer.android.com/reference/kotlin/androidx/fragment/compose/package-summary#AndroidFragment) composable. You pass a `Fragment` class to `AndroidFragment`, which then adds an instance of that class directly into the composition. `AndroidFragment` also provides a `fragmentState` object to create the `AndroidFragment` with a given state, `arguments` to pass into the new fragment, and an `onUpdate` callback that provides the fragment from the composition. Like many other built-in composables, `AndroidFragment` accepts a `Modifier` parameter that you can use, for example, to set its position in the parent composable.
|
||||
|
||||
Call `AndroidFragment` in Compose as follows:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun FragmentInComposeExample() {
|
||||
AndroidFragment<MyFragment>()
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Calling the Android framework from Compose
|
||||
|
||||
Compose operates within the Android framework classes. For example, it's hosted
|
||||
on Android View classes, like `Activity` or `Fragment`, and might use Android
|
||||
framework classes like the `Context`, system resources,
|
||||
`Service`, or `BroadcastReceiver`.
|
||||
Compose operates within the Android framework classes. For example, it's hosted on Android View classes, like `Activity` or `Fragment`, and might use Android framework classes like the `Context`, system resources, `Service`, or `BroadcastReceiver`.
|
||||
|
||||
To learn more about system resources, see [Resources in Compose](https://developer.android.com/develop/ui/compose/resources).
|
||||
|
||||
### Composition Locals
|
||||
|
||||
[`CompositionLocal`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal)
|
||||
classes allow passing data implicitly through composable functions. They're
|
||||
usually provided with a value in a certain node of the UI tree. That value can
|
||||
be used by its composable descendants without declaring the `CompositionLocal`
|
||||
as a parameter in the composable function.
|
||||
[`CompositionLocal`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal) classes allow passing data implicitly through composable functions. They're usually provided with a value in a certain node of the UI tree. That value can be used by its composable descendants without declaring the `CompositionLocal` as a parameter in the composable function.
|
||||
|
||||
`CompositionLocal` is used to propagate values for Android framework types in
|
||||
Compose such as `Context`, `Configuration` or the `View` in which the Compose
|
||||
code is hosted with the corresponding
|
||||
[`LocalContext`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalContext()),
|
||||
[`LocalConfiguration`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalConfiguration()),
|
||||
or
|
||||
[`LocalView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalView()).
|
||||
Note that `CompositionLocal` classes are prefixed with `Local` for better
|
||||
discoverability with auto-complete in the IDE.
|
||||
`CompositionLocal` is used to propagate values for Android framework types in Compose such as `Context`, `Configuration` or the `View` in which the Compose code is hosted with the corresponding [`LocalContext`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalContext()), [`LocalConfiguration`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalConfiguration()), or [`LocalView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalView()). Note that `CompositionLocal` classes are prefixed with `Local` for better discoverability with auto-complete in the IDE.
|
||||
|
||||
Access the current value of a `CompositionLocal` by using its `current`
|
||||
property. For example, the code below shows a toast message by providing
|
||||
`LocalContext.current` into the `Toast.makeToast` method.
|
||||
Access the current value of a `CompositionLocal` by using its `current` property. For example, the code below shows a toast message by providing `LocalContext.current` into the `Toast.makeToast` method.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -194,18 +161,16 @@ fun ToastGreetingButton(greeting: String) {
|
||||
Text("Greet")
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Broadcast receivers
|
||||
|
||||
To showcase `CompositionLocal` and [side
|
||||
effects](https://developer.android.com/develop/ui/compose/side-effects), if a
|
||||
[`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) needs to be registered from
|
||||
a composable function, use of `LocalContext` to use the current context, and
|
||||
`rememberUpdatedState` and `DisposableEffect` side effects.
|
||||
To showcase `CompositionLocal` and [side effects](https://developer.android.com/develop/ui/compose/side-effects), if a [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) needs to be registered from a composable function, use of `LocalContext` to use the current context, and `rememberUpdatedState` and `DisposableEffect` side effects.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -247,18 +212,16 @@ fun HomeScreen() {
|
||||
|
||||
/* Rest of the HomeScreen */
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Other interactions
|
||||
|
||||
If there isn't a utility defined for the interaction you need, the best practice
|
||||
is to follow the general Compose guideline,
|
||||
*data flows down, events flow up* (discussed at more length in [Thinking
|
||||
in Compose](https://developer.android.com/develop/ui/compose/mental-model)). For example, this composable
|
||||
launches a different activity:
|
||||
If there isn't a utility defined for the interaction you need, the best practice is to follow the general Compose guideline, *data flows down, events flow up* (discussed at more length in [Thinking in Compose](https://developer.android.com/develop/ui/compose/mental-model)). For example, this composable launches a different activity:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
class OtherInteractionsActivity : ComponentActivity() {
|
||||
@@ -281,6 +244,7 @@ fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
|
||||
Text(data.title)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
+14
-31
@@ -1,9 +1,7 @@
|
||||
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
|
||||
> repository"](https://android-developers.googleblog.com/2024/04/jetpack-compose-compiler-moving-to-kotlin-repository.html).
|
||||
> **Note:** The Compose Compiler Gradle Plugin is only available from Kotlin 2.0+. For migration instructions, see ["Jetpack Compose compiler moving to the Kotlin repository"](https://android-developers.googleblog.com/2024/04/jetpack-compose-compiler-moving-to-kotlin-repository.html).
|
||||
|
||||
### Set up with Gradle version catalogs
|
||||
|
||||
@@ -21,8 +19,7 @@ Set up the Compose Compiler Gradle plugin:
|
||||
// Add this line
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
3. 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 {
|
||||
// Existing plugins
|
||||
@@ -36,14 +33,11 @@ Set up the Compose Compiler Gradle plugin:
|
||||
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
|
||||
section.
|
||||
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 section.
|
||||
|
||||
### Set up the Compose Compiler without Gradle version catalogs
|
||||
|
||||
Add the plugin to `build.gradle.kts` files associated with modules where Compose
|
||||
is used:
|
||||
Add the plugin to `build.gradle.kts` files associated with modules where Compose is used:
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" // this version matches your Kotlin version
|
||||
@@ -59,9 +53,7 @@ Add the classpath to your top-level project `build.gradle.kts` file:
|
||||
|
||||
### Configuration options with the Compose Compiler Gradle Plugin
|
||||
|
||||
To configure the Compose compiler using the Gradle plugin, add the
|
||||
`composeCompiler` block to the module's `build.gradle.kts` file at the top
|
||||
level:
|
||||
To configure the Compose compiler using the Gradle plugin, add the `composeCompiler` block to the module's `build.gradle.kts` file at the top level:
|
||||
|
||||
android { ... }
|
||||
|
||||
@@ -74,10 +66,9 @@ 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.06.00`.
|
||||
Always use the latest Compose BOM version: `2026.06.01`.
|
||||
|
||||
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:
|
||||
|
||||
@@ -103,7 +94,7 @@ Add the Compose BOM and the subset of Compose library dependencies:
|
||||
|
||||
dependencies {
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
|
||||
def composeBom = platform('androidx.compose:compose-bom:2026.06.01')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
|
||||
@@ -142,7 +133,7 @@ Add the Compose BOM and the subset of Compose library dependencies:
|
||||
|
||||
dependencies {
|
||||
|
||||
val composeBom = platform("androidx.compose:compose-bom:2026.06.00")
|
||||
val composeBom = platform("androidx.compose:compose-bom:2026.06.01")
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
|
||||
@@ -178,20 +169,12 @@ 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).
|
||||
> **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).
|
||||
|
||||
## `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`.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
+2
-8
@@ -14,18 +14,12 @@ Prioritize layouts that meet the following criteria:
|
||||
|
||||
### 3. Risk assessment
|
||||
|
||||
Evaluate the migration risk based on:
|
||||
\* **Reusability:** Find layouts with **minimum reuse** across the project to limit regression impact.
|
||||
\* **Accessibility:** Ensure the layout has an **easily accessible entry point** (e.g., used in a simple Activity, Fragment, or as a standalone include).
|
||||
Evaluate the migration risk based on: \* **Reusability:** Find layouts with **minimum reuse** across the project to limit regression impact. \* **Accessibility:** Ensure the layout has an **easily accessible entry point** (e.g., used in a simple Activity, Fragment, or as a standalone include).
|
||||
|
||||
*** ** * ** ***
|
||||
|
||||
## Output requirements
|
||||
|
||||
Provide a ranked list of the top 3-5 candidates. For each candidate, include:
|
||||
1. **File path:** (e.g., `res/layout/item_user_profile.xml`)
|
||||
2. **Rationale:** Why this is a good candidate based on the provided criteria.
|
||||
3. **Complexity score:** A rating from 1-5 (1 being simplest).
|
||||
4. **Dependency count:** List of custom/external views found within.
|
||||
Provide a ranked list of the top 3-5 candidates. For each candidate, include: 1. **File path:** (e.g., `res/layout/item_user_profile.xml`) 2. **Rationale:** Why this is a good candidate based on the provided criteria. 3. **Complexity score:** A rating from 1-5 (1 being simplest). 4. **Dependency count:** List of custom/external views found within.
|
||||
|
||||
**Action:** If you support user interaction, ask the user to choose which XML to proceed with. Else proceed with the best option, based on the previous criteria.
|
||||
+5
-7
@@ -1,7 +1,6 @@
|
||||
## 1. Structural analysis \& mapping
|
||||
|
||||
**Identify the precise mapping** between XML elements and Compose equivalents.
|
||||
You must determine:
|
||||
**Identify the precise mapping** between XML elements and Compose equivalents. You must determine:
|
||||
|
||||
- The exact `@Composable` functions (e.g., `ConstraintLayout`, `Column`, `LazyColumn`) that replace the XML tag hierarchy.
|
||||
- The specific parameters and `Modifier` extensions required to replicate XML attributes (e.g., `layout_width`, `padding`, `elevation`).
|
||||
@@ -9,9 +8,7 @@ You must determine:
|
||||
|
||||
## 2. Migration execution
|
||||
|
||||
**Convert the XML layout code to Jetpack Compose**, ensuring the visual
|
||||
hierarchy and layout logic are preserved while leveraging Compose's declarative
|
||||
nature.
|
||||
**Convert the XML layout code to Jetpack Compose**, ensuring the visual hierarchy and layout logic are preserved while leveraging Compose's declarative nature.
|
||||
|
||||
## 3. Theming \& design system integrity
|
||||
|
||||
@@ -23,8 +20,7 @@ nature.
|
||||
|
||||
## 4. Component layering \& reusability
|
||||
|
||||
Evaluate if the XML layout serves as a foundation-level design system component
|
||||
(reused across the app with a distinct role). If it is:
|
||||
Evaluate if the XML layout serves as a foundation-level design system component (reused across the app with a distinct role). If it is:
|
||||
|
||||
- **Create a reusable composable:** Do not just inline the code. Define a new standalone `@Composable`.
|
||||
- **Parameterization:** Expose specific parameters for variable data (text, colors, styles) and use `Modifier` for layout-specific customizations.
|
||||
@@ -44,6 +40,7 @@ Example before migration:
|
||||
|
||||
Example after migration:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -73,6 +70,7 @@ fun RoundedBorderlessButton(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
@@ -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-07-07'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- Jetpack Compose
|
||||
- Styles
|
||||
@@ -30,13 +30,11 @@ metadata:
|
||||
- The project must use `compileSdk` version 37 or higher.
|
||||
- The project must use `androidx.compose.foundation:foundation` version `1.12.0-alpha01` or higher.
|
||||
- Alternatively, the project must use Compose BOM version `2026.04.01` or higher.
|
||||
- The API requires this exact package: `import
|
||||
androidx.compose.foundation.style.Style`
|
||||
- The API requires this exact package: `import androidx.compose.foundation.style.Style`
|
||||
|
||||
### 2. Configure compiler options to enable experimental API
|
||||
|
||||
You must opt-in to the experimental API at the project level. Add the following
|
||||
block to your module's `build.gradle.kts`:
|
||||
You must opt-in to the experimental API at the project level. Add the following block to your module's `build.gradle.kts`:
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
@@ -49,12 +47,9 @@ block to your module's `build.gradle.kts`:
|
||||
|
||||
Refer to the official documentation to complete specific development tasks:
|
||||
|
||||
- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals
|
||||
Guide](references/android/develop/ui/compose/styles/fundamentals.md).
|
||||
- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling
|
||||
Guide](references/android/develop/ui/compose/styles/state-animations.md).
|
||||
- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers
|
||||
Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md).
|
||||
- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals Guide](references/android/develop/ui/compose/styles/fundamentals.md).
|
||||
- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling Guide](references/android/develop/ui/compose/styles/state-animations.md).
|
||||
- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md).
|
||||
- Theme Level Integration: To connect style definitions with custom themes, follow [Theming with Styles](references/android/develop/ui/compose/styles/theming.md) and [Custom Themes in Compose](references/android/develop/ui/compose/designsystems/custom.md).
|
||||
|
||||
## Step-by-Step Migration Workflow
|
||||
@@ -71,6 +66,7 @@ Refer to the official documentation to complete specific development tasks:
|
||||
1. Create a new file named `ComponentStyles.kt` in your theme directory.
|
||||
2. Define a top-level data class to hold your component styles, for example, the Jetsnack one is called `JetsnackStyles`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
object ExampleComponentStyles {
|
||||
@@ -81,13 +77,14 @@ Refer to the official documentation to complete specific development tasks:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
3. Expose this class through your custom theme with a static reference, don't
|
||||
use `CompositionLocals` here as it's not required.
|
||||
3. Expose this class through your custom theme with a static reference, don't use `CompositionLocals` here as it's not required.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
@@ -104,13 +101,14 @@ Refer to the official documentation to complete specific development tasks:
|
||||
val styles: ComponentStyles = ComponentStyles
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
4. Provide extensions on `StyleScope` to reference theme tokens directly if
|
||||
they are exposed using `CompositionLocals`. For example:
|
||||
4. Provide extensions on `StyleScope` to reference theme tokens directly if they are exposed using `CompositionLocals`. For example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val StyleScope.colors: JetsnackColors
|
||||
@@ -121,14 +119,15 @@ Refer to the official documentation to complete specific development tasks:
|
||||
|
||||
val StyleScope.shapes: Shapes
|
||||
get() = LocalJetsnackTheme.currentValue.shapes
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Step 3: Migrate a component to Styles API
|
||||
|
||||
For each custom component (for example, `CustomButton`), complete the following
|
||||
sequence:
|
||||
For each custom component (for example, `CustomButton`), complete the following sequence:
|
||||
|
||||
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.
|
||||
@@ -137,8 +136,7 @@ sequence:
|
||||
- **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. 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.
|
||||
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`.
|
||||
@@ -148,6 +146,7 @@ sequence:
|
||||
|
||||
Before Migration:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -172,12 +171,14 @@ fun CustomButton(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
After Migration:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Exposed via ComponentStyles.kt
|
||||
@@ -215,6 +216,7 @@ fun CustomButton(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
+42
-69
@@ -1,7 +1,4 @@
|
||||
While Material is our recommended design system and Jetpack Compose ships an
|
||||
implementation of Material, you are not forced to use it. Material is built
|
||||
entirely on public APIs, so it's possible to create your own design system in
|
||||
the same manner.
|
||||
While Material is our recommended design system and Jetpack Compose ships an implementation of Material, you are not forced to use it. Material is built entirely on public APIs, so it's possible to create your own design system in the same manner.
|
||||
|
||||
There are several approaches you might take:
|
||||
|
||||
@@ -9,21 +6,15 @@ There are several approaches you might take:
|
||||
- [Replace one or more Material systems](https://developer.android.com/develop/ui/compose/designsystems/custom#replacing-systems) --- `Colors`, `Typography`, or `Shapes` --- with custom implementations while keeping the others.
|
||||
- [Implement a fully custom design system](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom) to replace `MaterialTheme`.
|
||||
|
||||
You may also want to continue using Material components with a custom design
|
||||
system. It's possible to do this but there are things to keep in mind to suit
|
||||
the approach you've taken.
|
||||
You may also want to continue using Material components with a custom design system. It's possible to do this but there are things to keep in mind to suit the approach you've taken.
|
||||
|
||||
To learn more about the lower-level constructs and APIs used by `MaterialTheme`
|
||||
and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide.
|
||||
To learn more about the lower-level constructs and APIs used by `MaterialTheme` and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide.
|
||||
|
||||
## Extend Material Theming
|
||||
|
||||
Compose Material closely models
|
||||
[Material Theming](https://m3.material.io/)
|
||||
to make it straightforward and type-safe to follow the Material guidelines.
|
||||
However, it's possible to extend the color, typography, and shape sets with
|
||||
additional values. The simplest approach is to add extension properties:
|
||||
Compose Material closely models [Material Theming](https://m3.material.io/) to make it straightforward and type-safe to follow the Material guidelines. However, it's possible to extend the color, typography, and shape sets with additional values. The simplest approach is to add extension properties:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Use with MaterialTheme.colorScheme.snackbarAction
|
||||
@@ -38,26 +29,21 @@ val Typography.textFieldInput: TextStyle
|
||||
// Use with MaterialTheme.shapes.card
|
||||
val Shapes.card: Shape
|
||||
get() = RoundedCornerShape(size = 20.dp)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
This provides consistency with `MaterialTheme` usage APIs. An example of this
|
||||
defined by Compose itself is
|
||||
[`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)),
|
||||
which determines the surface color that should be used depending on the
|
||||
elevation.
|
||||
This provides consistency with `MaterialTheme` usage APIs. An example of this defined by Compose itself is [`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)), which determines the surface color that should be used depending on the elevation.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** This approach is only recommended for straightforward theming value additions, or for values that are the same in different themes. If you have multiple themes, it's better to define a class with new properties instead.
|
||||
|
||||
Another approach is to define an extended theme that "wraps" `MaterialTheme` and
|
||||
its values.
|
||||
Another approach is to define an extended theme that "wraps" `MaterialTheme` and its values.
|
||||
|
||||
Suppose you want to add two additional colors --- `caution` and `onCaution`, a
|
||||
yellow color used for actions that are semi-dangerous --- whilst keeping the
|
||||
existing Material colors:
|
||||
Suppose you want to add two additional colors --- `caution` and `onCaution`, a yellow color used for actions that are semi-dangerous --- whilst keeping the existing Material colors:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
@@ -96,22 +82,20 @@ object ExtendedTheme {
|
||||
@Composable
|
||||
get() = LocalExtendedColors.current
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
This is similar to `MaterialTheme` usage APIs. It also supports multiple themes
|
||||
as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`.
|
||||
This is similar to `MaterialTheme` usage APIs. It also supports multiple themes as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`.
|
||||
|
||||
### Use Material components
|
||||
|
||||
When extending Material Theming, existing `MaterialTheme` values are maintained
|
||||
and Material components still have reasonable defaults.
|
||||
When extending Material Theming, existing `MaterialTheme` values are maintained and Material components still have reasonable defaults.
|
||||
|
||||
If you want to use extended values in components, wrap them in your own
|
||||
composable functions, directly setting the values you want to alter, and
|
||||
exposing others as parameters to the containing composable:
|
||||
If you want to use extended values in components, wrap them in your own composable functions, directly setting the values you want to alter, and exposing others as parameters to the containing composable:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -131,13 +115,14 @@ fun ExtendedButton(
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
You would then replace usages of `Button` with `ExtendedButton` where
|
||||
appropriate.
|
||||
You would then replace usages of `Button` with `ExtendedButton` where appropriate.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -149,19 +134,18 @@ fun ExtendedApp() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Replace Material subsystems
|
||||
|
||||
Instead of extending Material Theming, you may want to replace one or more
|
||||
systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation,
|
||||
while maintaining the others.
|
||||
Instead of extending Material Theming, you may want to replace one or more systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation, while maintaining the others.
|
||||
|
||||
Suppose you want to replace the type and shape systems while keeping the color
|
||||
system:
|
||||
Suppose you want to replace the type and shape systems while keeping the color system:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
@@ -222,22 +206,21 @@ object ReplacementTheme {
|
||||
@Composable
|
||||
get() = LocalReplacementShapes.current
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Use Material components
|
||||
|
||||
When one or more systems of `MaterialTheme` have been replaced, using Material
|
||||
components as-is may result in unwanted Material color, type, or shape values.
|
||||
When one or more systems of `MaterialTheme` have been replaced, using Material components as-is may result in unwanted Material color, type, or shape values.
|
||||
|
||||
If you want to use replacement values in components, wrap them in your own
|
||||
composable functions, directly setting the values for the relevant system, and
|
||||
exposing others as parameters to the containing composable.
|
||||
If you want to use replacement values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable.
|
||||
|
||||
> [!NOTE]
|
||||
> **Note:** Not all values may be exposed as parameters in Material composables, in particular with `CompositionLocal` composables (such as `LocalTextStyle`). In such cases you may need to wrap `content` lambdas in provider functions (like `ProvideTextStyle`).
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -259,13 +242,14 @@ fun ReplacementButton(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
You would then replace usages of `Button` with `ReplacementButton` where
|
||||
appropriate.
|
||||
You would then replace usages of `Button` with `ReplacementButton` where appropriate.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -277,33 +261,28 @@ fun ReplacementApp() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Implement a fully custom design system
|
||||
|
||||
You may want to replace Material Theming with a fully custom design system.
|
||||
Consider that `MaterialTheme` provides the following systems:
|
||||
You may want to replace Material Theming with a fully custom design system. Consider that `MaterialTheme` provides the following systems:
|
||||
|
||||
- `Colors`, `Typography`, and `Shapes`: Material Theming systems
|
||||
- `TextSelectionColors`: Colors used for text selection by `Text` and `TextField`
|
||||
- `Ripple` and `RippleTheme`: Material implementation of `Indication`
|
||||
|
||||
If you want to continue using Material components, you must replace some of
|
||||
these systems in your custom themes or handle the systems in your
|
||||
components to avoid unwanted behavior.
|
||||
If you want to continue using Material components, you must replace some of these systems in your custom themes or handle the systems in your components to avoid unwanted behavior.
|
||||
|
||||
However, design systems are not limited to the concepts Material relies on. You
|
||||
can modify existing systems and introduce entirely new ones --- with new classes
|
||||
and types --- to make other concepts compatible with themes.
|
||||
However, design systems are not limited to the concepts Material relies on. You can modify existing systems and introduce entirely new ones --- with new classes and types --- to make other concepts compatible with themes.
|
||||
|
||||
In the following code, we model a custom color system that includes gradients
|
||||
(`List<Color>`), include a type system, introduce a new elevation system,
|
||||
and exclude other systems provided by `MaterialTheme`:
|
||||
In the following code, we model a custom color system that includes gradients (`List<Color>`), include a type system, introduce a new elevation system, and exclude other systems provided by `MaterialTheme`:
|
||||
|
||||

|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
@@ -383,23 +362,20 @@ object CustomTheme {
|
||||
@Composable
|
||||
get() = LocalCustomElevation.current
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Use Material components
|
||||
|
||||
When no `MaterialTheme` is present, using Material components as-is will result
|
||||
in unwanted Material color, type, and shape values and indication behavior.
|
||||
When no `MaterialTheme` is present, using Material components as-is will result in unwanted Material color, type, and shape values and indication behavior.
|
||||
|
||||
If you want to use custom values in components, wrap them in your own composable
|
||||
functions, directly setting the values for the relevant system, and exposing
|
||||
others as parameters to the containing composable.
|
||||
If you want to use custom values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable.
|
||||
|
||||
We recommend that you access values you set from your custom theme.
|
||||
Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or
|
||||
other systems, you can hardcode them.
|
||||
We recommend that you access values you set from your custom theme. Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or other systems, you can hardcode them.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -438,6 +414,7 @@ fun CustomButton(
|
||||
}
|
||||
|
||||
val ButtonShape = RoundedCornerShape(percent = 50)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -445,11 +422,7 @@ val ButtonShape = RoundedCornerShape(percent = 50)
|
||||
> [!NOTE]
|
||||
> **Note:** `Button` uses `rememberRipple()` internally to provide a `Ripple` `Indication`. It's a good idea to check the source code when implementing other custom components that wrap existing components.
|
||||
|
||||
If you've introduced new class types --- such as `List<Color>` to represent
|
||||
gradients --- then it may be better to implement components from scratch instead
|
||||
of wrapping them. For an example, take a look at
|
||||
[`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt)
|
||||
from the Jetsnack sample.
|
||||
If you've introduced new class types --- such as `List<Color>` to represent gradients --- then it may be better to implement components from scratch instead of wrapping them. For an example, take a look at [`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt) from the Jetsnack sample.
|
||||
|
||||
## Recommended for you
|
||||
|
||||
|
||||
+52
-58
@@ -6,10 +6,7 @@ There are three ways you can adopt Styles throughout your app:
|
||||
|
||||
## Available properties on Styles
|
||||
|
||||
Styles support many of the same properties that modifiers support; however, not
|
||||
everything that is a modifier can be replicated with a Style. You still need
|
||||
modifiers for certain behaviors, like interactions, custom drawing, or stacking
|
||||
of properties.
|
||||
Styles support many of the same properties that modifiers support; however, not everything that is a modifier can be replicated with a Style. You still need modifiers for certain behaviors, like interactions, custom drawing, or stacking of properties.
|
||||
|
||||
| Grouping | Properties | Inherited by children |
|
||||
|---|---|---|
|
||||
@@ -36,6 +33,7 @@ of properties.
|
||||
|
||||
Components that expose a `Style` parameter allow you to set their styling:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
BaseButton(
|
||||
@@ -44,13 +42,14 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Click me")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Within the style lambda, you can set various properties, such as `externalPadding`
|
||||
or `background`:
|
||||
Within the style lambda, you can set various properties, such as `externalPadding` or `background`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
BaseButton(
|
||||
@@ -59,19 +58,18 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Click me")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
For the full list of supported properties, see [Available properties on
|
||||
Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles).
|
||||
For the full list of supported properties, see [Available properties on Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles).
|
||||
|
||||
## Apply Styles using modifiers for components with no existing parameter
|
||||
|
||||
For components that lack a built-in style parameter, you can still apply styles
|
||||
with the `styleable` modifier. This approach is also useful when developing your
|
||||
own custom components.
|
||||
For components that lack a built-in style parameter, you can still apply styles with the `styleable` modifier. This approach is also useful when developing your own custom components.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Row(
|
||||
@@ -79,13 +77,14 @@ Row(
|
||||
) {
|
||||
BaseText("Content")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Similar to the `style` parameter, you can include properties like `background`,
|
||||
`contentPadding`, or `externalPadding` inside the lambda.
|
||||
Similar to the `style` parameter, you can include properties like `background`, `contentPadding`, or `externalPadding` inside the lambda.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
Row(
|
||||
@@ -95,6 +94,7 @@ Row(
|
||||
) {
|
||||
BaseText("Content")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -102,35 +102,28 @@ Row(
|
||||
> [!NOTE]
|
||||
> **Note:** When using `Modifier.styleable`, the child composables won't have those properties applied to them, unless they are inherited properties. Only the container with the `styleable` modifier has those properties applied.
|
||||
|
||||
Multiple chained `Modifier.styleable` modifiers are additive with non-inherited
|
||||
properties on the applied composable, behaving similarly to multiple modifiers
|
||||
defining the same properties. For inherited properties, these are overridden,
|
||||
and the last `styleable` modifier in the chain sets the values.
|
||||
Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, behaving similarly to multiple modifiers defining the same properties. For inherited properties, these are overridden, and the last `styleable` modifier in the chain sets the values.
|
||||
|
||||
When using `Modifier.styleable`, you may also want to create and supply a
|
||||
`StyleState` to be used with the modifier to apply state-based styling. For more
|
||||
details, see [State and animations with
|
||||
Styles](https://developer.android.com/develop/ui/compose/styles/state-animations).
|
||||
When using `Modifier.styleable`, you may also want to create and supply a `StyleState` to be used with the modifier to apply state-based styling. For more details, see [State and animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations).
|
||||
|
||||
## Define a standalone Style
|
||||
|
||||
You can define a standalone Style for reusability purposes:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val style = Style { background(Color.Blue) }
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
You can then pass that defined style into a composable's style parameter or with
|
||||
`Modifier.styleable`. When using `Modifier.styleable`, you also need to create a
|
||||
`StyleState` object. `StyleState` is covered in detail in the [State and
|
||||
animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation.
|
||||
You can then pass that defined style into a composable's style parameter or with `Modifier.styleable`. When using `Modifier.styleable`, you also need to create a `StyleState` object. `StyleState` is covered in detail in the [State and animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation.
|
||||
|
||||
The following example shows how you can apply a Style either directly through a
|
||||
component's built-in parameters, or through a `Modifier.styleable`:
|
||||
The following example shows how you can apply a Style either directly through a component's built-in parameters, or through a `Modifier.styleable`:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val style = Style { background(Color.Blue) }
|
||||
@@ -147,12 +140,14 @@ Column(
|
||||
) {
|
||||
BaseText("Column content")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
You can also pass that Style into multiple components:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val style = Style { background(Color.Blue) }
|
||||
@@ -176,15 +171,16 @@ Row(
|
||||
) {
|
||||
BaseText("Row")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Add multiple Style properties
|
||||
|
||||
You can add multiple Style properties by setting different properties on each
|
||||
line:
|
||||
You can add multiple Style properties by setting different properties on each line:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
BaseButton(
|
||||
@@ -196,6 +192,7 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Button")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -203,12 +200,9 @@ BaseButton(
|
||||
> [!IMPORTANT]
|
||||
> **Important:** Unlike modifier-based styling, properties in Styles override one another; the last property defined takes precedence.
|
||||
|
||||
Properties in Styles are not additive, unlike modifier-based styling. Styles
|
||||
take the last set value in the list of properties within one style block. In the
|
||||
following example, with the background set twice, the `TealColor` is the applied
|
||||
background. For padding, `contentPaddingTop` overrides the top
|
||||
padding set by `contentPadding` and does not combine the values.
|
||||
Properties in Styles are not additive, unlike modifier-based styling. Styles take the last set value in the list of properties within one style block. In the following example, with the background set twice, the `TealColor` is the applied background. For padding, `contentPaddingTop` overrides the top padding set by `contentPadding` and does not combine the values.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
BaseButton(
|
||||
@@ -227,6 +221,7 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Click me!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -236,9 +231,9 @@ overrides](https://developer.android.com/static/develop/ui/compose/styles/images
|
||||
|
||||
## Merge multiple style objects
|
||||
|
||||
You can create multiple Style objects and pass them into the style parameter of
|
||||
your composable.
|
||||
You can create multiple Style objects and pass them into the style parameter of your composable.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val style1 = Style { background(TealColor) }
|
||||
@@ -252,6 +247,7 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Click me!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -259,12 +255,9 @@ BaseButton(
|
||||
 **Figure 2.** Button with background color and `contentPaddingTop` set.
|
||||
|
||||
When multiple Styles specify the same property, the last set
|
||||
property is chosen. Because properties are not additive in Styles, the last
|
||||
padding passed in overrides the `contentPaddingHorizontal` set by the initial
|
||||
`contentPadding`. Additionally, the last background color overrides the
|
||||
background color set by the initial style passed in.
|
||||
When multiple Styles specify the same property, the last set property is chosen. Because properties are not additive in Styles, the last padding passed in overrides the `contentPaddingHorizontal` set by the initial `contentPadding`. Additionally, the last background color overrides the background color set by the initial style passed in.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val style1 = Style {
|
||||
@@ -285,12 +278,12 @@ BaseButton(
|
||||
) {
|
||||
BaseText("Click me!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
In this case, the styling applied has a light gray background and `32.dp` padding,
|
||||
except for the left and right padding, which has a value of `8.dp`.
|
||||
In this case, the styling applied has a light gray background and `32.dp` padding, except for the left and right padding, which has a value of `8.dp`.
|
||||
 **Figure 3.** Button with `contentPadding` that's overridden by different Styles.
|
||||
|
||||
@@ -299,9 +292,7 @@ Styles](https://developer.android.com/static/develop/ui/compose/styles/images/bu
|
||||
> [!NOTE]
|
||||
> **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
|
||||
composable overrides the inherited parent styling for that specific child.
|
||||
Certain style properties, such as `contentColor` and text style-related properties, propagate to the child composables. A style set on a child composable overrides the inherited parent styling for that specific child.
|
||||
 **Figure 4.** Style propagation with `Style`, `styleable`, and direct parameters.
|
||||
|
||||
@@ -317,9 +308,9 @@ parameters](https://developer.android.com/static/develop/ui/compose/styles/image
|
||||
|
||||
### Parent styling
|
||||
|
||||
You can set text properties (such as `contentColor`) from the parent composable,
|
||||
and they propagate to all child `Text` composables.
|
||||
You can set text properties (such as `contentColor`) from the parent composable, and they propagate to all child `Text` composables.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val styleState = remember { MutableStyleState(null) }
|
||||
@@ -336,6 +327,7 @@ Column(
|
||||
BaseText("certain properties")
|
||||
BaseText("from their parents")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -345,10 +337,9 @@ inheritance](https://developer.android.com/static/develop/ui/compose/styles/imag
|
||||
|
||||
### Child override of properties
|
||||
|
||||
You can also set styling on a specific `Text` composable. If the parent composable
|
||||
has styling set, the styling set on the child composable overrides the
|
||||
parent composable's styling.
|
||||
You can also set styling on a specific `Text` composable. If the parent composable has styling set, the styling set on the child composable overrides the parent composable's styling.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val styleState = remember { MutableStyleState(null) }
|
||||
@@ -367,6 +358,7 @@ Column(
|
||||
BaseText("override properties")
|
||||
BaseText("set by their parents")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -376,39 +368,40 @@ properties](https://developer.android.com/static/develop/ui/compose/styles/image
|
||||
|
||||
## Implement custom Style properties
|
||||
|
||||
You can create custom properties that map to existing Style definitions by using
|
||||
extension functions on the `StyleScope`, as shown in the following example:
|
||||
You can create custom properties that map to existing Style definitions by using extension functions on the `StyleScope`, as shown in the following example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
fun StyleScope.outlinedBackground(color: Color) {
|
||||
border(1.dp, color)
|
||||
background(color)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Apply this new property within a Style definition:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val customExtensionStyle = Style {
|
||||
outlinedBackground(Color.Blue)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Creating new styleable properties is unsupported. If your use case
|
||||
requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128).
|
||||
Creating new styleable properties is unsupported. If your use case requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128).
|
||||
|
||||
## Read `CompositionLocal` values
|
||||
|
||||
It's a common pattern to store design system tokens within a `CompositionLocal`,
|
||||
to access the variables without needing to pass them as parameters. Styles
|
||||
can access `CompositionLocal`s to retrieve system-wide values within a style:
|
||||
It's a common pattern to store design system tokens within a `CompositionLocal`, to access the variables without needing to pass them as parameters. Styles can access `CompositionLocal`s to retrieve system-wide values within a style:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val buttonStyle = Style {
|
||||
@@ -416,6 +409,7 @@ val buttonStyle = Style {
|
||||
shape(RoundedCornerShape(50))
|
||||
background(Brush.verticalGradient(LocalCustomColors.currentValue.background))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
+38
-50
@@ -1,15 +1,8 @@
|
||||
<br />
|
||||
|
||||
The Styles API offers a declarative and streamlined approach to managing UI
|
||||
changes during interaction states like `hovered`, `focused`, and `pressed`. With
|
||||
this API, you can significantly decrease the boilerplate code typically required
|
||||
when using modifiers.
|
||||
The Styles API offers a declarative and streamlined approach to managing UI changes during interaction states like `hovered`, `focused`, and `pressed`. With this API, you can significantly decrease the boilerplate code typically required when using modifiers.
|
||||
|
||||
To facilitate reactive styling, `StyleState` acts as a stable, read-only
|
||||
interface that tracks the active state of an element (such as its enabled,
|
||||
pressed, or focused status). Within a `StyleScope`, you can access this through
|
||||
the `state` property to implement conditional logic directly in your Style
|
||||
definitions.
|
||||
To facilitate reactive styling, `StyleState` acts as a stable, read-only interface that tracks the active state of an element (such as its enabled, pressed, or focused status). Within a `StyleScope`, you can access this through the `state` property to implement conditional logic directly in your Style definitions.
|
||||
|
||||
## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled
|
||||
|
||||
@@ -21,15 +14,13 @@ Styles come with built-in support for common interactions:
|
||||
- Enabled
|
||||
- Toggled
|
||||
|
||||
It's also possible to support custom states. See the [Custom State Styling with
|
||||
StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information.
|
||||
It's also possible to support custom states. See the [Custom State Styling with StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information.
|
||||
|
||||
### Handle interaction states with Style parameters
|
||||
|
||||
The following example demonstrates modifying the `background` and `borderColor`
|
||||
in response to interaction states, specifically switching to purple when hovered
|
||||
and blue when focused:
|
||||
The following example demonstrates modifying the `background` and `borderColor` in response to interaction states, specifically switching to purple when hovered and blue when focused:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Preview
|
||||
@@ -56,16 +47,17 @@ private fun OpenButton() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
**Figure 1.** Changing background color based on hovered and focused states.
|
||||
|
||||
You can also create nested state definitions. For example, you can define a
|
||||
specific style for when a button is being both pressed and hovered
|
||||
simultaneously:
|
||||
You can also create nested state definitions. For example, you can define a specific style for when a button is being both pressed and hovered simultaneously:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -99,6 +91,7 @@ private fun OpenButton_CombinedStates() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -107,18 +100,15 @@ private fun OpenButton_CombinedStates() {
|
||||
|
||||
### Custom composables with Modifier.styleable
|
||||
|
||||
When creating your own `styleable` components, you must connect an
|
||||
`interactionSource` to a `styleState`. Then, pass this state into
|
||||
`Modifier.styleable` to utilize it.
|
||||
When creating your own `styleable` components, you must connect an `interactionSource` to a `styleState`. Then, pass this state into `Modifier.styleable` to utilize it.
|
||||
|
||||
Consider a scenario where your design system includes a `GradientButton`. You
|
||||
may want to create a `LoginButton` that inherits from `GradientButton`, but
|
||||
alters its colors during interactions, like being pressed.
|
||||
Consider a scenario where your design system includes a `GradientButton`. You may want to create a `LoginButton` that inherits from `GradientButton`, but alters its colors during interactions, like being pressed.
|
||||
|
||||
- To enable `interactionSource` style updates, include an `interactionSource` as a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a new `MutableInteractionSource`.
|
||||
- Initialize the `styleState` by providing the `interactionSource`. Make sure the `styleState`'s enabled status reflects the value of the provided enabled parameter.
|
||||
- Assign the `interactionSource` to the `focusable` and `clickable` modifiers. Finally, apply the `styleState` to the modifier's `styleable` parameter.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -147,13 +137,14 @@ private fun GradientButton(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
You can now use the `interactionSource` state to drive style modifications with
|
||||
the pressed, focused, and hovered options inside the style block:
|
||||
You can now use the `interactionSource` state to drive style modifications with the pressed, focused, and hovered options inside the style block:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Preview
|
||||
@@ -174,6 +165,7 @@ fun LoginButton() {
|
||||
BaseText("Login")
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -182,12 +174,9 @@ fun LoginButton() {
|
||||
|
||||
## Animate style changes
|
||||
|
||||
Styles state changes come with built-in animation support. You can wrap the new
|
||||
property within any state change block with `animate` to automatically add
|
||||
animations between different states. This is similar to the `animate*AsState`
|
||||
APIs. The following example animates the `borderColor` from black to blue when
|
||||
the state changes to focused:
|
||||
Styles state changes come with built-in animation support. You can wrap the new property within any state change block with `animate` to automatically add animations between different states. This is similar to the `animate*AsState` APIs. The following example animates the `borderColor` from black to blue when the state changes to focused:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val animatingStyle = Style {
|
||||
@@ -222,16 +211,16 @@ private fun AnimatingStyleChanges() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
**Figure 4.** Animating color changes on press.
|
||||
|
||||
The `animate` API accepts an `animationSpec` to change the duration or shape of
|
||||
the animation curve. The following example animates the size of the box with a
|
||||
`spring` spec:
|
||||
The `animate` API accepts an `animationSpec` to change the duration or shape of the animation curve. The following example animates the size of the box with a `spring` spec:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
val animatingStyleSpec = Style {
|
||||
@@ -267,6 +256,7 @@ fun AnimatingStyleChangesSpec() {
|
||||
)
|
||||
.styleable(styleState, animatingStyleSpec))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
@@ -275,11 +265,7 @@ fun AnimatingStyleChangesSpec() {
|
||||
|
||||
## Custom state styling with StyleState
|
||||
|
||||
Depending on your composable use case, you may have different styles that are
|
||||
backed by custom states. For example, if you have a media app, you may want to
|
||||
have different styling for the buttons in your `MediaPlayer` composable
|
||||
depending on the playback state of the player. Follow these steps to create and
|
||||
use your own custom state:
|
||||
Depending on your composable use case, you may have different styles that are backed by custom states. For example, if you have a media app, you may want to have different styling for the buttons in your `MediaPlayer` composable depending on the playback state of the player. Follow these steps to create and use your own custom state:
|
||||
|
||||
1. Define custom key
|
||||
2. Create `StyleState` extension
|
||||
@@ -287,11 +273,9 @@ use your own custom state:
|
||||
|
||||
### Define custom key
|
||||
|
||||
To create a custom state-based style, first create a
|
||||
[`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the
|
||||
app launches, the media player is in the `Stopped` state, so it's initialized in
|
||||
this way:
|
||||
To create a custom state-based style, first create a [`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the app launches, the media player is in the `Stopped` state, so it's initialized in this way:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
enum class PlayerState {
|
||||
@@ -301,16 +285,16 @@ enum class PlayerState {
|
||||
}
|
||||
|
||||
val playerStateKey = StyleStateKey(PlayerState.Stopped)
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Create StyleState extension functions
|
||||
|
||||
Define an extension function on `StyleState` to query the current `playState`.
|
||||
Then, create extension functions on `StyleScope` with your custom states passing
|
||||
in the `playStateKey`, a lambda with the specific state, and the style.
|
||||
Define an extension function on `StyleState` to query the current `playState`. Then, create extension functions on `StyleScope` with your custom states passing in the `playStateKey`, a lambda with the specific state, and the style.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Extension Function on MutableStyleState to query and set the current playState
|
||||
@@ -324,16 +308,16 @@ fun StyleScope.playerPlaying(block: () -> Unit) {
|
||||
fun StyleScope.playerPaused(block: () -> Unit) {
|
||||
state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
### Link to custom state
|
||||
|
||||
Define the `styleState` in your composable and set the `styleState.playState`
|
||||
equal to incoming state. Pass `styleState` into the `styleable` function on the
|
||||
modifier.
|
||||
Define the `styleState` in your composable and set the `styleState.playState` equal to incoming state. Pass `styleState` into the `styleable` function on the modifier.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -352,13 +336,14 @@ fun MediaPlayer(
|
||||
///..
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
Within the `style` lambda, you can apply state-based styling for custom states,
|
||||
using the previously defined extension functions.
|
||||
Within the `style` lambda, you can apply state-based styling for custom states, using the previously defined extension functions.
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
@@ -385,12 +370,14 @@ fun StyleStateKeySample() {
|
||||
style = style,
|
||||
state = PlayerState.Stopped)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
The following code is the full snippet for this example:
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
enum class PlayerState {
|
||||
@@ -456,6 +443,7 @@ fun StyleStateKeySample() {
|
||||
style = style,
|
||||
state = PlayerState.Stopped)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
+5
-12
@@ -1,7 +1,4 @@
|
||||
Styles differ from modifiers by design. Styles don't replace modifiers; instead,
|
||||
the two systems coexist with different goals. Internally, a Style is a modifier.
|
||||
You can do everything Styles can do with modifiers, but not all functionality in
|
||||
modifiers is available in Styles.
|
||||
Styles differ from modifiers by design. Styles don't replace modifiers; instead, the two systems coexist with different goals. Internally, a Style is a modifier. 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 a theme-wide set of properties for a component.
|
||||
@@ -19,8 +16,7 @@ The following is a comparison between Styles versus modifiers:
|
||||
|
||||
## Limitations of modifiers
|
||||
|
||||
Modifiers have many benefits in the current Compose landscape. However, Styles
|
||||
address some limitations of modifiers, which the following list describes:
|
||||
Modifiers have many benefits in the current Compose landscape. However, Styles 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.
|
||||
@@ -29,18 +25,15 @@ address some limitations of modifiers, which the following list describes:
|
||||
|
||||
## Limitations of Styles
|
||||
|
||||
While Styles can fill in some of the gaps that modifiers have, they also have
|
||||
some limitations, which show how they cannot entirely replace modifiers:
|
||||
While Styles can fill in some of the gaps that modifiers have, they also have some limitations, which show how they cannot entirely replace modifiers:
|
||||
|
||||
- Styles are specialized Modifiers. While a modifier can do anything a Style does, the reverse is not true. Consequently, Styles can supplement, but cannot replace, modifiers.
|
||||
- Styles are limited to visual configuration (backgrounds, padding, borders). They cannot handle behaviors like click logic, gesture detection, or accessibility semantics.
|
||||
- Resolving a Style into its final state is *more expensive than applying a
|
||||
single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this.
|
||||
- Resolving a Style into its final state is *more expensive than applying a single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this.
|
||||
|
||||
## When to use Styles over modifiers
|
||||
|
||||
While the choice to use Styles is largely dependent on your app and use cases,
|
||||
the following guidance helps determine when to prefer a style over a modifier:
|
||||
While the choice to use Styles is largely dependent on your app and use cases, the following guidance helps determine when to prefer a style over a modifier:
|
||||
|
||||
- **To achieve theme-wide consistency:** Styles are designed to be "lifted" into a global theme. Instead of passing repetitive Modifiers to every component, you can define a single Style in your theme to create a unified look across the entire app.
|
||||
- **When performing frequent animations:** Styles evaluate during the Layout and Draw phases, allowing properties like color or scale to animate while bypassing the Composition phase entirely. This significantly reduces performance overhead. Use a Style instead of a modifier when doing visual property animations.
|
||||
|
||||
+21
-46
@@ -1,8 +1,7 @@
|
||||
> [!NOTE]
|
||||
> **Note:** Styles are `@Experimental` and likely to change in upcoming releases, with Material support for Styles added in future releases. If you have any feedback, [file Styles issues](https://issuetracker.google.com/issues/new?component=612128).
|
||||
|
||||
There are several ways you can build out your apps using Styles. What you choose
|
||||
depends on where your app sits in relation to its adoption of Material Design:
|
||||
There are several ways you can build out your apps using Styles. What you choose depends on where your app sits in relation to its adoption of Material Design:
|
||||
|
||||
1. Fully custom design system, not using Material Design
|
||||
- **Recommendation**: Define component styles that consume values from the theme, and expose style parameters on design system components.
|
||||
@@ -11,15 +10,9 @@ depends on where your app sits in relation to its adoption of Material Design:
|
||||
|
||||
## The Style layer
|
||||
|
||||
In the traditional Compose model, customization often relies heavily on
|
||||
overriding global tokens (colors and typography) provided by `MaterialTheme`, or
|
||||
wrapping and overriding properties of a design system composable where possible.
|
||||
Sometimes, there are properties within the Material layer that are not exposed
|
||||
through the subsystems or parameters, but are hardcoded defaults on the
|
||||
component itself.
|
||||
In the traditional Compose model, customization often relies heavily on overriding global tokens (colors and typography) provided by `MaterialTheme`, or wrapping and overriding properties of a design system composable where possible. Sometimes, there are properties within the Material layer that are not exposed through the subsystems or parameters, but are hardcoded defaults on the component itself.
|
||||
|
||||
With the Styles API, there's a new layer of abstraction that's a bridge between
|
||||
subsystems and components: **Styles**.
|
||||
With the Styles API, there's a new layer of abstraction that's a bridge between subsystems and components: **Styles**.
|
||||
|
||||
| Layer | Responsibility | Example |
|
||||
|---|---|---|
|
||||
@@ -32,11 +25,9 @@ subsystems and components: **Styles**.
|
||||
|
||||
### Atomic versus monolithic Styles
|
||||
|
||||
With the Styles API, you can break down a Style into separate atomic styles.
|
||||
Instead of defining complex, component-specific styles like `baseButtonStyle`,
|
||||
you can also create small, single-purpose utility styles. These act as your
|
||||
"atoms".
|
||||
With the Styles API, you can break down a Style into separate atomic styles. Instead of defining complex, component-specific styles like `baseButtonStyle`, you can also create small, single-purpose utility styles. These act as your "atoms".
|
||||
|
||||
<br />
|
||||
|
||||
```kotlin
|
||||
// Define single-purpose "atomic" styles
|
||||
@@ -69,19 +60,18 @@ val interactiveShadowAtomic = Style {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### 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.
|
||||
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.
|
||||
|
||||
**Traditional (non-atomic)**:
|
||||
|
||||
|
||||
```kotlin
|
||||
// One large monolithic style
|
||||
val buttonStyle = Style {
|
||||
@@ -89,49 +79,39 @@ val buttonStyle = Style {
|
||||
shape(RoundedCornerShape(8.dp))
|
||||
background(Color.Blue)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
**Atomic refactor**:
|
||||
|
||||
|
||||
```kotlin
|
||||
// Combine atoms to create the final appearance
|
||||
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Adopt Styles in your design system
|
||||
|
||||
Consider the following options when adopting Styles within your design system,
|
||||
depending on where in the spectrum your design system lies.
|
||||
Consider the following options when adopting Styles within your design system, depending on where in the spectrum your design system lies.
|
||||
|
||||
### Custom design system with Styles
|
||||
|
||||
***Consider when**: You've been handed an extensive brand guide that is not
|
||||
based on Material Design, and you are not planning to use Material Design*.
|
||||
***Consider when**: You've been handed an extensive brand guide that is not based on Material Design, and you are not planning to use Material Design*.
|
||||
|
||||
***Strategy**: Implement a fully custom design system, and expose styles as part
|
||||
of the theme*.
|
||||
***Strategy**: Implement a fully custom design system, and expose styles as part of the theme*.
|
||||
|
||||
This option is the custom path if you don't use Material as your main design
|
||||
system language. You bypass `MaterialTheme` entirely for visual definitions and
|
||||
have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that
|
||||
acts as a container for your Styles.
|
||||
This option is the custom path if you don't use Material as your main design system language. You bypass `MaterialTheme` entirely for visual definitions and have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that acts as a container for your Styles.
|
||||
|
||||
- **How it works** : Create a `CompanyTheme` object that holds `Style` objects for every component in your system. Your components (either wrappers around Material logic or custom `Box` or `Layout` implementations) consume these styles directly, and expose a `Style` parameter for consumers of your design system.
|
||||
- **The Style layer**: Styles are the primary definition of your design system. Tokens are named variables fed into these styles. This allows for deep customization, such as defining unique animations for state changes (for example, animating scale and color on press).
|
||||
|
||||
If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and
|
||||
want to adopt styles, add your list of styles to your Theme. This lets you
|
||||
access your base styles from anywhere in your project.
|
||||
|
||||
1. Create a `Styles` class that stores the various styles in your application
|
||||
and create the defaults. For example, in the Jetsnack app - the class is
|
||||
named `JetsnackStyles`:
|
||||
If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and want to adopt styles, add your list of styles to your Theme. This lets you access your base styles from anywhere in your project.
|
||||
|
||||
1. Create a `Styles` class that stores the various styles in your application and create the defaults. For example, in the Jetsnack app - the class is named `JetsnackStyles`:
|
||||
|
||||
```kotlin
|
||||
object JetsnackStyles{
|
||||
@@ -154,13 +134,12 @@ access your base styles from anywhere in your project.
|
||||
contentColor(colors.textPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
2. Provide `Styles` as part of your overall theme, and expose helper extension
|
||||
functions on `StyleScope` to access the subsystems:
|
||||
|
||||
2. Provide `Styles` as part of your overall theme, and expose helper extension functions on `StyleScope` to access the subsystems:
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
@@ -215,13 +194,13 @@ access your base styles from anywhere in your project.
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
3. Access `JetsnackStyles` within your composable:
|
||||
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun CustomButton(modifier: Modifier,
|
||||
@@ -245,13 +224,9 @@ access your base styles from anywhere in your project.
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<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` 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.
|
||||
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` 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.
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
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-06'
|
||||
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.10.1")
|
||||
|
||||
- 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.
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
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.10.1")
|
||||
implementation("androidx.media3:media3-ui:1.10.1")
|
||||
implementation("androidx.media3:media3-session:1.10.1")
|
||||
implementation("androidx.media3:media3-cast:1.10.1")
|
||||
|
||||
### Groovy
|
||||
|
||||
implementation "androidx.media3:media3-exoplayer:1.10.1"
|
||||
implementation "androidx.media3:media3-ui:1.10.1"
|
||||
implementation "androidx.media3:media3-session:1.10.1"
|
||||
implementation "androidx.media3:media3-cast:1.10.1"
|
||||
|
||||
## 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
|
||||
|
||||
<br />
|
||||
|
||||
### 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.
|
||||
|
||||
<br />
|
||||
|
||||
```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.
|
||||
|
||||
<br />
|
||||
|
||||
### 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>
|
||||
|
||||
<br />
|
||||
|
||||
### 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.
|
||||
|
||||
<br />
|
||||
|
||||
### 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`:
|
||||
|
||||
<br />
|
||||
|
||||
### 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.
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
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).
|
||||
|
||||
<br />
|
||||
|
||||
### 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,13 @@
|
||||
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)
|
||||
@@ -8,7 +8,7 @@ description: Learn how to install and migrate to Jetpack Navigation 3, and how t
|
||||
license: Complete terms in LICENSE.txt
|
||||
metadata:
|
||||
author: Google LLC
|
||||
last-updated: '2026-07-18'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- recipe
|
||||
- Android
|
||||
@@ -63,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
|
||||
|
||||
@@ -86,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
|
||||
|
||||
@@ -95,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,7 +1,18 @@
|
||||
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
|
||||
creates a flexible app navigation system by providing:
|
||||
<br />
|
||||
|
||||
## 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 --skill 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 creates a flexible app navigation system by providing:
|
||||
|
||||
- Conventions for modeling a back stack, where each entry on the back stack represents content that the user has navigated to
|
||||
- A UI that automatically updates with back stack changes (including animations)
|
||||
@@ -11,28 +22,23 @@ creates a flexible app navigation system by providing:
|
||||
|
||||
At a high level, you implement Navigation 3 in the following ways:
|
||||
|
||||
1. Define the content that users can navigate to in your app, each with a unique key, and add a function to resolve that key to the content. See [Resolve keys
|
||||
to content](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys).
|
||||
1. Define the content that users can navigate to in your app, each with a unique key, and add a function to resolve that key to the content. See [Resolve keys to content](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys).
|
||||
2. Create a back stack that keys are pushed onto and removed as users navigate your app. See [Create a back stack](https://developer.android.com/guide/navigation/navigation-3/basics#create-back).
|
||||
3. Use a [`NavDisplay`](https://developer.android.com/reference/kotlin/androidx/navigation3/ui/NavDisplay.composable) to display your app's back stack. Whenever the back stack changes, it updates the UI to display relevant content. See [Display
|
||||
the back stack](https://developer.android.com/guide/navigation/navigation-3/basics#display-back).
|
||||
3. Use a [`NavDisplay`](https://developer.android.com/reference/kotlin/androidx/navigation3/ui/NavDisplay.composable) to display your app's back stack. Whenever the back stack changes, it updates the UI to display relevant content. See [Display the back stack](https://developer.android.com/guide/navigation/navigation-3/basics#display-back).
|
||||
4. Modify `NavDisplay`'s [scene strategies](https://developer.android.com/guide/navigation/navigation-3/custom-layouts) as needed to support adaptive layouts and different platforms.
|
||||
|
||||
You can see the [full source code](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation3/) for Navigation 3 on AOSP.
|
||||
|
||||
## Improvements upon Jetpack Navigation
|
||||
|
||||
Navigation 3 improves upon the original Jetpack Navigation API in the following
|
||||
ways:
|
||||
Navigation 3 improves upon the original Jetpack Navigation API in the following ways:
|
||||
|
||||
- Provides a simpler integration with Compose
|
||||
- Offers you full control of the back stack
|
||||
- Makes it possible to create layouts that can read more than one destination from the back stack at the same time, allowing them to adapt to changes in window size and other inputs.
|
||||
|
||||
Read more about Navigation 3's principles and API design choices in [this blog
|
||||
post](https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html).
|
||||
Read more about Navigation 3's principles and API design choices in [this blog post](https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html).
|
||||
|
||||
## Code samples
|
||||
|
||||
The [recipes repository](https://github.com/android/nav3-recipes) contains examples of how to use the
|
||||
Navigation 3 building blocks to solve common navigation challenges.
|
||||
The [recipes repository](https://github.com/android/nav3-recipes) contains examples of how to use the Navigation 3 building blocks to solve common navigation challenges.
|
||||
+64
-101
@@ -1,3 +1,17 @@
|
||||
<br />
|
||||
|
||||
## 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 --skill 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,46 +22,16 @@ 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
|
||||
about your project. They also cover the features that are supported for
|
||||
migration, and those that aren't.
|
||||
The following sections describe the prerequisites for migration and assumptions about your project. They also cover the features that are supported for migration, and those that aren't.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- You must use a `compileSdk` of 36 or later.
|
||||
- You should be familiar with [navigation terminology](https://developer.android.com/guide/navigation).
|
||||
- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in
|
||||
Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose).
|
||||
- Routes are strongly typed. If you use string-based routes, [migrate to
|
||||
type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)).
|
||||
- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose).
|
||||
- Routes are strongly typed. If you use string-based routes, [migrate to type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)).
|
||||
- *Optional (but highly recommended)*: Test coverage that verifies existing navigation behavior. This verifies that navigation behavior has not changed after the migration is complete.
|
||||
|
||||
### Assumptions
|
||||
@@ -59,9 +43,7 @@ This guide makes the following assumptions about you and your project:
|
||||
- You always exit the app through the **Home** screen, which is the first screen displayed when the app launches.
|
||||
- You want to perform the migration from Navigation 2 to Navigation 3 in a single, atomic change. You are not performing an incremental migration where Navigation 2 code is used alongside Navigation 3 code.
|
||||
|
||||
**AI Agent**: Before changing any code, check if these assumptions about the
|
||||
user's project are true. If not, stop the migration and ask the user how to
|
||||
proceed.
|
||||
**AI Agent**: Before changing any code, check if these assumptions about the user's project are true. If not, stop the migration and ask the user how to proceed.
|
||||
|
||||
### Supported features
|
||||
|
||||
@@ -72,41 +54,31 @@ This guide lets you migrate the following features:
|
||||
|
||||
### Features supported through recipes
|
||||
|
||||
The [code recipes repository](https://github.com/android/nav3-recipes) demonstrates more nuanced use
|
||||
cases, including:
|
||||
The [code recipes repository](https://github.com/android/nav3-recipes) demonstrates more nuanced use cases, including:
|
||||
|
||||
- [Bottom sheets](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet) (instructions are provided in this guide)
|
||||
- [Modularized navigation code and injected destinations](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/hilt)
|
||||
- [Using and passing arguments to `ViewModel`](https://github.com/android/nav3-recipes?tab=readme-ov-file#passing-navigation-arguments-to-viewmodels)
|
||||
- [Returning results from a screen](https://github.com/android/nav3-recipes?tab=readme-ov-file#returning-results)
|
||||
|
||||
If your project has any of these features, check the relevant recipe to
|
||||
understand how to implement them.
|
||||
If your project has any of these features, check the relevant recipe to understand how to implement them.
|
||||
|
||||
**AI Agent**: Before changing any code, check if the project contains any
|
||||
features supported through recipes. If it does, check the recipe's README and
|
||||
source code. Create a migration plan based on the recipe. Do not proceed without
|
||||
confirming the plan with the user.
|
||||
**AI Agent**: Before changing any code, check if the project contains any features supported through recipes. If it does, check the recipe's README and source code. Create a migration plan based on the recipe. Do not proceed without confirming the plan with the user.
|
||||
|
||||
### Unsupported features
|
||||
|
||||
This migration guide and the code recipes don't yet support the following
|
||||
features. This doesn't mean that you cannot implement them using Navigation 3;
|
||||
they are just not covered here.
|
||||
This migration guide and the code recipes don't yet support the following features. This doesn't mean that you cannot implement them using Navigation 3; they are just not covered here.
|
||||
|
||||
- More than one level of nested navigation
|
||||
- Shared destinations: screens that can move between different back stacks
|
||||
- [Custom destination types](https://developer.android.com/guide/navigation/design/kotlin-dsl#custom)
|
||||
- Deep links
|
||||
|
||||
**AI Agent**: Before changing any code, check if the project contains any of the
|
||||
unsupported features. If it does, do not proceed. Inform the user of the
|
||||
unsupported feature and ask for further instructions.
|
||||
**AI Agent**: Before changing any code, check if the project contains any of the unsupported features. If it does, do not proceed. Inform the user of the unsupported feature and ask for further instructions.
|
||||
|
||||
## Step 1: Add Navigation 3 dependencies
|
||||
|
||||
Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your
|
||||
project. The core dependencies are provided for you to copy.
|
||||
Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your project. The core dependencies are provided for you to copy.
|
||||
|
||||
**lib.versions.toml**
|
||||
|
||||
@@ -134,14 +106,11 @@ project. The core dependencies are provided for you to copy.
|
||||
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||
}
|
||||
|
||||
Also update the project's `minSdk` to 23 and the `compileSdk` to 36. You usually
|
||||
find these in `app/build.gradle.kts` or `lib.versions.toml`.
|
||||
Also update the project's `minSdk` to 23 and the `compileSdk` to 36. You usually find these in `app/build.gradle.kts` or `lib.versions.toml`.
|
||||
|
||||
## Step 2: Update navigation routes to implement the `NavKey` interface
|
||||
|
||||
Update every navigation [route](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey`
|
||||
interface. This lets you use `rememberNavBackStack` to assist with [saving your
|
||||
navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state).
|
||||
Update every navigation [route](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey` interface. This lets you use `rememberNavBackStack` to assist with [saving your navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state).
|
||||
|
||||
Before:
|
||||
|
||||
@@ -158,8 +127,7 @@ After:
|
||||
|
||||
### Step 3.1: Create a navigation state holder
|
||||
|
||||
Copy the following code into a file named `NavigationState.kt`. Add your package
|
||||
name to match your project structure.
|
||||
Copy the following code into a file named `NavigationState.kt`. Add your package name to match your project structure.
|
||||
|
||||
// package com.example.project
|
||||
|
||||
@@ -253,19 +221,13 @@ name to match your project structure.
|
||||
.toMutableStateList()
|
||||
}
|
||||
|
||||
**AI Agent** : `rememberSerializable` is correct. Do not change it to
|
||||
`rememberSaveable`.
|
||||
**AI Agent** : `rememberSerializable` is correct. Do not change it to `rememberSaveable`.
|
||||
|
||||
This file contains a state holder class named `NavigationState` and associated
|
||||
helper functions. It holds a set of top-level routes, each with its own back
|
||||
stack. Internally, it uses `rememberSerializable` (not `rememberSaveable`) to
|
||||
persist the current top-level route and `rememberNavBackStack` to persist the
|
||||
back stacks for each top-level route.
|
||||
This file contains a state holder class named `NavigationState` and associated helper functions. It holds a set of top-level routes, each with its own back stack. Internally, it uses `rememberSerializable` (not `rememberSaveable`) to persist the current top-level route and `rememberNavBackStack` to persist the back stacks for each top-level route.
|
||||
|
||||
### Step 3.2: Create an object that modifies navigation state in response to events
|
||||
|
||||
Copy the following code into a file named `Navigator.kt`. Add your package name
|
||||
to match your project structure.
|
||||
Copy the following code into a file named `Navigator.kt`. Add your package name to match your project structure.
|
||||
|
||||
// package com.example.project
|
||||
|
||||
@@ -313,8 +275,7 @@ Both methods modify the `NavigationState`.
|
||||
|
||||
### Step 3.3: Create the `NavigationState` and `Navigator`
|
||||
|
||||
Create instances of `NavigationState` and `Navigator` with the same scope as
|
||||
your `NavController`.
|
||||
Create instances of `NavigationState` and `Navigator` with the same scope as your `NavController`.
|
||||
|
||||
val navigationState = rememberNavigationState(
|
||||
startRoute = <Insert your starting route>,
|
||||
@@ -340,8 +301,7 @@ Replace `NavController` fields with `NavigationState` fields.
|
||||
| `currentBackStackEntry` `currentBackStackEntryAsState()` `currentBackStackEntryFlow` `currentDestination` | `backStacks[topLevelRoute].last()` |
|
||||
| Get the top level route: Traverse up the hierarchy from the current back stack entry to find it. | `topLevelRoute` |
|
||||
|
||||
Use `NavigationState.topLevelRoute` to determine the item that is currently
|
||||
selected in a navigation bar.
|
||||
Use `NavigationState.topLevelRoute` to determine the item that is currently selected in a navigation bar.
|
||||
|
||||
Before:
|
||||
|
||||
@@ -356,22 +316,36 @@ After:
|
||||
|
||||
val isSelected = key == navigationState.topLevelRoute
|
||||
|
||||
Verify that you have removed all references to `NavController`, including
|
||||
any imports.
|
||||
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:
|
||||
|
||||
// In your destination screen or host
|
||||
val lifecycleOwner = navController.currentBackStackEntry
|
||||
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
|
||||
|
||||
After:
|
||||
|
||||
// Inside the destination composable
|
||||
val state by flow.collectAsStateWithLifecycle()
|
||||
|
||||
## 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)
|
||||
using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder),
|
||||
usually inside `NavHost`'s trailing lambda. It is common to use extension
|
||||
functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate).
|
||||
In Navigation 2, you [define your destinations](https://developer.android.com/guide/navigation/design#compose) using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder), usually inside `NavHost`'s trailing lambda. It is common to use extension functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate).
|
||||
|
||||
In Navigation 3, you define your destinations using an `entryProvider`. This
|
||||
`entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the
|
||||
`entryProvider` does not define parent-child relationships between entries.
|
||||
In Navigation 3, you define your destinations using an `entryProvider`. This `entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the `entryProvider` does not define parent-child relationships between entries.
|
||||
|
||||
In this migration guide, parent-child relationships are modelled
|
||||
as follows:
|
||||
In this migration guide, parent-child relationships are modelled as follows:
|
||||
|
||||
- `NavigationState` has a set of top-level routes (the parent routes) and a stack for each one. It keeps track of the current top-level route and its associated stack.
|
||||
- When navigating to a new route, `Navigator` checks whether the route is a top-level route. If it is, the current top-level route and stack are updated. If it's not, it's a child route and is added to the current stack.
|
||||
@@ -379,35 +353,26 @@ 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`.
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
## Step 5.2: Move destinations into the `entryProvider`
|
||||
### Step 5.2: Move destinations into the `entryProvider`
|
||||
|
||||
For each destination defined inside `NavHost`, do the following based on the
|
||||
destination type:
|
||||
For each destination defined inside `NavHost`, do the following based on the destination type:
|
||||
|
||||
- `navigation`: Delete it along with the route. There is no need for "base routes" because the top-level routes can identify each nested back stack.
|
||||
- `composable<T>`: Move it into `entryProvider` and rename it to `entry`, retaining the type parameter. For example, `composable<RouteA>` becomes `entry<RouteA>`.
|
||||
- `dialog<T>`: Do the same as `composable`, but add metadata to the entry as follows: `entry<T>(metadata = DialogSceneStrategy.dialog())`.
|
||||
- [`bottomSheet`](https://developer.android.com/reference/kotlin/androidx/compose/material/navigation/package-summary#(androidx.navigation.NavGraphBuilder).bottomSheet(kotlin.String,kotlin.collections.List,kotlin.collections.List,kotlin.Function2)): [Follow the bottom sheet recipe here](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet). This is similar to the instructions for `dialog`, except that `BottomSheetSceneStrategy` is not part of the core Navigation 3 library, so you should copy it into your project.
|
||||
|
||||
**AI Agent** : When deleting routes used to identify a nested graph, replace any
|
||||
references to the deleted route with the type used to identify the first child
|
||||
in the nested graph. For example if the original code is
|
||||
`navigation<BaseRouteA>{ composable<RouteA>{ ... } }`, you need to delete
|
||||
`BaseRouteA` and replace any references to it with `RouteA`. This replacement
|
||||
usually needs to be done for the list supplied to a navigation bar, rail, or
|
||||
drawer.
|
||||
**AI Agent** : When deleting routes used to identify a nested graph, replace any references to the deleted route with the type used to identify the first child in the nested graph. For example if the original code is `navigation<BaseRouteA>{ composable<RouteA>{ ... } }`, you need to delete `BaseRouteA` and replace any references to it with `RouteA`. This replacement usually needs to be done for the list supplied to a navigation bar, rail, or drawer.
|
||||
|
||||
You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to
|
||||
`EntryProviderScope<T>` extension functions, and then move them.
|
||||
You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to `EntryProviderScope<T>` extension functions, and then move them.
|
||||
|
||||
Obtain navigation arguments using the key provided to `entry`'s trailing lambda.
|
||||
|
||||
@@ -493,6 +458,4 @@ Remove all Navigation 2 imports and library dependencies.
|
||||
|
||||
## Summary
|
||||
|
||||
Congratulations! Your project is now migrated to Navigation 3. If you or your AI
|
||||
agent has run into any problems using this guide, [file a bug
|
||||
here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D).
|
||||
Congratulations! Your project is now migrated to Navigation 3. If you or your AI agent has run into any problems using this guide, [file a bug here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D).
|
||||
+2
@@ -144,4 +144,6 @@ class AnimatedActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -86,4 +86,6 @@ class BasicActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -82,4 +82,6 @@ class BasicDslActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -87,4 +87,6 @@ class BasicSaveableActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+3
@@ -102,6 +102,8 @@ class BottomSheetActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -192,4 +194,5 @@ class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
+2
@@ -197,4 +197,6 @@ class TopLevelBackStack<T: Any>(startKey: T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# 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 })
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -176,6 +176,7 @@ fun <T : NavKey> rememberNavBackStack(vararg elements: T): NavBackStack<T> {
|
||||
NavBackStack(*elements)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -227,4 +228,5 @@ class Navigator(
|
||||
|
||||
fun goBack() = backStack.removeLastOrNull()
|
||||
}
|
||||
|
||||
```
|
||||
-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}}"
|
||||
```
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# 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)
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
# 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>(),
|
||||
)
|
||||
|
||||
```
|
||||
+45
-23
@@ -1,34 +1,54 @@
|
||||
# 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.
|
||||
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
|
||||
into "App B".
|
||||
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
|
||||
trigger that in either the existing Task, or in a new Task.
|
||||
"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
|
||||
and how to manage the Task stack properly in order to support both Back and Up buttons.
|
||||
"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)
|
||||
The core helper functions for navigateUp and building synthetic backStack can be 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)
|
||||
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/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 +73,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()
|
||||
@@ -152,4 +172,6 @@ private val MENU_OPTIONS_LOCATION = mapOf(
|
||||
private val MENU_OPTIONS_TASK_STACK = mapOf(
|
||||
TAG_TASK_STACK to listOf(TAG_CURRENT_TASK, TAG_NEW_TASK),
|
||||
)
|
||||
|
||||
|
||||
```
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
# 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}}"
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -104,4 +104,6 @@ class DialogActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
# 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.
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -138,4 +138,6 @@ class MaterialListDetailActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+2
@@ -142,4 +142,6 @@ class MaterialSupportingPaneActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+9
@@ -75,6 +75,8 @@ private fun ProfileScreen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -118,6 +120,8 @@ class HiltModularActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -236,6 +240,8 @@ private fun ConversationDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -261,6 +267,7 @@ class Navigator(startDestination: Any) {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -280,4 +287,6 @@ object AppModule {
|
||||
@ActivityRetainedScoped
|
||||
fun provideNavigator() : Navigator = Navigator(startDestination = ConversationList)
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+8
@@ -66,6 +66,8 @@ private fun ProfileScreen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -181,6 +183,8 @@ private fun ConversationDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -200,6 +204,7 @@ class Navigator(startDestination: Any) {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -217,6 +222,7 @@ val appModule = module {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -284,4 +290,6 @@ class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+57
-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")
|
||||
@@ -68,6 +78,8 @@ class Navigator(val state: NavigationState){
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -204,6 +216,8 @@ class NavigationState(
|
||||
listOf(startRoute, topLevelRoute)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -225,10 +239,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 +254,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 +294,6 @@ data class NavBarItem(
|
||||
)
|
||||
|
||||
class MultipleStacksActivity : ComponentActivity() {
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setEdgeToEdgeConfig()
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -290,18 +306,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,15 +336,18 @@ class MultipleStacksActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}) { innerPadding ->
|
||||
NavDisplay(
|
||||
entries = navigationState.toDecoratedEntries(entryProvider),
|
||||
onBack = { navigator.goBack() }
|
||||
onBack = { navigator.goBack() },
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -343,13 +370,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 +390,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,4 +471,6 @@ fun EntryProviderScope<NavKey>.featureCSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+5
@@ -133,6 +133,8 @@ class RouteBViewModel @AssistedInject constructor(
|
||||
fun create(navKey: RouteB): RouteBViewModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
# Passing Arguments to ViewModels (Basic)
|
||||
@@ -257,6 +259,8 @@ class RouteBViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
# Passing Arguments to ViewModels (Koin)
|
||||
@@ -368,4 +372,5 @@ fun ScreenB(viewModel: RouteBViewModel) {
|
||||
}
|
||||
|
||||
class RouteBViewModel(val navKey: RouteB) : ViewModel()
|
||||
|
||||
```
|
||||
+6
@@ -41,6 +41,7 @@ import androidx.lifecycle.ViewModel
|
||||
class HomeViewModel : ViewModel() {
|
||||
var person by mutableStateOf<Person?>(null)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -70,6 +71,7 @@ data object Home : NavKey
|
||||
|
||||
@Serializable
|
||||
class PersonDetailsForm : NavKey
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -95,6 +97,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Person(val name: String, val favoriteColor: String)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -180,6 +183,7 @@ fun PersonDetailsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -270,4 +274,6 @@ class ResultEventActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+6
@@ -41,6 +41,7 @@ import androidx.lifecycle.ViewModel
|
||||
class HomeViewModel : ViewModel() {
|
||||
var person by mutableStateOf<Person?>(null)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -70,6 +71,7 @@ data object Home : NavKey
|
||||
|
||||
@Serializable
|
||||
class PersonDetailsForm : NavKey
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -95,6 +97,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Person(val name: String, val favoriteColor: String)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -180,6 +183,7 @@ fun PersonDetailsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -263,4 +267,6 @@ class ResultStateActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+4
@@ -168,6 +168,8 @@ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : S
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -287,6 +289,7 @@ private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) {
|
||||
removeIf { it is ConversationDetail }
|
||||
add(detailRoute)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -432,4 +435,5 @@ fun ProfileScreen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
+4
@@ -123,6 +123,8 @@ class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : Scen
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -241,4 +243,6 @@ private fun NavBackStack<NavKey>.addProductRoute(productId: Int) {
|
||||
add(productRoute)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
+7
-14
@@ -1,11 +1,8 @@
|
||||
This guide outlines the process of replacing string-based routes with
|
||||
serializable Kotlin types to achieve compile-time safety and eliminate runtime
|
||||
crashes caused by typos or incorrect argument types.
|
||||
This guide outlines the process of replacing string-based routes with serializable Kotlin types to achieve compile-time safety and eliminate runtime crashes caused by typos or incorrect argument types.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting the migration, verify that your project meets the following
|
||||
requirements:
|
||||
Before starting the migration, verify that your project meets the following requirements:
|
||||
|
||||
1. **Navigation version**: Update to Jetpack Navigation 2.8.0 or higher
|
||||
2. **Kotlin serialization plugin**:
|
||||
@@ -43,8 +40,7 @@ Replace your constant route strings with `@Serializable` objects and classes.
|
||||
|
||||
## Step 2: Update the NavHost Configuration
|
||||
|
||||
Update your `NavHost` to use the new generic types in the `composable` and
|
||||
`dialog` function.
|
||||
Update your `NavHost` to use the new generic types in the `composable` and `dialog` function.
|
||||
|
||||
**Before:**
|
||||
|
||||
@@ -83,8 +79,7 @@ Replace string-interpolated navigation calls with class instances.
|
||||
|
||||
## Step 4: Accessing Arguments in ViewModels
|
||||
|
||||
If you use a `ViewModel`, you can now extract the route object directly from the
|
||||
`SavedStateHandle`.
|
||||
If you use a `ViewModel`, you can now extract the route object directly from the `SavedStateHandle`.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
@@ -98,10 +93,9 @@ If you use a `ViewModel`, you can now extract the route object directly from the
|
||||
|
||||
## Step 5: (Advanced) Handling Custom Types
|
||||
|
||||
If you need to pass complex data classes (not just primitives), you must define
|
||||
a custom `NavType`.
|
||||
If you need to pass complex data classes (not just primitives), you must define a custom `NavType`.
|
||||
|
||||
1. **Create the Custom Type** : \`\`\`kotlin val SearchFilterType = object : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null)
|
||||
1. **Create the Custom Type** : \`\`\`kotlin val SearchFilterType = object : NavType (isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null)
|
||||
|
||||
override fun parseValue(value: String): SearchFilter =
|
||||
Json.decodeFromString(Uri.decode(value))
|
||||
@@ -124,6 +118,5 @@ a custom `NavType`.
|
||||
|
||||
- **Sealed Hierarchies**: For large apps, group your routes using a sealed interface or class to keep the navigation structure organized
|
||||
- **Object Instances** : For routes without parameters, always use `object` instead of `class` to avoid unnecessary allocations
|
||||
- **Nullable Types** : The new API supports nullable types (for example, `data
|
||||
class Search(val query: String?)`) and provides default values automatically
|
||||
- **Nullable Types** : The new API supports nullable types (for example, `data class Search(val query: String?)`) and provides default values automatically
|
||||
- **Testing** : Use `navController.currentBackStackEntry?.hasRoute<T>()` to check the current destination in a type-safe manner during UI tests
|
||||
@@ -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-07-18'
|
||||
last-updated: '2026-08-06'
|
||||
keywords:
|
||||
- R8
|
||||
- proguard
|
||||
@@ -20,33 +20,39 @@ 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
|
||||
- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and `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
|
||||
|
||||
|
||||
@@ -6,17 +6,13 @@ On each step, keep the user informed of the progress by displaying the output.
|
||||
|
||||
### 2. Generate proto
|
||||
|
||||
The report and files must be generated at `{project_root}/tmp/r8analysis`. If
|
||||
the folder is not present, create it. For example:
|
||||
The report and files must be generated at `{project_root}/tmp/r8analysis`. If the folder is not present, create it. For example:
|
||||
|
||||
mkdir -p "$PWD/tmp/r8analysis"
|
||||
|
||||
### 3. Remove existing files
|
||||
|
||||
To make sure that this invocation doesn't source data from previous runs, remove
|
||||
the intermediate files `keepruleradius.json` and `analysis_result.txt` and
|
||||
remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example
|
||||
bash commands:
|
||||
To make sure that this invocation doesn't source data from previous runs, remove the intermediate files `keepruleradius.json` and `analysis_result.txt` and remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example bash commands:
|
||||
|
||||
# Remove the intermediate JSON and the directory containing protobuf files
|
||||
rm tmp/r8analysis/keepruleradius.json
|
||||
@@ -29,19 +25,14 @@ bash commands:
|
||||
|
||||
### 4. Generate the Configuration Analyzer report
|
||||
|
||||
Run the R8 enabled build with the system property
|
||||
"-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to
|
||||
generate Configuration Analyzer report
|
||||
Run the R8 enabled build with the system property "-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to generate Configuration Analyzer report
|
||||
|
||||
./gradlew assembleRelease \
|
||||
-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis
|
||||
|
||||
### 5. Convert to JSON
|
||||
|
||||
To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into
|
||||
json, run the following script. The json must be generated in
|
||||
`{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is
|
||||
in the same directory.
|
||||
To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into json, run the following script. The json must be generated in `{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is in the same directory.
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -93,8 +84,7 @@ in the same directory.
|
||||
|
||||
### 6. Analyze
|
||||
|
||||
Run the following analysis script on the generated JSON to get the impact of the
|
||||
keep rules and sort it.
|
||||
Run the following analysis script on the generated JSON to get the impact of the keep rules and sort it.
|
||||
|
||||
import json, sys
|
||||
|
||||
@@ -150,8 +140,7 @@ Outputs `analysis_result.txt` containing scores and rule impacts.
|
||||
|
||||
### 7. Report impactful rules
|
||||
|
||||
Identify the keep rules with the highest impact and the subsumed rules using the
|
||||
following script.
|
||||
Identify the keep rules with the highest impact and the subsumed rules using the following script.
|
||||
|
||||
import json, sys
|
||||
|
||||
@@ -198,27 +187,22 @@ following script.
|
||||
if __name__ == "__main__":
|
||||
report("tmp/r8analysis/keepruleradius.json")
|
||||
|
||||
Add this data to the `analysis_result.txt` with the top impactful rules and
|
||||
subsumed rules.
|
||||
Add this data to the `analysis_result.txt` with the top impactful rules and subsumed rules.
|
||||
|
||||
### 8. Compare with previous report
|
||||
|
||||
If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script
|
||||
to compare the previous run. Use this to compare with the current values
|
||||
If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script to compare the previous run. Use this to compare with the current values
|
||||
|
||||
### 9. Remove generated files
|
||||
|
||||
After the final report and analysis results are generated, remove the
|
||||
intermediate files `keepruleradius.json` and `analysis_result.txt` and remove
|
||||
the proto files in "{project_root}/tmp/r8analysis" folder
|
||||
After the final report and analysis results are generated, remove the intermediate files `keepruleradius.json` and `analysis_result.txt` and remove the proto files in "{project_root}/tmp/r8analysis" folder
|
||||
|
||||
rm tmp/r8analysis/keepruleradius.json
|
||||
rm tmp/r8analysis/*.pb
|
||||
|
||||
### 10. Protobuf Python bindings
|
||||
|
||||
The following script `keep_radius_pb2.py` is required by the conversion script
|
||||
in Step 5.
|
||||
The following script `keep_radius_pb2.py` is required by the conversion script in Step 5.
|
||||
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user