Updates skills (2026-04-08 15:56)

This commit is contained in:
android-devrel-github-bot
2026-04-08 15:56:48 +00:00
parent 2f7dcf7da2
commit 6e634f4d69
64 changed files with 3759 additions and 3472 deletions
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+89
View File
@@ -0,0 +1,89 @@
---
name: agp-9-upgrade
description: Upgrades, or migrates, an Android project to use Android Gradle Plugin
(AGP) version 9
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- Android Gradle Plugin 9
- AGP 9
- AGP Upgrade
- AGP Migration
- New AGP DSL
- Migrate to built-in Kotlin
---
## 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.
## 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, and confirm when done.
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.
## Steps
If AGP is already at 9 or higher, then do the following:
### Step 1: Migrate to built-in Kotlin.
See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information.
### Step 2. Migrate to the new AGP DSL.
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.
### Step 3. Migrate kapt to KSP or legacy-kapt
If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on
version 2.3.6 or higher.
See [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed information.
### Step 4. BuildConfig
If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md)
for detailed information.
### Step 5. Update gradle.properties
After the migration, check gradle.properties. Remove the following flags:
1. android.builtInKotlin
2. android.newDsl
3. android.uniquePackageNames
4. android.enableAppCompileTimeRClass
Additionally, delete all temporary files you've created.
## Guidelines
- Never write or run python scripts.
- Only search the Gradle dependency cache when inspecting external dependencies, and only as a last resort.
- Never add `android.disallowKotlinSourceSets=false` to `gradle.properties`.
- When verifying changes, don't run the `clean` task. This is a waste of time.
## Verification
After migration, verify the following:
1. Gradle IDE sync succeeds.
2. `./gradlew help` succeeds.
3. `./gradlew build --dry-run` succeeds.
## 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.
@@ -0,0 +1,416 @@
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`.
> [!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.
## 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:
Failed to apply plugin 'org.jetbrains.kotlin.android'.
> Cannot add extension with name 'kotlin', as there is an extension already registered with that name.
...or
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:
> [!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).
1. [Remove the `kotlin-android` plugin](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-remove-kotlin-android-plugin)
2. [Migrate the `kotlin-kapt` plugin if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-kapt-plugin)
3. [Migrate the `android.kotlinOptions{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-options)
4. [Migrate the `kotlin.sourceSets{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-source-sets)
### 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.
### With version catalogs
### Kotlin
```kotlin
// Module-level build file
plugins {
alias(libs.plugins.kotlin.android)
}
```
### Groovy
```groovy
// Module-level build file
plugins {
alias(libs.plugins.kotlin.android)
}
```
### No version catalogs
### Kotlin
```kotlin
// Module-level build file
plugins {
id("org.jetbrains.kotlin.android")
}
```
### Groovy
```groovy
// Module-level build file
plugins {
id 'org.jetbrains.kotlin.android'
}
```
Then, remove the plugin from your top-level build file:
### With version catalogs
### Kotlin
```kotlin
// Top-level build file
plugins {
alias(libs.plugins.kotlin.android) apply false
}
```
### Groovy
```groovy
// Top-level build file
plugins {
alias(libs.plugins.kotlin.android) apply false
}
```
### No version catalogs
### Kotlin
```kotlin
// Top-level build file
plugins {
id("org.jetbrains.kotlin.android") version "KOTLIN_VERSION" apply false
}
```
### Groovy
```groovy
// Top-level build file
plugins {
id 'org.jetbrains.kotlin.android' version 'KOTLIN_VERSION' apply false
}
```
If you use version catalogs, also remove the plugin definition from the
version catalog TOML file (usually `gradle/libs.versions.toml`):
```toml
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "KOTLIN_VERSION" }
```
### 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).
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:
```toml
[plugins]
android-application = { id = "com.android.application", version.ref = "AGP_VERSION" }
# Add the following plugin definition
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "AGP_VERSION" }
# Remove the following plugin definition
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "KOTLIN_VERSION" }
```
Then, update your build files:
### Kotlin
```kotlin
// Top-level build file
plugins {
alias(libs.plugins.legacy.kapt) apply false
alias(libs.plugins.kotlin.kapt) apply false
}
```
### Groovy
```groovy
// Top-level build file
plugins {
alias(libs.plugins.legacy.kapt) apply false
alias(libs.plugins.kotlin.kapt) apply false
}
```
### Kotlin
```kotlin
// Module-level build file
plugins {
alias(libs.plugins.legacy.kapt)
alias(libs.plugins.kotlin.kapt)
}
```
### Groovy
```groovy
// Module-level build file
plugins {
alias(libs.plugins.legacy.kapt)
alias(libs.plugins.kotlin.kapt)
}
```
> [!NOTE]
> **Note:** If you declare the `kotlin-kapt` plugin in the `plugins{}` block as `kotlin("kapt") version "<KOTLIN_VERSION>"`, then remove that line instead.
### 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.
For example, update this code:
### Kotlin
```kotlin
android {
kotlinOptions {
languageVersion = "2.0"
jvmTarget = "11"
}
}
```
### Groovy
```groovy
android {
kotlinOptions {
languageVersion = "2.0"
jvmTarget = "11"
}
}
```
...to the new DSL:
### Kotlin
```kotlin
kotlin {
compilerOptions {
languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0
// Optional: Set jvmTarget
// jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
```
### Groovy
```groovy
kotlin {
compilerOptions {
languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0
// Optional: Set jvmTarget
// jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
```
> [!NOTE]
> **Note:** With built-in Kotlin, you don't need to set `kotlin.compilerOptions.jvmTarget` because its value defaults to `android.compileOptions.targetCompatibility`.
### 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.
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
```kotlin
# Adding Kotlin source directories to kotlin.sourceSets is not supported
kotlin.sourceSets.named("main") {
kotlin.srcDir("additionalSourceDirectory/kotlin")
}
# Adding Kotlin source directories to AndroidSourceSet.java is also not supported
android.sourceSets.named("main") {
java.directories += "additionalSourceDirectory/kotlin"
}
# Add Kotlin source directories to AndroidSourceSet.kotlin
android.sourceSets.named("main") {
kotlin.directories += "additionalSourceDirectory/kotlin"
}
```
### Groovy
```groovy
# Adding Kotlin source directories to kotlin.sourceSets is not supported
kotlin.sourceSets.named("main") {
kotlin.srcDir("additionalSourceDirectory/kotlin")
}
# Adding Kotlin source directories to AndroidSourceSet.java is also not supported
android.sourceSets.named("main") {
java.directories.add("additionalSourceDirectory/kotlin")
}
# Add Kotlin source directories to AndroidSourceSet.kotlin
android.sourceSets.named("main") {
kotlin.directories.add("additionalSourceDirectory/kotlin")
}
```
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
```kotlin
androidComponents.onVariants { variant ->
variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin")
variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT)
}
```
### Groovy
```groovy
androidComponents.onVariants { variant ->
variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin")
variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT)
}
```
## 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.
## 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.
> [!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).
## 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.
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.
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.
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.
To disable built-in Kotlin for a module,
set `enableKotlin = false` in that module's build file:
### Kotlin
```kotlin
android {
enableKotlin = false
}
```
### Groovy
```groovy
android {
enableKotlin = false
}
```
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
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
In a build script:
android {
buildFeatures {
buildConfig = true
}
}
In custom build-logic for an app module:
extensions.configure<com.android.build.api.dsl.ApplicationExtension> {
buildFeatures {
buildConfig = true
}
}
In custom build-logic for a library module:
extensions.configure<com.android.build.api.dsl.LibraryExtension> {
buildFeatures {
buildConfig = true
}
}
In custom build-logic using `CommonExtension`:
extensions.configure<com.android.build.api.dsl.CommonExtension> {
buildFeatures {
buildConfig = true
}
}
### Step 2: Migrate to the new API
Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https://developer.android.com/agents/skills/build/agp/agp-9-upgrade/references/recipes)
repository.
**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",
value = "\"Some value\"",
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.
@@ -0,0 +1,39 @@
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.
## 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`.
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/`.
## 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.
## 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.
@@ -0,0 +1,30 @@
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.
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.
Kotlin DSL:
tasks.withType<Test>().configureEach {
// https://github.com/cashapp/paparazzi/issues/2111
reports.html.required = false
}
Groovy DSL:
tasks.withType(Test).configureEach {
// https://github.com/cashapp/paparazzi/issues/2111
reports.html.required = false
}
@@ -0,0 +1,59 @@
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.
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.
## Compatibility table
| AGP version | gradle-recipes branch |
|---|---|
| 9.0.x | agp-9.0 |
| 9.1.x | agp-9.1 |
| 9.2.x | agp-9.2 |
## Recipes and use-cases
The following table links use-cases to recipes.
| Recipe | Use-case |
|---|---|
| addCustomBuildConfigFields | Add custom BuildConfig fields |
| listenToArtifacts | Rename APK |
Additional details for each use-case follow.
### Add custom BuildConfig fields
See the detailed guide at [BuildConfig](https://developer.android.com/agents/skills/build/agp/agp-9-upgrade/references/buildconfig).
### Renaming an APK
In the old DSL, an APK could be renamed very simply. Here's an example:
android {
applicationVariants.all {
outputs.all {
val output = this as com.android.build.gradle.api.ApkVariantOutput
val fileName = output.outputFileName
if (fileName.contains("release")) {
output.outputFileName = "my-cool-new-name.apk"
}
}
}
}
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.
**DO NOT** search the web for examples of how to do this. Use the
**gradle-recipes** repository examples **only**.
@@ -1,9 +1,15 @@
---
name: migrate-xml-views-to-jetpack-compose
description: Provides a structured workflow for migrating Android XML Views to Jetpack Compose. This skill details the step-by-step process, from planning and dependency setup to attribute conversion, style migration, and validation. Use this skill when you need to incrementally migrate XML Views to Jetpack Compose in a project. It solves the problem of converting legacy UI code into modern, declarative Compose components while maintaining interoperability.
description: Provides a structured workflow for migrating Android XML Views to Jetpack
Compose. This skill details the step-by-step process, from planning and dependency
setup to attribute conversion, style migration, and validation. Use this skill when
you need to incrementally migrate XML Views to Jetpack Compose in a project. It
solves the problem of converting legacy UI code into modern, declarative Compose
components while maintaining interoperability.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- skill
- Jetpack Compose
- migration
- XML
@@ -13,59 +19,105 @@ metadata:
- UI development
---
# Migrate XML Views to Jetpack Compose
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.
Jetpack Compose supports interoperability with Views --- you
can use Compose in Views, and Views in Compose. This allows adoption of
Compose in existing View-based apps without having to migrate all Views
immediately.
## Objective
## Migration steps
To systematically convert a single legacy XML layout into modern, declarative
Jetpack Compose UI while maintaining pixel-perfect visual parity and functional
integrity.
1. **Create a plan:** Create a robust and step-by-step plan for performing the migration. We recommend a prioritized backlog of migration tasks.
2. **Identify the XML candidate for migration :** Identify and start from the smallest components that are leaf nodes in the hierarchy, and expand the migration plan from the bottom up to progressively higher components in the hierarchy. Good candidates for initial migration are small, stateless, and have fewer dependencies.
3. **Analyze the hierarchy:** Once you identify the XML View to migrate, analyze its XML layout structure and implementation.
4. **Capture the initial state:** Run a screenshot test to capture the initial state of the selected XML View.
5. **Prerequisite: Set up Compose dependencies** Identify if the project has Compose dependencies and Compiler set up. If it doesn't, follow [Setup Compose dependencies and Compiler](references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md.txt).
6. **Prerequisite: Set up Compose theming** Identify if the project has Compose theming setup already. If it doesn't, follow ompose theming. Keep the original XML theming while the app is interop [Migrate XML Theme to Compose](references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md.txt) to understand patterns of how to state and until the project is fully migrated to Compose.
7. **Migrate the XML View to Compose:** Start the conversion of the XML code to Compose, apply the appropriate theming, and add Compose Previews for migrated composables. For common migration scenarios, refer to additional resources. For example, for migrating to Lazy APIs in Compose, follow the steps in [Migrate RecyclerView to Compose](references/android/develop/ui/compose/migrate/migration-scenarios/recycler-view.md.txt).
8. **Replace usages:** Replace the previous usages of the XML View to use the new Compose component. To add Compose in Views, follow the steps in [Compose in Views](references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md.txt). To add Views in Compose, follow the steps in [Views in Compose](references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md.txt).
9. **Validate the migration:** Verify that the initial state captured in the screenshot test is same as the Compose Preview of the migrated composable. If they don't match, iterate on the new composable UI and improve it to align it with the initial state. Create new Compose UI tests for the new composable.
10. **XML removal:** Once the newly migrated composable is matching the initial XML UI, remove the obsolete XML View code and its tests.
## Summary of the 10-step migration process
## Common migration scenarios
1. **Identify the optimal XML candidate for migration**
2. **Analyze the project and layout**
3. **Create a plan**
4. **Capture the XML View UI**
5. **Set up Compose dependencies and compiler**
6. **Set up Compose theming**
7. **Migrate the XML layout to Compose**
8. **Validate the migration**
9. **Replace usages**
10. **XML code removal**
Verify `dp` and `sp` extensions are used (`16.dp`, `20.sp`) in composables.
If `tools:text` is present in the XML View, use it in a separate `@Preview`
composable.
## Detailed steps
### Attribute to Modifier conversion
### Step 1: Identify the optimal XML candidate for migration
Most XML attributes become part of the `modifier` chain or parameters of the
composable function.
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).
| XML Attribute | Compose Equivalent |
|---|---|
| `android:layout_width="match_parent"` | `Modifier.fillMaxWidth()` |
| `android:layout_height="match_parent"` | `Modifier.fillMaxHeight()` |
| `android:layout_width="wrap_content"` | (Default behavior, usually no modifier needed) |
| `android:padding="Xdp"` | `Modifier.padding(X.dp)` |
| `android:layout_margin="Xdp"` | `Modifier.padding(X.dp)` (Outer padding) |
| `android:gravity="center"` | `contentAlignment = Alignment.Center` (Box) or `horizontalAlignment` / `verticalArrangement` (Column/Row) |
| `android:background="@color/white"` | `Modifier.background(colorResource(R.color.white))` |
| `android:visibility="gone"` | Wrap in `if (visible) { ... }` block |
### Step 2: Analyze the project and layout
## Migrate styles (styles.xml)
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.
XML styles often combine multiple attributes to create a style. In Compose,
this is done by creating a **composable** variation with a specific style.
### Step 3: Create a plan
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.
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.
- **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:**
- *XML:* `<Button style="@style/MyPrimaryButton" ... />`
- *Compose:* `MyPrimaryButton(onClick = { ... })`
- **Common Attribute Groups:** If a style sets common modifiers (like padding + height), extract them into a readable extension property or a shared Modifier variable.
### 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.
### 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.
### 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.
### 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.
### Step 8: Replace usages
Replace the usages of the migrated XML layout to use the new Compose component.
- To add Compose in Views, use [Compose in Views](references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md).
- To add Views in Compose, use [Views in Compose](references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md).
### 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.
### 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.
@@ -0,0 +1,42 @@
## 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.
## 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.
## 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.
## 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).
## 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.
*** ** * ** ***
> **Pro-tip:** Always prioritize the "Existing infrastructure" over "Default templates." If the project has a custom way of handling spacing or colors, composable code, or any other project layer, your generated Compose code must reflect that specific implementation.
@@ -0,0 +1,168 @@
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.
When you start the migration from XML to Compose, migrate the theming to
Material 3 Compose theming.
## Glossary
| Term | Definition |
|---|---|
| `MaterialTheme` | The composable function that provides theming (colors, typography, shapes) to Compose UI components. |
| `Shape` | A Compose object used to define custom component shapes for a `MaterialTheme`. |
| `Typography` | A Compose object used to define custom text styles (font families, sizes, weights) for a `MaterialTheme`. |
| `Color` | A Compose object used to define custom color schemes for `MaterialTheme`. |
| XML Theme | The Android theming system defined in XML files, used by the View system. |
## Limitations
Before migrating, be aware of the following limitations:
- This guide focuses on migrating to Material 3 only. For migrating from alternative design systems, see [Material 2](https://developer.android.com/develop/ui/compose/designsystems/material) or [Custom design systems in Compose](https://developer.android.com/develop/ui/compose/designsystems/custom).
- The ultimate goal is a complete migration to Compose, which allows for the removal of XML theming. This guide explains how to migrate, but it doesn't explain how to finally remove XML theming.
## 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.
## Step 2: Identify theme source files
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.
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.
Examples:
| XML color name | Material 3 role |
|---|---|
| `colorPrimary` | `primary` |
| `colorPrimaryDark` / `colorPrimaryVariant` | `primaryContainer` or `secondary` |
| `colorAccent` | `secondary` or `tertiary` |
| `colorOnPrimary` | `onPrimary` |
| `android:colorBackground` | `background` |
| `colorSurface` | `surface` |
| `colorOnSurface` | `onSurface` |
| `colorError` | `error` |
| `colorOnError` | `onError` |
| `colorOutline` | `outline` |
| `colorSurfaceVariant` | `surfaceVariant` |
| `colorOnSurfaceVariant` | `onSurfaceVariant` |
*** ** * ** ***
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.
## Step 4: Migrate custom shapes and typography
- If your app uses custom shapes:
1. In your Compose code, define a `Shape` object to replicate your XML shape definitions.
2. Provide this `Shape` object to your `MaterialTheme`.
For more details, see [shapes](https://developer.android.com/develop/ui/compose/designsystems/material3#shapes).
- If your app uses custom typography:
1. In your Compose code, define a `Typography` object in your Compose code to replicate your XML text styles and font definitions.
2. Provide this `Typography` object to your `MaterialTheme`.
For more details, see [typography](https://developer.android.com/develop/ui/compose/designsystems/material3#typography).
| Compose role | XML name |
|---|---|
| `displayLarge` | `TextAppearance.Material3.DisplayLarge` |
| `displayMedium` | `TextAppearance.Material3.DisplayMedium` |
| `displaySmall` | `TextAppearance.Material3.DisplaySmall` |
| `headlineLarge` | `TextAppearance.Material3.HeadlineLarge` |
| `headlineMedium` | `TextAppearance.Material3.HeadlineMedium` |
| `headlineSmall` | `TextAppearance.Material3.HeadlineSmall` |
| `titleLarge` | `TextAppearance.Material3.TitleLarge` |
| `titleMedium` | `TextAppearance.Material3.TitleMedium` |
| `titleSmall` | `TextAppearance.Material3.TitleSmall` |
| `bodyLarge` | `TextAppearance.Material3.BodyLarge` |
| `bodyMedium` | `TextAppearance.Material3.BodyMedium` |
| `bodySmall` | `TextAppearance.Material3.BodySmall` |
| `labelLarge` | `TextAppearance.Material3.LabelLarge` |
| `labelMedium` | `TextAppearance.Material3.LabelMedium` |
| `labelSmall` | `TextAppearance.Material3.LabelSmall` |
## Step 5: Migrate styles (styles.xml)
XML styles (styles.xml) system defines styles and appearance of:
1. Widgets, components, themes for windows and dialogs
2. Typography
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:
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 to composables, 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.
- **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:**
- *XML:* `<Button style="@style/MyPrimaryButton" ... />`
- *Compose:* `MyPrimaryButton(onClick = { ... })`
- **Common Attribute Groups:** If a style sets common modifiers (like padding + height), extract them into a readable extension property or a shared Modifier variable.
### Common examples
| XML | Compose |
|---|---|
| `Theme.MaterialComponents.*` | `MaterialTheme(colorScheme, typography, shapes) { }` |
| `TextAppearance.Material3.BodyMedium` | `TextStyle(...)` defined in `Typography(bodyMedium = ...)` |
| `ShapeAppearance.*.SmallComponent` | `Shapes(small = RoundedCornerShape(X.dp))` |
| `Widget.MaterialComponents.Button` | `Button(colors = ButtonDefaults.buttonColors(...))` |
| `Widget.MaterialComponents.CardView` | `Card(shape=..., elevation=..., colors=...)` |
| `Widget.*.TextInputLayout.OutlinedBox` | `OutlinedTextField(colors = OutlinedTextFieldDefaults.colors(...))` |
| `Widget.*.Chip.Filter` | `FilterChip(colors = FilterChipDefaults.filterChipColors(...))` |
| `Widget.*.Toolbar.Primary` | `TopAppBar(colors = TopAppBarDefaults.topAppBarColors(...))` |
| `Widget.*.FloatingActionButton` | `FloatingActionButton(containerColor = ...)` |
| `backgroundTint` | `containerColor` in `ComponentDefaults.ComponentColors()` |
| `android:textColor` | `contentColor` in `ComponentDefaults.ComponentColors()` |
| `cornerRadius` | `shape = RoundedCornerShape(X.dp)` |
| `android:elevation` | `elevation = ComponentDefaults.elevation(defaultElevation = X.dp)` |
| `android:padding` | `contentPadding = PaddingValues(...)` or `Modifier.padding()` |
| `android:minHeight` | `Modifier.heightIn(min = X.dp)` |
| `strokeColor` + `strokeWidth` | `border = BorderStroke(width, color)` |
| `android:textSize` | `fontSize = X.sp` in `TextStyle` |
## 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.
Verify all new Compose theme values match the existing XML values.
Don't hardcode any migrated values.
@@ -1,75 +0,0 @@
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.
When you start the migration from XML to Compose, migrate the theming to
Material 3 Compose theming.
## Glossary
| Term | Definition |
|---|---|
| `MaterialTheme` | The composable function that provides theming (colors, typography, shapes) to Compose UI components. |
| `Shape` | A Compose object used to define custom component shapes for a `MaterialTheme`. |
| `Typography` | A Compose object used to define custom text styles (font families, sizes, weights) for a `MaterialTheme`. |
| `Color` | A Compose object used to define custom color schemes for `MaterialTheme`. |
| XML Theme | The Android theming system defined in XML files, used by the View system. |
## Limitations
Before migrating, be aware of the following limitations:
- This guide focuses on migrating to Material 3 only. For migrating from alternative design systems, see [Material 2](https://developer.android.com/develop/ui/compose/designsystems/material) or [Custom design systems in Compose](https://developer.android.com/develop/ui/compose/designsystems/custom).
- The ultimate goal is a complete migration to Compose, which allows for the removal of XML theming. This guide explains how to migrate, but it doesn't explain how to finally remove XML theming.
## 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.
## Step 2: Identify theme source files
Identify and locate all XML resources and files necessary for theming:
light and dark color schemes, 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
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.
## Step 4: Migrate custom shapes and typography
- If your app uses custom shapes:
1. In your Compose code, define a `Shape` object to replicate your XML shape definitions.
2. Provide this `Shape` object to your `MaterialTheme`.
For more details, see [shapes](https://developer.android.com/develop/ui/compose/designsystems/material3#shapes).
- If your app uses custom typography:
1. In your Compose code, define a `Typography` object in your Compose code to replicate your XML text styles and font definitions.
2. Provide this `Typography` object to your `MaterialTheme`.
For more details, see [typography](https://developer.android.com/develop/ui/compose/designsystems/material3#typography).
## Step 5: 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.
Verify all new Compose theme values match the existing XML values.
Don't hardcode any migrated values.
@@ -290,16 +290,4 @@ layout.
</LinearLayout>
```
![Composable displayed within layout editor](https://developer.android.com/static/develop/ui/compose/images/layout-editor-composable-preview.png)
## Next steps
Now that you know the interoperability APIs to use Compose in Views, learn
how to use [Views in Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose).
## Recommended for you
- Note: link text is displayed when JavaScript is off
- [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations)
- [Migration strategy {:#migration-strategy}](https://developer.android.com/develop/ui/compose/migrate/strategy)
- [Compare Compose and View performance](https://developer.android.com/develop/ui/compose/ergonomics)
![Composable displayed within layout editor](https://developer.android.com/static/develop/ui/compose/images/layout-editor-composable-preview.png)
@@ -4,7 +4,7 @@ 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.
To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1))
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
@@ -187,53 +187,12 @@ fun ToastGreetingButton(greeting: String) {
<br />
For a more complete example, take a look at the [Case Study:
BroadcastReceivers](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose#case-study-broadcastreceivers) section at the end of this
document.
### Broadcast receivers
## 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:
```kotlin
class OtherInteractionsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get data from savedInstanceState
setContent {
MaterialTheme {
ExampleComposable(data, onButtonClick = {
startActivity(Intent(this, MyActivity::class.java))
})
}
}
}
}
@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
Button(onClick = onButtonClick) {
Text(data.title)
}
}
```
<br />
## Case study: Broadcast receivers
For a more realistic example of features you might want to migrate or implement
in Compose, and to showcase `CompositionLocal` and [side
effects](https://developer.android.com/develop/ui/compose/side-effects), let's say a
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.
The solution makes use of `LocalContext` to use the current context, and
a composable function, use of `LocalContext` to use the current context, and
`rememberUpdatedState` and `DisposableEffect` side effects.
@@ -281,14 +240,36 @@ fun HomeScreen() {
<br />
## Next steps
## Other interactions
Now that you know the interoperability APIs when using Compose in Views and vice
versa, explore the [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations) page to learn more.
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:
## Recommended for you
- Note: link text is displayed when JavaScript is off
- [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations)
- [Side-effects in Compose](https://developer.android.com/develop/ui/compose/side-effects)
- [Locally scoped data with CompositionLocal](https://developer.android.com/develop/ui/compose/compositionlocal)
```kotlin
class OtherInteractionsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get data from savedInstanceState
setContent {
MaterialTheme {
ExampleComposable(data, onButtonClick = {
startActivity(Intent(this, MyActivity::class.java))
})
}
}
}
}
@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
Button(onClick = onButtonClick) {
Text(data.title)
}
}
```
<br />
@@ -1,294 +0,0 @@
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.
To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#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.
```kotlin
@Composable
fun CustomView() {
var selectedItem by remember { mutableIntStateOf(0) }
// Adds view to Compose
AndroidView(
modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
factory = { context ->
// Creates view
MyView(context).apply {
// Sets up listeners for View -> Compose communication
setOnClickListener {
selectedItem = 1
}
}
},
update = { view ->
// View's been inflated or state read in this block has been updated
// Add logic here if necessary
// As selectedItem is read here, AndroidView will recompose
// whenever the state changes
// Example of Compose -> View communication
view.selectedItem = selectedItem
}
)
}
@Composable
fun ContentExample() {
Column(Modifier.fillMaxSize()) {
Text("Look at this CustomView!")
CustomView()
}
}
```
<br />
> [!NOTE]
> **Note:** Prefer to construct a View in the `AndroidView` `factory` lambda instead of using `remember` to hold a View reference outside of `AndroidView`.
## `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).
```kotlin
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
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.
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.
```kotlin
@Composable
fun AndroidViewInLazyList() {
LazyColumn {
items(100) { index ->
AndroidView(
modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
factory = { context ->
MyView(context)
},
update = { view ->
view.selectedItem = index
},
onReset = { view ->
view.clear()
}
)
}
}
}
```
<br />
## Fragments in Compose
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.
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:
```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`.
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` 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.
```kotlin
@Composable
fun ToastGreetingButton(greeting: String) {
val context = LocalContext.current
Button(onClick = {
Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
}) {
Text("Greet")
}
}
```
<br />
For a more complete example, take a look at the [Case Study:
BroadcastReceivers](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose#case-study-broadcastreceivers) section at the end of this
document.
## 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:
```kotlin
class OtherInteractionsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get data from savedInstanceState
setContent {
MaterialTheme {
ExampleComposable(data, onButtonClick = {
startActivity(Intent(this, MyActivity::class.java))
})
}
}
}
}
@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
Button(onClick = onButtonClick) {
Text(data.title)
}
}
```
<br />
## Case study: Broadcast receivers
For a more realistic example of features you might want to migrate or implement
in Compose, and to showcase `CompositionLocal` and [side
effects](https://developer.android.com/develop/ui/compose/side-effects), let's say a
[`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) needs to be registered from
a composable function.
The solution makes use of `LocalContext` to use the current context, and
`rememberUpdatedState` and `DisposableEffect` side effects.
```kotlin
@Composable
fun SystemBroadcastReceiver(
systemAction: String,
onSystemEvent: (intent: Intent?) -> Unit
) {
// Grab the current context in this part of the UI tree
val context = LocalContext.current
// Safely use the latest onSystemEvent lambda passed to the function
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
// If either context or systemAction changes, unregister and register again
DisposableEffect(context, systemAction) {
val intentFilter = IntentFilter(systemAction)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
currentOnSystemEvent(intent)
}
}
context.registerReceiver(broadcast, intentFilter)
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
}
@Composable
fun HomeScreen() {
SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
val isCharging = /* Get from batteryStatus ... */ true
/* Do something if the device is charging */
}
/* Rest of the HomeScreen */
}
```
<br />
## Next steps
Now that you know the interoperability APIs when using Compose in Views and vice
versa, explore the [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations) page to learn more.
## Recommended for you
- Note: link text is displayed when JavaScript is off
- [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations)
- [Side-effects in Compose](https://developer.android.com/develop/ui/compose/side-effects)
- [Locally scoped data with CompositionLocal](https://developer.android.com/develop/ui/compose/compositionlocal)
@@ -1,161 +0,0 @@
[`RecyclerView`](https://developer.android.com/develop/ui/views/layout/recyclerview) is a View component that makes it easy to efficiently display
large sets of data. Instead of creating views for each item in the data set,
`RecyclerView` improves the performance of your app by keeping a small pool of
views and recycling through them as you scroll through those items.
In Compose, you can use [Lazy lists](https://developer.android.com/develop/ui/compose/lists#lazy) to accomplish the same thing. This page
describes how you can migrate your `RecyclerView` implementation to use Lazy lists
in Compose.
## Migration steps
To migrate your `RecyclerView` implementation to Compose, follow these steps:
1. Comment out or remove the `RecyclerView` from your UI hierarchy and add a
`ComposeView` to replace it if none is present in the hierarchy yet. This
is the container for the Lazy list that you'll add:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- <androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/recycler_view"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent />"-->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
2. Determine what type of Lazy list composable you need based on your
`RecyclerView`'s layout manager (see table below). The composable you select
will be the top-level composable of the `ComposeView` you added in the
previous step.
| `LayoutManager` | Composable |
|---|---|
| `LinearLayoutManager` | `LazyColumn` or `LazyRow` |
| `GridLayoutManager` | `LazyVerticalGrid` or `LazyHorizontalGrid` |
| `StaggeredGridLayoutManager` | `LazyVerticalStaggeredGrid` or `LazyHorizontalStaggeredGrid` |
```kotlin
// recyclerView.layoutManager = LinearLayoutManager(context)
composeView.setContent {
LazyColumn(Modifier.fillMaxSize()) {
// We use a LazyColumn since the layout manager of the RecyclerView is a vertical LinearLayoutManager
}
}
```
<br />
3. Create a corresponding composable for each view type in your
`RecyclerView.Adapter` implementation. Each view type typically maps to a
`ViewHolder` subclass, though this may not always be the case. These
composables will be used as the UI representation for different types of
elements in your list:
```kotlin
@Composable
fun ListItem(data: MyData, modifier: Modifier = Modifier) {
Row(modifier.fillMaxWidth()) {
Text(text = data.name)
// ... other composables required for displaying `data`
}
}
```
<br />
The logic in your `RecyclerView.Adapter`'s `onCreateViewHolder()` and
`onBindViewHolder()` methods will be replaced by these composables and the
state that you provide them with. In Compose, there is no separation between
creating a composable for an item and binding data into it---these concepts are
coalesced.
4. Within the `content` slot of the Lazy list (the trailing lambda parameter),
use the `items()` function (or an equivalent overload) to iterate through the
data for your list. In the `itemContent` lambda, invoke the appropriate
composable item for your data:
```kotlin
val data = listOf<MyData>(/* ... */)
composeView.setContent {
LazyColumn(Modifier.fillMaxSize()) {
items(data) {
ListItem(it)
}
}
}
```
<br />
> [!TIP]
> **Tip:** Provide additional parameters to `items()` to optimize your list: use the `key` parameter to provide a unique key for the underlying data so that scroll position will be maintained when items change, or use the `contentType` parameter to specify a content type for the underlying data (this is a similar concept to `RecyclerView`'s view types) so you can reuse item compositions more efficiently.
## Common use cases
### Item decorations
`RecyclerView` has the concept of an `ItemDecoration`, which you can use to add a
special drawing for items in the list. For example, you can add an
`ItemDecoration` to add dividers between items:
```kotlin
val itemDecoration = DividerItemDecoration(recyclerView.context, LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(itemDecoration)
```
<br />
Compose does not have an equivalent concept of item decorations. Instead, you
can add any UI decorations in the list directly in the composition. For example,
to add dividers to the list, you can use the `Divider` composable after each
item:
```kotlin
LazyColumn(Modifier.fillMaxSize()) {
itemsIndexed(data) { index, d ->
ListItem(d)
if (index != data.size - 1) {
HorizontalDivider()
}
}
}
```
<br />
### Item animations
An `ItemAnimator` can be set on a `RecyclerView` to animate the appearance of
items as changes are made to the adapter. By default, `RecyclerView` uses
[`DefaultItemAnimator`](https://developer.android.com/reference/androidx/recyclerview/widget/DefaultItemAnimator) which provides basic animations on remove, add, and
move events.
Lazy lists have a similar concept through the `animateItemPlacement` modifier.
See [Item animations](https://developer.android.com/develop/ui/compose/lists#item-animations) to learn more.
## Additional resources
For more information about migrating a `RecyclerView` to Compose, see the
following resources:
- [Lists and Grids](https://developer.android.com/develop/ui/compose/lists#item-animations): Documentation for how to implement lists and grids in Compose.
- [Jetpack Compose Interop: Using Compose in a RecyclerView](https://medium.com/androiddevelopers/jetpack-compose-interop-using-compose-in-a-recyclerview-569c7ec7a583): Blog post for efficiently using Compose within a `RecyclerView`.
## Recommended for you
- Note: link text is displayed when JavaScript is off
- [Lists and grids](https://developer.android.com/develop/ui/compose/lists)
- [Migrate `CoordinatorLayout` to Compose](https://developer.android.com/develop/ui/compose/migrate/migration-scenarios/coordinator-layout)
- [Other considerations](https://developer.android.com/develop/ui/compose/migrate/other-considerations)
@@ -107,7 +107,7 @@ you need to your dependencies from the following block:
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2026.02.01')
def composeBom = platform('androidx.compose:compose-bom:2026.03.00')
implementation composeBom
androidTestImplementation composeBom
@@ -132,7 +132,7 @@ you need to your dependencies from the following block:
implementation 'androidx.compose.material3.adaptive:adaptive'
// Optional - Integration with activities
implementation 'androidx.activity:activity-compose:1.12.4'
implementation 'androidx.activity:activity-compose:1.13.0'
// Optional - Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0'
// Optional - Integration with LiveData
@@ -146,7 +146,7 @@ you need to your dependencies from the following block:
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.02.01")
val composeBom = platform("androidx.compose:compose-bom:2026.03.00")
implementation(composeBom)
androidTestImplementation(composeBom)
@@ -171,7 +171,7 @@ you need to your dependencies from the following block:
implementation("androidx.compose.material3.adaptive:adaptive")
// Optional - Integration with activities
implementation("androidx.activity:activity-compose:1.12.4")
implementation("androidx.activity:activity-compose:1.13.0")
// Optional - Integration with ViewModels
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
// Optional - Integration with LiveData
@@ -0,0 +1,31 @@
### 1. Analysis scope
**Action:** Use find files and examine all XML layout files within the project (typically located in `res`directories). For each file, parse the view hierarchy and metadata.
### 2. Selection criteria
Prioritize layouts that meet the following criteria:
- **Hierarchy depth:** Target **leaf nodes** or components at the bottom of the UI tree.
- **Complexity:** Select layouts with the **smallest number of nested children** and minimal logic.
- **State management:** Prioritize **stateless** components or those with the fewest UI state variables.
- **Dependency footprint:** Identify layouts with **zero to minimal external UI dependencies**.
- **Isolation:** Focus on **self-contained** components that do not rely heavily on parent context or complex data binding.
### 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).
*** ** * ** ***
## 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.
**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.
@@ -0,0 +1,74 @@
## 1. Structural analysis \& mapping
**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`).
\* The appropriate state management strategy for interactive elements.
## 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.
## 3. Theming \& design system integrity
**Do not use hard-coded values.** Follow these rules for styling:
\* **Token Alignment:** Cross-reference XML dimension, color, and style attributes with the existing Compose `Theme` (e.g., `MaterialTheme.colorScheme` or custom design system tokens).
\* **Reuse over Creation:** If matching values exist in the current Compose theme, reuse them. If a value is missing but required for the design, define it within the theme structure rather than hard-coding it in the Composable.
\* **Project Consistency:** You **MUST** strictly adhere to existing code conventions, naming standards, and implementation patterns found in the project. **Prioritize** project-specific reusable components over generic Material defaults.
## 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:
\* **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.
\* **Feature parity \& restriction:** Ensure the new composable enforces the same UI constraints as the original XML component, preventing unauthorized style overrides while maintaining the intended flexibility.
Example before migration:
`xml
<style
name="Widget.Rounded.Button"
parent="Widget.Button.Borderless">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:minWidth">@dimen/min_width</item>
<item name="android:paddingStart">@dimen/padding_2</item>
<item name="android:paddingEnd">@dimen/padding_2</item>
<item name="cornerRadius">8dp</item>
</style>`
Example after migration:
## ```kotlin
@Composable
fun RoundedBorderlessButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
TextButton(
onClick, modifier
.defaultMinSize(minWidth = dimensionResource(R.dimen.min_width))
.padding(
start = dimensionResource(R.dimen.padding_2),
end = dimensionResource(R.dimen.padding_2)
), enabled, shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Medium
)
)
}
}
```
## 5. Output requirements
- Provide the full Kotlin file content.
- Include necessary imports.
- Add documentation comments (`/** ... */`) explaining the mapping logic for complex transformations.
+109
View File
@@ -0,0 +1,109 @@
---
name: navigation-3
description: Learn how to install and migrate to Jetpack Navigation 3, and how to
implement features and patterns such as deep links, multiple backstacks, scenes
(dialogs, bottom sheets, list-detail, two-pane, supporting pane), conditional navigation
(such as logged-in navigation vs anonymous), returning results from flows, integration
with Hilt, ViewModel, Kotlin, and view interoperability.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- recipe
- Android
- Navigation 2
- Navigation 3
- migration
- Compose
- guide
- dependencies
- NavKey
- NavHost
- NavDisplay
- BottomSheet
- list-detail
- scenes
- two-pane
- supporting pane
- multiple backstacks
- dialog
- Hilt
- ViewModel
- View interop.
---
## Migration guide
- *[Navigation 2 to Navigation 3 migration guide](references/android/guide/navigation/navigation-3/migration-guide.md)*: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements.
### Requirements
- *[Guide: Migrate to type-safe navigation in Compose](https://developer.android.com/guide/navigation/type-safe-destinations)* : Step-by-step guide to migrating an Android application from string-based navigation to **Type-Safe Navigation** in Jetpack Compose using Jetpack Navigation 2.
## Developer documentation
- \*[Navigation 3](references/android/guide/navigation/navigation-3/index.md). Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations.
## Recipes
Code examples showcasing common patterns.
### Basic API usage
- *[Basic](references/android/guide/navigation/navigation-3/recipes/basic.md)*: Shows most basic API usage.
- *[Saveable back stack](references/android/guide/navigation/navigation-3/recipes/basicsaveable.md)*: Shows basic API usage with a persistent back stack.
- *[Entry provider DSL](references/android/guide/navigation/navigation-3/recipes/basicdsl.md)*: Shows basic API usage using the entryProvider DSL.
### Common UI
- *[Common UI](references/android/guide/navigation/navigation-3/recipes/common-ui.md)*: Demonstrates how to implement a common navigation UI pattern with a bottom navigation bar and multiple back stacks, where each tab in the navigation bar has its own navigation history.
### 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.
### Scenes
#### Use built-in Scenes
- *[Dialog](references/android/guide/navigation/navigation-3/recipes/dialog.md)*: Shows how to create a Dialog.
#### Create custom Scenes
- *[BottomSheet](references/android/guide/navigation/navigation-3/recipes/bottomsheet.md)*: Shows how to create a BottomSheet destination.
- *[List-Detail Scene](references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md)*: Demonstrates how to implement adaptive list-detail layouts using the Navigation 3 Scenes API.
- *[Two pane Scene](references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md)*: Demonstrates how to implement adaptive two-pane layouts using the Navigation 3 Scenes API.
### Material Adaptive
- *[Material List-Detail](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md)*: Demonstrates how to implement an adaptive list-detail layout using Material 3 Adaptive.
- *[Material Supporting Pane](references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md)*: Demonstrates how to implement an adaptive supporting pane layout using Material 3 Adaptive.
### 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.
### Common back stack behavior
- *[Multiple back stacks](references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md)*: Shows how to create multiple top level routes, each with its own back stack. Top level routes are displayed in a navigation bar allowing users to switch between them. State is retained for each top level route, and the navigation state persists config changes and process death.
### Conditional navigation
- *[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.
### 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.
- *[Modularized navigation code (Koin)](references/android/guide/navigation/navigation-3/recipes/modular-koin.md)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI.
### Working with ViewModel
#### Passing navigation arguments
- *[Basic ViewModel](references/android/guide/navigation/navigation-3/recipes/passingarguments.md)* : Navigation arguments are passed to a `ViewModel` constructed using `viewModel()`
### Returning results
- *[Returning Results as Events](references/android/guide/navigation/navigation-3/recipes/results-event.md)* : Returning results as events to content in another `NavEntry`
- *[Returning Results as State](references/android/guide/navigation/navigation-3/recipes/results-state.md)* : Returning results as state stored in a `CompositionLocal`
@@ -472,7 +472,7 @@ Replace `NavHost` with `NavDisplay`.
- Delete `NavHost` and replace it with `NavDisplay`.
- Specify `entries = navigationState.toEntries(entryProvider)` as a parameter. This converts the navigation state into the entries that `NavDisplay` shows using the `entryProvider`.
- Connect `NavDisplay.onBack` to `navigator.goBack()`. This causes `navigator` to update the navigation state when `NavDisplay`'s built-in back handler completes.
- If you have dialog destinations, add `DialogSceneStrategy` to `NavDisplay`'s `sceneStrategy` parameter.
- If you have dialog destinations, add `DialogSceneStrategy` to `NavDisplay`'s `sceneStrategies` parameter.
For example:
@@ -481,7 +481,7 @@ For example:
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
## Step 7: Remove Navigation 2 dependencies
@@ -31,6 +31,7 @@ import androidx.compose.material3.Text
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.metadata
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.content.ContentGreen
@@ -78,26 +79,32 @@ class AnimatedActivity : ComponentActivity() {
}
}
entry<ScreenC>(
metadata = NavDisplay.transitionSpec {
metadata = metadata {
// Slide new content up, keeping the old content in place underneath
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(1000)
) togetherWith ExitTransition.KeepUntilTransitionsFinished
} + NavDisplay.popTransitionSpec {
put(NavDisplay.TransitionKey) {
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(1000)
) togetherWith ExitTransition.KeepUntilTransitionsFinished
}
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
} + NavDisplay.predictivePopTransitionSpec {
put(NavDisplay.PopTransitionKey) {
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
}
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
put(NavDisplay.PredictivePopTransitionKey) {
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
}
}
) {
ContentGreen("This is Screen C")
@@ -76,7 +76,7 @@ class BottomSheetActivity : ComponentActivity() {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = bottomSheetStrategy,
sceneStrategies = listOf(bottomSheetStrategy),
entryProvider = entryProvider {
entry<RouteA> {
ContentGreen("Welcome to Nav3") {
@@ -111,15 +111,22 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.rememberLifecycleOwner
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavMetadataKey
import androidx.navigation3.runtime.get
import androidx.navigation3.runtime.metadata
import androidx.navigation3.scene.OverlayScene
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SceneStrategy
import androidx.navigation3.scene.SceneStrategyScope
import com.example.nav3recipes.bottomsheet.BottomSheetSceneStrategy.Companion.bottomSheet
/** An [OverlayScene] that renders an [entry] within a [ModalBottomSheet]. */
@OptIn(ExperimentalMaterial3Api::class)
internal class BottomSheetScene<T : Any>(
internal data class BottomSheetScene<T : Any>(
override val key: T,
override val previousEntries: List<NavEntry<T>>,
override val overlaidEntries: List<NavEntry<T>>,
@@ -131,11 +138,14 @@ internal class BottomSheetScene<T : Any>(
override val entries: List<NavEntry<T>> = listOf(entry)
override val content: @Composable (() -> Unit) = {
val lifecycleOwner = rememberLifecycleOwner()
ModalBottomSheet(
onDismissRequest = onBack,
properties = modalBottomSheetProperties,
) {
entry.Content()
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
entry.Content()
}
}
}
}
@@ -150,9 +160,9 @@ internal class BottomSheetScene<T : Any>(
class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> {
override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {
val lastEntry = entries.lastOrNull()
val bottomSheetProperties = lastEntry?.metadata?.get(BOTTOM_SHEET_KEY) as? ModalBottomSheetProperties
return bottomSheetProperties?.let { properties ->
val lastEntry = entries.lastOrNull() ?: return null
val bottomSheetProperties = lastEntry.metadata[BottomSheetKey] ?: return null
return bottomSheetProperties.let { properties ->
@Suppress("UNCHECKED_CAST")
BottomSheetScene(
key = lastEntry.contentKey as T,
@@ -173,12 +183,13 @@ class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> {
* @param modalBottomSheetProperties properties that should be passed to the containing
* [ModalBottomSheet].
*/
@OptIn(ExperimentalMaterial3Api::class)
fun bottomSheet(
modalBottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()
): Map<String, Any> = mapOf(BOTTOM_SHEET_KEY to modalBottomSheetProperties)
fun bottomSheet(modalBottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()) =
metadata {
put(BottomSheetKey, modalBottomSheetProperties)
}
internal const val BOTTOM_SHEET_KEY = "bottomsheet"
object BottomSheetKey : NavMetadataKey<ModalBottomSheetProperties>
}
}
```
@@ -134,7 +134,7 @@ class ConditionalActivity : ComponentActivity() {
}
}
}
entry<Profile> {
< > entryProfile {
ContentBlue("Profile screen (only accessible once logged in)") {
Button(onClick = dropUnlessResumed {
isLoggedIn = false
@@ -143,12 +143,12 @@ class ConditionalActivity : ComponentActivity() {
Text("Logout")
}
}
}
entry<Login> { key ->
< > >}
entryLogin { key -
ContentYellow("Login screen. Logged in? $isLoggedIn") {
Button(onClick = dropUnlessResumed {
isLoggedIn = true
key.redirectToKey?.let { targetKey ->
> key.redirectToKey?.let { targetKey -
backStack.remove(key)
navigator.navigate(targetKey)
}
@@ -165,15 +165,13 @@ class ConditionalActivity : ComponentActivity() {
// An overload of `rememberNavBackStack` that returns a subtype of `NavKey`.
// See https://issuetracker.google.com/issues/463382671 for a discussion of this function
// See https://issuetracker.google.com/issues/<463382671 >for a discussion of this function
@Composable
fun <T : NavKey> rememberNavBackStack(vararg elements: T): NavBackStack<T> {
fun T : N<a>vKey rememberNavBackStack(vararg elements: T): NavBackStackT {
return rememberSerializable(
serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer())
) {
NavBackStack(*elements)
serializer = NavBackStackSerializer(elementSerializer = NavKeyShttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/conditional/ConditionalActivity.kt NavBackStack(*elements)
}
}
}ConditionalActivity.kt
```
```
@@ -210,12 +208,12 @@ import androidx.navigation3.runtime.NavBackStack
* @property isLoggedIn A lambda that returns whether the user is logged in.
*/
class Navigator(
private val backStack: NavBackStack<ConditionalNavKey>,
private val onNavigateToRestrictedKey: (targetKey: ConditionalNavKey?) -> ConditionalNavKey,
private val isLoggedIn: () -> Boolean,
private val< backStack: NavBa>ckStackConditionalNavKey,
private val onNavigateToRestrictedKey: (targetKe>y: ConditionalNavKey?) - ConditionalNavKey,
priv>ate val isLoggedIn: () - Boolean,
) {
fun navigate(key: ConditionalNavKey) {
if (key.requiresLogin && !isLoggedIn()) {
&& if (key.requiresLogin !isLoggedIn()) {
val loginKey = onNavigateToRestrictedKey(key)
backStack.add(loginKey)
} else {
@@ -223,6 +221,6 @@ class Navigator(
}
}
fun goBack() = backStack.removeLastOrNull()
}
fun goBack() = backSthttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/conditional/Navigator.ktstOrNull()
}Navigator.kt
```
@@ -40,12 +40,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.compose.dropUnlessResumed
import com.example.nav3recipes.deeplink.common.EntryScreen
import com.example.nav3recipes.deeplink.common.LIST_FIRST_NAMES
import com.example.nav3recipes.deeplink.common.LIST_LOCATIONS
import com.example.nav3recipes.deeplink.common.MenuDropDown
import com.example.nav3recipes.deeplink.common.PaddedButton
import com.example.nav3recipes.deeplink.common.TextContent
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.LIST_FIRST_NAMES
import com.example.nav3recipes.common.deeplink.LIST_LOCATIONS
import com.example.nav3recipes.common.deeplink.MenuDropDown
import com.example.nav3recipes.common.deeplink.PaddedButton
import com.example.nav3recipes.common.deeplink.TextContent
internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com"
@@ -86,13 +86,13 @@ import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern
import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest
import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult
import com.example.nav3recipes.deeplink.basic.util.KeyDecoder
import com.example.nav3recipes.deeplink.common.TextContent
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.common.EntryScreen
import com.example.nav3recipes.deeplink.common.FriendsList
import com.example.nav3recipes.deeplink.common.LIST_USERS
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.common.deeplink.FriendsList
import com.example.nav3recipes.common.deeplink.LIST_USERS
/**
* Parses a target deeplink into a NavKey. There are several crucial steps involved:
@@ -211,24 +211,24 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.compose.dropUnlessResumed
import com.example.nav3recipes.deeplink.common.PaddedButton
import com.example.nav3recipes.deeplink.common.EMPTY
import com.example.nav3recipes.deeplink.common.EntryScreen
import com.example.nav3recipes.deeplink.common.FIRST_NAME_JOHN
import com.example.nav3recipes.deeplink.common.FIRST_NAME_JULIE
import com.example.nav3recipes.deeplink.common.FIRST_NAME_MARY
import com.example.nav3recipes.deeplink.common.FIRST_NAME_TOM
import com.example.nav3recipes.deeplink.common.LOCATION_BC
import com.example.nav3recipes.deeplink.common.LOCATION_BR
import com.example.nav3recipes.deeplink.common.LOCATION_CA
import com.example.nav3recipes.deeplink.common.LOCATION_US
import com.example.nav3recipes.deeplink.common.MenuDropDown
import com.example.nav3recipes.deeplink.common.MenuTextInput
import com.example.nav3recipes.common.deeplink.PaddedButton
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.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.deeplink.common.TextContent
import com.example.nav3recipes.common.deeplink.TextContent
/**
* This activity allows the user to create a deep link and make a request with it.
@@ -389,7 +389,6 @@ private val MENU_OPTIONS_SEARCH = mapOf(
private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name)
```
```
@@ -76,7 +76,7 @@ class DialogActivity : ComponentActivity() {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = dialogStrategy,
sceneStrategies = listOf(dialogStrategy),
entryProvider = entryProvider {
entry<RouteA> {
ContentGreen("Welcome to Nav3") {
@@ -98,7 +98,7 @@ class MaterialListDetailActivity : ComponentActivity() {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = listDetailStrategy,
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider {
entry<ConversationList>(
metadata = ListDetailSceneStrategy.listPane(
@@ -106,7 +106,7 @@ class MaterialSupportingPaneActivity : ComponentActivity() {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = supportingPaneStrategy,
sceneStrategies = listOf(supportingPaneStrategy),
entryProvider = entryProvider {
entry<MainVideo>(
metadata = SupportingPaneSceneStrategy.mainPane()
@@ -38,8 +38,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel() {
var person by mutableStateOf<Person?>(null)
}
var pers<on by m>utableSthttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/HomeViewModel.ktl)
}HomeViewModel.kt
```
```
@@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable
data object Home : NavKey
@Serializable
class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/f4e6ca737c03896895a509a3dbc41cb9bf747ad5/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt
class Personhttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.ktm : NavKeyNavKeys.kt
```
```
@@ -90,7 +90,11 @@ class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/f4e
package com.example.nav3recipes.results.common
data class Person(val name: String, val favoriteColor: String)
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Person(val name: String, val favoriteColor: ParcelablePerson.kt
```
```
@@ -127,8 +131,8 @@ import com.example.nav3recipes.content.ContentGreen
@Composable
fun HomeScreen(
person: Person?,
onNext: () -> Unit
person: Perso>n?,
onNext: () - Unit
) {
ContentBlue("Hello ${person?.name ?: "unknown person"}") {
@@ -143,9 +147,9 @@ fun HomeScreen(
}
}
@Composable
@>Composable
fun PersonDetailsScreen(
onSubmit: (Person) -> Unit
onSubmit: (Person) - Unit
) {
ContentGreen("About you") {
@@ -167,15 +171,15 @@ fun PersonDetailsScreen(
name = nameTextState.text.toString(),
favoriteColor = favoriteColorTextState.text.toString()
)
onSubmit(person)
&& onSubmit(person)
},
enabled = nameTextState.text.isNotBlank() &&
favoriteColorTextState.text.isNotBlank()
enabled = nameTextState.text.isNotBlank()
favorithttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/ScreenContent.kttext.isNotBlank()
) {
Text("Submit")
}
}
}
}ScreenContent.kt
```
```
@@ -210,18 +214,16 @@ import androidx.compose.runtime.LaunchedEffect
* @param resultKey the key that should be associated with this effect
* @param onResult the callback to invoke when a result is received
*/
@Composable
inline fun <reified T> ResultEffect(
@Co<mposable<>/span>
inline fun reified T ResultEffect(
resultEventBus: ResultEventBus = LocalResultEventBus.current,
resultKey: String = T::class.toString(),
crossinline onResult: suspend (T) -> Unit
crossinline onR>esult: suspend (T) - Unit
) {
LaunchedEffect(resultKey, resultEventBus.channelMap[resultKey]) {
resultEventBus.getResultFlow<T>(resultKey)?.collect { result ->
onResult.invoke(result as T)
}
}
}
resultEv<e>ntBus.getResultFlowT(resultKey)>?.collect { result -
onResult.invoke(result as https://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/event/ResultEffect.kt }
}ResultEffect.kt
```
```
@@ -270,7 +272,7 @@ class ResultEventActivity : ComponentActivity() {
setContent {
val resultBus = remember { ResultEventBus() }
Scaffold { paddingValues ->
Scaffo>ld { paddingValues -
val backStack = rememberNavBackStack(Home)
@@ -279,9 +281,9 @@ class ResultEventActivity : ComponentActivity() {
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Home> {
val viewModel = viewModel<HomeViewModel>(key = Home.toString())
ResultEffect<Person>(resultBus) { person ->
< > entryHome {
val v<iewModel = vi>ewModelHomeViewModel(key = Home.toString())
< > ResultEffectPerson(r>esultBus) { person -
viewModel.person = person
}
@@ -291,20 +293,17 @@ class ResultEventActivity : ComponentActivity() {
onNext = { backStack.add(PersonDetailsForm()) }
)
}
entry<PersonDetailsForm> {
< en>tryPersonDetailsForm {
PersonDetailsScreen(
onSubmit = { person ->
resultBus.sendResult<Person>(result = person)
o>nSubmit = { person -
<result>Bus.sendResultPerson(result = person)
backStack.removeLastOrNull()
}
)
}
}
)
}
}
}
}
sultEventActivity.kt
```
```
@@ -340,7 +339,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
* Local for receiving results in a [ResultEventBus]
*/
object LocalResultEventBus {
private val LocalResultEventBus: ProvidableCompositionLocal<ResultEventBus?> =
private val LocalResultEventBus: Provid<ableComposition>LocalResultEventBus? =
compositionLocalOf { null }
/**
@@ -354,8 +353,8 @@ object LocalResultEventBus {
* Provides a [ResultEventBus] to the composition
*/
infix fun provides(
bus: ResultEventBus
): ProvidedValue<ResultEventBus?> {
bus: Resul<tEventBus
)>: ProvidedValueResultEventBus? {
return LocalResultEventBus.provides(bus)
}
}
@@ -368,18 +367,18 @@ class ResultEventBus {
/**
* Map from the result key to a channel of results.
*/
val channelMap = mutableStateMapOf<String, Channel<Any?>>()
val <channelMap = mu<tabl>>eStateMapOfString, ChannelAny?()
/**
* Provides a flow for the given resultKey.
*/
inline fun <reified T> getResultFlow(resultKey: String = T::class.toString()) =
* Provides a flow for the given resu<ltKey.
> */
inline fun reified T getResultFlow(resultKey: String = T::class.toString()) =
channelMap[resultKey]?.receiveAsFlow()
/**
* Sends a result into the channel associated with the given resultKey.
*/
inline fun <reified T> sendResult(resultKey: String = T::class.toString(), result: T) {
* Sends a result into the channel associated with the given resu<ltKey.
> */
inline fun reified T sendResult(resultKey: String = T::class.toString(), result: T) {
if (!channelMap.contains(resultKey)) {
channelMap[resultKey] = Channel(capacity = BUFFERED, onBufferOverflow = BufferOverflow.SUSPEND)
}
@@ -387,10 +386,10 @@ class ResultEventBus {
}
/**
* Removes all results associated with the given key from the store.
*/
inline fun <reified T> removeResult(resultKey: String = T::class.toString()) {
channelMap.remove(resultKey)
* Removes all results associated with the given key from the <store.
> */
inline fun reified T removeResult(resultKey: String = T::class.toString()) {
channehttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/event/ResultEventBus.kttKey)
}
}
}ResultEventBus.kt
```
@@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable
data object Home : NavKey
@Serializable
class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/f4e6ca737c03896895a509a3dbc41cb9bf747ad5/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt
class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt
```
```
@@ -90,7 +90,11 @@ class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/f4e
package com.example.nav3recipes.results.common
data class Person(val name: String, val favoriteColor: String)
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/Person.kt
```
```
@@ -202,10 +206,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
@@ -231,14 +232,14 @@ class ResultStateActivity : ComponentActivity() {
modifier = Modifier.padding(paddingValues),
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Home>{
entry<Home> {
val person = resultStore.getResultState<Person?>()
HomeScreen(
person = person,
onNext = { backStack.add(PersonDetailsForm()) }
)
}
entry<PersonDetailsForm>{
entry<PersonDetailsForm> {
PersonDetailsScreen(
onSubmit = { person ->
resultStore.setResult<Person>(result = person)
@@ -281,6 +282,7 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
/**
@@ -311,7 +313,7 @@ object LocalResultStore {
* Provides a [ResultStore] that will be remembered across configuration changes.
*/
@Composable
fun rememberResultStore() : ResultStore {
fun rememberResultStore(): ResultStore {
return rememberSaveable(saver = ResultStoreSaver()) {
ResultStore()
}
@@ -350,10 +352,18 @@ class ResultStore {
}
}
/** Saver to save and restore the NavController across config change and process death. */
/** Saver to save and restore the ResultStore across config change and process death. */
private fun ResultStoreSaver(): Saver<ResultStore, *> =
Saver(
save = { it.resultStateMap },
restore = { ResultStore().apply { resultStateMap.putAll(it) } },
mapSaver(
save = { resultStore ->
resultStore.resultStateMap.mapValues { it.value.value }
},
restore = { restoredMap ->
ResultStore().apply {
restoredMap.forEach { (key, value) ->
resultStateMap[key] = mutableStateOf(value)
}
}
}
)
```
@@ -46,19 +46,20 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavMetadataKey
import androidx.navigation3.runtime.contains
import androidx.navigation3.runtime.metadata
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SceneStrategy
import androidx.navigation3.scene.SceneStrategyScope
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import com.example.nav3recipes.scenes.listdetail.ListDetailScene.Companion.DETAIL_KEY
import com.example.nav3recipes.scenes.listdetail.ListDetailScene.Companion.LIST_KEY
/**
* A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
*
*/
class ListDetailScene<T : Any>(
data class ListDetailScene<T : Any>(
override val key: Any,
override val previousEntries: List<NavEntry<T>>,
val listEntry: NavEntry<T>,
@@ -72,7 +73,7 @@ class ListDetailScene<T : Any>(
}
// Let the detail entry know not to display a back button.
CompositionLocalProvider(LocalBackButtonVisibility provides false){
CompositionLocalProvider(LocalBackButtonVisibility provides false) {
Column(modifier = Modifier.weight(0.6f)) {
AnimatedContent(
targetState = detailEntry,
@@ -92,21 +93,25 @@ class ListDetailScene<T : Any>(
}
companion object {
internal const val LIST_KEY = "ListDetailScene-List"
internal const val DETAIL_KEY = "ListDetailScene-Detail"
/**
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
* in the list pane of a [ListDetailScene].
*/
fun listPane() = mapOf(LIST_KEY to true)
fun listPane() = metadata {
put(ListKey, true)
}
/**
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
* in the detail pane of a the [ListDetailScene].
*/
fun detailPane() = mapOf(DETAIL_KEY to true)
fun detailPane() = metadata {
put(DetailKey, true)
}
}
object ListKey : NavMetadataKey<Boolean>
object DetailKey : NavMetadataKey<Boolean>
}
/**
@@ -114,7 +119,7 @@ class ListDetailScene<T : Any>(
* a back button. Default is `true`. It is set to `false` for a detail `NavEntry` when being
* displayed in a `ListDetailScene`.
*/
val LocalBackButtonVisibility = compositionLocalOf{ true }
val LocalBackButtonVisibility = compositionLocalOf { true }
@Composable
fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> {
@@ -145,8 +150,10 @@ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : S
}
val detailEntry =
entries.lastOrNull()?.takeIf { it.metadata.containsKey(DETAIL_KEY) } ?: return null
val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: return null
entries.lastOrNull()?.takeIf { it.metadata.contains(ListDetailScene.DetailKey) }
?: return null
val listEntry =
entries.findLast { it.metadata.contains(ListDetailScene.ListKey) } ?: return null
// We use the list's contentKey to uniquely identify the scene.
// This allows the detail panes to be animated in and out by the scene, rather than
@@ -161,7 +168,6 @@ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : S
)
}
}
```
```
@@ -187,6 +193,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
@@ -218,7 +225,8 @@ data class ConversationDetail(
val colorId: Int
) : NavKey
@Serializable data object Profile : NavKey
@Serializable
data object Profile : NavKey
class ListDetailActivity : ComponentActivity() {
@@ -234,35 +242,38 @@ class ListDetailActivity : ComponentActivity() {
val backStack = rememberNavBackStack(ConversationList)
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = listDetailStrategy,
modifier = Modifier.padding(paddingValues),
entryProvider = entryProvider {
entry<ConversationList>(
metadata = ListDetailScene.listPane()
) {
ConversationListScreen(
onConversationClicked = { detailRoute ->
backStack.addDetail(detailRoute)
}
)
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategies = listOf(listDetailStrategy),
sharedTransitionScope = this,
modifier = Modifier.padding(paddingValues),
entryProvider = entryProvider {
entry<ConversationList>(
metadata = ListDetailScene.listPane()
) {
ConversationListScreen(
onConversationClicked = { detailRoute ->
backStack.addDetail(detailRoute)
}
)
}
entry<ConversationDetail>(
metadata = ListDetailScene.detailPane()
) { conversationDetail ->
ConversationDetailScreen(
conversationDetail = conversationDetail,
onBack = { backStack.removeLastOrNull() },
onProfileClicked = { backStack.add(Profile) }
)
}
entry<Profile> {
ProfileScreen()
}
}
entry<ConversationDetail>(
metadata = ListDetailScene.detailPane()
) { conversationDetail ->
ConversationDetailScreen(
conversationDetail = conversationDetail,
onBack = { backStack.removeLastOrNull() },
onProfileClicked = { backStack.add(Profile) }
)
}
entry<Profile> {
ProfileScreen()
}
}
)
)
}
}
}
}
@@ -300,23 +311,23 @@ package com.example.nav3recipes.scenes.listdetail
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -329,7 +340,9 @@ fun ConversationListScreen(
onConversationClicked: (ConversationDetail) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
) {
items(10) { index ->
val conversationId = index + 1
@@ -21,18 +21,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavMetadataKey
import androidx.navigation3.runtime.contains
import androidx.navigation3.runtime.metadata
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SceneStrategy
import androidx.navigation3.scene.SceneStrategyScope
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
// --- TwoPaneScene ---
/**
* A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split.
*/
class TwoPaneScene<T : Any>(
data class TwoPaneScene<T : Any>(
override val key: Any,
override val previousEntries: List<NavEntry<T>>,
val firstEntry: NavEntry<T>,
@@ -51,20 +53,23 @@ class TwoPaneScene<T : Any>(
}
companion object {
internal const val TWO_PANE_KEY = "TwoPane"
/**
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
* in a two-pane layout.
*/
fun twoPane() = mapOf(TWO_PANE_KEY to true)
fun twoPane() = metadata {
put(TwoPaneKey, true)
}
}
object TwoPaneKey : NavMetadataKey<Boolean>
}
@Composable
fun <T: Any> rememberTwoPaneSceneStrategy() : TwoPaneSceneStrategy<T> {
fun <T : Any> rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy<T> {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(windowSizeClass){
return remember(windowSizeClass) {
TwoPaneSceneStrategy(windowSizeClass)
}
}
@@ -90,7 +95,7 @@ class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : Scen
// Condition 2: Only return a Scene if there are two entries, and both have declared
// they can be displayed in a two pane scene.
return if (lastTwoEntries.size == 2
&& lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) }
&& lastTwoEntries.all { it.metadata.contains(TwoPaneScene.TwoPaneKey) }
) {
val firstEntry = lastTwoEntries.first()
val secondEntry = lastTwoEntries.last()
@@ -118,7 +123,6 @@ class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : Scen
}
```
```
@@ -143,6 +147,8 @@ package com.example.nav3recipes.scenes.twopane
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
@@ -181,46 +187,49 @@ class TwoPaneActivity : ComponentActivity() {
val backStack = rememberNavBackStack(Home)
val twoPaneStrategy = rememberTwoPaneSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = twoPaneStrategy,
entryProvider = entryProvider {
entry<Home>(
metadata = TwoPaneScene.twoPane()
) {
ContentRed("Welcome to Nav3") {
Button(onClick = { backStack.addProductRoute(1) }) {
Text("View the first product")
}
}
}
entry<Product>(
metadata = TwoPaneScene.twoPane()
) { product ->
ContentBase(
"Product ${product.id} ",
Modifier.background(colors[product.id % colors.size])
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategies = listOf(twoPaneStrategy),
sharedTransitionScope = this,
entryProvider = entryProvider {
entry<Home>(
metadata = TwoPaneScene.twoPane()
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = dropUnlessResumed {
backStack.addProductRoute(product.id + 1)
}) {
Text("View the next product")
}
Button(onClick = dropUnlessResumed {
backStack.add(Profile)
}) {
Text("View profile")
ContentRed("Welcome to Nav3") {
Button(onClick = { backStack.addProductRoute(1) }) {
Text("View the first product")
}
}
}
entry<Product>(
metadata = TwoPaneScene.twoPane()
) { product ->
ContentBase(
"Product ${product.id} ",
Modifier.background(colors[product.id % colors.size])
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = dropUnlessResumed {
backStack.addProductRoute(product.id + 1)
}) {
Text("View the next product")
}
Button(onClick = dropUnlessResumed {
backStack.add(Profile)
}) {
Text("View profile")
}
}
}
}
entry<Profile> {
ContentGreen("Profile (single pane only)")
}
}
entry<Profile> {
ContentGreen("Profile (single pane only)")
}
}
)
)
}
}
}
}
@@ -1,106 +0,0 @@
---
name: jetpack-navigation-3-skill
description: Learn how to install and migrate to Jetpack Navigation 3, and how to implement features and patterns such as deep links, multiple backstacks, scenes (dialogs, bottom sheets, list-detail, two-pane, supporting pane), conditional navigation (such as logged-in navigation vs anonymous), returning results from flows, integration with Hilt, ViewModel, Kotlin, and view interoperability.
metadata:
keywords:
- skill
- recipe
- Android
- Navigation 2
- Navigation 3
- migration
- Compose
- guide
- dependencies
- NavKey
- NavHost
- NavDisplay
- BottomSheet
- list-detail
- scenes
- two-pane
- supporting pane
- multiple backstacks
- dialog
- Hilt
- ViewModel
- View interop.
---
# Jetpack Navigation 3 Skill
## Migration guide
- *[Navigation 2 to Navigation 3 migration guide](references/android/guide/navigation/navigation-3/migration-guide.md.txt)*: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements.
### Requirements
- *[Guide: Migrate to type-safe navigation in Compose](references/android/guide/navigation/type-safe-destinations.md.txt)* : Step-by-step guide to migrating an Android application from string-based navigation to **Type-Safe Navigation** in Jetpack Compose using Jetpack Navigation 2.
## Developer documentation
- \*[Navigation 3](references/android/guide/navigation/navigation-3/index.md.txt). Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations.
## Recipes
Code examples showcasing common patterns.
### Basic API usage
- *[Basic](references/android/guide/navigation/navigation-3/recipes/basic.md.txt)*: Shows most basic API usage.
- *[Saveable back stack](references/android/guide/navigation/navigation-3/recipes/basicsaveable.md.txt)*: Shows basic API usage with a persistent back stack.
- *[Entry provider DSL](references/android/guide/navigation/navigation-3/recipes/basicdsl.md.txt)*: Shows basic API usage using the entryProvider DSL.
### Common UI
- *[Common UI](references/android/guide/navigation/navigation-3/recipes/common-ui.md.txt)*: Demonstrates how to implement a common navigation UI pattern with a bottom navigation bar and multiple back stacks, where each tab in the navigation bar has its own navigation history.
### Deep links
- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md.txt)*: 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.txt)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior.
### Scenes
#### Use built-in Scenes
- *[Dialog](references/android/guide/navigation/navigation-3/recipes/dialog.md.txt)*: Shows how to create a Dialog.
#### Create custom Scenes
- *[BottomSheet](references/android/guide/navigation/navigation-3/recipes/bottomsheet.md.txt)*: Shows how to create a BottomSheet destination.
- *[List-Detail Scene](references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md.txt)*: Demonstrates how to implement adaptive list-detail layouts using the Navigation 3 Scenes API.
- *[Two pane Scene](references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md.txt)*: Demonstrates how to implement adaptive two-pane layouts using the Navigation 3 Scenes API.
### Material Adaptive
- *[Material List-Detail](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md.txt)*: Demonstrates how to implement an adaptive list-detail layout using Material 3 Adaptive.
- *[Material Supporting Pane](references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md.txt)*: Demonstrates how to implement an adaptive supporting pane layout using Material 3 Adaptive.
### Animations
- *[Animations](references/android/guide/navigation/navigation-3/recipes/animations.md.txt)*: Shows how to override the default animations for all destinations and a single destination.
### Common back stack behavior
- *[Multiple back stacks](references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md.txt)*: Shows how to create multiple top level routes, each with its own back stack. Top level routes are displayed in a navigation bar allowing users to switch between them. State is retained for each top level route, and the navigation state persists config changes and process death.
### Conditional navigation
- *[Conditional navigation](references/android/guide/navigation/navigation-3/recipes/conditional.md.txt)*: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding.
### Architecture
- *[Modularized navigation code (Hilt)](references/android/guide/navigation/navigation-3/recipes/modular-hilt.md.txt)*: Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI.
- *[Modularized navigation code (Koin)](references/android/guide/navigation/navigation-3/recipes/modular-koin.md.txt)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI.
### Working with ViewModel
#### Passing navigation arguments
- *[Basic ViewModel](references/android/guide/navigation/navigation-3/recipes/passingarguments.md.txt)* : Navigation arguments are passed to a `ViewModel` constructed using `viewModel()`
### Returning results
- *[Returning Results as Events](references/android/guide/navigation/navigation-3/recipes/results-event.md.txt)* : Returning results as events to content in another `NavEntry`
- *[Returning Results as State](references/android/guide/navigation/navigation-3/recipes/results-state.md.txt)* : Returning results as state stored in a `CompositionLocal`
@@ -1,129 +0,0 @@
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:
1. **Navigation version**: Update to Jetpack Navigation 2.8.0 or higher
2. **Kotlin serialization plugin**:
3. Add the plugin to `libs.versions.toml`:
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
- Add the dependencies to your top-level `build.gradle.kts` and module-level `build.gradle.kts`.
## Step 1: Define Your Destinations
Replace your constant route strings with `@Serializable` objects and classes.
- **For screens without arguments** : Use a `data object`
- **For screens with arguments** : Use a `data class`
**Before (string based):**
const val ROUTE_HOME = "home"
const val ROUTE_PROFILE = "profile/{userId}"
**After (type safe):**
import kotlinx.serialization.Serializable
@Serializable
object Home
@Serializable
data class Profile(val userId: String)
## Step 2: Update the NavHost Configuration
Update your `NavHost` to use the new generic types in the `composable` and
`dialog` function.
**Before:**
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(...) }
composable("profile/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
ProfileScreen(userId)
}
}
**After:**
NavHost(navController, startDestination = Home) {
composable<Home> {
HomeScreen(...)
}
composable<Profile> { backStackEntry ->
// The library automatically handles argument extraction
val profile: Profile = backStackEntry.toRoute()
ProfileScreen(profile.userId)
}
}
## Step 3: Implement Type-Safe Navigation Calls
Replace string-interpolated navigation calls with class instances.
**Before:**
navController.navigate("profile/user123")
**After:**
navController.navigate(Profile(userId = "user123"))
## Step 4: Accessing Arguments in ViewModels
If you use a `ViewModel`, you can now extract the route object directly from the
`SavedStateHandle`.
**Implementation:**
class ProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// Automatically parses arguments into the Profile class
private val profile = savedStateHandle.toRoute<Profile>()
val userId = profile.userId
}
## Step 5: (Advanced) Handling Custom Types
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)
override fun parseValue(value: String): SearchFilter =
Json.decodeFromString(Uri.decode(value))
override fun put(bundle: Bundle, key: String, value: SearchFilter) {
bundle.putString(key, Json.encodeToString(value))
}
}
2. **Register it in the Graph**:
```kotlin
composable<Search>(
typeMap = mapOf(typeOf<SearchFilter>() to SearchFilterType)
) { ... }
## Best practices and tips
- **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
- **Testing** : Use `navController.currentBackStackEntry?.hasRoute<T>()` to check the current destination in a type-safe manner during UI tests
@@ -1,516 +0,0 @@
---
name: migrate-from-navigation-2-to-navigation-3
description: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements.
metadata:
keywords:
- skill
- recipe
- Android
- Navigation 2
- Navigation 3
- migration
- Compose
- guide
- dependencies
- NavKey
- NavHost
- NavDisplay
---
# Migrate from Navigation 2 to Navigation 3
To migrate your app from [Navigation 2](references/android/guide/navigation/index.md.txt) to Navigation 3, follow these steps:
1. Add the Navigation 3 dependencies.
2. Update your navigation routes to implement the `NavKey` interface.
3. Create classes to hold and modify your navigation state.
4. Replace `NavController` with these classes.
5. Move your destinations from `NavHost`'s `NavGraph` into an `entryProvider`.
6. Replace `NavHost` with `NavDisplay`.
7. Remove Navigation 2 dependencies.
<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](references/android/studio/gemini/overview.md.txt)
<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.
### Prerequisites
- You must use a `compileSdk` of 36 or later.
- You should be familiar with [navigation terminology](references/android/guide/navigation/index.md.txt).
- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in
Compose](references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md.txt).
- 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
This guide makes the following assumptions about you and your project:
- You have one or several top-level routes (usually displayed in a bottom navigation bar), and each has its own back stack.
- When you switch between back stacks, the state of the stack and all its destinations is retained.
- 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.
### Supported features
This guide lets you migrate the following features:
- Destinations defined as composable functions
- Dialogs (a destination shown on top of another destination)
### Features supported through recipes
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.
**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.
- More than one level of nested navigation
- Shared destinations: screens that can move between different back stacks
- [Custom destination types](references/android/guide/navigation/design/kotlin-dsl.md.txt)
- 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.
## Step 1: Add Navigation 3 dependencies
Use the [Get started](references/android/guide/navigation/navigation-3/get-started.md.txt) page to add the Navigation 3 dependencies to your
project. The core dependencies are provided for you to copy.
**lib.versions.toml**
[versions]
nav3Core = "1.0.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
**app/build.gradle.kts**
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
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`.
## Step 2: Update navigation routes to implement the `NavKey` interface
Update every navigation [route](references/android/guide/navigation/index.md.txt) so that it implements the `NavKey`
interface. This lets you use `rememberNavBackStack` to assist with [saving your
navigation state](references/android/guide/navigation/navigation-3/save-state.md.txt).
Before:
@Serializable data object RouteA
After:
@Serializable data object RouteA : NavKey
> [!NOTE]
> **Note:** The `@Serializable` annotation is provided by the KotlinX Serialization plugin. You can add this by following [these project setup steps](references/android/guide/navigation/navigation-3/get-started.md.txt).
## Step 3: Create classes to hold and modify your navigation state
### 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.
// package com.example.project
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
/**
* Create a navigation state that persists config changes and process death.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
val topLevelRoute = rememberSerializable(
startRoute, topLevelRoutes,
serializer = MutableStateSerializer(NavKeySerializer())
) {
mutableStateOf(startRoute)
}
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the current top level route
* @param backStacks - the back stacks for each top level route
*/
class NavigationState(
val startRoute: NavKey,
topLevelRoute: MutableState<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
}
**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.
### 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.
// package com.example.project
import androidx.navigation3.runtime.NavKey
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState){
fun navigate(route: NavKey){
if (route in state.backStacks.keys){
// This is a top level route, just switch to it.
state.topLevelRoute = route
} else {
state.backStacks[state.topLevelRoute]?.add(route)
}
}
fun goBack(){
val currentStack = state.backStacks[state.topLevelRoute] ?:
error("Stack for ${state.topLevelRoute} not found")
val currentRoute = currentStack.last()
// If we're at the base of the current route, go back to the start route stack.
if (currentRoute == state.topLevelRoute){
state.topLevelRoute = state.startRoute
} else {
currentStack.removeLastOrNull()
}
}
}
The `Navigator` class provides two navigation event methods:
- `navigate` to a specific route.
- `goBack` from the current route.
Both methods modify the `NavigationState`.
> [!IMPORTANT]
> **Architecture principles:** These classes follow the principles of [Unidirectional Data Flow](references/android/topic/architecture/index.md.txt):
>
> - The `Navigator` handles navigation events and uses them to update `NavigationState`.
> - The UI (provided by `NavDisplay`) observes `NavigationState` and reacts to any changes in that state by updating its UI.
### Step 3.3: Create the `NavigationState` and `Navigator`
Create instances of `NavigationState` and `Navigator` with the same scope as
your `NavController`.
val navigationState = rememberNavigationState(
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
)
val navigator = remember { Navigator(navigationState) }
## Step 4: Replace `NavController`
Replace `NavController` navigation event methods with `Navigator` equivalents.
| **`NavController` field or method** | **`Navigator` equivalent** |
|---|---|
| `navigate()` | `navigate()` |
| `popBackStack()` | `goBack()` |
Replace `NavController` fields with `NavigationState` fields.
| **`NavController` field or method** | **`NavigationState` equivalent** |
|---|---|
| `currentBackStack` | `backStacks[topLevelRoute]` |
| `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.
Before:
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
After:
val isSelected = key == navigationState.topLevelRoute
Verify that you have removed all references to `NavController`, including
any imports.
## Step 5: Move your destinations from `NavHost`'s `NavGraph` into an `entryProvider`
In Navigation 2, you [define your destinations](references/android/guide/navigation/design/index.md.txt)
using the [NavGraphBuilder DSL](references/android/guide/navigation/design/kotlin-dsl.md.txt),
usually inside `NavHost`'s trailing lambda. It is common to use extension
functions here as described in [Encapsulate your navigation code](references/android/guide/navigation/design/encapsulate.md.txt).
In Navigation 3, you define your destinations using an `entryProvider`. This
`entryProvider` resolves a route to a [`NavEntry`](references/android/guide/navigation/navigation-3/basics.md.txt). Importantly, the
`entryProvider` does not define parent-child relationships between entries.
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.
> [!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`
Create an `entryProvider` [using the DSL](references/android/guide/navigation/navigation-3/basics.md.txt) at the same scope as the
`NavigationState`.
val entryProvider = entryProvider {
}
## Step 5.2: Move destinations into the `entryProvider`
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.
You can refactor [`NavGraphBuilder` extension functions](references/android/guide/navigation/design/encapsulate.md.txt) to
`EntryProviderScope<T>` extension functions, and then move them.
Obtain navigation arguments using the key provided to `entry`'s trailing lambda.
For example:
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{
val id = entry.toRoute<RouteA>().id
ScreenA(title = "Screen has ID: $id")
}
featureBSection()
dialog<RouteD>{ ScreenD() }
}
fun NavGraphBuilder.featureBSection() {
navigation<BaseRouteB>(startDestination = RouteB) {
composable<RouteB> { ScreenB() }
}
}
becomes:
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey
val entryProvider = entryProvider {
entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
featureBSection()
entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}
fun EntryProviderScope<NavKey>.featureBSection() {
entry<RouteB> { ScreenB() }
}
## Step 6: Replace `NavHost` with `NavDisplay`
Replace `NavHost` with `NavDisplay`.
- Delete `NavHost` and replace it with `NavDisplay`.
- Specify `entries = navigationState.toEntries(entryProvider)` as a parameter. This converts the navigation state into the entries that `NavDisplay` shows using the `entryProvider`.
- Connect `NavDisplay.onBack` to `navigator.goBack()`. This causes `navigator` to update the navigation state when `NavDisplay`'s built-in back handler completes.
- If you have dialog destinations, add `DialogSceneStrategy` to `NavDisplay`'s `sceneStrategy` parameter.
For example:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
)
## Step 7: Remove Navigation 2 dependencies
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).
@@ -1,165 +0,0 @@
When using the Kotlin DSL to construct your graph, keeping destinations and
navigation events in a single file can be difficult to maintain. This is
especially true if you have multiple independent features.
## Extract destinations
You should move your destinations into [`NavGraphBuilder`](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) extension
functions. They should live close to the routes which define them, and the
screens that they display. For example, consider the following app-level code
that creates a destination which shows a list of contacts:
// MyApp.kt
@Serializable
object Contacts
@Composable
fun MyApp() {
...
NavHost(navController, startDestination = Contacts) {
composable<Contacts> { ContactsScreen( /* ... */ ) }
}
}
You should move the navigation-specific code into a separate file:
// ContactsNavigation.kt
@Serializable
object Contacts
fun NavGraphBuilder.contactsDestination() {
composable<Contacts> { ContactsScreen( /* ... */ ) }
}
// MyApp.kt
@Composable
fun MyApp() {
...
NavHost(navController, startDestination = Contacts) {
contactsDestination()
}
}
The routes and destination definitions are now separate from the main app and
you can update them independently. The main app is only dependent on a single
extension function. In this case, that is
`NavGraphBuilder.contactsDestination()`.
The `NavGraphBuilder` extension function forms the bridge between a stateless
screen-level composable function and Navigation-specific logic. This layer can
also define where the state comes from and how you handle events.
> [!NOTE]
> **Note:** This is essentially the same approach as when you move screens to different composable functions instead of defining them in one monolithic composable function.
## Example
The following snippet introduces a new destination to display a contact's
details, and updates the existing contact list destination to [expose a
navigation event](https://developer.android.com/guide/navigation/use-graph/navigate#nav-from-composable) to display the contact's details.
Here's a typical set of screens that can be `internal` to their own module, so
that other modules cannot access them:
// ContactScreens.kt
// Displays a list of contacts
@Composable
internal fun ContactsScreen(
uiState: ContactsUiState,
onNavigateToContactDetails: (contactId: String) -> Unit
) { ... }
// Displays the details for an individual contact
@Composable
internal fun ContactDetailsScreen(contact: ContactDetails) { ... }
### Create destinations
The following [`NavGraphBuilder`](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) extension function creates a destination
which shows the `ContactsScreen` composable. In addition, it now connects
the screen with a `ViewModel` that provides the screen UI state and handles the
screen-related business logic.
Navigation events, such as navigating to the contact details destination, are
exposed to the caller rather than being handled by the `ViewModel`.
// ContactsNavigation.kt
@Serializable
object Contacts
// Adds contacts destination to `this` NavGraphBuilder
fun NavGraphBuilder.contactsDestination(
// Navigation events are exposed to the caller to be handled at a higher level
onNavigateToContactDetails: (contactId: String) -> Unit
) {
composable<Contacts> {
// The ViewModel as a screen level state holder produces the screen
// UI state and handles business logic for the ConversationScreen
val viewModel: ContactsViewModel = hiltViewModel()
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
ContactsScreen(
uiState,
onNavigateToContactDetails
)
}
}
You can use the same approach to create a destination which displays the
`ContactDetailsScreen`. In this case, instead of obtaining the UI state from a
view model, you can obtain it directly from the `NavBackStackEntry`.
// ContactsNavigation.kt
@Serializable
internal data class ContactDetails(val id: String)
fun NavGraphBuilder.contactDetailsScreen() {
composable<ContactDetails> { navBackStackEntry ->
ContactDetailsScreen(contact = navBackStackEntry.toRoute())
}
}
### Encapsulate navigation events
In the same way that you encapsulate destinations, you can encapsulate
navigation events to avoid exposing route types unnecessarily. Do this by
creating extension functions on `NavController`.
// ContactsNavigation.kt
fun NavController.navigateToContactDetails(id: String) {
navigate(route = ContactDetails(id = id))
}
### Bring it together
The navigation code for displaying contacts is now cleanly separated from the
app's navigation graph. The app needs to:
- Call `NavGraphBuilder` extension functions to create destinations
- Connect those destinations by calling `NavController` extension functions for navigation events
// MyApp.kt
@Composable
fun MyApp() {
...
NavHost(navController, startDestination = Contacts) {
contactsDestination(onNavigateToContactDetails = { contactId ->
navController.navigateToContactDetails(id = contactId)
})
contactDetailsDestination()
}
}
## In summary
- Encapsulate your navigation code for a related set of screens by placing it in a separate file
- Expose destinations by creating extension functions on `NavGraphBuilder`
- Expose navigation events by creating extension functions on `NavController`
- Use `internal` to keep screens and route types private
@@ -1,364 +0,0 @@
The [Navigation component](https://developer.android.com/guide/navigation) uses a *navigation graph* to manage your app's
navigation. The navigation graph is a data structure that contains each
destination within your app and the connections between them.
> [!NOTE]
> **Note:** The navigation graph is distinct from the [back stack](https://developer.android.com/guide/navigation/backstack), which is a stack within the `NavController` that holds destinations the user has recently visited.
## Destination types
There are three general types of destinations: hosted, dialog, and activity. The
following table outlines these three destination types and their purposes.
| Type | Description | Use cases |
|---|---|---|
| Hosted | Fills the entire navigation host. That is, the size of a hosted destination is the same as the size of the navigation host and previous destinations are not visible. | Main and detail screens. |
| Dialog | Presents overlay UI components. This UI is not tied to the location of the navigation host or its size. Previous destinations are visible underneath the destination. | Alerts, selections, forms. |
| Activity | Represents unique screens or features within the app. | Serve as an exit point to the navigation graph that starts a new Android activity that is managed separately from the Navigation component. In modern Android development, an app consists of a single activity. Activity destinations are therefore best used when interacting with third party activities or as part of [the migration process](https://developer.android.com/guide/navigation/migrate). |
This document contains examples of hosted destinations, which are the most
common and fundamental destinations. See the following guides for information on
the other destinations:
- [Dialog destinations](https://developer.android.com/guide/navigation/design/dialog-destinations)
- [Activity destinations](https://developer.android.com/guide/navigation/design/activity-destinations)
### Frameworks
Although the same general workflow applies in every case, how exactly you create
a navigation host and graph depends on the UI framework you use.
- **Compose:** Use the `NavHost` composable. Add a `NavGraph` to it using the [Kotlin DSL](https://developer.android.com/guide/navigation/navigation-kotlin-dsl). You can create the graph in two ways:
- **As part of the NavHost:** Construct the navigation graph directly as part of adding the `NavHost`.
- **Programmatically:** Use the [`NavController.createGraph()`](https://developer.android.com/reference/androidx/navigation/NavController#(androidx.navigation.NavController).createGraph(kotlin.String,kotlin.String,kotlin.Function1)) method to create a `NavGraph` and pass it to the `NavHost` directly.
- **Fragments:** When using fragments with the views UI framework, use a `NavHostFragment` as the host. There are several ways to create a navigation graph:
- **Programmatically:** Use the Kotlin DSL to create a `NavGraph` and directly apply it on the `NavHostFragment`.
- The `createGraph()` function used with the Kotlin DSL for both fragments and Compose is the same.
- **XML:** Write your navigation host and graph directly in XML.
- **Android Studio editor:** Use the GUI editor in Android Studio to create and adjust your graph as an XML resource file.
> [!NOTE]
> **Note:** How you interact with the graph through the `NavController` is similar between frameworks. See the [Navigate to a destination](https://developer.android.com/guide/navigation/use-graph/navigate) guide for more details.
## Compose
In Compose, use a serializable object or class to define a *route*. A route
describes how to get to a destination, and contains all the information that the
destination requires.
Use the `@Serializable` annotation to automatically create the necessary
serialization and deserialization methods for your route types. This annotation
is provided by the [Kotlin Serialization
plugin](https://kotlinlang.org/docs/serialization.html). [Follow these
instructions to add this plugin](https://developer.android.com/guide/navigation#set-up).
Once you have defined your routes, use the `NavHost` composable to create your
navigation graph. Consider the following example:
@Serializable
object Profile
@Serializable
object FriendsList
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Profile) {
composable<Profile> { ProfileScreen( /* ... */ ) }
composable<FriendsList> { FriendsListScreen( /* ... */ ) }
// Add more destinations similarly.
}
1. A serializable object represents each of the two routes, `Profile` and `FriendsList`.
2. The call to the `NavHost` composable passes a `NavController` and a route for the start destination.
3. The lambda passed to the `NavHost` ultimately calls [`NavController.createGraph()`](https://developer.android.com/reference/androidx/navigation/NavController#(androidx.navigation.NavController).createGraph(kotlin.String,kotlin.String,kotlin.Function1)) and returns a `NavGraph`.
4. Each route is supplied as a type argument to [`NavGraphBuilder.composable<T>()`](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder#(androidx.navigation.NavGraphBuilder).composable(kotlin.collections.Map,kotlin.collections.List,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function2)) which adds the destination to the resulting `NavGraph`.
5. The lambda passed to `composable` is what the `NavHost` displays for that destination.
> [!CAUTION]
> **Caution:** Instead of passing a type to `composable()`, you can pass a `route` string or an integer `id`. However, this makes it much more difficult to manage passing additional arguments to the destination.
### Understand the lambda
To better understand the lambda that creates the `NavGraph`, consider that to
build the same graph as in the preceding snippet, you could create the
`NavGraph` separately using [`NavController.createGraph()`](https://developer.android.com/reference/androidx/navigation/NavController#(androidx.navigation.NavController).createGraph(kotlin.String,kotlin.String,kotlin.Function1)) and pass it to
the `NavHost` directly:
val navGraph by remember(navController) {
navController.createGraph(startDestination = Profile)) {
composable<Profile> { ProfileScreen( /* ... */ ) }
composable<FriendsList> { FriendsListScreen( /* ... */ ) }
}
}
NavHost(navController, navGraph)
> [!IMPORTANT]
> **Important:** A [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) is associated with a single `NavHost` composable. The `NavHost` provides the `NavController` access to its navigation graph. When you use the `NavController` to navigate to a destination, you cause the `NavController` to interact with its associated `NavHost`.
### Pass arguments
If you need to pass data to a destination, define the route with a class that
has parameters. For example, the `Profile` route is a data class with a `name`
parameter.
@Serializable
data class Profile(val name: String)
Whenever you need to pass arguments to that destination, you create an instance
of your route class, passing the arguments to the class constructor.
> [!NOTE]
> **Note:** Use a `data class` for a route with arguments, and an `object` or `data
> object` for a route with no arguments.
For optional arguments, create nullable fields with a default value.
@Serializable
data class Profile(val nickname: String? = null)
### Obtain route instance
You can obtain the route instance with `NavBackStackEntry.toRoute()` or
`SavedStateHandle.toRoute()`. When you create a destination using
`composable()`, the `NavBackStackEntry` is available as a parameter.
@Serializable
data class Profile(val name: String)
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Profile(name="John Smith")) {
composable<Profile> { backStackEntry ->
val profile: Profile = backStackEntry.toRoute()
ProfileScreen(name = profile.name) }
}
Note the following in this snippet:
- The `Profile` route specifies the starting destination in the navigation graph, with `"John Smith"` as the argument for `name`.
- The destination itself is the `composable<Profile>{}` block.
- The `ProfileScreen` composable takes the value of `profile.name` for its own `name` argument.
- As such, the value `"John Smith"` passes through to `ProfileScreen`.
### Minimal example
A complete example of a `NavController` and `NavHost` working together:
@Serializable
data class Profile(val name: String)
@Serializable
object FriendsList
// Define the ProfileScreen composable.
@Composable
fun ProfileScreen(
profile: Profile
onNavigateToFriendsList: () -> Unit,
) {
Text("Profile for ${profile.name}")
Button(onClick = { onNavigateToFriendsList() }) {
Text("Go to Friends List")
}
}
// Define the FriendsListScreen composable.
@Composable
fun FriendsListScreen(onNavigateToProfile: () -> Unit) {
Text("Friends List")
Button(onClick = { onNavigateToProfile() }) {
Text("Go to Profile")
}
}
// Define the MyApp composable, including the `NavController` and `NavHost`.
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = Profile(name = "John Smith")) {
composable<Profile> { backStackEntry ->
val profile: Profile = backStackEntry.toRoute()
ProfileScreen(
profile = profile,
onNavigateToFriendsList = {
navController.navigate(route = FriendsList)
}
)
}
composable<FriendsList> {
FriendsListScreen(
onNavigateToProfile = {
navController.navigate(
route = Profile(name = "Aisha Devi")
)
}
)
}
}
}
As the snippet demonstrates, instead of passing the `NavController` to your
composables, expose an event to the `NavHost`. That is, your composables should
have a parameter of type `() -> Unit` for which the `NavHost` passes a lambda
that calls `NavController.navigate()`.
> [!NOTE]
> **Note:** By using the parameters of the route class you can pass data to the given destination with full type safety. For example, in the previous code `Profile.name` ensures that `name` is always a `String`.
## Fragments
As outlined in the preceding sections, when using fragments you have the option
to create a navigation graph programmatically using the Kotlin DSL, XML, or the
Android Studio editor.
The following sections detail these different approaches.
> [!NOTE]
> **Note:** The Navigation component is designed for apps that have one main activity with multiple fragment destinations. The main activity is associated with a navigation graph and contains a `NavHostFragment` that is responsible for swapping destinations as needed. In an app with multiple activity destinations, each activity has its own navigation graph.
### Programmatically
The Kotlin DSL provides a programmatic way of creating a navigation graph with
fragments. In many ways this is neater and more modern than using an XML
resource file.
Consider the following example, which implements a two-screen navigation graph.
First it is necessary to create the `NavHostFragment`, which must *not* include
an `app:navGraph` element:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Next, pass the `id` of the `NavHostFragment` to
[`NavController.findNavController`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.view.View))(). This associates the NavController with
the `NavHostFragment`.
Subsequently, the call to [`NavController.createGraph()`](https://developer.android.com/reference/androidx/navigation/NavController#(androidx.navigation.NavController).createGraph(kotlin.String,kotlin.String,kotlin.Function1)) links the graph to
the `NavController` and consequently also to the `NavHostFragment`:
@Serializable
data class Profile(val name: String)
@Serializable
object FriendsList
// Retrieve the NavController.
val navController = findNavController(R.id.nav_host_fragment)
// Add the graph to the NavController with `createGraph()`.
navController.graph = navController.createGraph(
startDestination = Profile(name = "John Smith")
) {
// Associate each destination with one of the route constants.
fragment<ProfileFragment, Profile> {
label = "Profile"
}
fragment<FriendsListFragment, FriendsList>() {
label = "Friends List"
}
// Add other fragment destinations similarly.
}
Using the DSL in this way is very similar to the workflow outlined in the
preceding section on [Compose](https://developer.android.com/guide/navigation/design#compose). For example, both there and here, the
`NavController.createGraph()` function generates the `NavGraph`. Likewise, while
`NavGraphBuilder.composable()` adds composable destinations to the graph, here
[`NavGraphBuilder.fragment()`](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder#(androidx.navigation.NavGraphBuilder).fragment(kotlin.Int)) adds a fragment destination.
For more information on how to use the Kotlin DSL, see [Build a graph with the
NavGraphBuilder DSL](https://developer.android.com/guide/navigation/navigation-kotlin-dsl).
### XML
You can directly write the XML yourself. The following example mirrors and is
equivalent to the two-screen example from the preceding section.
First, create a `NavHostFragment`. This serves as the navigation host which
contains the actual navigation graph.
A minimal implementation of a `NavHostFragment`:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/nav_graph" />
</FrameLayout>
The `NavHostFragment` contains the attribute `app:navGraph`. Use this attribute
to connect your navigation graph to the navigation host. The following is an
example of how you might implement the graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/profile">
<fragment
android:id="@+id/profile"
android:name="com.example.ProfileFragment"
android:label="Profile">
<!-- Action to navigate from Profile to Friends List. -->
<action
android:id="@+id/action_profile_to_friendslist"
app:destination="@id/friendslist" />
</fragment>
<fragment
android:id="@+id/friendslist"
android:name="com.example.FriendsListFragment"
android:label="Friends List" />
<!-- Add other fragment destinations similarly. -->
</navigation>
You use actions to define the connections between different destinations. In
this example, the `profile` fragment contains an action that navigates to
`friendslist`. For more information, see [Use Navigation actions and
fragments](https://developer.android.com/guide/navigation/design/actions).
> [!NOTE]
> **Note:** The DSL example does not define actions because they don't apply in that context. When using the DSL, [use `NavController.navigate()` directly](https://developer.android.com/guide/navigation/design/kotlin-dsl#host).
### Editor
You can manage your app's navigation graph using the Navigation Editor in
Android Studio. This is essentially a GUI you can use to create and edit your
`NavigationFragment` XML, as seen in the preceding section.
For more information, see [Navigation editor](https://developer.android.com/guide/navigation/design/editor).
## Nested graphs
You can also use nested graphs. This involves using a graph as a navigation
destination. For more information, see [Nested graphs](https://developer.android.com/guide/navigation/design/nested-graphs).
## Further Reading
For more core navigation concepts, see the following guides:
- **[Overview](https://developer.android.com/guide/navigation):** Make sure to read the general overview of the Navigation component.
- **[Activity destinations](https://developer.android.com/guide/navigation/design/activity-destinations):** Examples of how to implement destinations that take the user to activities.
- **[Dialog destinations](https://developer.android.com/guide/navigation/design/dialog-destinations):** Examples of how to create destinations that take the user to a dialog.
- **[Navigate to a destination](https://developer.android.com/guide/navigation/use-graph/navigate):** A detailed guide that covers how to navigate from one destination to another.
- **[Nested graphs](https://developer.android.com/guide/navigation/design/nested-graphs):** An in-depth guide on how to nest one navigation graph within another.
@@ -1,409 +0,0 @@
The Navigation component provides a Kotlin-based domain-specific language, or
DSL, that relies on Kotlin's [type-safe
builders](https://kotlinlang.org/docs/reference/type-safe-builders.html)
. This API lets you declaratively compose your graph in your Kotlin code, rather
than inside an XML resource. This can be useful if you want to build your app's
navigation dynamically. For example, your app could download and cache a
navigation configuration from an external web service and then use that
configuration to dynamically build a navigation graph in your activity's
`onCreate()` function.
## Dependencies
To use the Kotlin DSL with Fragments, add the following dependency to your app's
`build.gradle` file:
### Groovy
```groovy
dependencies {
def nav_version = "2.9.7"
api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}
```
### Kotlin
```kotlin
dependencies {
val nav_version = "2.9.7"
api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}
```
## Building a graph
Here is a basic example based on the [Sunflower
app](https://github.com/android/sunflower/tree/views). For this
example, we have two destinations: `home` and `plant_detail`. The `home`
destination is present when the user first launches the app. This destination
displays a list of plants from the user's garden. When the user selects one of
the plants, the app navigates to the `plant_detail` destination.
Figure 1 shows these destinations along with the arguments required by the
`plant_detail` destination and an action, `to_plant_detail`, that the app uses
to navigate from `home` to `plant_detail`.
![The Sunflower app has two destinations along with an action that
connects them.](https://developer.android.com/static/images/guide/navigation/navigation-kotlin-dsl-1.png) **Figure 1.** The Sunflower app has two destinations, `home` and `plant_detail`, along with an action that connects them.
### Hosting a Kotlin DSL Nav Graph
Before you can build your app's navigation graph, you need a place to host the
graph. This example uses fragments, so it hosts the graph in a
[`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment)
inside of a
[`FragmentContainerView`](https://developer.android.com/reference/androidx/fragment/app/FragmentContainerView):
<!-- activity_garden.xml -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</FrameLayout>
Notice that the `app:navGraph` attribute is not set in this example. The graph
isn't defined as a [resource](https://developer.android.com/guide/topics/resources/providing-resources) in
the `res/navigation` folder so it needs to be set as part of the `onCreate()`
process in the activity.
In XML, an action ties together a destination ID with one or more arguments.
However, when using the Navigation DSL a route can contain arguments as part of
the route. This means that there is no concept of actions when using the DSL.
The next step is to define the routes that you will use when defining your
graph.
### Create routes for your graph
[XML-based navigation graphs](https://developer.android.com/guide/navigation/design#xml) are parsed as part
of the Android build process. A numeric constant is created for each `id`
attribute defined in the graph. These build time generated static IDs are not
available when building your navigation graph at runtime so the Navigation DSL
uses [serializable
types](https://kotlinlang.org/docs/serialization.html) instead of
IDs. Each route is represented by a unique type.
When dealing with arguments, these are [built into the route
type](https://developer.android.com/jetpack/compose/navigation#nav-with-args). This lets you have type safety
for your navigation arguments.
@Serializable data object Home
@Serializable data class Plant(val id: String)
### Build a graph with the NavGraphBuilder DSL
Once you've defined your routes, you can build the navigation graph.
val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
startDestination = Home
) {
fragment<HomeFragment, Home> {
label = resources.getString(R.string.home_title)
}
fragment<PlantDetailFragment, PlantDetail> {
label = resources.getString(R.string.plant_detail_title)
}
}
In this example, two fragment destinations are defined using the
[`fragment()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#(androidx.navigation.NavGraphBuilder).fragment(kotlin.collections.Map,kotlin.Function1))
DSL builder function. This function requires two [type
arguments](https://kotlinlang.org/docs/generics.html#generic-functions)
.
First, a [`Fragment`](https://developer.android.com/reference/kotlin/androidx/fragment/app/Fragment) class
that provides the UI for this destination. Setting this has the same effect as
setting the `android:name` attribute on fragment destinations that are defined
using XML.
Second, the route. This must be a serializable type which extends from `Any`. It
should contain any navigation arguments that will be used by this destination,
and their types.
The function also accepts an optional lambda for additional configuration, such
as the destination label, as well as embedded builder functions for custom
arguments and deep links.
### Navigating with your Kotlin DSL graph
Finally, you can navigate from `home` to `plant_detail` using
[`NavController.navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(kotlin.Any,kotlin.Function1))
calls:
private fun navigateToPlant(plantId: String) {
findNavController().navigate(route = PlantDetail(id = plantId))
}
In `PlantDetailFragment`, you can obtain the navigation arguments by obtaining
the current
[`NavBackStackEntry`](https://developer.android.com/reference/kotlin/androidx/navigation/NavBackStackEntry)
and calling
[`toRoute`](https://developer.android.com/reference/kotlin/androidx/navigation/NavBackStackEntry#(androidx.navigation.NavBackStackEntry).toRoute())
on it to obtain the route instance.
val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id
If `PlantDetailFragment` is using a `ViewModel`, obtain the route instance using
[`SavedStateHandle.toRoute`](https://developer.android.com/reference/kotlin/androidx/lifecycle/SavedStateHandle#(androidx.lifecycle.SavedStateHandle).toRoute(kotlin.collections.Map)).
val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id
The rest of this guide describes common navigation graph elements, destinations,
and how to use them when building your graph.
## Destinations
The Kotlin DSL provides built-in support for three destination types:
`Fragment`, `Activity`, and `NavGraph` destinations, each of which has its own
inline extension function available for building and configuring the
destination.
### Fragment destinations
The
[`fragment()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#(androidx.navigation.NavGraphBuilder).fragment(kotlin.collections.Map,kotlin.Function1))
DSL function can be parameterized with the fragment class for the UI and the
route type used to uniquely identify this destination, followed by a lambda
where you can provide additional configuration as described in the [Navigating
with your Kotlin DSL graph](https://developer.android.com/guide/navigation/design/kotlin-dsl#navigate) section.
fragment<MyFragment, MyRoute> {
label = getString(R.string.fragment_title)
// custom argument types, deepLinks
}
### Activity destination
The
[`activity()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(androidx.navigation.NavGraphBuilder).activity(kotlin.collections.Map,kotlin.Function1))
DSL function takes a type parameter for the route but is not parameterized to
any implementing activity class. Instead, you set an optional `activityClass` in
a trailing lambda. This flexibility lets you define an activity destination for
an activity that should be launched using an [implicit
intent](https://developer.android.com/guide/components/intents-filters#ExampleSend), where an explicit
activity class wouldn't make sense. As with fragment destinations, you can also
configure a label, custom arguments, and deep links.
activity<MyRoute> {
label = getString(R.string.activity_title)
// custom argument types, deepLinks...
activityClass = MyActivity::class
}
### Navigation graph destination
The
[`navigation()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(androidx.navigation.NavGraphBuilder).navigation(kotlin.Any,kotlin.collections.Map,kotlin.Function1))
DSL function can be used to build a [nested navigation
graph](https://developer.android.com/guide/navigation/navigation-nested-graphs). This function takes a type
parameter for the route to assign to this graph. It also takes two arguments:
the route of the starting destination of the graph, and a lambda to further
configure the graph. Valid elements include other destinations, custom argument
types, deep links, and a [descriptive label for the
destination](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder#label()).
This label can be useful for binding the navigation graph to UI components using
[`NavigationUI`](https://developer.android.com/guide/navigation/navigation-ui).
@Serializable data object HomeGraph
@Serializable data object Home
navigation<HomeGraph>(startDestination = Home) {
// label, other destinations, deep links
}
### Supporting custom destinations
If you're using a [new destination type](https://developer.android.com/guide/navigation/navigation-add-new)
that does not directly support the Kotlin DSL, you can add these destinations to
your Kotlin DSL using
[`addDestination()`](https://developer.android.com/reference/androidx/navigation/NavGraph#addDestination(androidx.navigation.NavDestination)):
// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
route = Graph.CustomDestination.route
}
addDestination(customDestination)
As an alternative, you can also use the unary plus operator to add a newly
constructed destination directly to the graph:
// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
route = Graph.CustomDestination.route
}
### Providing destination arguments
Destination arguments can be defined as part of the route class. These can be
defined the same way you would for any Kotlin class. Required arguments are
defined as non-nullable types and optional arguments are defined with default
values.
The underlying mechanism for representing routes and their arguments is string
based. Using strings to model routes allows navigation state to be stored and
restored from disk during [configuration
changes](https://developer.android.com/guide/topics/resources/runtime-changes) and [system-initiated process
death](https://developer.android.com/topic/libraries/architecture/saving-states#options). For this reason,
each navigation argument needs to be serializable, that is, it should have a
method that converts the in-memory representation of the argument value to a
`String`.
The [Kotlin serialization
plugin](https://kotlinlang.org/docs/serialization.html)
automatically generates serialization methods for [basic
types](https://kotlinlang.org/docs/basic-types.html) when the
`@Serializable` annotation is added to an object.
@Serializable
data class MyRoute(
val id: String,
val myList: List<Int>,
val optionalArg: String? = null
)
fragment<MyFragment, MyRoute>
#### Providing custom types
For custom argument types, you'll need to provide a custom `NavType` class. This
lets you control exactly how your type is parsed from a route or deep link.
For example, a route used to define a search screen could contain a class that
represents the search parameters:
@Serializable
data class SearchRoute(val parameters: SearchParameters)
@Serializable
@Parcelize
data class SearchParameters(
val searchQuery: String,
val filters: List<String>
)
A custom `NavType` could be written as:
val SearchParametersType = object : NavType<SearchParameters>(
isNullableAllowed = false
) {
override fun put(bundle: Bundle, key: String, value: SearchParameters) {
bundle.putParcelable(key, value)
}
override fun get(bundle: Bundle, key: String): SearchParameters {
return bundle.getParcelable(key) as SearchParameters
}
override fun serializeAsValue(value: SearchParameters): String {
// Serialized values must always be Uri encoded
return Uri.encode(Json.encodeToString(value))
}
override fun parseValue(value: String): SearchParameters {
// Navigation takes care of decoding the string
// before passing it to parseValue()
return Json.decodeFromString<SearchParameters>(value)
}
}
This can then be used in your Kotlin DSL like any other type:
fragment<SearchFragment, SearchRoute>(
typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
label = getString(R.string.plant_search_title)
}
When navigating to the destination, create an instance of your route:
val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))
The parameter can be obtained from the route in the destination:
val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters
## Deep links
Deep links can be added to any destination, just as they can with an XML driven
navigation graph. All of the same procedures defined in [Creating a deep link
for a destination](https://developer.android.com/guide/navigation/navigation-deep-link) apply to the process
of creating a deep link using the Kotlin DSL.
When creating an [implicit deep link](https://developer.android.com/guide/navigation/navigation-deep-link)
however, you don't have an XML navigation resource that can be analyzed for
`<deepLink>` elements. Therefore, you cannot rely on placing a `<nav-graph>`
element in your `AndroidManifest.xml` file and must instead add [intent
filters](https://developer.android.com/training/app-links/deep-linking) to your activity manually. The intent
filter you supply should match the base path, action, and mimetype of
your app's deep links.
Deep links are added to a destination by calling the `deepLink` function inside
the destination's lambda. It accepts the route as a parameterized type, and a
parameter `basePath` for the base path of the URL used for the deep link.
You can also add an action and mimetype using the
[`deepLinkBuilder`](https://developer.android.com/reference/kotlin/androidx/navigation/NavDeepLinkDslBuilder)
trailing lambda.
The following example creates a deep link URI for the `Home` destination.
@Serializable data object Home
fragment<HomeFragment, Home>{
deepLink<Home>(basePath = "www.example.com/home"){
// Optionally, specify the action and/or mime type that this destination
// supports
action = "android.intent.action.MY_ACTION"
mimeType = "image/*"
}
}
### URI format
The deep link URI format is automatically generated from the route's fields
using the following rules:
- Required parameters are appended as *path parameters* (example: `/{id}`)
- Parameters with a default value (optional parameters) are appended as *query
parameters* (example: `?name={name}`)
- Collections are appended as *query parameters* (example: `?items={value1}&items={value2}`)
- The order of parameters matches the order of the fields in the route
For example, the following route type:
@Serializable data class PlantDetail(
val id: String,
val name: String,
val colors: List<String>,
val latinName: String? = null,
)
has a generated URI format of:
`basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}`
There is no limit to the number of deep links you can add. Each time you call
[`deepLink()`](https://developer.android.com/reference/kotlin/androidx/navigation/NavDestinationBuilder#deepLink(kotlin.String))
a new deep link is appended to a list that is maintained for that destination.
## Limitations
The [Safe Args](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args) plugin is
incompatible with the Kotlin DSL, as the plugin looks for XML resource files to
generate `Directions` and `Arguments` classes.
@@ -1,145 +0,0 @@
[Video](https://www.youtube.com/watch?v=Y0Cs2MQxyIs)
Navigation refers to the interactions that let users navigate across, into, and
back out from the different pieces of content within your app.
Android Jetpack's Navigation component includes the [Navigation
library](https://developer.android.com/jetpack/androidx/releases/navigation), [Safe Args Gradle plug-in](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args),
and tooling to help you implement app navigation. The Navigation component
handles diverse navigation use cases, from straightforward button clicks to more
complex patterns, such as app bars and the navigation drawer.
> [!IMPORTANT]
> **Important:** The Navigation component also ensures a consistent and predictable user experience by adhering to an established [set of principles](https://developer.android.com/guide/navigation/navigation-principles).
## Key concepts
The following table provides an overview of the key concepts in
navigation and the main types that you use to implement them.
| Concept | Purpose | Type |
|---|---|---|
| Host | A UI element that contains the current navigation destination. That is, when a user navigates through an app, the app essentially swaps destinations in and out of the navigation host. | - **Compose** : [`NavHost`](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#NavHost(androidx.navigation.NavHostController,androidx.navigation.NavGraph,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1)) - **Fragments** : [`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment) |
| Graph | A data structure that defines all the navigation destinations within the app and how they connect together. | [`NavGraph`](https://developer.android.com/reference/androidx/navigation/NavGraph) |
| Controller | The central coordinator for managing navigation between destinations. The controller offers methods for navigating between destinations, handling deep links, managing the back stack, and more. | [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) |
| Destination | A node in the navigation graph. When the user navigates to this node, the host displays its content. | [`NavDestination`](https://developer.android.com/reference/androidx/navigation/NavDestination) Typically created when constructing the navigation graph. |
| Route | Uniquely identifies a destination and any data required by it. You can navigate using routes. Routes take you to destinations. | Any serializable data type. |
> [!IMPORTANT]
> **Important:** Whether you are using Compose, views, or a custom UI framework, these concepts always apply when implementing navigation. However, the specific ways in which you use them can differ.
## Benefits and features
The Navigation component provides a number of other benefits and features,
including the following:
- **Animations and transitions:** Provides standardized resources for animations and transitions.
- **Deep linking:** Implements and handles deep links that take the user directly to a destination.
- **UI patterns:** Supports patterns such as navigation drawers and bottom navigation with minimal additional work.
- **Type safety:** Includes support for passing data between destinations with [type safety](https://developer.android.com/guide/navigation/design/type-safety).
- **ViewModel support:** Enables scoping a `ViewModel` to a navigation graph to share UI-related data between the graph's destinations.
- **Fragment transactions:** Fully supports and handles fragment transactions.
- **Back and up:** Handles back and up actions correctly by default.
> [!NOTE]
> **Note:** If you are using XML for your navigation graphs, use Android Studio's [Navigation Editor](https://developer.android.com/guide/navigation/design/editor) to view and edit your graphs.
> [!NOTE]
> **Note:** Android 13 introduces predictive back navigation, which works with the Navigation component for Android devices. Implement predictive back navigation in your app as soon as possible. Otherwise, users might experience unexpected behavior in a future Android release.
## Set up your environment
To include navigation support in your project, add the following dependencies to
your app's `build.gradle` file:
### Groovy
```groovy
plugins {
// Kotlin serialization plugin for type safe routes and navigation arguments
id 'org.jetbrains.kotlin.plugin.serialization' version '2.0.21'
}
dependencies {
def nav_version = "2.9.7"
// Jetpack Compose Integration
implementation "androidx.navigation:navigation-compose:$nav_version"
// Views/Fragments Integration
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Feature module support for Fragments
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
// JSON serialization library, works with the Kotlin serialization plugin.
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
}
```
### Kotlin
```kotlin
plugins {
// Kotlin serialization plugin for type safe routes and navigation arguments
kotlin("plugin.serialization") version "2.0.21"
}
dependencies {
val nav_version = "2.9.7"
// Jetpack Compose integration
implementation("androidx.navigation:navigation-compose:$nav_version")
// Views/Fragments integration
implementation("androidx.navigation:navigation-fragment:$nav_version")
implementation("androidx.navigation:navigation-ui:$nav_version")
// Feature module support for Fragments
implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")
// Testing Navigation
androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
// JSON serialization library, works with the Kotlin serialization plugin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
```
For information on adding other architecture components to your project, see
[Add components to your project](https://developer.android.com/topic/libraries/architecture/adding-components#navigation).
## Next steps
For more documentation and resources related to the Navigation component, see
the following resources.
### Detailed guides
For more information on how to implement a navigation host and `NavController`,
as well as detail on how they interact with Compose and other UI frameworks, see
the following guides:
- [Create a navigation controller](https://developer.android.com/guide/navigation/navcontroller): Outlines how to create a `NavController`.
- [Create your navigation graph](https://developer.android.com/guide/navigation/design): Details how to create a navigation host and a navigation graph.
- [Navigate to a destination](https://developer.android.com/guide/navigation/use-graph/navigate): Demonstrates how to use a `NavController` to move between the destinations in your graph.
### Codelabs
- [Learn Jetpack Navigation](https://developer.android.com/codelabs/android-navigation)
- [Fragments and the Navigation Component](https://developer.android.com/codelabs/basic-android-kotlin-training-fragments-navigation-component)
- [Build an adaptive app with dynamic navigation](https://developer.android.com/codelabs/basic-android-kotlin-compose-adaptive-navigation-for-large-screens#0)
### Videos
- [Navigating navigation](https://www.youtube.com/watch?v=09qjn706ITA)
- [10 best practices for moving to a single activity](https://www.youtube.com/watch?v=9O1D_Ytk0xg)
- [Single activity: Why, when, and how (Android Dev Summit '18)](https://www.youtube.com/watch?v=2k8x8V77CrU)
- [Android Jetpack: Manage UI navigation with navigation controller (Google
I/O '18)](https://www.youtube.com/watch?v=8GCXtCjtg40)
### Samples
@@ -1,205 +0,0 @@
Navigation describes the way users move around your app. Users interact with UI
elements, usually by tapping or clicking on them, and the app responds by
displaying new content. If the user wants to go back to the previous content,
they use the back gesture or tap the back button.
## Modeling navigation state
A convenient way of modeling this behavior is with a stack of content. As the
user navigates *forward* to new content, it is pushed on top of the stack. When
they go *back* from that content, it is popped off the stack and the previous
content is displayed. In navigation terms, this stack is usually referred to as
the **back stack** because it represents the content that the user *can go back
to*.
![A software keyboard action button (a checkmark icon) circled in red.](https://developer.android.com/static/images/topic/libraries/architecture/back-stack.png) **Figure 1.** Diagram showing how the back stack changes with user navigation events.
## Create a back stack
In Navigation 3, the back stack does not actually contain content. Instead, it
contains *references to content* , known as **keys**. Keys can be any type but
are usually simple, serializable data classes. Using references rather than
content has the following benefits:
- It is simple to navigate by pushing keys onto the back stack.
- As long as the keys are serializable, the back stack can be saved to persistent storage, allowing it to survive configuration changes and process death. This is important because users expect to leave your app, come back to it later, and pick up where they left off with the same content being displayed. See [Save your back stack](https://developer.android.com/guide/navigation/navigation-3/save-state#save-back) for more information.
A key concept in the Navigation 3 API is that you own the back stack. The
library:
- Expects that your back stack will be a snapshot-state backed `List<T>`, where `T` is the type of your back stack `keys`. You can use `Any` or you can provide your own, more strongly-typed keys. When you see the terms "push" or "pop", the underlying implementation is to add or remove items from the end of a list.
- Observes your back stack and reflects its state in the UI using a [`NavDisplay`](https://developer.android.com/reference/kotlin/androidx/navigation3/ui/package-summary#NavDisplay(kotlin.collections.List,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment,kotlin.Function0,kotlin.collections.List,androidx.navigation3.scene.SceneStrategy,androidx.compose.animation.SharedTransitionScope,androidx.compose.animation.SizeTransform,kotlin.Function1,kotlin.Function1,kotlin.Function2,kotlin.Function1)).
The following example shows how to create keys and a back stack, and modify the
back stack in response to user navigation events:
```kotlin
// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)
@Composable
fun MyApp() {
// Create a back stack, specifying the key the app should start with
val backStack = remember { mutableStateListOf<Any>(ProductList) }
// Supply your back stack to a NavDisplay so it can reflect changes in the UI
// ...more on this below...
// Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
backStack.add(ProductDetail(id = "ABC"))
// Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
backStack.removeLastOrNull()
}
```
<br />
## Resolve keys to content
Content is modeled in Navigation 3 using [`NavEntry`](https://developer.android.com/reference/kotlin/androidx/navigation3/runtime/NavEntry), which is a class
containing a composable function. It represents a *destination* - a single piece
of content that the user can navigate *forward to* and *back from*.
A `NavEntry` can also contain metadata - information about the content. This
metadata can be read by container objects, like `NavDisplay`, to help them
decide how to display the `NavEntry`'s content. For example, metadata can be
used to override the default animations for a specific `NavEntry`. NavEntry
`metadata` is a map of `String` keys to `Any` values, providing versatile data
storage.
To convert a `key` to a `NavEntry`, create an Entry Provider. This is a
function that accepts a `key` and returns a `NavEntry` for that `key`. It is
usually defined as a lambda parameter when creating a `NavDisplay`.
There are two ways to create an Entry Provider, either by creating a lambda
function directly, or by using the [`entryProvider`](https://developer.android.com/reference/kotlin/androidx/navigation3/runtime/package-summary#entryProvider(kotlin.Function1,kotlin.Function1)) DSL.
### Create an Entry Provider function directly
You typically create an Entry Provider function using a `when` statement, with
a branch for each of your keys.
```kotlin
entryProvider = { key ->
when (key) {
is ProductList -> NavEntry(key) { Text("Product List") }
is ProductDetail -> NavEntry(
key,
metadata = mapOf("extraDataKey" to "extraDataValue")
) { Text("Product ${key.id} ") }
else -> {
NavEntry(Unit) { Text(text = "Invalid Key: $it") }
}
}
}
```
<br />
### Use the `entryProvider` DSL
The `entryProvider` DSL can simplify your lambda function by avoiding the need
to test against each of your key types, and construct a `NavEntry` for each one.
Use the `entryProvider` builder function for this. It also includes default
fallback behavior (throwing an error) if the key isn't found.
```kotlin
entryProvider = entryProvider {
entry<ProductList> { Text("Product List") }
entry<ProductDetail>(
metadata = mapOf("extraDataKey" to "extraDataValue")
) { key -> Text("Product ${key.id} ") }
}
```
<br />
Note the following from the snippet:
- `entry` is used to define a `NavEntry` with the given type and composable content
- `entry` accepts a `metadata` parameter to set `NavEntry.metadata`
## Display the back stack
The back stack represents your app's navigation state. Whenever the back stack
changes, the app UI should reflect the new back stack state. In Navigation 3, a
`NavDisplay` observes your back stack and updates its UI accordingly. Construct
it with following parameters:
- Your back stack - this should be of type `SnapshotStateList<T>`, where `T` is the type of your back stack keys. It is an observable `List` so that it triggers recomposition of `NavDisplay` when it changes.
- An `entryProvider` to convert the keys in your back stack to `NavEntry` objects.
- Optionally, supply a lambda to the `onBack` parameter. This is called when the user triggers a back event.
The following example shows how to create a `NavDisplay`.
```kotlin
data object Home
data class Product(val id: String)
@Composable
fun NavExample() {
val backStack = remember { mutableStateListOf<Any>(Home) }
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = { key ->
when (key) {
is Home -> NavEntry(key) {
ContentGreen("Welcome to Nav3") {
Button(onClick = {
backStack.add(Product("123"))
}) {
Text("Click to navigate")
}
}
}
is Product -> NavEntry(key) {
ContentBlue("Product ${key.id} ")
}
else -> NavEntry(Unit) { Text("Unknown route") }
}
}
)
}
```
<br />
By default, the `NavDisplay` shows the topmost `NavEntry` on the back stack in a
single pane layout. The following recording shows this app running:
![`NavDisplay` default behavior with two
destinations.](https://developer.android.com/static/images/topic/libraries/architecture/nav3.gif) **Figure 2.** `NavDisplay` default behavior with two destinations.
## Putting it all together
The following diagram shows how data flows between the various objects in
Navigation 3:
![A visualization of how data flows between the various objects in Navigation 3.](https://developer.android.com/static/images/topic/libraries/architecture/data-flow.png) **Figure 3.** Diagram showing how data flows through various objects in Navigation 3.
1. **Navigation events initiate changes**. Keys are added or removed from the
back stack in response to user interactions.
2. **Change in back stack state triggers content retrieval** . The `NavDisplay`
(a composable that renders a back stack) observes the back stack. In its
default configuration, it displays the topmost back stack entry in a single
pane layout. When the top key on the back stack changes, the `NavDisplay` uses
this key to request the corresponding content from the entry provider.
3. **Entry provider supplies content** . The entry provider is a function that
resolves a key to a `NavEntry`. Upon receiving a key from the `NavDisplay`,
the entry provider provides the associated `NavEntry`, which contains both the
key and the content.
4. **Content is displayed** . The `NavDisplay` receives the `NavEntry` and
displays the content.
@@ -1,60 +0,0 @@
To get started with Navigation 3, add the library to your project along with any
supporting libraries. Use the following table to decide which libraries to add:
## Artifacts
| **Name** | **What it does** | **Artifact** |
|---|---|---|
| Navigation 3 runtime library | Core Navigation 3 API. Includes `NavEntry`, `EntryProvider` and the associated DSL. | [androidx.navigation3:navigation3-runtime](https://developer.android.com/jetpack/androidx/releases/navigation3) |
| Navigation 3 UI library | Provides classes to display content, including `NavDisplay` and `Scene`. | [androidx.navigation3:navigation3-ui](https://developer.android.com/jetpack/androidx/releases/navigation3) |
| ViewModel Lifecycle for Navigation 3 | Allows ViewModels to be scoped to entries in the back stack. | [androidx.lifecycle:lifecycle-viewmodel-navigation3](https://developer.android.com/jetpack/androidx/releases/lifecycle#lifecycle.lifecycle-viewmodel-navigation3) |
| Material 3 adaptive layouts for Navigation 3 | Provides adaptive layouts (SceneStrategies, Scenes and metadata definitions) for use with NavDisplay. | [androidx.compose.material3.adaptive:adaptive-navigation3](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive) |
| [KotlinX Serialization](https://kotlinlang.org/docs/serialization.html) | Allows navigation keys to be serialized. | Plugin: org.jetbrains.kotlin.plugin.serialization Library: org.jetbrains.kotlinx:kotlinx-serialization-core |
## Project setup
To add the Navigation 3 library to your existing project, add the following to
your `libs.versions.toml`:
[versions]
nav3Core = "1.0.1"
lifecycleViewmodelNav3 = "2.11.0-alpha01"
kotlinSerialization = "2.2.21"
kotlinxSerializationCore = "1.9.0"
material3AdaptiveNav3 = "1.3.0-alpha09"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Optional add-on libraries
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
[plugins]
# Optional plugins
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"}
Also, update your compile SDK to 36 or later:
[versions]
compileSdk = "36"
Add the following to your **app** build file `app/build.gradle.kts`:
plugins {
...
// Optional, provides the @Serialize annotation for autogeneration of Serializers.
alias(libs.plugins.jetbrains.kotlin.serialization)
}
dependencies {
...
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material3.adaptive.navigation3)
implementation(libs.kotlinx.serialization.core)
}
@@ -1,89 +0,0 @@
The following sections describe strategies for saving your back stack and
storing state associated with entries on your back stack.
## Save your back stack
Ensuring your app's navigation state persists across various lifecycle events,
including configuration changes and process death, is crucial for a good user
experience. In Navigation 3, you own your back stack, so there aren't strict
guidelines on how you should create or save it. However, Navigation 3 does offer
a convenience method that provides you with a saveable back stack:
[`rememberNavBackStack`](https://developer.android.com/reference/kotlin/androidx/navigation3/runtime/package-summary#rememberNavBackStack(kotlin.Array)).
### Use `rememberNavBackStack`
The `rememberNavBackStack` composable function is designed to create a back
stack that persists across configuration changes and process death.
For `rememberNavBackStack` to function correctly, each key in your back stack
must adhere to specific requirements:
- **Implement `NavKey` interface** : Every key in the back stack must implement the [`NavKey`](https://developer.android.com/reference/kotlin/androidx/navigation3/runtime/NavKey) interface. This acts as a marker interface that signals to the library that the key can be saved.
- **Have the `@Serializable` annotation** : In addition to implementing `NavKey`, your key classes and objects must be marked with the `@Serializable` annotation.
The following snippet shows a correct implementation of `rememberNavBackStack`:
```kotlin
@Serializable
data object Home : NavKey
@Composable
fun NavBackStack() {
val backStack = rememberNavBackStack(Home)
}
```
<br />
### Alternative: Storing in a `ViewModel`
Another approach to managing your back stack is to store it in a `ViewModel`.
For persistence through process death when using a `ViewModel` or any other
custom storage, you need to:
- **Ensure your keys are serializable** : Just like with `rememberNavBackStack`, your navigation keys must be serializable.
- **Handle serialization and deserialization manually** : You're responsible for manually saving the serialized representation of each key to, and deserializing it from, persistent storage (e.g., `SharedPreferences`, a database, or a file) when your app is going into the background or being restored.
## Scoping `ViewModel`s to `NavEntry`s
`ViewModels` are used to retain UI-related state across configuration changes,
such as screen rotations. By default, `ViewModels` are scoped to the nearest
`ViewModelStoreOwner`, which is typically your `Activity` or `Fragment`.
However, you might want to scope a `ViewModel` to a specific `NavEntry` (i.e., a
specific screen or destination) on the back stack, rather than the entire
`Activity`. This ensures that the `ViewModel`'s state is retained only while
that particular `NavEntry` is part of the back stack, and is cleared when the
`NavEntry` is popped.
The `androidx.lifecycle:lifecycle-viewmodel-navigation3` add-on library provides
a [`NavEntryDecorator`](https://developer.android.com/guide/navigation/navigation-3/naventrydecorators) that facilitates this. This decorator provides a
`ViewModelStoreOwner` for each `NavEntry`. When you create a `ViewModel` inside a
`NavEntry`'s content (e.g., using `viewModel()` in Compose), it is automatically
scoped to that specific `NavEntry`'s key on the back stack. This means the
`ViewModel` is created when the `NavEntry` is added to the back stack, and
cleared when it's removed.
To use [`NavEntryDecorator`](https://developer.android.com/guide/navigation/navigation-3/naventrydecorators#apply-decorators) for scoping `ViewModel`s to `NavEntry`s, follow
these steps:
1. Add the `androidx.lifecycle:lifecycle-viewmodel-navigation3` dependency to your `app/build.gradle.kts` file.
2. Add the default [`rememberSaveableStateHolderNavEntryDecorator()`](https://developer.android.com/reference/kotlin/androidx/navigation3/runtime/package-summary#rememberSaveableStateHolderNavEntryDecorator(androidx.compose.runtime.saveable.SaveableStateHolder)) to the list of `entryDecorators` when constructing a `NavDisplay`.
3. Add [`rememberViewModelStoreNavEntryDecorator()`](https://developer.android.com/reference/kotlin/androidx/lifecycle/viewmodel/navigation3/package-summary#rememberViewModelStoreNavEntryDecorator(androidx.lifecycle.ViewModelStoreOwner,kotlin.Function0)) to the list of `entryDecorators`.
```kotlin
NavDisplay(
entryDecorators = listOf(
// Add the default decorators for managing scenes and saving state
rememberSaveableStateHolderNavEntryDecorator(),
// Then add the view model store decorator
rememberViewModelStoreNavEntryDecorator()
),
backStack = backStack,
entryProvider = entryProvider { },
)
```
<br />
@@ -1,56 +0,0 @@
> [!NOTE]
> **Note:** Gemini in Android Studio is only available in the latest stable channel version of Android Studio and major versions released in the previous 10 months. If you are using an older version of Android Studio, you will need to update to use Gemini. For more information, see [Android Studio and Cloud services
> compatibility](https://developer.android.com/studio/releases#service-compat).
Gemini in Android Studio is your AI-powered coding companion for Android
development. It helps you be more productive by answering your development
queries, generating code, finding relevant resources, and encouraging best
practices. Most importantly, Gemini in Android Studio is uniquely equipped to
accelerate Android development---it can help you mock up and troubleshoot Compose
UIs, fix Gradle build errors, analyze crashes through integrations with Logcat
and App Quality Insights, and more.
[Video](https://www.youtube.com/watch?v=zGK1vIX87vw)
Gemini in Android Studio is available in the following editions:
- [No-cost tier](https://developer.android.com/studio/gemini/get-started)
- [Business tier](https://developer.android.com/studio/gemini/get-started-businesses), available through a subscription to [Gemini Code Assist](https://developers.google.com/gemini-code-assist/docs/overview) or membership in the [Google Developer Program](https://developers.google.com/program).
- Standard tier
- Enterprise tier
## What is the right tier for me?
If you're an individual freelance developer, student, or hobbyist we recommend
using [Gemini in Android Studio](https://developer.android.com/studio/gemini/get-started), available at no
cost. This includes access to a lightweight version of Gemini 2.5 Pro with a
smaller context window, which is appropriate for most tasks. If you need
to execute more complex tasks that would benefit from Gemini 2.5 Pro's full 1M
token context window, you can [add a Gemini API key](https://developer.android.com/studio/gemini/add-api-key)
and pay per-token.
If you're a professional developer who works as part of a team, ask your
administrator to consider subscribing to the [business tier](https://developer.android.com/studio/gemini/get-started-businesses). Gemini for
businesses includes additional features that make it easier to collaborate with
your teammates while incorporating your company's best practices and standards,
including but not limited to:
- Expanded 1M token context window
- [IP indemnification](https://cloud.google.com/gemini/docs/discover/works#how-gemini-protects)
- [VPC-SC and Private Google Access](https://developers.google.com/gemini-code-assist/docs/configure-vpc-service-controls)
- Integration with tools such as Firebase, Colab Enterprise, BigQuery data insights, Cloud Run, and Database Studio.
- Analytics to track your team's usage and productivity impact.
- (Enterprise tier only) Customized code suggestions from your codebases in external tools such as GitHub.
For more details about the different offerings, see
[Feature comparison](https://developer.android.com/studio/gemini/feature-comparison).
## What's new in Gemini in Android Studio
Gemini in Android Studio is under active development and updates frequently! You
can learn about the latest AI features and other preview features by checking
out the [Android Studio preview release notes](https://developer.android.com/studio/preview/features) and by
[downloading a preview](https://developer.android.com/studio/preview).
## Permissions
See [service permissions](https://developer.android.com/studio/services#service-permissions).
@@ -1,358 +0,0 @@
App architecture is the foundation of a high-quality Android application. A
well-defined architecture enables you to create a scalable, maintainable app
that can adapt to the ever-expanding ecosystem of Android devices, including
phones, tablets, foldables, ChromeOS devices, car displays, and XR.
## App composition
A typical Android app is composed of multiple [app components](https://developer.android.com/guide/components/fundamentals#components),
such as [services](https://developer.android.com/guide/components/services), [content providers](https://developer.android.com/guide/topics/providers/content-providers), and [broadcast
receivers](https://developer.android.com/guide/components/broadcasts). You declare these components in your
[app manifest](https://developer.android.com/guide/topics/manifest/manifest-intro).
The user interface of an app is also a component. Historically, UIs were built
using multiple [activities](https://developer.android.com/guide/components/activities/intro-activities). However, modern apps use a
single-activity architecture. A single `Activity` serves as a container for
screens implemented as [fragments](https://developer.android.com/guide/fragments) or Jetpack Compose destinations.
### Multiple form factors
Apps can run on multiple form factors, including not just phones, but also
tablets, foldables, ChromeOS devices, and more. An app can't assume a portrait
or landscape orientation. Configuration changes, such as device rotation or
folding and unfolding a foldable device, force your app to recompose its UI,
which affects app data and state.
### Resource constraints
Mobile devices---even large screen devices---are resource constrained,
so at any time, the operating system might stop some app processes to make room
for new ones.
### Variable launch conditions
In a resource-constrained environment, the components of your app can be
launched individually and out of order; what's more, the operating system or
user can destroy them at any time. As a result, don't store any application data
or state in your app components. Your app components should be self-contained,
independent of each other.
## Common architectural principles
If you can't use app components to store application data and state, how
should you design your app?
As Android apps grow in size, it's important to define an architecture that
allows the app to scale. A well-designed app architecture defines the boundaries
between parts of the app and the responsibilities each part should have.
### Separation of concerns
Design your app architecture to follow a few specific principles.
The most important principle is [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns). It's
a common mistake to write all your code in an [`Activity`](https://developer.android.com/guide/components/activities/intro-activities) or a
[`Fragment`](https://developer.android.com/guide/fragments).
The primary role of an `Activity` or `Fragment` is to host your app's UI. The
Android OS controls their lifecycle, frequently destroying and recreating them
in response to user actions like screen rotation or system events like low
memory.
This ephemeral nature makes them unsuitable for holding application data or
state. If you store data in an `Activity` or `Fragment`, that data is lost when
the component is recreated. To ensure data persistence and provide a stable user
experience, don't entrust state to these UI components.
### Adaptive layouts
Your app should gracefully handle configuration changes, such as device
[orientation changes](https://developer.android.com/develop/ui/compose/layouts/adaptive/app-orientation-aspect-ratio-resizability) or changes in the size of the app window.
Implement the adaptive [canonical layouts](https://developer.android.com/develop/ui/compose/layouts/adaptive/canonical-layouts) to provide an
optimal user experience on a variety of form factors.
### Drive UI from data models
Another important principle is that you should drive your UI from data models,
preferably persistent models. Data models represent the data of an app. They're
independent from the UI elements and other components in your app. This means
that they are not tied to the UI and app component lifecycle but will still be
destroyed when the OS removes the app's process from memory.
Persistent models are ideal for the following reasons:
- Users don't lose data if the Android OS destroys your app to free up
resources.
- Your app continues to work in cases when a network connection is
intermittent or unavailable.
Base your app architecture on data model classes to make your app robust and
testable.
### Single source of truth
When a new data type is defined in your app, you should assign a single source
of truth (SSOT) to it. The SSOT is the *owner* of that data, and only the SSOT
can modify or mutate it. To achieve this, the SSOT exposes the data using an
immutable type; to modify the data, the SSOT exposes functions or receives
events that other types can call.
This pattern has multiple benefits:
- Centralizes all changes to a particular type of data in one place
- Protects the data so that other types cannot tamper with it
- Makes changes to the data more traceable, and so, bugs are easier to spot
In an offline-first application, the source of truth for application data is
typically a database. In some other cases, the source of truth can be a
[`ViewModel`](https://developer.android.com/topic/libraries/architecture/viewmodel).
### Unidirectional data flow
The [single source of truth principle](https://developer.android.com/topic/architecture#single-source-of-truth) is often used
with the unidirectional data flow (UDF) pattern. In UDF, **state** flows in only
one direction, typically from parent component to child component. The events
that modify the data flow in the opposite direction.
In Android, state or data usually flow from the higher-scoped types of the
hierarchy to the lower-scoped ones. Events are usually triggered from the
lower-scoped types until they reach the SSOT for the corresponding data type.
For example, application data usually flows from data sources to the UI. User
events such as button presses flow from the UI to the SSOT where the
application data is modified and exposed in an immutable type.
This pattern better maintains data consistency, is less prone to errors, is
easier to debug, and provides all the benefits of the SSOT pattern.
## Recommended app architecture
Considering common architectural principles, each application should have at
least two layers:
- **UI layer:** Displays application data on the screen
- **Data layer:** Contains the business logic of your app and exposes application data
You can add an additional layer called the *domain layer* to simplify and reuse
the interactions between the UI and data layers.
![In a typical app architecture, the UI layer gets the application data
from the data layer or from the optional domain layer, which sits between
the UI layer and the data layer.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-overview.png) **Figure 1.** Diagram of a typical app architecture.
> [!NOTE]
> **Note:** The arrows in the diagrams in this guide represent dependencies between classes. For example, the domain layer depends on data layer classes.
### Modern app architecture
A modern Android app architecture uses the following techniques (among
others):
- Adaptive and layered architecture
- Unidirectional data flow (UDF) in all layers of the app
- UI layer with state holders to manage the complexity of the UI
- Coroutines and flows
- Dependency injection best practices
For more information, see
[Recommendations for Android architecture](https://developer.android.com/topic/architecture/recommendations).
### UI layer
The role of the UI layer (or *presentation layer*) is to display the application
data on screen. Whenever the data changes, either due to user interaction
(such as pressing a button) or external input (such as a network response), the
UI should update to reflect the changes.
The UI layer comprises two types of constructs:
- UI elements that render the data on the screen. You build these elements using [Jetpack Compose](https://developer.android.com/jetpack/compose) functions to support adaptive layouts.
- State holders (such as [`ViewModel`](https://developer.android.com/topic/libraries/architecture/viewmodel)) that hold data, expose it to the UI, and handle logic
![In a typical architecture, the UI layer's UI elements depend on state
holders, which in turn depend on classes from either the data layer or the
optional domain layer.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-overview-ui.png) **Figure 2.** The role of the UI layer in app architecture.
For adaptive UIs, state holders such as `ViewModel` objects expose UI state that
adapts to different [window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/window-size-classes). You can use
`currentWindowAdaptiveInfo()` to derive this UI state. Components like
`NavigationSuiteScaffold` can then use this information to automatically switch
between different navigation patterns (for example, `NavigationBar`,
`NavigationRail`, or `NavigationDrawer`) based on the available screen space.
To learn more, see the [UI layer page](https://developer.android.com/jetpack/guide/ui-layer).
### Data layer
The data layer of an app contains the *business logic*. Business logic is what
gives value to your app---it comprises rules that determine how your app
creates, stores, and changes data.
The data layer is made up of repositories each of which can contain zero to many
data sources. You should create a repository class for each different type of
data you handle in your app. For example, you might create a `MoviesRepository`
class for data related to movies or a `PaymentsRepository` class for data
related to payments.
![In a typical architecture, the data layer's repositories provide data
to the rest of the app and depend on the data sources.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-overview-data.png) **Figure 3.** The role of the data layer in app architecture.
Repository classes are responsible for the following:
- Exposing data to the rest of the app
- Centralizing changes to the data
- Resolving conflicts between multiple data sources
- Abstracting sources of data from the rest of the app
- Containing business logic
Each data source class should have the responsibility of working with only one
source of data, which can be a file, a network source, or a local database.
Data-source classes are the bridge between the application and the system for
data operations.
To learn more, see the [data layer page](https://developer.android.com/jetpack/guide/data-layer).
### Domain layer
The domain layer is an optional layer between the UI and data layers.
The domain layer is responsible for encapsulating complex business logic or
simpler business logic that is reused by multiple view models. The domain layer
is optional because not all apps have these requirements. Use it only when
needed, for example, to handle complexity or favor reusability.
![When it is included, the optional domain layer provides dependencies to
the UI layer and depends on the data layer.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-overview-domain.png) **Figure 4.** The role of the domain layer in app architecture.
Classes in the domain layer are commonly called *use cases* or *interactors* .
Each use case should have responsibility for a single functionality. For
example, your app could have a `GetTimeZoneUseCase` class if multiple view
models rely on time zones to display the proper message on the screen.
To learn more, see the [domain layer page](https://developer.android.com/jetpack/guide/domain-layer).
## Manage dependencies between components
Classes in your app depend on other classes to function properly. You can use
either of the following design patterns to gather the dependencies of a
particular class:
- [Dependency injection (DI)](https://developer.android.com/training/dependency-injection): Dependency injection allows classes to define their dependencies without constructing them. At runtime, another class is responsible for providing these dependencies.
- [Service locator](https://en.wikipedia.org/wiki/Service_locator_pattern): The service locator pattern provides a registry where classes can obtain their dependencies instead of constructing them.
These patterns allow you to scale your code because they provide clear patterns
for managing dependencies without duplicating code or adding complexity. The
patterns also allow you to quickly switch between test and production
implementations.
> [!IMPORTANT]
> **Key Point:** Use the dependency injection pattern and the [Hilt library](https://developer.android.com/training/dependency-injection/hilt-android) in Android apps. Hilt automatically constructs objects by walking the dependency tree, provides compile-time verification of dependencies, and creates dependency containers for Android framework classes.
## General best practices
Programming is a creative field, and building Android apps isn't an exception.
There are many ways to solve a problem; you might communicate data between
multiple activities or fragments, retrieve remote data and persist it locally
for offline mode, or handle any number of other common scenarios that nontrivial
apps encounter.
Although the following recommendations aren't mandatory, in most cases following
them makes your codebase more robust, testable, and maintainable.
**Don't store data in app components.**
Avoid designating your app's entry points---such as activities, services,
and broadcast receivers---as sources of data. The entry points should only
coordinate with other components to retrieve the subset of data that is relevant
to that entry point. Each app component is shortlived, depending
on the user's interaction with their device and capacity of the system.
**Reduce dependencies on Android classes.**
Your app components should be the only classes that rely on Android framework
SDK APIs such as [`Context`](https://developer.android.com/reference/android/content/Context) or [`Toast`](https://developer.android.com/guide/topics/ui/notifiers/toasts). Abstracting other classes in your
app away from the app components helps with testability and reduces
[coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)) within your app.
**Define clear boundaries of responsibility between modules in your app.**
Don't spread the code that loads data from the network across multiple classes
or packages in your codebase. Similarly, don't define multiple unrelated
responsibilities, such as data caching and data binding, in the same class.
Following the [recommended app architecture](https://developer.android.com/topic/architecture#recommended-app-arch) will help.
**Expose as little as possible from each module.**
Don't create shortcuts that expose internal implementation details. You might
gain a bit of time in the short term, but you are then likely to incur technical
debt many times over as your codebase evolves.
**Focus on the unique core of your app so it stands out from other apps.**
Don't reinvent the wheel by writing the same boilerplate code again and again.
Instead, focus your time and energy on what makes your app unique. Let the
Jetpack libraries and other recommended libraries handle the repetitive
boilerplate.
**Use canonical layouts and app design patterns.**
The Jetpack Compose libraries provide robust APIs for building adaptive user
interfaces. Use the [canonical layouts](https://developer.android.com/develop/ui/compose/layouts/adaptive/canonical-layouts) in your app to
optimize the user experience on multiple form factors and display sizes. Review
the [gallery](https://developer.android.com/large-screens/gallery) of app design patterns to select the layouts that work
best for your use cases.
**Preserve UI state across configuration changes.**
When designing for adaptive layouts, preserve UI state across configuration
changes such as display resizing, folding, and orientation changes. Your
architecture should verify that the user's current state is maintained,
providing a seamless experience.
**Design reusable and composable UI components.**
Build UI components that are reusable and composable to support adaptive design.
This lets you combine and rearrange components to fit various screen sizes and
postures without significant refactoring.
**Consider how to make each part of your app testable in isolation.**
A well-defined API for fetching data from the network facilitates testing the
module that persists that data in a local database. If instead, you mix the
logic from these two functions in one place, or distribute your networking code
across your entire codebase, testing becomes much more difficult, if not
impossible.
**Types are responsible for their concurrency policy.**
If a type is performing long-running blocking work, the type should be
responsible for moving that computation to the right thread. The type knows the
kind of computation that it is doing and in which thread the computation should
be executed. Types should be mainsafe, meaning they're safe to call from
the main thread without blocking it.
**Persist as much relevant and fresh data as possible.**
That way, users can enjoy your app's functionality even when their device is in
offline mode. Remember that not all of your users enjoy constant,
highspeed connectivity, and even if they do, they can get bad reception
in crowded places.
## Benefits of architecture
Having a good architecture implemented in your app brings a lot of benefits to
the project and engineering teams:
- Improves the maintainability, quality, and robustness of the overall app.
- Allows the app to scale. More people and more teams can contribute to the same codebase with minimal code conflicts.
- Helps with onboarding. As architecture brings consistency to your project, new members of the team can quickly get up to speed and be more efficient in less amount of time.
- Easier to test. A good architecture encourages simpler types which are generally easier to test.
- Bugs can be investigated methodically with well defined processes.
Investing in architecture also has a direct impact on users. They benefit from a
more stable application and more features due to a more productive engineering
team. However, architecture also requires an upfront time investment. To
help you justify this time to the rest of your organization, take a look at
these [case studies](https://developer.android.com/quality) where other companies share their success stories
about having a good architecture in their app.
## Samples
The following samples demonstrate good app architecture:
@@ -0,0 +1,93 @@
---
name: play-billing-library-version-upgrade
description: Use this skill when upgrading or migrating an Android project from
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- android
- play billing
- play billing library
- pbl
- upgrade
- ''
---
any legacy Google Play Billing Library (PBL) version to the latest stable
version of PBL.
keywords_public: android, play billing, play billing library, pbl, upgrade,
migration, deprecation, google play
## Phase 0: Intent Message
**Reporting Action**: Before proceeding, immediately tell the user: "I will
upgrade Play Billing Library to the latest version."
## Phase 1: Discovery \& Situational Awareness
1. **Primary Check (Build Version)** : Locate the project's billing dependency (e.g., `com.android.billingclient:billing`) in `build.gradle`, `build.gradle.kts`, or `libs.versions.toml`.
2. **Initial Compilation Test**: Attempt to sync and build the project immediately.
3. **Fallback Discovery (Effective Version)** :
- **Trigger**: Only if the build fails immediately, scan the source code for deprecated artifacts.
- **Logic** : The presence of deprecated APIs indicates the **"Effective
Version"** ---defined as the version where those specific APIs were **last
available**, not when they were introduced.
- **Example** : If `SkuDetails` is present, treat the baseline as **PBL v7** or earlier (regardless of the version string in `build.gradle`).
4. **Identify Target \& Path:** Access the version tool or release notes to find the latest stable version and calculate a \[Direct/Stepped\] migration path based on the **Effective Version** baseline.
- **Calculate Migration Path** :
- If the **Effective Version** is within 2 major versions of the target: Plan a **Direct Migration**.
- If it is more than 2 major versions behind: Plan a **Stepped
Migration**. Migrate by two major versions at a time (e.g., v4 -\> v6 -\> v8) until you are within two versions of the target.
- **Reporting Action**: Before proceeding, tell the user: "I've detected you are effectively on PBL \[Current\] and the latest is \[Target\]. I am planning a \[direct/stepped\] migration path."
## Phase 2: Contextual Document Mapping \& Planning
For every major version jump identified in your path, you **MUST** synthesize
instructions from:
- **Migration Guide** : `https://developer.android.com/google/play/billing/migrate-gpblv[X]` (where X is the target major version).
- **Release Highlights** : The "Deprecations" and "Breaking Changes" sections of the relevant release notes at `https://developer.android.com/google/play/billing/release-notes`.
- **Developer Documentation**: Consult your knowledge of the Google Play Billing documentation regarding the relevant features used in this app (e.g., Subscriptions, One-Time Products).
- **Develop the Plan**: Identify every specific code change required (API removals, class replacements, logic shifts) and print this out as a checklist.
## Phase 3: Instructions for Execution
*Reporting Action: For each of the following steps, give a brief explanation of
what you will be doing prior to execution, and a brief summary of what you
accomplished afterwards.*
### Step 1: SDK \& Environment Alignment
- **Action** : Update `build.gradle` to meet SDK requirements (e.g., "PBL 8 requires `compileSdk` 35").
- **Gradle Version**: Verify if the new library requires a newer Android Gradle Plugin (AGP) or Kotlin version.
### Step 2: Intent-based Refactoring
Analyze the intent of the existing code rather than performing purely textual
string replacement.
- **Action** : You **MUST** follow all deprecation instructions and refactor patterns from **both** the [references/migration-logic.md](references/migration-logic.md) section, the official migration guides, and the general documentation pages identified in Phase 2.
- **Verification** : Verify you are doing **all steps from all documentation** and then making sure you follow the specific directions from the checklist in the references.
### Step 3: Sequential Verification (Only applicable for Stepped Migrations)
1. **Upgrade** to the first major intermediate version in your path.
2. **Run `./gradlew assembleDebug`** to verify no intermediate breaking changes were missed.
3. **Repeat** until you reach the final target version.
### Step 4: Final Validation Checklist
1. **Smart Checklist Verification:**
2. Open [references/version-checklist.md](references/version-checklist.md) and locate the **Smart Version-Specific Checklist**.
3. **Action**: For every version between your \[Detected Effective Version\] and \[Detected New Version\], verify that every item has been addressed in the code using "Find in Files" or structural analysis.
4. **Tests** : Run all unit and implementation tests (`./gradlew test`).
5. **Clean Build** : Verify the project completes a full clean build: `./gradlew clean assembleDebug`. Then, run `./gradlew sync` and `./gradlew build` so that the user can immediately test the new version manually.
## Final Report
Explain the "Why" to the developer:
- "I updated your SDK to \[Version\] because PBL \[Version\] requires it for \[Reason from docs\]."
- "I removed your custom `retryConnection()` logic because it is now handled natively by the library using `enableAutoServiceReconnection()`."
- "Successfully upgraded from PBL \[Old\] to PBL \[New\] and verified with unit tests. Based on an analysis of features in the latest library and this application's current feature set, I suggest exploring \[New Feature\] (e.g., Prepaid Plans or Installments) from the latest release because it is now available but not yet implemented."
@@ -0,0 +1,76 @@
## High-Impact Migration Logic
### 1. Connection Management (The v8+ Reconnection Shift)
**Intent**: Move from developer-managed state (manual retries) to
library-managed state.
- **Remove** : Manual `startConnection()` calls or retry timers inside `onServiceDisconnected()`.
- **Add** : `.enableAutoServiceReconnection()` to `BillingClient.Builder`.
- **Logic** : In v8+, the library handles transient disconnections. Your `onServiceDisconnected` must only be used for logging or updating UI state (e.g., "Billing service temporarily unavailable").
### 2. Product Querying \& Models (The v5-v8 Architectural Shift)
**Intent**: Support the "One Product, Multiple Offers" model introduced in v5
and refined in v8.
- **Data Model Swap** :
- **Legacy** : `SkuDetails` (1:1 mapping of ID to price).
- **Modern** : `ProductDetails`. A single `ProductDetails` can contain multiple `SubscriptionOfferDetails` (Base Plans + Offers).
- **Result Handling (v8+ Logic)** :
- **Change** : `queryProductDetailsAsync` no longer returns a list in the listener.
- **New Intent** : You must receive a `QueryProductDetailsResult` object.
- **Refactor** : `kotlin
// PBL 8+ Pattern
billingClient.queryProductDetailsAsync(params) { result: QueryProductDetailsResult ->
val responseCode = result.billingResult.responseCode
val productDetailsList = result.productDetailsList // Retrieve list from result object
// Process list...
}`
### 3. Subscription Modernization (v6 \& v7)
**Intent**: Support "Base Plans" and "Offers" instead of legacy standalone SKUs.
- **Subscription Upgrades/Downgrades (v6+)**:
- **Legacy** : `setOldSkuPurchaseToken()` in `BillingFlowParams`.
- **Modern** : Use `SubscriptionUpdateParams`. You must specify the `PurchaseToken` of the existing subscription and the `ReplacementMode` (which replaces the deprecated `ProrationMode`).
- **Logic** : Verify that the `ReplacementMode` matches the business intent (e.g., `CHARGE_FULL_PRICE` versus `WITH_TIME_PRORATION`).
- **Installment Plans (v7+)**:
- **Intent**: Allow users to pay for a subscription in monthly installments.
- **Check** : Look for `InstallmentPlanDetails` within `SubscriptionOfferDetails`. If the app supports high-ticket subscriptions, it is mandatory to implement the `installmentPlanDetails` UI.
### 4. Purchase Handling \& History (v6+)
**Intent**: Move away from local-only purchase caches to real-time status
checks.
- **Active Purchases** :
- **Deprecated** : `queryPurchases()` (synchronous).
- **Mandatory** : `queryPurchasesAsync()`. You must pass `QueryPurchasesParams` containing the `ProductType` (`INAPP` or `SUBS`).
- **Purchase History (Pagination Intent)** :
- **v6+ Change** : `queryPurchaseHistoryAsync` is optimized for pagination.
- **Logic** : If the app has thousands of historical transactions, verify you are using the `PurchaseHistoryRecord` list correctly to avoid memory overhead.
### 5. Security \& Pending Transactions (The "Always On" Rule)
- **Mandatory** : `enablePendingPurchases()` has been required since v3, but in v8+, verify that it is called before `.build()`. You must also include `.enableOneTimeProducts()` on the `enablePendingPurchases()` builder.
- **Optional** : If the app sells prepaid subscriptions, you must also include `.enablePrepaidPlans()`.
- **Intent**: This handles "Slow/Delayed" payments (like cash or bank transfers). Without this, the app will crash on initialization in modern versions.
### 6. SDK \& Environment Requirements
- **PBL 7.0** : Requires `compileSdk 34` or higher.
- **PBL 8.0** : Requires `compileSdk 35`.
- **Kotlin** : Verify that `kotlin-stdlib` is updated to at least 1.9.x to support new library coroutine extensions.
### 7. User-Facing Features (Post-Upgrade Recommendations)
Once the upgrade is complete, the following features are enabled by these
versions:
- **v7** : **Installments** (Monthly payments for annual plans).
- **v8** : **Prepaid Plans** (Users can top-up time without auto-renewing).
- **v8** : **Personalized Pricing** (Show legal disclosure if price varies by user).
@@ -0,0 +1,46 @@
## Play Billing Library: Smart Version-Specific Checklist
Use this checklist to verify that every technical requirement between your
\[Current Version\] and \[Target Version\] has been met.
## PBL v1.x through v3.x
- \[ \] **\[v1.0\] Builder Pattern** : Verify `BillingClient.newBuilder(context)` is used.
- \[ \] **\[v2.0\] Mandatory Acknowledgment** : Verify `acknowledgePurchase()` or `consumeAsync()` is called within 3 days.
- \[ \] **\[v2.0\] Response Types** : Logic must handle `BillingResult` objects instead of raw integers.
- \[ \] **\[v3.0\] Legacy Removal** : Verify `ChildDirected` and `UnderAgeOfConsent` parameters are deleted.
## PBL v4.x Series
- \[ \] **Async Purchasing** : Confirm `queryPurchases()` is replaced with `queryPurchasesAsync()`.
- \[ \] **Multi-SKU Accessors** : Replace `getSku()` with `getSkus()` (returns a list) in `Purchase` objects.
- \[ \] **Subscription Refactor** : Verify `setSubscriptionUpdateParams()` is used for change logic.
## PBL v5.x Series
- \[ \] **Data Model Swap** : Replace all instances of `SkuDetails` with `ProductDetails`.
- \[ \] **Personalized Pricing** : Implement `setIsOfferPersonalized()` for EU price disclosures.
## PBL v6.x Series
- \[ \] **Replacement Mode** : Replace `ProrationMode` with the `ReplacementMode` enum.
- \[ \] **User Choice Billing** : Replace `AlternativeBillingListener` with `UserChoiceBillingListener`.
## PBL v7.x Series
- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher.
- \[ \] **Pending Purchases** : Replace parameterless `enablePendingPurchases()` with `enablePendingPurchases(PendingPurchaseParams)`.
- \[ \] **API Cleanup** : Replace `setOldSkuPurchaseToken()` with `setOldPurchaseToken()`.
## PBL v8.x Series
- \[ \] **SDK Compliance** : `compileSdk` is set to 35.
- \[ \] **Terminology Shift**: Rename "in-app items" to "one-time products" in UI/strings.
- \[ \] **Signature Enforcement** : `onProductDetailsResponse` signature MUST be `(BillingResult, QueryProductDetailsResult)`.
- \[ \] **Auto-Reconnection** : Verify `enableAutoServiceReconnection()` is used in the builder.
- \[ \] **Min SDK Increase** : Verify `minSdkVersion` is at least 23.
## Future Versions (PBL 9.0.0+)
- \[ \] **Dynamic Checklist Generation** : For any version \>=9.0.0, you **MUST** synthesize a new checklist for each new version header found in the [Release Notes](https://developer.android.com/google/play/billing/release-notes).
- \[ \] **Identify Version Delta** : Review "Breaking Changes" and "Removed APIs" for the new version and create a list of terms to `grep`.
+48
View File
@@ -0,0 +1,48 @@
---
name: r8-analyzer
description: Analyzes Android build files and R8 keep rules to identify redundancies,
broad package-wide rules, and rules that subsume library consumer keep rules. Use
when developers want to optimize their app's size, remove redundant or overly broad
keep rules, or troubleshoot Proguard configurations.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- R8
- proguard
- keep rules
- app size
- optimization
- Build
---
book_path: /build/_book.yaml
project_path: /build/_project.yaml
keywords: category:Build
description: Analyzes Android build files and R8 keep rules to identify redundancies, broad package-wide rules, and rules that subsume library consumer keep rules. Use when developers want to optimize their app's size, remove redundant or overly broad keep rules, or troubleshoot Proguard configurations.
keywords_public: R8, proguard, keep rules, app size, optimization
## Core workflow
- \[ \] Step 1: Create a file called R8_Configuration_Analysis.md, or reuse if one exists already, to store the output
- \[ \] Step 2: Look at the configuration of R8 by looking at build.gradle, build.gradle.kts, gradle.properties in the codebase using [references/CONFIGURATION.md](references/CONFIGURATION.md) as the reference. Inform the developer and add the analysis to the report file
- \[ \] Step 3: If the AGP version is less than 9, suggest moving to AGP 9.0 version as AGP 9.0 includes optimizations(https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#agp-r8-behavior-changes).
- \[ \] Step 4: Look at the proguard files in the codebase and evaluate each keep rule in the following specific order: a. **Libraries check** : Check rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). If the app has keep rules targeting libraries - Google, AndroidX, Kotlin, Kotlinx, Room, Gson, Retrofit, inform the user that these are not required and suggest removal of these rules. b. **Impact analysis** : For the remaining keep rules, assess them based on the impact hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md). (Note: Do NOT assess the impact of keep rules already covered in the libraries check step).
- \[ \] Step 5: Identify subsuming keep rules in the remaining keep rules based on the hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and suggest removing the broader keep rules.
- \[ \] Step 6: For each remaining keep rule, analyze in detail the code affected by the rule by examining the code and adjacent files to understand why it was written. Look for reflection usage in those packages, and suggest a narrow and specific keep rule for the scenario using [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md).
- \[ \] Step 7: For every keep rule inform concisely and to the point what action needs to be taken - whether the rule needs to be removed/refined.
- If refining the rule, give instructions on finding a narrower and specific keep rule using the [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md).
- If removing, provide reasoning on why it needs to be removed.
- \[ \] Step 8: After keep analysis, order the keep rule analysis based on the impact to the codebase hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md)
- \[ \] Step 9: Advise the user to run tests using [UI
automator](references/android/training/testing/other-components/ui-automator.md) to assess that there is no issue with the suggested changes, concentrating on the packages where keep rules will be affected.
## Mandatory rules
- Don't make any changes in keep rule files
- Don't say about what level each keep rule is.
- Don't generate parts of the report if there is no keep rule to report in that section.
- Don't mention the generated files.
- Don't mention exceptions that occur during execution.
- Don't mention the benefits of R8
- Don't mention any files of this skill
+44
View File
@@ -0,0 +1,44 @@
To achieve maximum utilization of R8, the codebase must be configured correctly
depending on the build script language (Kotlin DSL vs. Groovy DSL).
## 1. App Modules (`com.android.application`)
The app's `build.gradle` or `build.gradle.kts` file should enable minification
and resource shrinking within the `release` build type or the apps custom build
type for release and performance testing. It MUST use the optimized default file
(`proguard-android-optimize.txt`).
**Kotlin DSL (`build.gradle.kts`):**
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
**Groovy DSL (`build.gradle`):**
buildTypes {
release {
minifyEnabled = true
shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
## 2. `gradle.properties` Flags
**Full Mode:** R8 Full Mode enables the entire optimizations
- **AGP 8.0+** : Enabled by default. Ensure `android.enableR8.fullMode=false` is **NOT** present.
- **Pre-AGP 8.0** : Should be explicitly enabled with `android.enableR8.fullMode=true`.
**Optimized Resource Shrinking:** If the AGP version of the project is less than
9.0 and more than 8.6, explicitly enable the new resource shrinker:
android.r8.optimizedResourceShrinking=true
@@ -0,0 +1,83 @@
Keep rules prevent optimization of R8, these rules are listed in the order of
the scope of what it retains in the codebase.
## 1. Package-Wide Wildcards
The following types of keep rules prevents all the optimization of R8 in a
package, these must be avoided at any costs and must be refined to target a
specific class or classes.
-keep class com.example.package.** { *; } - Prevents optimization of all the classess including members in the package and subpackages
-keep class com.example.package.* { *; } - Prevents optimization of all the classes including members in the package
-keep class **.package.** { *; } - Prevents optimization of all the classess including members in all the package containing name - package.
Depending on the package level the number of classes gets affected changes, so
if the package level is higher, more classes are affected. Suggest to refine
the keep rule
## 2. Inversion operator
Avoid using the inversion operator ! in keep rules because it will
unintentionally prevent optimization in every class in your application. So if
you have any keep rule with !operator, make sure you remove that with a narrow
and specific keep rule
!keep class com.example.MyClass{*;}
This keeps the entire app
other than this class. Optimization are disabled for the entire class other
than this class.
## 3. Keep Rules for both class and members
Keep rules with -keep option and wildcard(`*`) inside braces forces R8 to retain
specific classes and their members exactly as defined. These type of keep rules
prevent any optimization in the entire class and keeps the entire class
-keep class com.example.MyClass { *; }
## 4. Keepclassmembers
Keep rules with -keepclassmembers and wildcard(`*`) inside braces option Forces
R8 to retain the members that are defined.
-keepclassmembers class com.example.MyClass { *; }
## 5. Modifiers with Keep Specification
-Keeps the class and **all** members, but uses modifiers to allow specific
optimizations (like obfuscation). Retains significant code (members) but allows
some flexibility.
-keep,allowobfuscation class com.example.MyClass { *; }
-keep,allowshrinking class com.example.MyClass { *; }
### 6. Modifiers with specific method but no modifier
Keeps the class and modifier but no optimizations are enabled
-keep class com.example.MyClass { void myMethod(); }
## 7. Class-Name Only Preservation
Keeps only the class name. R8 will remove all methods and fields if they are not
used.
-keep class com.example.MyClass
## 8. Modifiers without Member Specification
Keeps the class entry point using modifiers, but implies no specific member
retention logic in the rule itself
-keep,allowobfuscation class com.example.MyClass
-keep,allowshrinking class com.example.MyClass
-keep,allowaccessmodification class com.example.MyClass
## 9. Conditional Keep Rules
Only triggers if specific conditions are met (e.g., if class members exist).
These are the most narrow and optimization-friendly rules.
-keepclassmembers class com.example.MyClass { <fields>; }
-keepclasseswithmembers class * { native <methods>; }
+222
View File
@@ -0,0 +1,222 @@
This document outlines common "bad" or redundant keep rules for standard Android
development and popular libraries. Modern toolchains and libraries include their
own consumer keep rules embedded in their AAR/JAR files, making many manual
configurations unnecessary or even harmful to code optimization.
*** ** * ** ***
## Case: Global Keep Rules
**Common Mistakes:**
`proguard
-dontshrink
-dontobfuscate
-dontoptimize`
**The Fix:** These keep rules completely disable the core optimizations of R8
for the entire codebase. They must be removed from the codebase.
*** ** * ** ***
## Case: Android Components
Keep rules required for Android components like Activity, Fragment, ViewModel,
Views, Services or Broadcast receivers are redundant. AAPT2 and R8 contain the
logic to automatically keep components declared in the `AndroidManifest.xml` or
referenced in XML layout files.
**Common Mistakes:**
`proguard
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.view.View
-keepclassmembers class * extends android.app.Fragment { public void *(android.view.View); }`
**The Fix:** Delete these manual rules. AAPT2 handles this automatically.
*** ** * ** ***
## Case: Official Android and Kotlin Libraries
Keep rules targeting official library packages like AndroidX, Kotlin, and
Kotlinx are redundant as they are bundled within the libraries themselves.
Manual rules are often broader than what is strictly needed.
**Common Mistakes:**
`proguard
-keep class androidx.** { *; }
-keep class kotlinx.** { *; }
-keep class kotlin.** { *; }`
**The Fix:** Delete these manual rules. Rely on the consumer keep rules packaged
within these dependencies.
*** ** * ** ***
## Case: Gson
### Overly Broad Data Model Rules
The most common mistake is keeping entire packages of data models (POJOs/DTOs),
keeping data models at all for deserialization is unnecessary.
-keep class com.example.app.models.** { *; }
-keep class com.example.app.package.models.* { *; }
### Redundant Interface \& Adapter Rules
These rules added for TypeAdapter are unnecessary and are already covered by
the library, and prevent R8 from effectively shrinking and optimizing custom
adapters. R8 can determine if the adapter implementation are used. Keeping them
globally prevents the removal of unused adapter implementations.
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
### Unnecessary TypeToken Rules
There is no need to handle generic type erasure, Gson's own rules handle the
necessary `TypeToken` preservation.
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
### Internal and Example Packages
Keeping internal library logic prevents the compiler from stripping away dead
code within the library.
-keep class com.google.gson.internal.** { *; }
-keep class com.google.gson.internal.reflect.** { *; }
-keep class com.google.gson.internal.UnsafeAllocator { *; }
-keep class com.google.gson.stream.** { *; }
- **Keeps Unused Code:** Prevents R8 from removing models that are never actually used in the code.
- **Prevents Method Stripping:** Keeps all getters, setters, `toString()`, `equals()`, and `hashCode()` methods, even if they are never called.
- **Blocks Obfuscation:** Prevents the class names from being obfuscated, which is unnecessary for Gson if you use `@SerializedName`.
**The Fix:**
1. Use `@SerializedName` on every field in your data classes uses so that the field is retained after R8 optimization
2. Modern Gson (**v2.11.0+** ) bundles its own rules ([View Gson's embedded
ProGuard
rules](https://github.com/google/gson/blob/main/gson/src/main/resources/META-INF/proguard/gson.pro)). The bundled keep rules retains the `@SerializedName` annotated fields. If you are on an older version, move towards Gson version 2.11 because it has the necessary keep rules and delete the keep rules that target the classes used for gson serialization and deserialization
*** ** * ** ***
## Case: Retrofit
Retrofit has shipped with its own consumer keep rules from 2.9.0 and higher, so
any keep rules for the library or classes depending on Retrofit is detrimental
to the optimization process.
### Blanket Library Preservation
This is the most harmful Retrofit rule as it disables any shrinking for the
entire library.
-keep class retrofit2.** { *; }
-keep class retrofit2.api.** { *; }
-keep class com.package.example.retrofit.api.** { *; }
### Manual Annotation Keeps
Retrofit's consumer rules automatically keep the interfaces annotated with
`@GET`, `@POST`, `@DELETE`, `@PUT`, `@HEAD`, `@OPTIONS`, `@PATCH`, making these
manual rules obsolete.
`-keepclasseswithmembers class * { @retrofit2.http.* <methods>; }`
### Redundant Network Response and Adapter Rules
Network responses and third-party adapter wrappers (like RxJava) are often
overly preserved by developers out of caution.
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep class retrofit2.adapter.rxjava2.Result { *; }
Fix: Verify you are using Retrofit 2.9.0 and higher. Retrofit from 2.9.0 bundles
rules that detect its own HTTP annotations (@GET, @POST) ([View Retrofit's
embedded ProGuard
rules](https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro)).
It will automatically keep the method signatures it needs to work.
*** ** * ** ***
## Case: Kotlin Coroutines
Kotlin Coroutines comes heavily optimized out of the box with embedded R8 rules
(`kotlinx-coroutines-core` includes its own rules).
### Blanket Coroutine Library Rules
Keeping everything under `kotlinx.coroutines` is extremely detrimental to app
size, as coroutines contain a vast amount of internal APIs that aren't used.
`-keepclassmembers class kotlinx.coroutines.** { *; }`
### Redundant Internal Continuations
These low-level coroutine elements are preserved safely by the library's own
consumer rules. Manually adding these prevents R8 from performing internal
optimizations (such as removing unused continuations or inlining).
-keepclassmembers class kotlin.coroutines.SafeContinuation { *; }
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
### Dispatcher and Exception Handler Rules
Sometimes developers notice crashes related to Missing Classes on old Android
versions and add these rules, but if you are using an up-to-date version of
Coroutines, these are handled automatically or are not an issue.
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
**Fix** Remove any broad `kotlinx` keep rules. Coroutines (**v1.7.0+** ) bundle
the necessary keep rules ([View Coroutines' embedded ProGuard
rules](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro)).
*** ** * ** ***
## Case: Parcelable
**Common Mistakes:** Legacy projects often contain `-keep class * implements
android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`.
**The Fix:**
1. Add the `kotlin-parcelize` plugin.
2. **Use `@Parcelize`:** Replace manual `writeToParcel` logic with the `@Parcelize` annotation.
3. **Delete All Parcelable Rules:** The plugin automatically generates the required rules.
4. The default proguard file `proguard-android-optimize.txt` contains the keep rules for keeping all the parcelable classes
5. **Ideal Rule:** **None.** Delete all manual Parcelable keeps.
*** ** * ** ***
## Case: Room Database
**Common Mistakes:** Keeping DAO interfaces or the generated `_Impl` classes
manually.
-keep class * extends androidx.room.RoomDatabase
-keep class *_*Impl { *; }
**The Fix:** Room generates its own ProGuard rules for the code it creates.
Manual rules are redundant and prevent R8 from optimizing the database access
layers.
- **Ideal Rule:** **None.** Delete all manual Room or DAO keeps.
*** ** * ** ***
## Summary
If you have updated your libraries to the versions mentioned, your
`proguard-rules.pro` must not contain any keep rules for the libraries
mentioned here.
+139
View File
@@ -0,0 +1,139 @@
A categorized summary of the keep rule examples, including the code patterns to
look for (imports/usage) and the corresponding suggested rules.
### 1. Reflection: Classes Loaded by Name
**Scenario:** A library or app loads a class dynamically using a string name
- **Look for:**
`Class.forName("...")`,
`getDeclaredConstructor().newInstance()`, or interfaces used for dynamic loading.
- **Example Code:**
`kotlin
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask`
- **Suggested Keep Rule:**
\`\`\`proguard
-keep class \* implements com.example.library.StartupTask {
(); } \`\`\`
### 2. Reflection: Classes Passed using `::class.java`
**Scenario:** An app passes a class reference directly to a library function.
- **Look for:** `::class.java` (Kotlin) or `.class` (Java) passed as an argument.
- **Example Code:**
`kotlin
fun <T> register(clazz: Class<T>) { }
// Usage:
register(MyService::class.java)`
- **Suggested Keep Rule:**
\`\`\`proguard
# Keep the class itself (R8 usually handles this, but explicit rules ensure stability)
-keep class com.example.app.MyService {
(); } \`\`\`
### 3. Annotation-Based Reflection (Methods/Classes)
**Scenario:** Using custom annotations to mark methods or classes for reflective
execution.
**Look for:** Custom `@interface` definitions and `getDeclaredMethods()`
filtered by annotation.
**Example Code:**
`kotlin
annotation class ReflectiveExecutor
// Logic: find methods annotated with @ReflectiveExecutor and invoke them`
- **Suggested Keep Rule:** \`\`\`proguard # Keep the annotation itself -keep @interface com.example.library.ReflectiveExecutor
# Keep members of any class annotated with this specific annotation
-keepclassmembers class \* {
@com.example.library.ReflectiveExecutor \*;
}
\`\`\`
### 4. Optional Dependencies (Soft Dependencies)
**Scenario:** A core library checks if an optional module is present in the
classpath.
- **Look for:** `try-catch` blocks around `Class.forName()` used to toggle features.
- **Example Code:** \`\`\`kotlin private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
try {
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
} catch (e: ClassNotFoundException) { /\* skip feature \*/ }
\`\`\`
- **Suggested Keep Rule:** `proguard
# Preserve the optional class so the check doesn't fail due to shrinking
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}`
### 5. Accessing Private Members
**Scenario:** Using reflection to access internal fields or methods not exposed
with public APIs.
- **Look for:** `getDeclaredField("...")` or `getDeclaredMethod("...")` followed by `isAccessible = true`.
- **Example Code:**
`kotlin
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true`
- **Suggested Keep Rule:**
\`\`\`proguard
# Specifically keep the private field/method by name and type
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
\`\`\`
### 6. Parcelable (Manual Implementation)
**Scenario:** Implementing `Parcelable` without using the `@Parcelize`
annotation.
- **Look for:** `implements Parcelable` and a static `CREATOR` field.
- **Example Code:**
`kotlin
class MyData : Parcelable {
// Manual implementation with CREATOR field
}`
- **Suggested Keep Rule:**
*(Note: If using `import kotlinx.parcelize.Parcelize`, R8/ProGuard rules are
generated automatically. If manual, use the following:)*
`proguard
-keepclassmembers class * implements android.os.Parcelable {
static android.os.Parcelable$Creator CREATOR;
}`
### 7. Enums and Obfuscation
**Scenario:** App uses `Enum.valueOf("STRING_NAME")` indirectly (e.g.,using JSON
deserialization) and the enum names get obfuscated.
- **Look for:** Unnecessary generic Enum keep rules in ProGuard files.
- **Example Code:**
\`\`\`proguard
# Unnecessary rule
-keepclassmembers enum \* { \*; }
\`\`\`
- **Suggested Keep Rule:**
\*(Note: The default `proguard-android-optimize.txt` already contains the optimal
rules for Enums (keeping `values()` and `valueOf(String)`). Any additional
manual rules for Enums are redundant.) # No manual rule needed. Use default
proguard-android-optimize.txt.
@@ -0,0 +1,312 @@
The UI Automator testing framework provides a set of APIs to build UI tests that
interact with user apps and system apps.
> [!NOTE]
> **Note:** This documentation covers the modern approach to writing UI Automator tests, introduced with [UI Automator 2.4](https://developer.android.com/jetpack/androidx/releases/test-uiautomator#2.4.0). This approach makes your tests more concise, readable, and robust. The API is under development, and we strongly recommend using it for any new development with UI Automator. The [legacy API guidance](https://developer.android.com/training/testing/other-components/ui-automator-legacy) is also available.
## Introduction to modern UI Automator testing
UI Automator 2.4 introduces a streamlined, Kotlin-friendly Domain Specific
Language (DSL) that simplifies writing UI tests for Android. This new API
surface focuses on predicate-based element finding and explicit control over app
states. Use it to create more maintainable and reliable automated tests.
UI Automator lets you test an app from outside of the app's process. This
lets you test release versions with minification applied. UI Automator also
helps when writing macrobenchmark tests.
Key features of the modern approach include:
- A dedicated `uiAutomator` test scope for cleaner and more expressive test code.
- Methods like `onElement`, `onElements`, and `onElementOrNull` for finding UI elements with clear predicates.
- Built-in waiting mechanism for conditional elements `onElement*(timeoutMs:
Long = 10000)`
- Explicit app state management such as `waitForStable` and `waitForAppToBeVisible`.
- Direct interaction with accessibility window nodes for multi-window testing scenarios.
- Built-in screenshot capabilities and a `ResultsReporter` for visual testing and debugging.
## Set up your project
To begin using the modern UI Automator APIs, update your project's
`build.gradle.kts` file to include the [latest dependency](https://developer.android.com/jetpack/androidx/releases/test-uiautomator#2.4.0):
### Kotlin
dependencies {
...
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05")
}
### Groovy
dependencies {
...
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05"
}
## Core API concepts
The following sections describe core concepts of the modern UI Automator API.
### The uiAutomator test scope
Access all new UI Automator APIs within the **`uiAutomator { ... }`**
block. This function creates a `UiAutomatorTestScope` that provides a concise
and type-safe environment for your test operations.
uiAutomator {
// All your UI Automator actions go here
startApp("com.example.targetapp")
onElement { textAsString() == "Hello, World!" }.click()
}
### Find UI elements
Use UI Automator APIs with predicates to locate UI elements. These predicates
let you define conditions for properties such as text, selected or focused
state, and content description.
- `onElement { predicate }`: Returns the first UI element that matches the
predicate within a default timeout. The function throws an exception if it
doesn't locate a matching element.
// Find a button with the text "Submit" and click it
onElement { textAsString() == "Submit" }.click()
// Find a UI element by its resource ID
onElement { viewIdResourceName == "my_button_id" }.click()
// Allow a permission request
watchFor(PermissionDialog) {
clickAllow()
}
- `onElementOrNull { predicate }`: Similar to `onElement`, but returns
`null` if the function finds no matching element within the timeout. It
doesn't throw an exception. Use this method for optional elements.
val optionalButton = onElementOrNull { textAsString() == "Skip" }
optionalButton?.click() // Click only if the button exists
- `onElements { predicate }`: Waits until at least one UI element matches
the given predicate, then returns a list of all matching UI elements.
// Get all items in a list Ui element
val listItems = onElements { className == "android.widget.TextView" && isClickable }
listItems.forEach { it.click() }
Here are some tips for using `onElement` calls:
- Chain `onElement` calls for nested elements: You can chain `onElement`
calls to find elements within other elements, following a parent-child
hierarchy.
// Find a parent Ui element with ID "first", then its child with ID "second",
// then its grandchild with ID "third", and click it.
onElement { viewIdResourceName == "first" }
.onElement { viewIdResourceName == "second" }
.onElement { viewIdResourceName == "third" }
.click()
- Specify a timeout for `onElement*` functions by passing a value representing
milliseconds.
// Find a Ui element with a zero timeout (instant check)
onElement(0) { viewIdResourceName == "something" }.click()
// Find a Ui element with a custom timeout of 10 seconds
onElement(10_000) { textAsString() == "Long loading text" }.click()
### Interact with UI elements
Interact with UI elements by simulating clicks or setting text in editable
fields.
// Click a Ui element
onElement { textAsString() == "Tap Me" }.click()
// Set text in an editable field
onElement { className == "android.widget.EditText" }.setText("My input text")
// Perform a long click
onElement { contentDescription == "Context Menu" }.longClick()
## Handle app states and watchers
Manage the lifecycle of your app and handle unexpected UI elements that might
appear during your tests.
### App lifecycle management
The APIs provide ways to control the state of the app under test:
// Start a specific app by package name. Used for benchmarking and other
// self-instrumenting tests.
startApp("com.example.targetapp")
// Start a specific activity within the target app
startActivity(SomeActivity::class.java)
// Start an intent
startIntent(myIntent)
// Clear the app's data (resets it to a fresh state)
clearAppData("com.example.targetapp")
### Handle unexpected UI
The `watchFor` API lets you define handlers for unexpected UI elements,
such as permission dialogs, that might appear during your test flow. This
uses the internal watcher mechanism but offers more flexibility.
import androidx.test.uiautomator.PermissionDialog
@Test
fun myTestWithPermissionHandling() = uiAutomator {
startActivity(MainActivity::class.java)
// Register a watcher to click "Allow" if a permission dialog appears
watchFor(PermissionDialog) { clickAllow() }
// Your test steps that might trigger a permission dialog
onElement { textAsString() == "Request Permissions" }.click()
// Example: You can register a different watcher later if needed
clearAppData("com.example.targetapp")
// Now deny permissions
startApp("com.example.targetapp")
watchFor(PermissionDialog) { clickDeny() }
onElement { textAsString() == "Request Permissions" }.click()
}
`PermissionDialog` is an example of a `ScopedWatcher<T>`, where `T` is the
object passed as a scope to the block in `watchFor`. You can create custom
watchers based on this pattern.
### Wait for app visibility and stability
Sometimes tests need to wait for elements to become visible or stable.
UI Automator offers several APIs to help with this.
The `waitForAppToBeVisible("com.example.targetapp")` waits for a UI element with
the given package name to appear on the screen within a customizable timeout.
// Wait for the app to be visible after launching it
startApp("com.example.targetapp")
waitForAppToBeVisible("com.example.targetapp")
Use the `waitForStable()` API to verify that the app's UI is considered stable
before interacting with it.
// Wait for the entire active window to become stable
activeWindow().waitForStable()
// Wait for a specific Ui element to become stable (e.g., after a loading animation)
onElement { viewIdResourceName == "my_loading_indicator" }.waitForStable()
> [!NOTE]
> **Note:** In most cases, `waitForStable()` isn't strictly necessary when using `onElement { ... }` because `onElement` already includes a timeout. Use `waitForStable()` primarily in combination with `onElements { ... }` to verify that all UI elements are visible, when you know that the UI is in an unstable state, or for specific screenshot testing scenarios where you need the UI to completely settle before capturing. `waitForStable()` works by waiting until no changes are detected in the accessibility tree for a set period. Note that this UI stability check doesn't guarantee that the app is fully idle, as background tasks might still be running.
## Use UI Automator for Macrobenchmarks and Baseline Profiles
Use UI Automator for performance testing with [Jetpack Macrobenchmark](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview)
and for generating [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/overview), as it provides a reliable way to
interact with your app and measure performance from an end-user perspective.
Macrobenchmark uses UI Automator APIs to drive the UI and measure interactions.
For example, in startup benchmarks, you can use `onElement` to detect when UI
content is fully loaded, enabling you to measure [Time to Full Display
(TTFD)](https://developer.android.com/topic/performance/vitals/launch-time#time-full). In jank benchmarks, UI Automator APIs are used to scroll lists or
run animations to measure frame timings. Functions like `startActivity()` or
`startIntent()` are useful for getting the app into the correct state before
measurement begins.
When [generating Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile), you automate your app's critical user
journeys (CUJs) to record which classes and methods require pre-compilation. UI
Automator is an ideal tool for writing these automation scripts. The modern
DSL's predicate-based element finding and built-in wait mechanisms (`onElement`)
lead to more robust and deterministic test execution compared to other methods.
This stability reduces flakiness and ensures that the generated Baseline Profile
accurately reflects the code paths executed during your most important user
flows.
## Advanced features
The following features are useful for more complex testing scenarios.
### Interact with multiple windows
The UI Automator APIs let you directly interact with and inspect UI
elements. This is particularly useful for scenarios involving multiple windows,
such as Picture-in-Picture (PiP) mode or split-screen layouts.
// Find the first window that is in Picture-in-Picture mode
val pipWindow = windows()
.first { it.isInPictureInPictureMode == true }
// Now you can interact with elements within that specific window
pipWindow.onElement { textAsString() == "Play" }.click()
### Screenshots and visual assertions
Capture screenshots of the entire screen, specific windows, or
individual UI elements directly within your tests. This is helpful for visual
regression testing and debugging.
uiautomator {
// Take a screenshot of the entire active window
val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot()
fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png"))
// Take a screenshot of a specific UI element (e.g., a button)
val buttonBitmap: Bitmap = onElement { viewIdResourceName == "my_button" }.takeScreenshot()
buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png"))
// Example: Take a screenshot of a PiP window
val pipWindowScreenshot = windows()
.first { it.isInPictureInPictureMode == true }
.takeScreenshot()
pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png"))
}
The `saveToFile` extension function for Bitmap simplifies saving the captured
image to a specified path.
### Use ResultsReporter for debugging
The `ResultsReporter` helps you associate test artifacts, like screenshots,
directly with your test results in Android Studio for easier inspection and
debugging.
uiAutomator {
startApp("com.example.targetapp")
val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results
val file = reporter.addNewFile(
filename = "my_screenshot",
title = "Accessible button image" // Title that appears in Android Studio test results
)
// Take a screenshot of an element and save it using the reporter
onElement { textAsString() == "Accessible button" }
.takeScreenshot()
.saveToFile(file)
// Report the artifacts to instrumentation, making them visible in Android Studio
reporter.reportToInstrumentation()
}
## Migrate from older UI Automator versions
If you have existing UI Automator tests written with older API surfaces, use the
following table as a reference to migrate to the modern approach:
| Action type | Old UI Automator method | New UI Automator method |
|---|---|---|
| Entry point | `UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())` | Wrap test logic in the `uiAutomator { ... }` scope. |
| Find UI elements | `device.findObject(By.res("com.example.app:id/my_button"))` | `onElement { viewIdResourceName == "my\_button" }` |
| Find UI elements | `device.findObject(By.text("Click Me"))` | `onElement { textAsString() == "Click Me" }` |
| Wait for idle UI | `device.waitForIdle()` | Prefer `onElement`'s built-in timeout mechanism; otherwise, `activeWindow().waitForStable()` |
| Find child elements | Manually nested `findObject` calls | `onElement().onElement()` chaining |
| Handle permission dialogs | `UiAutomator.registerWatcher()` | `watchFor(PermissionDialog)` |
+425
View File
@@ -0,0 +1,425 @@
---
name: edge-to-edge
description: Use this skill to migrate your Jetpack Compose app to add adaptive edge-to-edge
support and troubleshoot common issues. Use this skill to fix UI components (like
buttons or lists) that are obscured by or overlapping with the navigation bar or
status bar, fix IME insets, and fix system bar legibility.
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
keywords:
- android
- compose
- system bars
- edge-to-edge
- status bar
- navigation bar
---
## Prerequisites
- Project **MUST** use Android Jetpack Compose.
- Project **MUST** target SDK 35 or later. If the SDK is lower than 35, increase the SDK to 35.
## Step 1: plan
1. Locate and analyze all Activity classes to detect which have existing edge-to-edge support. For every Activity without edge-to-edge, plan to make each Activity edge-to-edge.
2. In each Activity, Locate and analyze all lists and FAB components to detect which have existing edge-to-edge support. For every component without edge-to-edge support, plan to make each of these components edge-to-edge.
3. In each Activity, scan for `TextField`, `OutlinedTextField`, or `BasicTextField`. If found, then you **MUST** verify the IME doesn't hide the input field by following the IME section of this skill.
## Step 2: add edge-to-edge support
1. Add `enableEdgeToEdge` before `setContent` in `onCreate` in each Activity that does not already call `enableEdgeToEdge`.
2. Add `android:windowSoftInputMode="adjustResize"` in the AndroidManifest.xml for all Activities that use a soft keyboard.
## Step 3: apply insets
- The app **MUST** apply system insets, or align content to rulers, so critical
UI remains tappable. Choose only one method to avoid double padding:
1. **PREFERRED:** When available, use `Scaffold`s and pass `PaddingValues` to the content lambda.
```kotlin
Scaffold { innerPadding ->
// innerPadding accounts for system bars and any Scaffold components
LazyColumn(
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(innerPadding),
contentPadding = innerPadding
) { /* Content */ }
}
```
<br />
1. **PREFERRED:** When available, use the automatic inset handling or padding modifiers in material components.
- Material 3 Components manages safe areas for its own components, including:
- `TopAppBar`
- `SmallTopAppBar`
- `CenterAlignedTopAppBar`
- `MediumTopAppBar`
- `LargeTopAppBar`
- `BottomAppBar`
- `ModalDrawerSheet`
- `DismissibleDrawerSheet`
- `PermanentDrawerSheet`
- `ModalBottomSheet`
- `NavigationBar`
- `NavigationRail`
- For Material 2 Components, use the `windowInsets`parameter to apply insets manually for `BottomAppBar`, `TopAppBar` and `BottomNavigation`. **DO NOT** apply padding to the parent container; instead, pass insets directly to the App Bar component. Applying padding to the parent container prevents the App Bar background from drawing into the system bar area. For example, for `TopAppBar`, choose only one of the following options:
1. **PREFERRED:** `TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)`
2. `TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))`
3. `TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))`
2. For components outside a Scaffold, use padding modifiers, such as `Modifier.safeDrawingPadding()` or `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`.
```kotlin
Box(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
) {
Button(
onClick = {},
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text("Login")
}
}
```
<br />
3. For deeply nested components with excessive padding, use `WindowInsetsRulers` (e.g. `Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)`). See the *IME* section for a code sample.
4. When you need an element (e.g. a custom header or decorative scrim) to
equal the dimensions of a system bar, use inset size modifiers (e.g.
`Modifier.windowInsetsTopHeight(WindowInsets.systemBars)`).
See the *Lists* section for a code sample.
## Adaptive Scaffolds
- `NavigationSuiteScaffold` manages safe areas for its own components, like the `NavigationRail` or `NavigationBar`. However, the adaptive scaffolds (e.g. `NavigationSuiteScaffold`, `ListDetailPaneScaffold`) don't propagate PaddingValues to their inner contents. You **MUST** apply insets to **individual** screens or components (e.g., list `contentPadding` or FAB padding) as described in *Step 3* . **DO NOT** apply `safeDrawingPadding` or similar modifiers to the `NavigationSuiteScaffold` parent. This clips and prevents an edge-to-edge screen.
## IME
- For each Activity with a soft keyboard, check that `android:windowSoftInputMode="adjustResize"` is set in the AndroidManifest.xml. DO NOT use `SOFT_INPUT_ADJUST_RESIZE` because it is deprecated. Then, maintain focus on the input field. Choose one:
- 1. **PREFERRED:** Add `Modifier.fitInside(WindowInsetsRulers.Ime.current)` to the content container. This is preferred over `imePadding()` because it reduces jank and extra padding caused by forgetting to consume insets upstream in the hierarchy.
- 2. Add `imePadding` to the content container. The padding modifier **MUST** be placed before `Modifier.verticalScroll()`. Do NOT use `Modifier.imePadding()` if the parent already accounts for the IME with `contentWindowInsets` (e.g. `contentWindowInsets =
WindowInsets.safeDrawing`). Doing so will cause double padding.
### IMEs with Scaffolds code patterns
#### RIGHT
RIGHT because `contentWindowInsets` contains IME insets, which are passed to the
content lambda as `innerPadding`.
```kotlin
// RIGHT
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
```
<br />
*** ** * ** ***
RIGHT because `fitInside` fits the content to the IME insets regardless of
`contentWindowInsets`.
```kotlin
// RIGHT
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
```
<br />
*** ** * ** ***
RIGHT because the default `contentWindowInsets` does not contain IME insets, and
`imePadding()` applies IME insets:
```kotlin
// RIGHT
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
```
<br />
#### WRONG
WRONG because there will be excess padding when the IME opens. IME insets are
applied twice, once with innerPadding, which contains IME insets from the passed
`contentWindowInsets` values, and once with `imePadding`:
```kotlin
// WRONG
Scaffold( contentWindowInsets = WindowInsets.safeDrawing ) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
```
<br />
*** ** * ** ***
WRONG because the IME will cover up the content. Scaffold's default
`contentWindowInsets` does NOT contain IME insets.
```kotlin
// WRONG
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
```
<br />
### IMEs without Scaffolds code patterns
#### RIGHT
The following code samples WILL NOT cause excessive padding.
```kotlin
// RIGHT
Box(
// Insets consumed
modifier = Modifier.safeDrawingPadding() // or imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
```
<br />
*** ** * ** ***
```kotlin
// RIGHT
Box(
// Insets consumed
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
```
<br />
*** ** * ** ***
```kotlin
// RIGHT
Box(
// Insets not consumed, but irrelevant due to fitInside
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* Content */ }
}
```
<br />
#### WRONG
The following code sample WILL cause excessive padding because IME insets are
applied twice:
```kotlin
// WRONG
Box(
// Insets not consumed
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
```
<br />
## Navigation Bar Contrast \& System Bar Icons
- If the Activity uses `enableEdgeToEdge` from `WindowCompat`, you **MUST** set
`isAppearanceLightNavigationBars` and `isAppearanceLightStatusBars` to the
inverse of the device theme for apps that support light and dark theme so the
system bar icons are legible. It's recommended to do this in your theme file.
DO NOT do this if the Activities use `enableEdgeToEdge` from `ComponentActivity`
because it handles the icon colors automatically.
```kotlin
// Only use if calling `enableEdgeToEdge` from `WindowCompat`.
// Apply to your theme file.
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as? Activity)?.window ?: return@SideEffect
val controller = WindowCompat.getInsetsController(window, view)
// Dark icons for Light Mode (!darkTheme), Light icons for Dark Mode
controller.isAppearanceLightStatusBars = !darkTheme
controller.isAppearanceLightNavigationBars = !darkTheme
}
}
MaterialTheme(content = content)
}
```
<br />
- If any screen uses a `Scaffold` or a `NavigationSuiteScaffold` with a bottom
bar (e.g., `BottomAppBar`, `NavigationBar`), set
`window.isNavigationBarContrastEnforced = false` in the corresponding Activity
for SDK 29+. This prevents the system from adding a translucent background to
the navigation bar, verifying your bottom bar colors extend to the bottom of the
screen.
## Lists
- Apply inset padding (like `Scaffold`'s `innerPadding`) to the `contentPadding` parameter of scrollable components (e.g. `LazyColumn`, `LazyRow`). DO NOT apply it as a `Modifier.padding()` to the list's parent container, as this clips the content and prevents it from scrolling behind the system bars.
- Create a translucent composable covering the system bar so that the icons are still legible.
```kotlin
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge sets window.isNavigationBarContrastEnforced = true
// which is used to add a translucent scrim to three-button navigation
enableEdgeToEdge()
setContent {
MyTheme {
// Main content
MyContent()
// After drawing main content, draw status bar protection
StatusBarProtection()
}
}
}
}
@Composable
private fun StatusBarProtection(
color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(
with(LocalDensity.current) {
(WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
}
)
.background(
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 1f),
color.copy(alpha = 0.8f),
Color.Transparent
)
)
)
)
}
```
<br />
## Dialogs
If both the following conditions are true, then the Dialog is full screen and
must be made edge-to-edge:
1. The `DialogProperties` contains `usePlatformDefaultWidth = false`.
2. The Dialog calls `Modifier.fillMaxSize()`.
To make a full screen Dialog edge-to-edge, set `decorFitsSystemWindows = false`
in the `DialogProperties`.
```kotlin
Dialog(
onDismissRequest = { /* Handle dismiss */ },
properties = DialogProperties(
// 1. Allows the dialog to span the full width of the screen
usePlatformDefaultWidth = false,
// 2. Allows the dialog to draw behind status and navigation bars
decorFitsSystemWindows = false
)
) { /* Content */ }
```
<br />
## Checklist
- \[ \] Does every `Activity` call `enableEdgeToEdge()`?
- \[ \] Is `adjustResize` set in the `AndroidManifest.xml`?
- \[ \] Does every `TextField`, `OutlinedTextField`, or `BasicTextField` have a parent with `imePadding()`, `fitInside`, `Modifier.safeDrawingPadding()`, `Modifier.safeContentPadding()`, `Modifier.safeGesturesPadding()`, or `contentWindowInsets` set to `WindowInsets.safeDrawing` or `WindowInsets.ime`?
- \[\] Does the first and last list item draw away from the system bars by passing insets to `contentPadding`?
- \[\] Do FABs draw above the navigation bars by either being inside a Scaffold or by applying `Modifier.safeDrawingPadding()`?
- \[\] Does the project build? Run `./gradlew build` to be sure.