From a08cd23c06c8e58273e5f7a2231dca79a0841fa8 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 09:02:19 +0700 Subject: [PATCH 01/14] refactor: update package structure and namespace to io.github.amirisback.androidapp --- .gitignore | 4 +- app/build.gradle.kts | 2 +- .../androidapp}/ProjectApplication.kt | 4 +- .../androidapp}/common/base/BaseActivity.kt | 2 +- .../androidapp}/common/base/BaseAdapter.kt | 4 +- .../androidapp}/common/base/BaseFragment.kt | 2 +- .../androidapp}/common/base/BaseViewHolder.kt | 2 +- .../androidapp}/common/base/BaseViewModel.kt | 2 +- .../androidapp}/common/base/IBaseActivity.kt | 2 +- .../androidapp}/common/base/IBaseFragment.kt | 2 +- .../common/callback/OnItemClickCallback.kt | 4 +- .../androidapp}/common/callback/Resource.kt | 2 +- .../androidapp}/common/ext/BuildConfigExt.kt | 4 +- .../androidapp}/common/ext/GsonExt.kt | 2 +- .../androidapp}/di/DatabaseModule.kt | 6 +-- .../androidapp}/di/NetworkModule.kt | 10 ++--- .../androidapp}/di/RepositoryModule.kt | 6 +-- .../androidapp}/di/UseCaseModule.kt | 6 +-- .../androidapp}/domain/db/DBConfig.kt | 2 +- .../androidapp}/domain/db/ProjectDatabase.kt | 10 ++--- .../androidapp}/domain/db/dao/MealDao.kt | 6 +-- .../androidapp}/domain/model/AreaModel.kt | 2 +- .../androidapp}/domain/model/CategoryModel.kt | 2 +- .../domain/model/IngredientModel.kt | 2 +- .../domain/model/MealFilterModel.kt | 2 +- .../androidapp}/domain/model/MealModel.kt | 4 +- .../domain/response/CategoryResponse.kt | 4 +- .../domain/response/MealResponse.kt | 2 +- .../domain/source/meal/MealApiService.kt | 42 +++++++++---------- .../domain/source/meal/MealConstant.kt | 2 +- .../domain/source/meal/MealDaoSource.kt | 8 ++-- .../domain/source/meal/MealDataSource.kt | 21 +++++----- .../androidapp}/domain/source/meal/MealUrl.kt | 2 +- .../source/meal/repository/MealRepository.kt | 16 +++---- .../meal/repository/MealRepositoryImpl.kt | 20 ++++----- .../source/meal/usecase/MealInteractor.kt | 20 ++++----- .../domain/source/meal/usecase/MealUseCase.kt | 16 +++---- .../androidapp}/ui/about/AboutUsActivity.kt | 6 +-- .../androidapp}/ui/detail/DetailActivity.kt | 10 ++--- .../androidapp}/ui/detail/DetailViewModel.kt | 10 ++--- .../ui/favorite/FavoriteFragment.kt | 18 ++++---- .../ui/favorite/FavoriteViewModel.kt | 10 ++--- .../androidapp}/ui/main/MainActivity.kt | 12 +++--- .../androidapp}/ui/main/MainAdapter.kt | 12 +++--- .../androidapp}/ui/main/MainFragment.kt | 14 +++---- .../androidapp}/ui/main/MainViewModel.kt | 10 ++--- .../amirisback/androidapp}/util/Constant.kt | 2 +- buildSrc/src/main/kotlin/ProjectSetting.kt | 8 ++-- gradle.properties | 5 +-- gradle/libs.versions.toml | 18 ++++---- gradle/wrapper/gradle-wrapper.properties | 4 +- 51 files changed, 192 insertions(+), 196 deletions(-) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ProjectApplication.kt (90%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/BaseActivity.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/BaseAdapter.kt (96%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/BaseFragment.kt (93%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/BaseViewHolder.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/BaseViewModel.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/IBaseActivity.kt (87%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/base/IBaseFragment.kt (87%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/callback/OnItemClickCallback.kt (65%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/callback/Resource.kt (90%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/ext/BuildConfigExt.kt (84%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/common/ext/GsonExt.kt (89%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/di/DatabaseModule.kt (76%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/di/NetworkModule.kt (90%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/di/RepositoryModule.kt (73%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/di/UseCaseModule.kt (75%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/db/DBConfig.kt (87%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/db/ProjectDatabase.kt (85%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/db/dao/MealDao.kt (83%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/model/AreaModel.kt (91%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/model/CategoryModel.kt (94%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/model/IngredientModel.kt (93%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/model/MealFilterModel.kt (93%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/model/MealModel.kt (98%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/response/CategoryResponse.kt (84%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/response/MealResponse.kt (91%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/MealApiService.kt (64%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/MealConstant.kt (93%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/MealDaoSource.kt (91%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/MealDataSource.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/MealUrl.kt (94%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/repository/MealRepository.kt (81%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/repository/MealRepositoryImpl.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/usecase/MealInteractor.kt (82%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/domain/source/meal/usecase/MealUseCase.kt (80%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/about/AboutUsActivity.kt (81%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/detail/DetailActivity.kt (92%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/detail/DetailViewModel.kt (86%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/favorite/FavoriteFragment.kt (83%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/favorite/FavoriteViewModel.kt (70%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/main/MainActivity.kt (88%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/main/MainAdapter.kt (83%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/main/MainFragment.kt (84%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/ui/main/MainViewModel.kt (78%) rename app/src/main/java/{com/frogobox/kickstart => io/github/amirisback/androidapp}/util/Constant.kt (95%) diff --git a/.gitignore b/.gitignore index 3ed3dcb..14c48a6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ local.properties app/version.properties # SonarQube -.sonar/ \ No newline at end of file +.sonar/ + +gradle\gradle-daemon-jvm.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd1f849..7d2a25b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ base { android { - namespace = "com.frogobox.kickstart" + namespace = "io.github.amirisback.androidapp" compileSdk = ProjectSetting.PROJECT_COMPILE_SDK defaultConfig { diff --git a/app/src/main/java/com/frogobox/kickstart/ProjectApplication.kt b/app/src/main/java/io/github/amirisback/androidapp/ProjectApplication.kt similarity index 90% rename from app/src/main/java/com/frogobox/kickstart/ProjectApplication.kt rename to app/src/main/java/io/github/amirisback/androidapp/ProjectApplication.kt index 119f098..e22b751 100644 --- a/app/src/main/java/com/frogobox/kickstart/ProjectApplication.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ProjectApplication.kt @@ -1,7 +1,7 @@ -package com.frogobox.kickstart +package io.github.amirisback.androidapp import android.content.Context -import com.frogobox.kickstart.common.ext.appIsDebug +import io.github.amirisback.androidapp.common.ext.appIsDebug import com.frogobox.sdk.FrogoApplication import dagger.hilt.android.HiltAndroidApp diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/BaseActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/common/base/BaseActivity.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt index 8e42ffd..3d9a586 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/BaseActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base import androidx.viewbinding.ViewBinding import com.frogobox.ads.ui.FrogoAdBindActivity diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/BaseAdapter.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt similarity index 96% rename from app/src/main/java/com/frogobox/kickstart/common/base/BaseAdapter.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt index e403465..1b86188 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/BaseAdapter.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt @@ -1,9 +1,9 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.frogobox.kickstart.common.callback.OnItemClickCallback +import io.github.amirisback.androidapp.common.callback.OnItemClickCallback /** Standard BaseAdapter Handling BaseModel List and BaseViewHolder**/ diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/BaseFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt similarity index 93% rename from app/src/main/java/com/frogobox/kickstart/common/base/BaseFragment.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt index 78e7e5c..4dfe19a 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/BaseFragment.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base import androidx.viewbinding.ViewBinding import com.frogobox.sdk.view.FrogoBindFragment diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/BaseViewHolder.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/common/base/BaseViewHolder.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt index 6a36709..14e836e 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/BaseViewHolder.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/BaseViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewModel.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/common/base/BaseViewModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewModel.kt index 517f471..c61c842 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/BaseViewModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewModel.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base import com.frogobox.sdk.view.FrogoViewModel diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/IBaseActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseActivity.kt similarity index 87% rename from app/src/main/java/com/frogobox/kickstart/common/base/IBaseActivity.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseActivity.kt index 7913f05..289aec9 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/IBaseActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseActivity.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base /** diff --git a/app/src/main/java/com/frogobox/kickstart/common/base/IBaseFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseFragment.kt similarity index 87% rename from app/src/main/java/com/frogobox/kickstart/common/base/IBaseFragment.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseFragment.kt index f9db666..895720c 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/base/IBaseFragment.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/IBaseFragment.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.base +package io.github.amirisback.androidapp.common.base /** * Created by Faisal Amir on 13/05/2020 diff --git a/app/src/main/java/com/frogobox/kickstart/common/callback/OnItemClickCallback.kt b/app/src/main/java/io/github/amirisback/androidapp/common/callback/OnItemClickCallback.kt similarity index 65% rename from app/src/main/java/com/frogobox/kickstart/common/callback/OnItemClickCallback.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/callback/OnItemClickCallback.kt index 2c6a77c..0354179 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/callback/OnItemClickCallback.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/callback/OnItemClickCallback.kt @@ -1,10 +1,10 @@ -package com.frogobox.kickstart.common.callback +package io.github.amirisback.androidapp.common.callback import android.view.View interface OnItemClickCallback { fun onItemClick(view: View, objects: Any, position: Int?) fun onItemLongClick(view: View, objects: Any, position: Int?) { - throw kotlin.RuntimeException("Stub!"); + throw kotlin.RuntimeException("Stub!") } } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/kickstart/common/callback/Resource.kt b/app/src/main/java/io/github/amirisback/androidapp/common/callback/Resource.kt similarity index 90% rename from app/src/main/java/com/frogobox/kickstart/common/callback/Resource.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/callback/Resource.kt index 05524b7..9dbcdb8 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/callback/Resource.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/callback/Resource.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.callback +package io.github.amirisback.androidapp.common.callback /** * Created by faisalamircs on 09/09/2025 diff --git a/app/src/main/java/com/frogobox/kickstart/common/ext/BuildConfigExt.kt b/app/src/main/java/io/github/amirisback/androidapp/common/ext/BuildConfigExt.kt similarity index 84% rename from app/src/main/java/com/frogobox/kickstart/common/ext/BuildConfigExt.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/ext/BuildConfigExt.kt index 72ea101..6b73588 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/ext/BuildConfigExt.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/ext/BuildConfigExt.kt @@ -1,6 +1,6 @@ -package com.frogobox.kickstart.common.ext +package io.github.amirisback.androidapp.common.ext -import com.frogobox.kickstart.BuildConfig +import io.github.amirisback.androidapp.BuildConfig /** * Created by faisalamir on 21/04/22 diff --git a/app/src/main/java/com/frogobox/kickstart/common/ext/GsonExt.kt b/app/src/main/java/io/github/amirisback/androidapp/common/ext/GsonExt.kt similarity index 89% rename from app/src/main/java/com/frogobox/kickstart/common/ext/GsonExt.kt rename to app/src/main/java/io/github/amirisback/androidapp/common/ext/GsonExt.kt index 38049e7..f65be73 100644 --- a/app/src/main/java/com/frogobox/kickstart/common/ext/GsonExt.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/ext/GsonExt.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.common.ext +package io.github.amirisback.androidapp.common.ext import com.google.gson.Gson import com.google.gson.reflect.TypeToken diff --git a/app/src/main/java/com/frogobox/kickstart/di/DatabaseModule.kt b/app/src/main/java/io/github/amirisback/androidapp/di/DatabaseModule.kt similarity index 76% rename from app/src/main/java/com/frogobox/kickstart/di/DatabaseModule.kt rename to app/src/main/java/io/github/amirisback/androidapp/di/DatabaseModule.kt index adfccb9..29f24ef 100644 --- a/app/src/main/java/com/frogobox/kickstart/di/DatabaseModule.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/di/DatabaseModule.kt @@ -1,8 +1,8 @@ -package com.frogobox.kickstart.di +package io.github.amirisback.androidapp.di import android.content.Context -import com.frogobox.kickstart.domain.db.ProjectDatabase -import com.frogobox.kickstart.domain.db.dao.MealDao +import io.github.amirisback.androidapp.domain.db.ProjectDatabase +import io.github.amirisback.androidapp.domain.db.dao.MealDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/frogobox/kickstart/di/NetworkModule.kt b/app/src/main/java/io/github/amirisback/androidapp/di/NetworkModule.kt similarity index 90% rename from app/src/main/java/com/frogobox/kickstart/di/NetworkModule.kt rename to app/src/main/java/io/github/amirisback/androidapp/di/NetworkModule.kt index 9a1f5c1..1daf905 100644 --- a/app/src/main/java/com/frogobox/kickstart/di/NetworkModule.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/di/NetworkModule.kt @@ -1,11 +1,11 @@ -package com.frogobox.kickstart.di +package io.github.amirisback.androidapp.di import android.content.Context import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.frogobox.kickstart.domain.source.meal.MealApiService -import com.frogobox.kickstart.common.ext.appIsDebug -import com.frogobox.kickstart.util.Constant -import com.frogobox.kickstart.domain.source.meal.MealUrl +import io.github.amirisback.androidapp.domain.source.meal.MealApiService +import io.github.amirisback.androidapp.common.ext.appIsDebug +import io.github.amirisback.androidapp.util.Constant +import io.github.amirisback.androidapp.domain.source.meal.MealUrl import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/frogobox/kickstart/di/RepositoryModule.kt b/app/src/main/java/io/github/amirisback/androidapp/di/RepositoryModule.kt similarity index 73% rename from app/src/main/java/com/frogobox/kickstart/di/RepositoryModule.kt rename to app/src/main/java/io/github/amirisback/androidapp/di/RepositoryModule.kt index 5ed77d8..84047ba 100644 --- a/app/src/main/java/com/frogobox/kickstart/di/RepositoryModule.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/di/RepositoryModule.kt @@ -1,7 +1,7 @@ -package com.frogobox.kickstart.di +package io.github.amirisback.androidapp.di -import com.frogobox.kickstart.domain.source.meal.repository.MealRepository -import com.frogobox.kickstart.domain.source.meal.repository.MealRepositoryImpl +import io.github.amirisback.androidapp.domain.source.meal.repository.MealRepository +import io.github.amirisback.androidapp.domain.source.meal.repository.MealRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/frogobox/kickstart/di/UseCaseModule.kt b/app/src/main/java/io/github/amirisback/androidapp/di/UseCaseModule.kt similarity index 75% rename from app/src/main/java/com/frogobox/kickstart/di/UseCaseModule.kt rename to app/src/main/java/io/github/amirisback/androidapp/di/UseCaseModule.kt index d745dff..a814b48 100644 --- a/app/src/main/java/com/frogobox/kickstart/di/UseCaseModule.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/di/UseCaseModule.kt @@ -1,7 +1,7 @@ -package com.frogobox.kickstart.di +package io.github.amirisback.androidapp.di -import com.frogobox.kickstart.domain.source.meal.usecase.MealInteractor -import com.frogobox.kickstart.domain.source.meal.usecase.MealUseCase +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealInteractor +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/frogobox/kickstart/domain/db/DBConfig.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/db/DBConfig.kt similarity index 87% rename from app/src/main/java/com/frogobox/kickstart/domain/db/DBConfig.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/db/DBConfig.kt index 7130eab..0365eac 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/db/DBConfig.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/db/DBConfig.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.db +package io.github.amirisback.androidapp.domain.db /** * Created by faisalamircs on 09/09/2025 diff --git a/app/src/main/java/com/frogobox/kickstart/domain/db/ProjectDatabase.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/db/ProjectDatabase.kt similarity index 85% rename from app/src/main/java/com/frogobox/kickstart/domain/db/ProjectDatabase.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/db/ProjectDatabase.kt index a9d0264..a07ad6b 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/db/ProjectDatabase.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/db/ProjectDatabase.kt @@ -1,13 +1,13 @@ -package com.frogobox.kickstart.domain.db +package io.github.amirisback.androidapp.domain.db import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import com.frogobox.kickstart.common.ext.appDatabaseName -import com.frogobox.kickstart.common.ext.appIsDebug -import com.frogobox.kickstart.domain.db.dao.MealDao -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.common.ext.appDatabaseName +import io.github.amirisback.androidapp.common.ext.appIsDebug +import io.github.amirisback.androidapp.domain.db.dao.MealDao +import io.github.amirisback.androidapp.domain.model.MealModel /** * Created by Faisal Amir diff --git a/app/src/main/java/com/frogobox/kickstart/domain/db/dao/MealDao.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/db/dao/MealDao.kt similarity index 83% rename from app/src/main/java/com/frogobox/kickstart/domain/db/dao/MealDao.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/db/dao/MealDao.kt index dcea455..26629be 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/db/dao/MealDao.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/db/dao/MealDao.kt @@ -1,10 +1,10 @@ -package com.frogobox.kickstart.domain.db.dao +package io.github.amirisback.androidapp.domain.db.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query -import com.frogobox.kickstart.domain.db.DBConfig.TABLE_MEALS -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.domain.db.DBConfig.TABLE_MEALS +import io.github.amirisback.androidapp.domain.model.MealModel import kotlin.collections.List /** diff --git a/app/src/main/java/com/frogobox/kickstart/domain/model/AreaModel.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/model/AreaModel.kt similarity index 91% rename from app/src/main/java/com/frogobox/kickstart/domain/model/AreaModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/model/AreaModel.kt index 90e947e..6740300 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/model/AreaModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/model/AreaModel.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.model +package io.github.amirisback.androidapp.domain.model import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/frogobox/kickstart/domain/model/CategoryModel.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/model/CategoryModel.kt similarity index 94% rename from app/src/main/java/com/frogobox/kickstart/domain/model/CategoryModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/model/CategoryModel.kt index 7f73c38..4b70dee 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/model/CategoryModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/model/CategoryModel.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.model +package io.github.amirisback.androidapp.domain.model import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/frogobox/kickstart/domain/model/IngredientModel.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/model/IngredientModel.kt similarity index 93% rename from app/src/main/java/com/frogobox/kickstart/domain/model/IngredientModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/model/IngredientModel.kt index 30e18af..6c170c6 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/model/IngredientModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/model/IngredientModel.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.model +package io.github.amirisback.androidapp.domain.model import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/frogobox/kickstart/domain/model/MealFilterModel.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/model/MealFilterModel.kt similarity index 93% rename from app/src/main/java/com/frogobox/kickstart/domain/model/MealFilterModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/model/MealFilterModel.kt index 4d40ad8..662f00f 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/model/MealFilterModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/model/MealFilterModel.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.model +package io.github.amirisback.androidapp.domain.model import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/frogobox/kickstart/domain/model/MealModel.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/model/MealModel.kt similarity index 98% rename from app/src/main/java/com/frogobox/kickstart/domain/model/MealModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/model/MealModel.kt index f188cd7..b4dcb17 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/model/MealModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/model/MealModel.kt @@ -1,9 +1,9 @@ -package com.frogobox.kickstart.domain.model +package io.github.amirisback.androidapp.domain.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.frogobox.kickstart.domain.db.DBConfig.TABLE_MEALS +import io.github.amirisback.androidapp.domain.db.DBConfig.TABLE_MEALS import com.google.gson.annotations.SerializedName /** diff --git a/app/src/main/java/com/frogobox/kickstart/domain/response/CategoryResponse.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/response/CategoryResponse.kt similarity index 84% rename from app/src/main/java/com/frogobox/kickstart/domain/response/CategoryResponse.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/response/CategoryResponse.kt index 234cda6..08438f0 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/response/CategoryResponse.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/response/CategoryResponse.kt @@ -1,6 +1,6 @@ -package com.frogobox.kickstart.domain.response +package io.github.amirisback.androidapp.domain.response -import com.frogobox.kickstart.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.model.CategoryModel import com.google.gson.annotations.SerializedName /** diff --git a/app/src/main/java/com/frogobox/kickstart/domain/response/MealResponse.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/response/MealResponse.kt similarity index 91% rename from app/src/main/java/com/frogobox/kickstart/domain/response/MealResponse.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/response/MealResponse.kt index 473873c..86d34a7 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/response/MealResponse.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/response/MealResponse.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.response +package io.github.amirisback.androidapp.domain.response import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealApiService.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealApiService.kt similarity index 64% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealApiService.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealApiService.kt index 3b7575c..26a3906 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealApiService.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealApiService.kt @@ -1,25 +1,25 @@ -package com.frogobox.kickstart.domain.source.meal +package io.github.amirisback.androidapp.domain.source.meal -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.response.CategoryResponse -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.model.MealFilterModel -import com.frogobox.kickstart.domain.response.MealResponse -import com.frogobox.kickstart.domain.source.meal.MealConstant.PATH_API_KEY -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_AREA -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_CATEGORY -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_FIRST_LETTER -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_ID -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_INGREDIENT -import com.frogobox.kickstart.domain.source.meal.MealConstant.QUERY_NAME -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_CATEGORIES -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_FILTER -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_LIST -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_LOOKUP_MEAL -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_RANDOM_MEAL -import com.frogobox.kickstart.domain.source.meal.MealUrl.URL_SEARCH_MEAL +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.response.CategoryResponse +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel +import io.github.amirisback.androidapp.domain.response.MealResponse +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.PATH_API_KEY +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_AREA +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_CATEGORY +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_FIRST_LETTER +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_ID +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_INGREDIENT +import io.github.amirisback.androidapp.domain.source.meal.MealConstant.QUERY_NAME +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_CATEGORIES +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_FILTER +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_LIST +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_LOOKUP_MEAL +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_RANDOM_MEAL +import io.github.amirisback.androidapp.domain.source.meal.MealUrl.URL_SEARCH_MEAL import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealConstant.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealConstant.kt similarity index 93% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealConstant.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealConstant.kt index 01c6eea..22f005b 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealConstant.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealConstant.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.source.meal +package io.github.amirisback.androidapp.domain.source.meal /** * Created by Faisal Amir diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDaoSource.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDaoSource.kt similarity index 91% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDaoSource.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDaoSource.kt index 96b4e37..ccd0e7a 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDaoSource.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDaoSource.kt @@ -1,8 +1,8 @@ -package com.frogobox.kickstart.domain.source.meal +package io.github.amirisback.androidapp.domain.source.meal -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.db.dao.MealDao -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.db.dao.MealDao +import io.github.amirisback.androidapp.domain.model.MealModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDataSource.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDataSource.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDataSource.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDataSource.kt index 0619eea..6731ae5 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealDataSource.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealDataSource.kt @@ -1,14 +1,13 @@ -package com.frogobox.kickstart.domain.source.meal - -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.db.dao.MealDao -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.response.CategoryResponse -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.model.MealFilterModel -import com.frogobox.kickstart.domain.response.MealResponse +package io.github.amirisback.androidapp.domain.source.meal + +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.response.CategoryResponse +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel +import io.github.amirisback.androidapp.domain.response.MealResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealUrl.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealUrl.kt similarity index 94% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealUrl.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealUrl.kt index f7eae40..d930948 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/MealUrl.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/MealUrl.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.domain.source.meal +package io.github.amirisback.androidapp.domain.source.meal /** * Created by faisalamir on 27/07/21 diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepository.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepository.kt similarity index 81% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepository.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepository.kt index bececaf..71eba9e 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepository.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepository.kt @@ -1,11 +1,11 @@ -package com.frogobox.kickstart.domain.source.meal.repository - -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealFilterModel -import com.frogobox.kickstart.domain.model.MealModel +package io.github.amirisback.androidapp.domain.source.meal.repository + +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel +import io.github.amirisback.androidapp.domain.model.MealModel import kotlinx.coroutines.flow.Flow /** diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepositoryImpl.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepositoryImpl.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepositoryImpl.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepositoryImpl.kt index 029866c..db1f914 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/repository/MealRepositoryImpl.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/repository/MealRepositoryImpl.kt @@ -1,13 +1,13 @@ -package com.frogobox.kickstart.domain.source.meal.repository - -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealFilterModel -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.source.meal.MealDaoSource -import com.frogobox.kickstart.domain.source.meal.MealDataSource +package io.github.amirisback.androidapp.domain.source.meal.repository + +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.source.meal.MealDaoSource +import io.github.amirisback.androidapp.domain.source.meal.MealDataSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealInteractor.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealInteractor.kt similarity index 82% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealInteractor.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealInteractor.kt index 4a5bf9f..9e8e6be 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealInteractor.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealInteractor.kt @@ -1,13 +1,13 @@ -package com.frogobox.kickstart.domain.source.meal.usecase - -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealFilterModel -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.source.meal.MealUrl -import com.frogobox.kickstart.domain.source.meal.repository.MealRepository +package io.github.amirisback.androidapp.domain.source.meal.usecase + +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.source.meal.MealUrl +import io.github.amirisback.androidapp.domain.source.meal.repository.MealRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject diff --git a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealUseCase.kt b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealUseCase.kt similarity index 80% rename from app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealUseCase.kt rename to app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealUseCase.kt index 2eaaa44..6b4b378 100644 --- a/app/src/main/java/com/frogobox/kickstart/domain/source/meal/usecase/MealUseCase.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/domain/source/meal/usecase/MealUseCase.kt @@ -1,11 +1,11 @@ -package com.frogobox.kickstart.domain.source.meal.usecase - -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.AreaModel -import com.frogobox.kickstart.domain.model.CategoryModel -import com.frogobox.kickstart.domain.model.IngredientModel -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.model.MealFilterModel +package io.github.amirisback.androidapp.domain.source.meal.usecase + +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.AreaModel +import io.github.amirisback.androidapp.domain.model.CategoryModel +import io.github.amirisback.androidapp.domain.model.IngredientModel +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.model.MealFilterModel import kotlinx.coroutines.flow.Flow /** diff --git a/app/src/main/java/com/frogobox/kickstart/ui/about/AboutUsActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt similarity index 81% rename from app/src/main/java/com/frogobox/kickstart/ui/about/AboutUsActivity.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt index 29eb6d7..0042a17 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/about/AboutUsActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt @@ -1,10 +1,10 @@ -package com.frogobox.kickstart.ui.about +package io.github.amirisback.androidapp.ui.about import android.content.Context import android.content.Intent import android.os.Bundle -import com.frogobox.kickstart.common.base.BaseActivity -import com.frogobox.kickstart.databinding.ActivityAboutUsBinding +import io.github.amirisback.androidapp.common.base.BaseActivity +import io.github.amirisback.androidapp.databinding.ActivityAboutUsBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint diff --git a/app/src/main/java/com/frogobox/kickstart/ui/detail/DetailActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt similarity index 92% rename from app/src/main/java/com/frogobox/kickstart/ui/detail/DetailActivity.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt index 1fc0eff..0d0c441 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/detail/DetailActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt @@ -1,13 +1,13 @@ -package com.frogobox.kickstart.ui.detail +package io.github.amirisback.androidapp.ui.detail import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels -import com.frogobox.kickstart.common.base.BaseActivity -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.databinding.ActivityDetailBinding -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.common.base.BaseActivity +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.databinding.ActivityDetailBinding +import io.github.amirisback.androidapp.domain.model.MealModel import com.frogobox.sdk.ext.getExtraExt import com.frogobox.sdk.ext.gone import com.frogobox.sdk.ext.setImageExt diff --git a/app/src/main/java/com/frogobox/kickstart/ui/detail/DetailViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailViewModel.kt similarity index 86% rename from app/src/main/java/com/frogobox/kickstart/ui/detail/DetailViewModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailViewModel.kt index fe079a8..05c0cad 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailViewModel.kt @@ -1,12 +1,12 @@ -package com.frogobox.kickstart.ui.detail +package io.github.amirisback.androidapp.ui.detail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.frogobox.kickstart.common.base.BaseViewModel -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.source.meal.usecase.MealUseCase +import io.github.amirisback.androidapp.common.base.BaseViewModel +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt similarity index 83% rename from app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteFragment.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt index 6ff89aa..bebb95a 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteFragment.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt @@ -1,21 +1,19 @@ -package com.frogobox.kickstart.ui.favorite +package io.github.amirisback.androidapp.ui.favorite import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager -import com.frogobox.kickstart.common.base.BaseFragment -import com.frogobox.kickstart.common.callback.OnItemClickCallback -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.databinding.FragmentFavoriteBinding -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.ui.detail.DetailActivity -import com.frogobox.kickstart.ui.main.MainAdapter +import io.github.amirisback.androidapp.common.base.BaseFragment +import io.github.amirisback.androidapp.common.callback.OnItemClickCallback +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.databinding.FragmentFavoriteBinding +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.detail.DetailActivity +import io.github.amirisback.androidapp.ui.main.MainAdapter import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.showLogD import com.frogobox.sdk.ext.showToast import com.frogobox.sdk.ext.visible import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteViewModel.kt similarity index 70% rename from app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteViewModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteViewModel.kt index 7e00a0c..d4d35d1 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/favorite/FavoriteViewModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteViewModel.kt @@ -1,12 +1,12 @@ -package com.frogobox.kickstart.ui.favorite +package io.github.amirisback.androidapp.ui.favorite import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.frogobox.kickstart.common.base.BaseViewModel -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.domain.source.meal.usecase.MealUseCase +import io.github.amirisback.androidapp.common.base.BaseViewModel +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/app/src/main/java/com/frogobox/kickstart/ui/main/MainActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt similarity index 88% rename from app/src/main/java/com/frogobox/kickstart/ui/main/MainActivity.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt index 89aa3b3..1061c13 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/main/MainActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt @@ -1,14 +1,14 @@ -package com.frogobox.kickstart.ui.main +package io.github.amirisback.androidapp.ui.main import android.content.res.ColorStateList import android.os.Bundle import androidx.activity.result.ActivityResult import androidx.activity.viewModels -import com.frogobox.kickstart.R -import com.frogobox.kickstart.common.base.BaseActivity -import com.frogobox.kickstart.databinding.ActivityMainBinding -import com.frogobox.kickstart.ui.favorite.FavoriteFragment -import com.frogobox.kickstart.ui.favorite.FavoriteViewModel +import io.github.amirisback.androidapp.R +import io.github.amirisback.androidapp.common.base.BaseActivity +import io.github.amirisback.androidapp.databinding.ActivityMainBinding +import io.github.amirisback.androidapp.ui.favorite.FavoriteFragment +import io.github.amirisback.androidapp.ui.favorite.FavoriteViewModel import com.frogobox.sdk.ext.getColorExt import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/frogobox/kickstart/ui/main/MainAdapter.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt similarity index 83% rename from app/src/main/java/com/frogobox/kickstart/ui/main/MainAdapter.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt index d8986f4..a9dca96 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/main/MainAdapter.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt @@ -1,12 +1,12 @@ -package com.frogobox.kickstart.ui.main +package io.github.amirisback.androidapp.ui.main import android.view.LayoutInflater import android.view.ViewGroup -import com.frogobox.kickstart.common.base.BaseAdapter -import com.frogobox.kickstart.common.base.BaseViewHolder -import com.frogobox.kickstart.common.callback.OnItemClickCallback -import com.frogobox.kickstart.databinding.ContentArticleVerticalBinding -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.common.base.BaseAdapter +import io.github.amirisback.androidapp.common.base.BaseViewHolder +import io.github.amirisback.androidapp.common.callback.OnItemClickCallback +import io.github.amirisback.androidapp.databinding.ContentArticleVerticalBinding +import io.github.amirisback.androidapp.domain.model.MealModel import com.frogobox.sdk.ext.setImageExt /** diff --git a/app/src/main/java/com/frogobox/kickstart/ui/main/MainFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt similarity index 84% rename from app/src/main/java/com/frogobox/kickstart/ui/main/MainFragment.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt index 6981800..9beb65c 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/main/MainFragment.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.ui.main +package io.github.amirisback.androidapp.ui.main import android.os.Bundle import android.view.LayoutInflater @@ -6,12 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager -import com.frogobox.kickstart.common.base.BaseFragment -import com.frogobox.kickstart.common.callback.OnItemClickCallback -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.databinding.FragmentMainBinding -import com.frogobox.kickstart.domain.model.MealModel -import com.frogobox.kickstart.ui.detail.DetailActivity +import io.github.amirisback.androidapp.common.base.BaseFragment +import io.github.amirisback.androidapp.common.callback.OnItemClickCallback +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.databinding.FragmentMainBinding +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.detail.DetailActivity import com.frogobox.sdk.ext.gone import com.frogobox.sdk.ext.showToast import com.frogobox.sdk.ext.visible diff --git a/app/src/main/java/com/frogobox/kickstart/ui/main/MainViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt similarity index 78% rename from app/src/main/java/com/frogobox/kickstart/ui/main/MainViewModel.kt rename to app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt index ec61bb5..76e9c12 100644 --- a/app/src/main/java/com/frogobox/kickstart/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt @@ -1,12 +1,12 @@ -package com.frogobox.kickstart.ui.main +package io.github.amirisback.androidapp.ui.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.frogobox.kickstart.common.base.BaseViewModel -import com.frogobox.kickstart.common.callback.Resource -import com.frogobox.kickstart.domain.source.meal.usecase.MealUseCase -import com.frogobox.kickstart.domain.model.MealModel +import io.github.amirisback.androidapp.common.base.BaseViewModel +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase +import io.github.amirisback.androidapp.domain.model.MealModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/app/src/main/java/com/frogobox/kickstart/util/Constant.kt b/app/src/main/java/io/github/amirisback/androidapp/util/Constant.kt similarity index 95% rename from app/src/main/java/com/frogobox/kickstart/util/Constant.kt rename to app/src/main/java/io/github/amirisback/androidapp/util/Constant.kt index 9f599e1..25d4d70 100644 --- a/app/src/main/java/com/frogobox/kickstart/util/Constant.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/util/Constant.kt @@ -1,4 +1,4 @@ -package com.frogobox.kickstart.util +package io.github.amirisback.androidapp.util import android.os.Environment diff --git a/buildSrc/src/main/kotlin/ProjectSetting.kt b/buildSrc/src/main/kotlin/ProjectSetting.kt index 4c15484..359ed55 100644 --- a/buildSrc/src/main/kotlin/ProjectSetting.kt +++ b/buildSrc/src/main/kotlin/ProjectSetting.kt @@ -14,10 +14,10 @@ object ProjectSetting { // Project settings - const val NAME_APP = "Frogo Kick Start Project" + const val NAME_APP = "Kick Start" - const val APP_DOMAIN = "com" - const val APP_PLAY_CONSOLE = "frogobox" + const val APP_DOMAIN = "io.github" + const val APP_PLAY_CONSOLE = "amirisback" // --------------------------------------------------------------------------------------------- @@ -35,7 +35,7 @@ object ProjectSetting { // --------------------------------------------------------------------------------------------- const val PROJECT_MIN_SDK = 23 - const val PROJECT_COMPILE_SDK = 36 + const val PROJECT_COMPILE_SDK = 37 const val PROJECT_TARGET_SDK = PROJECT_COMPILE_SDK // --------------------------------------------------------------------------------------------- diff --git a/gradle.properties b/gradle.properties index 622ef4a..8397f1b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,13 +15,10 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8.0. To opt-in to the future behavior, set the Gradle property # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true -android.nonFinalResIds=false \ No newline at end of file +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f9db12..af01726 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,25 @@ [versions] -agp = "9.2.0" -kotlin = "2.3.20" -ksp = "2.3.6" -hilt = "2.59.2" -googleServices = "4.4.4" +agp = "9.2.1" +kotlin = "2.4.0" +ksp = "2.3.9" +hilt = "2.60" +googleServices = "4.5.0" crashlytics = "3.0.7" constraintlayout = "2.2.1" -liveCycle = "2.10.0" +liveCycle = "2.11.0" roomKtx = "2.8.4" work = "2.11.2" swiperefreshlayout = "1.2.0" -firebaseBom = "34.12.0" +firebaseBom = "34.16.0" glide = "5.0.7" balloon = "1.7.6" chucker = "4.3.1" -mixpanel = "8.5.2" +mixpanel = "8.8.0" -frogoAndroid = "2.3.7" +frogoAndroid = "3.0.3" [libraries] # Android Kit diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c41eef..55c91d8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Thu Apr 23 15:08:06 WIB 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 751510053efb3220cd9ee605faf2abb265db3454 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 11:04:50 +0700 Subject: [PATCH 02/14] feat: initialize base project structure with Jetpack Compose, Hilt, and Room support --- .../amirisback/androidapp/ui/theme/Color.kt | 11 ++++ .../amirisback/androidapp/ui/theme/Theme.kt | 58 +++++++++++++++++++ .../amirisback/androidapp/ui/theme/Type.kt | 34 +++++++++++ 3 files changed, 103 insertions(+) create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/theme/Color.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/theme/Theme.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/theme/Type.kt diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Color.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Color.kt new file mode 100644 index 0000000..d392f09 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package io.github.amirisback.init.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Theme.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Theme.kt new file mode 100644 index 0000000..fd2fd3d --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package io.github.amirisback.init.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun InitTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Type.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Type.kt new file mode 100644 index 0000000..f7960f6 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package io.github.amirisback.init.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file From 96f6cc0003c9a955f46b1419974940d6e599fef3 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 11:05:02 +0700 Subject: [PATCH 03/14] feat: add Jetpack Compose support and project configuration for Android app development --- .gitignore | 2 +- app/build.gradle.kts | 14 + .../1.json | 286 ++++++++++++++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 13 + prompt_ai/MIGRATE_COMPOSE.md | 58 ++++ 6 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 app/schemas/io.github.amirisback.androidapp.domain.db.ProjectDatabase/1.json create mode 100644 prompt_ai/MIGRATE_COMPOSE.md diff --git a/.gitignore b/.gitignore index 14c48a6..872eace 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ app/version.properties # SonarQube .sonar/ -gradle\gradle-daemon-jvm.properties +gradle/gradle-daemon-jvm.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d2a25b..a48f5ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.hilt) alias(libs.plugins.kotlinParcelize) + alias(libs.plugins.kotlin.compose) } ksp { @@ -98,6 +99,7 @@ android { viewBinding = true buildConfig = true resValues = true + compose = true } compileOptions { @@ -138,4 +140,16 @@ dependencies { ksp(libs.androidx.room.compiler) ksp(libs.github.glide.compiler) + // Compose + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + debugImplementation(libs.androidx.compose.ui.tooling) + } \ No newline at end of file diff --git a/app/schemas/io.github.amirisback.androidapp.domain.db.ProjectDatabase/1.json b/app/schemas/io.github.amirisback.androidapp.domain.db.ProjectDatabase/1.json new file mode 100644 index 0000000..d5eaeb2 --- /dev/null +++ b/app/schemas/io.github.amirisback.androidapp.domain.db.ProjectDatabase/1.json @@ -0,0 +1,286 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "55fd7d6098321df8bda0298c83bc17c7", + "entities": [ + { + "tableName": "meal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`table_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `idMeal` TEXT, `strMeal` TEXT, `strDrinkAlternate` TEXT, `strCategory` TEXT, `strArea` TEXT, `strInstructions` TEXT, `strMealThumb` TEXT, `strTags` TEXT, `strYoutube` TEXT, `strIngredient1` TEXT, `strIngredient2` TEXT, `strIngredient3` TEXT, `strIngredient4` TEXT, `strIngredient5` TEXT, `strIngredient6` TEXT, `strIngredient7` TEXT, `strIngredient8` TEXT, `strIngredient9` TEXT, `strIngredient10` TEXT, `strIngredient11` TEXT, `strIngredient12` TEXT, `strIngredient13` TEXT, `strIngredient14` TEXT, `strIngredient15` TEXT, `strIngredient16` TEXT, `strIngredient17` TEXT, `strIngredient18` TEXT, `strIngredient19` TEXT, `strIngredient20` TEXT, `strMeasure1` TEXT, `strMeasure2` TEXT, `strMeasure3` TEXT, `strMeasure4` TEXT, `strMeasure5` TEXT, `strMeasure6` TEXT, `strMeasure7` TEXT, `strMeasure8` TEXT, `strMeasure9` TEXT, `strMeasure10` TEXT, `strMeasure11` TEXT, `strMeasure12` TEXT, `strMeasure13` TEXT, `strMeasure14` TEXT, `strMeasure15` TEXT, `strMeasure16` TEXT, `strMeasure17` TEXT, `strMeasure18` TEXT, `strMeasure19` TEXT, `strMeasure20` TEXT, `strSource` TEXT, `dateModified` TEXT)", + "fields": [ + { + "fieldPath": "table_id", + "columnName": "table_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idMeal", + "columnName": "idMeal", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeal", + "columnName": "strMeal", + "affinity": "TEXT" + }, + { + "fieldPath": "strDrinkAlternate", + "columnName": "strDrinkAlternate", + "affinity": "TEXT" + }, + { + "fieldPath": "strCategory", + "columnName": "strCategory", + "affinity": "TEXT" + }, + { + "fieldPath": "strArea", + "columnName": "strArea", + "affinity": "TEXT" + }, + { + "fieldPath": "strInstructions", + "columnName": "strInstructions", + "affinity": "TEXT" + }, + { + "fieldPath": "strMealThumb", + "columnName": "strMealThumb", + "affinity": "TEXT" + }, + { + "fieldPath": "strTags", + "columnName": "strTags", + "affinity": "TEXT" + }, + { + "fieldPath": "strYoutube", + "columnName": "strYoutube", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient1", + "columnName": "strIngredient1", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient2", + "columnName": "strIngredient2", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient3", + "columnName": "strIngredient3", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient4", + "columnName": "strIngredient4", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient5", + "columnName": "strIngredient5", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient6", + "columnName": "strIngredient6", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient7", + "columnName": "strIngredient7", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient8", + "columnName": "strIngredient8", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient9", + "columnName": "strIngredient9", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient10", + "columnName": "strIngredient10", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient11", + "columnName": "strIngredient11", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient12", + "columnName": "strIngredient12", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient13", + "columnName": "strIngredient13", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient14", + "columnName": "strIngredient14", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient15", + "columnName": "strIngredient15", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient16", + "columnName": "strIngredient16", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient17", + "columnName": "strIngredient17", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient18", + "columnName": "strIngredient18", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient19", + "columnName": "strIngredient19", + "affinity": "TEXT" + }, + { + "fieldPath": "strIngredient20", + "columnName": "strIngredient20", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure1", + "columnName": "strMeasure1", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure2", + "columnName": "strMeasure2", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure3", + "columnName": "strMeasure3", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure4", + "columnName": "strMeasure4", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure5", + "columnName": "strMeasure5", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure6", + "columnName": "strMeasure6", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure7", + "columnName": "strMeasure7", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure8", + "columnName": "strMeasure8", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure9", + "columnName": "strMeasure9", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure10", + "columnName": "strMeasure10", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure11", + "columnName": "strMeasure11", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure12", + "columnName": "strMeasure12", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure13", + "columnName": "strMeasure13", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure14", + "columnName": "strMeasure14", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure15", + "columnName": "strMeasure15", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure16", + "columnName": "strMeasure16", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure17", + "columnName": "strMeasure17", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure18", + "columnName": "strMeasure18", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure19", + "columnName": "strMeasure19", + "affinity": "TEXT" + }, + { + "fieldPath": "strMeasure20", + "columnName": "strMeasure20", + "affinity": "TEXT" + }, + { + "fieldPath": "strSource", + "columnName": "strSource", + "affinity": "TEXT" + }, + { + "fieldPath": "dateModified", + "columnName": "dateModified", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "table_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '55fd7d6098321df8bda0298c83bc17c7')" + ] + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index a0a6354..a7ea048 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.kotlinParcelize) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false + alias(libs.plugins.kotlin.compose) apply false } tasks.register("clean", Delete::class) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af01726..e95e910 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ ksp = "2.3.9" hilt = "2.60" googleServices = "4.5.0" crashlytics = "3.0.7" +composeBom = "2026.06.01" +activityCompose = "1.10.0" constraintlayout = "2.2.1" liveCycle = "2.11.0" @@ -30,6 +32,16 @@ androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "s androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-lifecycle-compiler = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "liveCycle" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomKtx" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "liveCycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "liveCycle" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } @@ -65,3 +77,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/prompt_ai/MIGRATE_COMPOSE.md b/prompt_ai/MIGRATE_COMPOSE.md new file mode 100644 index 0000000..d0c1f1e --- /dev/null +++ b/prompt_ai/MIGRATE_COMPOSE.md @@ -0,0 +1,58 @@ +# ROLE & SKILL + +Anda adalah seorang AI Agent Android Developer Senior yang memiliki spesialisasi (skill) dalam memigrasikan sistem UI lama (XML/View-based) ke Jetpack Compose modern. Anda menguasai best practices Android, arsitektur MVVM/MVI, State Management, Kotlin Coroutines, dan optimasi performa UI. + +# TUJUAN + +Tugas Anda adalah mengonversi kode layout XML dan logika View terkait (Activity/Fragment) yang saya berikan menjadi Jetpack Compose Composable Functions yang bersih, deklaratif, dan siap pakai.# KONTEKS & TUJUAN + +Saya ingin memigrasikan komponen UI Android dari View System (XML) ke Jetpack Compose menggunakan skill/tools yang tersedia di sistem ini. Migrasi ini harus mengikuti standar arsitektur modern (MVI/MVVM), menggunakan Material Design 3, dan memastikan state management terpisah dari UI. + +# INPUT DATA + +Gunakan skill pembaca file / workspace untuk mengambil source code berikut: + +1. File Layout XML: [PATH_KE_FILE_XML_ANDA, contoh: res/layout/activity_main.xml] +2. File Kategori/Style (jikit ada): [PATH_KE_STYLES_XML, contoh: res/values/themes.xml] +3. File Activity/Fragment Terkait: [PATH_KE_KOTLIN_FILE, contoh: MainActivity.kt] + +# INSTRUKSI MIGRASI + +Mohon proses file di atas dan buatkan kode Jetpack Compose dengan ketentuan sebagai berikut: + +1. Komponen UI & Layouting: + - Konversikan ViewGroup (ConstraintLayout, LinearLayout, RelativeLayout) ke Composable yang setara (Box, Column, Row, LazyColumn, atau ConstraintLayout Compose jika sangat kompleks). + - Gunakan komponen Material Design 3 (Button, OutlinedTextField, Card, Text, dll.). + - ConstraintLayout XML -> ConstraintLayout Compose (hanya jika kompleks) atau optimalkan menggunakan Row/Column/Box standar. + - RecyclerView -> LazyColumn / LazyRow. + - ImageView -> AsyncImage (Coil) jika memuat gambar dari URL. + +2. Styling & Resources: + - Gunakan `stringResource()`, `painterResource()`, dan `dimensionResource()` untuk menjaga modularitas resource. + - Sesuaikan warna dan tipografi menggunakan objek `MaterialTheme`. + - Styling & Themes: Gunakan token dari `MaterialTheme` (color, typography, shapes) alih-alih hardcoded colors/dimens dari XML, kecuali jika saya sebutkan lain. + +3. State & Event Handling: + - UI harus bersifat Stateless. Pisahkan State dan Event. + - Buat parameter lambda untuk event handling (misal: `onButtonClicked: () -> Unit`). + - Integrasikan dengan StateFlow/LiveData dari ViewModel yang ada di file Activity/Fragment asal (gunakan `collectAsStateWithLifecycle()`). + - State Management: Ubah semua state UI yang sebelumnya diatur manual (misal: setText(), setVisibility()) menggunakan `MutableState`, `remember`, atau `collectAsStateWithLifecycle()` dari ViewModel jika ada. + - Unidirectional Data Flow (UDF): Pastikan Composable bersifat stateless (menggunakan State Hoisting) di mana event dikirim ke atas (callbacks) dan data mengalir ke bawah. + +4. Preview: + - Sediakan `@Preview` fungsi Composable, lengkap dengan `ShowBackground = true` dan tema default-nya. + +5. Performa: Hindari recomposition yang tidak perlu. Gunakan `remember` untuk objek yang berat dan `derivedStateOf` jika ada kalkulasi state turunan. + +# OUTPUT YANG DIHARAPKAN + +Berikan output berupa full code untuk file Composable baru (`.kt`), serta berikan panduan singkat jika ada dependensi Gradle baru yang perlu ditambahkan atau perubahan minor yang harus saya lakukan di sisi ViewModel/Activity. + +--- + +# INPUT DATA + +## 1. File XML (Layout Asli) + +```xml +[TEMPELKAN KODE XML DI SINI] From 671e43d7ef19e0f02af07665512f13f2fdddb29a Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 11:20:08 +0700 Subject: [PATCH 04/14] feat: add comprehensive agent skill references and documentation for Jetpack Compose, navigation, and build migration --- .agents/skills/adaptive/SKILL.md | 301 +++++++ .../adaptive/flexbox/container-behavior.md | 112 +++ .../layouts/adaptive/flexbox/get-started.md | 69 ++ .../compose/layouts/adaptive/flexbox/index.md | 81 ++ .../layouts/adaptive/flexbox/item-behavior.md | 170 ++++ .../adaptive/grid/container-properties.md | 320 ++++++++ .../layouts/adaptive/grid/get-started.md | 51 ++ .../ui/compose/layouts/adaptive/grid/index.md | 73 ++ .../layouts/adaptive/grid/item-properties.md | 168 ++++ .../layouts/adaptive/mediaquery/index.md | 329 ++++++++ .../develop/ui/compose/tooling/debug.md | 114 +++ .../recipes/material-listdetail.md | 141 ++++ .agents/skills/agp-9-upgrade/SKILL.md | 103 +++ .../agp-9-upgrade/references/buildconfig.md | 54 ++ .../agp-9-upgrade/references/ksp-kapt.md | 39 + .../references/paparazzi-gradle-9.md | 30 + .../agp-9-upgrade/references/recipes.md | 62 ++ .agents/skills/edge-to-edge/SKILL.md | 426 ++++++++++ .agents/skills/jetpack-compose-m3/SKILL.md | 281 +++++++ .../wearables/compose/migrate-to-material3.md | 677 ++++++++++++++++ .../SKILL.md | 124 +++ .../analysis-of-the-project-and-layout.md | 42 + .../migrate-xml-theme-to-compose.md | 171 ++++ .../interoperability-apis/compose-in-views.md | 299 +++++++ .../interoperability-apis/views-in-compose.md | 286 +++++++ ...setup-compose-dependencies-and-compiler.md | 197 +++++ .../identify-optimal-xml-candidate.md | 31 + .../references/xml-layout-migration.md | 86 ++ .agents/skills/navigation-3/SKILL.md | 112 +++ .../guide/navigation/navigation-3/index.md | 38 + .../navigation-3/migration-guide.md | 498 ++++++++++++ .../navigation-3/recipes/animations.md | 147 ++++ .../navigation/navigation-3/recipes/basic.md | 89 +++ .../navigation-3/recipes/basicdsl.md | 85 ++ .../navigation-3/recipes/basicsaveable.md | 90 +++ .../navigation-3/recipes/bottomsheet.md | 195 +++++ .../navigation-3/recipes/common-ui.md | 200 +++++ .../navigation-3/recipes/conditional.md | 230 ++++++ .../recipes/deeplinks-advanced.md | 155 ++++ .../navigation-3/recipes/deeplinks-basic.md | 744 ++++++++++++++++++ .../navigation/navigation-3/recipes/dialog.md | 107 +++ .../recipes/material-listdetail.md | 141 ++++ .../recipes/material-supportingpane.md | 145 ++++ .../navigation-3/recipes/modular-hilt.md | 283 +++++++ .../navigation-3/recipes/modular-koin.md | 287 +++++++ .../recipes/multiple-backstacks.md | 436 ++++++++++ .../navigation-3/recipes/passingarguments.md | 371 +++++++++ .../navigation-3/recipes/results-event.md | 272 +++++++ .../navigation-3/recipes/results-state.md | 266 +++++++ .../navigation-3/recipes/scenes-listdetail.md | 435 ++++++++++ .../navigation-3/recipes/scenes-twopane.md | 244 ++++++ .../navigation/type-safe-destinations.md | 129 +++ .agents/skills/r8-analyzer/SKILL.md | 62 ++ .../references/CONFIGURATION-ANALYZER.md | 287 +++++++ .../r8-analyzer/references/CONFIGURATION.md | 44 ++ .../references/KEEP-RULES-IMPACT-HIERARCHY.md | 83 ++ .../r8-analyzer/references/REDUNDANT-RULES.md | 222 ++++++ .../references/REFLECTION-GUIDE.md | 139 ++++ .../r8-analyzer/references/REPORT_FORMAT.md | 51 ++ .../enable-app-optimization.md | 198 +++++ .../testing/other-components/ui-automator.md | 312 ++++++++ .agents/skills/styles/SKILL.md | 226 ++++++ .../ui/compose/designsystems/custom.md | 459 +++++++++++ .../develop/ui/compose/styles/fundamentals.md | 421 ++++++++++ .../ui/compose/styles/state-animations.md | 461 +++++++++++ .../ui/compose/styles/styles-vs-modifiers.md | 48 ++ .../develop/ui/compose/styles/theming.md | 257 ++++++ prompt_ai/MIGRATE_COMPOSE.md | 74 +- 68 files changed, 13871 insertions(+), 9 deletions(-) create mode 100644 .agents/skills/adaptive/SKILL.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md create mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md create mode 100644 .agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md create mode 100644 .agents/skills/agp-9-upgrade/SKILL.md create mode 100644 .agents/skills/agp-9-upgrade/references/buildconfig.md create mode 100644 .agents/skills/agp-9-upgrade/references/ksp-kapt.md create mode 100644 .agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md create mode 100644 .agents/skills/agp-9-upgrade/references/recipes.md create mode 100644 .agents/skills/edge-to-edge/SKILL.md create mode 100644 .agents/skills/jetpack-compose-m3/SKILL.md create mode 100644 .agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md create mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md create mode 100644 .agents/skills/navigation-3/SKILL.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/index.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md create mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md create mode 100644 .agents/skills/r8-analyzer/SKILL.md create mode 100644 .agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md create mode 100644 .agents/skills/r8-analyzer/references/CONFIGURATION.md create mode 100644 .agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md create mode 100644 .agents/skills/r8-analyzer/references/REDUNDANT-RULES.md create mode 100644 .agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md create mode 100644 .agents/skills/r8-analyzer/references/REPORT_FORMAT.md create mode 100644 .agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md create mode 100644 .agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md create mode 100644 .agents/skills/styles/SKILL.md create mode 100644 .agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md create mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md create mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md create mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md create mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/theming.md diff --git a/.agents/skills/adaptive/SKILL.md b/.agents/skills/adaptive/SKILL.md new file mode 100644 index 0000000..566374a --- /dev/null +++ b/.agents/skills/adaptive/SKILL.md @@ -0,0 +1,301 @@ +--- +name: adaptive +description: Instructions to make or update an app's UI so that it adapts to different + Android devices including phones, tablets, foldables, laptops, desktop, TV, Auto + and XR. It includes how to handle different window sizes, pointing devices (such + as mouse) and text entry devices (such as keyboard) using the Compose MediaQuery + API. It also covers multi-pane layouts using Navigation3 Scenes, adaptive UI components + (such as buttons) with varying target sizes, and adaptive layouts (including navigation + areas - nav rails and nav bars) using the Compose Grid and FlexBox APIs. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-02' + keywords: + - android + - ui + - adaptive + - Grid + - FlexBox + - MediaQuery + - navigation +--- + +## Prerequisites + +The app must: + +- Use Compose for all screens. If it's still using Fragments or Views, suggest using the XML to Compose skill to migrate those screens. +- Use Jetpack Navigation 3. If it doesn't, suggest the Navigation 3 skill to migrate the app. + +## Workflow to make an app adaptive + +To make an app adaptive, follow these steps or a subset of them adapting to the +task. + +- Step 1: Verify current UI +- Step 2: Make the navigation bar adaptive +- Step 3: Add multi-pane layouts +- Step 4: Make vertical lists adaptive by changing the number of columns +- Step 5: Hide app bars when scrolling + +## Step 1. Verify current UI + +Ensure that screenshot tests exist to verify the current UI on different form +factors. If they don't exist, add the [Compose Preview Screenshot Testing +tool](references/android/develop/ui/compose/tooling/debug.md). Use the following annotation to create previews for all the major form +factors. For example: + + +```kotlin +@Preview(name = "Phone", device = Devices.PHONE, showBackground = true) +@Preview(name = "Foldable", device = Devices.FOLDABLE, showBackground = true) +@Preview(name = "Tablet", device = Devices.TABLET, showBackground = true) +@Preview(name = "Desktop", device = Devices.DESKTOP, showBackground = true) +annotation class FormFactorPreviews + +@PreviewTest +@FormFactorPreviews +@Composable +fun FeedScreenPreview() { + SnippetsTheme { + Box { + Text("My Screen") + } + } +} +``` + +
+ +## Step 2. Make the navigation bar adaptive + +Bottom navigation bars are optimized for touch input when the user is holding a +phone in portrait mode. On larger screen hand-held devices, like tablets and +unfolded foldables, the navigation area must be accessible from the edge of the +screen (navigation rail). + +If you need to provide more screen space for the content, hide the +navigation area. Examples of this include: + +- Hiding the navigation bar when the user scrolls down and showing it again when the user scrolls up. The assumption is that when the user is scrolling down, they are consuming content but when scrolling up they are trying to navigate away from that content. +- Hiding the navigation area when its content is distracting. For example, in camera previews or when displaying a full-screen photo. + +When the detail screen is displayed full-screen on mobile, full-screen mode must +be deactivated on larger screens. + +Steps to migrate: + +- Locate the existing navigation bar. +- Convert each item to a `NavigationSuiteItem`. +- Identify whether the navigation bar's visibility changes. For example, if it is wrapped with an `AnimatedContent` or `AnimatedVisibility` composable. If so, follow the guidance in the "Control navigation area visibility". +- Replace the container that held the navigation bar (often a `Scaffold`) with `NavigationSuiteScaffold` from the Material 3 adaptive layouts library. +- Supply the navigation items using the `navigationItems` parameter of `NavigationSuiteScaffold`. + +### Step 2.1. Control navigation area visibility + +If the navigation bar's visibility changes - it is hidden under certain +scenarios or on certain screens - this behavior must be maintained with the +adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state` +parameter. + +Steps to migrate: + +- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. Use `isNavBarVisible` or `shouldShowNavBar` as the variable name. +- Create an instance of `NavigationSuiteScaffoldState` using `rememberNavigationSuiteScaffoldState()` and pass it to `NavigationSuiteScaffold`. +- When the navigation area visibility changes, use a `LaunchedEffect` to call `show` or `hide` on the `NavigationSuiteScaffoldState`. + +For example: + + +```kotlin +// Pass this variable to any composable that needs to control the navigation area visibility +var isNavBarVisible by remember { mutableStateOf(true) } +val scaffoldVisibilityState = rememberNavigationSuiteScaffoldState() + +NavigationSuiteScaffold( + navigationSuiteItems = navItems, + state = scaffoldVisibilityState +) { + // Main content +} + +LaunchedEffect(isNavBarVisible){ + if (isNavBarVisible) { + scaffoldVisibilityState.show() + } else { + scaffoldVisibilityState.hide() + } +} +``` + +
+ +## Step 3. Add multi-pane layouts using Navigation 3 Scenes + +Analyze the codebase looking for related screens - tapping on something in one +screen opens another screen that shows information related to the first. There +are two canonical screen relationships: list-detail and supporting pane. + +IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement +multi-pane layouts. Do not use `ListDetailPaneScaffold` or +`SupportingPaneScaffold`. + +### Step 3.1. List-detail + +#### Identify the list and detail screens + +List-detail layouts display a list of items (this is the list screen) and +clicking on an item opens a new screen that shows more details about that item +(the detail screen). + +Typical usage includes productivity apps like email, notes, and messaging. + +Unless requested explicitly, avoid this pattern when the detail content requires +substantial screen space (e.g., images or media that benefits from a full-screen +presentation). + +#### Add a Material list-detail SceneStrategy + +- Add the `androidx.compose.material3.adaptive:adaptive-navigation3` library +- Create an `androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy` using `rememberListDetailSceneStrategy` +- Pass the `ListDetailSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter + +#### Use metadata to identify the list and detail screens + +- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = { + })`. +- Use the `detailPlaceholder` parameter to add a placeholder on the detail screen when no list items are selected. +- Add metadata to the detail entry using `ListDetailSceneStrategy.detailPane()`. + +#### Important considerations + +- When a detail screen displays its content full-screen on mobile (content fills the entire screen, bars or rails are hidden), full-screen mode must be deactivated if it's part of a list-detail layout. +- Detail screens must not show a back arrow when on a list-detail layout. + +For a reference implementation, check the [Nav3 **Material** List Detail +recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md). + +### Step 3.2. Supporting pane + +Identify supporting pane screens where a main screen displays a single item, and +selecting it opens a "supporting screen" with more details. The supporting +screen complements the main screen and is shown in a supporting pane. + +#### Add a Material supporting pane `SceneStrategy` + +- If you haven't already, add the `androidx.compose.material3.adaptive:adaptive-navigation3` library +- Create an `androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy` using `rememberSupportingPaneSceneStrategy` +- Pass the `SupportingPaneSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter + +#### Use metadata to identify the main and supporting screens + +- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the main entry using `SupportingPaneSceneStrategy.mainPane()` +- Add metadata to the supporting entry using `SupportingPaneSceneStrategy.supportingPane()` + +### Step 3.3. Run screenshot tests + +If you have made changes, record new reference files. Ask the user to visually +verify that the new layouts are correct. + +## Step 4. Make vertical lists adaptive by changing the number of columns + +### Step 4.1. Make lazy lists adaptive + +Look for the following vertical list composables: `LazyColumn`, +`LazyVerticalGrid`, `LazyVerticalStaggeredGrid`. + +Steps to migrate: + +- Choose a suitable minimum width in dp for the column. The item must be clearly visible to the user at this width. +- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction later +- For `LazyVerticalGrid`: change the `columns` parameter to use `GridCells.Adaptive(.dp)` +- For `LazyVerticalStaggeredGrid`: change the `columns` parameter to use `StaggeredGridCells.Adaptive(.dp)` + +### Step 4.2. Migrate non-lazy lists to Grid + +WARNING: Grid is an experimental API available from Compose 1.11.0-beta01. +Confirm with the user that they are happy to use an experimental API in their +codebase. + +Look for any `Column` that contains multiple items of the same type and replace +it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy +layout. Do not place `Grid` inside the existing `Column`. Completely replace it. + +`Grid` is configured by supplying a lambda (an extension function on +`GridConfigurationScope`) to its `config` parameter. Inside the lambda, +`constraints` provides the minimum and maximum dimensions of the grid container +and can be used to change the number of rows and columns based on the available +size. For example, the following code configures `Grid` such that when the +available width is: + +- less than 800dp, a 2x4 grid is used +- 800dp or more, a 4x2 grid is used + + +```kotlin +Grid( + config = { + val maxWidthDp = constraints.maxWidth.toDp() + val (cols, rows) = if (maxWidthDp < 800.dp){ + 2 to 4 + } else{ + 4 to 2 + } + + val gapSizeDp = 8.dp + val cellSize = ((maxWidthDp - (gapSizeDp * (cols - 1))) / cols).coerceAtLeast(0.dp) + repeat(cols) { column(cellSize) } + repeat(rows) { row(cellSize) } + gap(gapSizeDp) + } +) { /** items **/ } +``` + +
+ +`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)` +annotation to any function that uses it. + +## Step 5: Hide App Bars when scrolling + +In an app with multiple top-level destinations, each screen must manage its own +app bar state independently. There are two main scroll behaviors: + +- `exitUntilCollapsedScrollBehavior`: Hides on scroll down, stays hidden while you scroll up until you reach the very top (0 offset). +- `enterAlwaysScrollBehavior`: Hides on scroll down, shows immediately on scroll up. + +## Final step: Build and test + +Build the app and run the local tests. If the project has screenshot tests, run +them but DO NOT update the reference images. Prompt the user to do this after +they have viewed the screenshot diffs. + +## Additional documentation for experimental adaptive APIs + +The following APIs are available from Compose 1.11.0-beta01. + +### FlexBox + +Check the FlexBox documentation: + +- [Overview](references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md) +- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md) +- [Set container behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md) +- [Set item behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md) + +## MediaQuery + +Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's +screen size, pointer precision, keyboard type, whether it has cameras or +microphones, and other device capabilities. + +## Grid + +Check the Grid documentation when you need to display a fixed number of items in +a grid layout: + +- [Overview](references/android/develop/ui/compose/layouts/adaptive/grid/index.md) +- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md) +- [Set container properties](references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md) +- [Set item properties](references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md) diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md new file mode 100644 index 0000000..41112fc --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md @@ -0,0 +1,112 @@ +To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` +block and supply it using the `config` parameter. + + +```kotlin +FlexBox( + config = { + direction(FlexDirection.Column) + wrap(FlexWrap.Wrap) + alignItems(FlexAlignItems.Center) + alignContent(FlexAlignContent.SpaceAround) + justifyContent(FlexJustifyContent.Center) + gap(16.dp) + } +) { // child items +} +``` + +
+ +Use `FlexBoxConfig` to define the layout direction, wrapping behavior, +alignment, and gaps between items. + +## Layout direction + +The `direction` function sets the main axis, which dictates the direction +items are laid out in. It accepts the following values: + +- `Row` (default): Sets the main axis to be horizontal. In left-to-right locales this will be left-to-right, with the opposite in right-to-left. +- `RowReverse`: Reverses the direction of `Row`. +- `Column`: Sets the main axis to be vertical, top-to-bottom. +- `ColumnReverse`: Reverses the direction of `Column`. + +## Align items and distribute extra space + +The following sections describe how to align items and distribute extra space +along the main and cross axes. + +### Along the main axis + +Use `justifyContent` to distribute items along the main axis. The following +table shows the behavior when the direction is `Row`. + +|---|---| +| | ![Illustration of a horizontal main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/main-axis.png) | +| `Start` | ![Items aligned to the start of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-start.png) | +| `Center` | ![Items aligned to the center of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-center.png) | +| `End` | ![Items aligned to the end of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-end.png) | +| `SpaceBetween` | ![Items distributed along the main axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacebetween.png) | +| `SpaceAround` | ![Items distributed along the main axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacearound.png) | +| `SpaceEvenly` | ![Items distributed along the main axis with space evenly around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spaceevenly.png) | + +### Along the cross axis + +Use `alignItems` to align items along the cross axis within a single line. This +behavior can be overridden by individual items using the +[`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment). + +The following images show the behavior when the direction is `Row`: + +|---|---|---|---|---|---| +| ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-start.png) | ![Items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-end.png) | ![Items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-center.png) | ![Items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-stretch.png) | ![Items aligned to their baseline along the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-baseline.png) | +| | `Start` | `End` | `Center` | `Stretch` | `Baseline` | + +Use `alignContent` to align lines to the cross axis and to distribute extra +space between lines. This property only applies when there are multiple lines +(wrapping is enabled). The following images show the behavior when the direction +is `Row`: + +|---|---|---|---|---|---|---| +| ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Multiple lines of items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-start.png) | ![Multiple lines of items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-end.png) | ![Multiple lines of items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-center.png) | ![Multiple lines of items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-stretch.png) | ![Multiple lines of items distributed along the cross axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacebetween.png) | ![Multiple lines of items distributed along the cross axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacearound.png) | +| | `Start` | `End` | `Center` | `Stretch` | `SpaceBetween` | `SpaceAround` | + +## Wrap items + +Wrapping lets a `FlexBox` container become multi-line, moving items that don't +fit onto a new row or column along the cross-axis. Configure wrapping behavior +using `wrap`. + +|---|---| +| **`FlexWrap` value** | **Example using direction `Row`** | +| `NoWrap` (default): Prevents items from wrapping. Items overflow if the main size is insufficient. | ![Items in a single line overflowing the container because wrapping is disabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-1.png) | +| `Wrap`: When there is insufficient space for an item (plus any [gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps)), a new line is created in the direction of the cross axis. For example, if the direction is `Row`, a new line is added **below**. | ![Items wrapping onto a new line below because wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-2.png) | +| `WrapReverse`: The same as `Wrap`, except the new line is added in the opposite direction to the cross axis. For example, if the direction is `Row`, a new line is added **above**. | ![Items wrapping onto a new line above because reverse wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-3.png) | + +The following example shows how the `FlexBox` wrapping algorithm works. The +`FlexBox` container has a main size of `100dp`, with `wrap` set to +`FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`, +`40dp`, and `50dp`, respectively. + +There is `100dp` available space in the line. Child 1 is `20dp`. +There is space, so Child 1 is placed into the line. +![First item placed in the FlexBox container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-1.png) **Figure 1.** First item placed in the `FlexBox` container. + +There is `80dp` available space in the line. The gap is `8dp`. Child 2 is +`40dp`. The required space is `48dp`. There is space, so the gap and Child 2 +are placed into the line. +![Second item placed in the FlexBox container after the first item.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-2.png) **Figure 2.** Second item placed in the `FlexBox` container after the first item. + +There is `32dp` available space in the line. The gap is `8dp`. Child 3 is +`50dp`. The required space is `58dp`. There is not enough space in the current +line, so Child 3 is placed in a new line. +![Third item placed on a new line because it doesn't fit on the first line.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-3.png) **Figure 3.** Third item placed on a new line because it doesn't fit on the first line. + +## Add gaps between items + +Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful +to avoid adding spacing modifiers to children. + +|---|---|---| +| ![Row gap adds vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-1.png) | ![Column gap adds horizontal space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-2.png) | ![Gap adds both horizontal and vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-3.png) | +| `rowGap` adds vertical space between items and lines. | `columnGap` adds horizontal space between items and lines. | `gap` is a convenience function that adds both `columnGap` and `rowGap`. | \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md new file mode 100644 index 0000000..8b00b01 --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md @@ -0,0 +1,69 @@ +This page describes how to implement basic `FlexBox` layouts. + +## Set up project + +1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's + `lib.versions.toml`. + + [versions] + compose = "1.12.0-beta02" + + [libraries] + androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } + +2. Add the library dependency to your app's `build.gradle.kts`. + + dependencies { + implementation(libs.androidx.compose.foundation.layout) + } + +## Create basic FlexBox layouts + +**Example 1** : `FlexBox` lays out two `Text` elements that are centrally +aligned. + + +```kotlin +FlexBox( + config = { + direction(FlexDirection.Column) + alignItems(FlexAlignItems.Center) + } +) { + Text(text = "Hello", fontSize = 48.sp) + Text(text = "World!", fontSize = 48.sp) +} +``` + +
+ +![Hello World text composables stacked on top of each other in a basic FlexBox implementation.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox.png) + +**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally +to fill the available space on each row. There is an `8.dp` +gap, both vertically and horizontally, between the items. + + +```kotlin +FlexBox( + config = { + wrap(FlexWrap.Wrap) + gap(8.dp) + } +) { + // All boxes have an intrinsic width of 100.dp + // Some grow to fill any remaining space on the row. + RedRoundedBox() + BlueRoundedBox() + GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) }) + OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) }) + PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) }) +} +``` + +
+ +![Two rows of colored items, with three unequally sized items distributed across the top row and two unequally sized items across the bottom row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox-2.png) + +To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set +item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior). \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md new file mode 100644 index 0000000..ddda0de --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md @@ -0,0 +1,81 @@ +> [!NOTE] +> **Note:** FlexBox is an experimental API and is likely to change in the future. To use it, annotate your code with `@ExperimentalFlexBoxApi`. Please file any issues or feedback on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&title=%5BFlexBox%5D). + +[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can +resize, wrap, align, and distribute space among items to optimally fill the +available space. It's a useful layout for different sized items and for resizing +items when the available space changes. + +With `FlexBox`, you can: + +- Control how items grow and shrink to fill the available space +- Wrap items onto new rows or columns when there isn't enough space for them +- Distribute extra space between items using convenient presets + +## When to use FlexBox + +`FlexBox` is usually used to display a small number of items *within* an +overall screen layout. For an overall screen layout, +`Grid` is usually a better choice. `FlexBox` does not support lazy-loading of +items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you +need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`. + +## Terminology and concepts + +> [!IMPORTANT] +> **Key Point:** `FlexBox` is heavily influenced by the [CSS Flexible Box Layout specification](https://www.w3.org/TR/css-flexbox-1/) and has almost identical concepts, terminology, and behavior. If you're familiar with `display: flex`, you'll find `FlexBox`'s properties and behavior almost identical. + +`FlexBox` lays out its items in either horizontal or vertical *lines* . This +direction of these lines establishes the *main axis* . 90 degrees to the main +axis is the *cross axis* . The length of the `FlexBox` along the main axis is +known as the *main size* . The corresponding cross axis length is known as the +*cross size* . These sizes and axes form the basis of `FlexBox`'s behavior. + + +![FlexBox with horizontal main axis and vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-row-2.png) **Figure 1.** Axes and sizes when the `FlexBox` direction is `Row`. ![FlexBox with vertical main axis and horizontal cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-column.png) **Figure 2.** Axes and sizes when the `FlexBox` direction is `Column`. + +
+ +### Apply properties + +You can apply `FlexBox` properties in two ways: + +- To the `FlexBox` container using `FlexBox(config)` +- To an item inside the `FlexBox` using `Modifier.flex` + +| **Container properties (`config`**) | **Item properties (`Modifier.flex`**) | +|---|---| +| - `direction` - the item layout direction - `wrap` - whether to wrap items if the **main size** is insufficient - `justifyContent` - how to **distribute** items along the **main axis** - `alignItems` - how to **align** items along the **cross axis** - `alignContent` - how to distribute extra space from the **cross size** when there are multiple lines - `rowGap` / `columnGap` - adds space between items and lines See [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) for more information about these properties. | - `basis` - the size of the item before any extra space from the **main size** is distributed - `grow` - the share of extra space from the **main size** that this item should receive - `shrink` - the share of space deficit from the **main size** that this item should receive - `alignSelf` - how to distribute extra space from the **cross size** to this item, overrides `alignItems` - `order` - controls the layout order See [Set item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior) for more information about these properties. | + +### Understand the `FlexBox` layout algorithm + +One of `FlexBox`'s most powerful features is its ability to resize its children +to best fit the space available to it. Understanding how `FlexBox` does this can +help you set `FlexBox` properties to optimize your UI for all possible sizes. + +`FlexBox`'s layout algorithm works in the following way: + +1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size) + to calculate its initial size along the main axis before any extra space is + distributed. + +2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if + present. + +3. **Build lines** : For each child, check if its initial size plus + [`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line. + If so, place this child into the line. If not, place it onto a new line if + [wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line + where it will overflow (it will be partially obscured by the edge of the + container). + +4. **Align or resize items in the main axis** : For each line, distribute extra + space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or + [aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them. + +5. **Align or resize items in the cross axis** : For each line, distribute extra + space to or between items and lines by [stretching or aligning + them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis). + +Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to +create a basic `FlexBox`. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md new file mode 100644 index 0000000..e4a5c0f --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md @@ -0,0 +1,170 @@ +Use `Modifier.flex` to control how an item changes size, order, and is aligned +inside a `FlexBox`. + +## Item size + +Use the `basis`, `grow`, and `shrink` functions to control an item's size. + + +```kotlin +FlexBox { + RedRoundedBox( + modifier = Modifier.flex { + basis(FlexBasis.Auto) + grow(1.0f) + shrink(0.5f) + } + ) +} +``` + +
+ +### Set initial size + +Use `basis` to specify the item's initial size before any extra space is +distributed. You can think of this as the item's *preferred* size. + +|---|---|---|---| +| **Value type** | **Behavior** | **Code snippet** Note: The boxes have a maximum intrinsic size of `100dp` | **Example using container width `600dp`** | +| `Auto` (default) | Use the item's maximum intrinsic size. For example, a `Text` composable's maximum intrinsic width is the width of all its text on a single line - no wrapping. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) BlueRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) } ``` | ![Items sized based on their intrinsic size using basis Auto.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-1.png) | +| Fixed `dp` | A fixed size in Dp. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(200.dp) } ) BlueRoundedBox( Modifier.flex { basis(100.dp) } ) } ``` | ![Items sized to a fixed dp value using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-2.png) | +| Percentage | A percentage of the container size. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(0.7f) } ) BlueRoundedBox( Modifier.flex { basis(0.3f) } ) } ``` | ![Items sized as a percentage of container size using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-3.png) | + +If the basis value is less than the item's intrinsic minimum size, the intrinsic +minimum size is used instead. For example, if a `Text` item that contains a word +requires `50dp` to display, but also has `basis = 10.dp`, a +value of `50dp` is used. + +### Grow items when there's space + +Use `grow` to specify how much an item grows when there is extra space. This is +space remaining in the `FlexBox` container after all the items' `basis` values +have been added up. The `grow` value indicates *how much* of the extra space a +given child will receive, relative to its siblings. By default, items won't +grow. + +The following example shows a `FlexBox` with three child items. Each has a basis +value of `100dp`. The first child has a positive `grow` value. Since there is +only one child with a `grow` value, the actual value is irrelevant - as long as +it's positive, the child receives all the extra space. + +The images show the `FlexBox` behavior when its container size is `600dp`. + +|---|---| +| ```kotlin FlexBox { RedRoundedBox( title = "400dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox(title = "100dp") GreenRoundedBox(title = "100dp") } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-1.png) Child 1 grows by `300dp` to fill the extra space. ![First item grows to fill 300dp of extra space.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-2.png) | + +In the following example, the container size and `basis` size are the same. The +difference is that each child has a different `grow` value. + +|---|---| +| ```kotlin FlexBox { RedRoundedBox( title = "150dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox( title = "200dp", modifier = Modifier.flex { grow(2f) } ) GreenRoundedBox( title = "250dp", modifier = Modifier.flex { grow(3f) } ) } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth, with different grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-3.png) The total grow value is 6. Child 1 grows by (1 / 6) \* 300 = `50dp` Child 2 grows by (2 / 6) \* 300 = `100dp` Child 3 grows by (3 / 6) \* 300 = `150dp` ![Items grow to fill 300dp of extra space based on relative grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-4.png) | + +### Shrink items when there's insufficient space + +Use `shrink` to specify how much an item shrinks when the `FlexBox` container +has insufficient space for all the items. `shrink` works the same way as `grow` +except that, instead of distributing *extra space* to items, the *space deficit* +is distributed to items. The `shrink` value specifies how much of the space +deficit the item receives, or rather, how much the item will shrink by. By +default, items have a `shrink` value of `1f`, meaning they shrink equally. + +The following example shows two `Text` composables with the same text. The first +child has a shrink value of `1f`, meaning it shrinks to absorb all the space +deficit. + + +```kotlin +FlexBox { + Text( + "The quick brown fox", + fontSize = 36.sp, + modifier = Modifier + .background(PastelRed) + .flex { shrink(1f) } + ) + Text( + "The quick brown fox", + fontSize = 36.sp, + modifier = Modifier + .background(PastelBlue) + .flex { shrink(0f) } + ) +} +``` + +
+ +As the container size shrinks, Child 1 shrinks. + +|---|---| +| **Container size** | **FlexBox UI** | +| `700dp` | ![Two items in a 700dp container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-1.png) | +| `500dp` | ![First item shrinks as container size reduces to 500dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-2.png) | +| `450dp` | ![First item shrinks further as container size reduces to 450dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-3.png) | + +## Item alignment + +Use `alignSelf` to control how an item is aligned to the cross axis. This +overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It +has all the same possible values, with the addition of `Auto` which inherits the +behavior of the `FlexBox` container. + +For example, this `FlexBox` has `alignItems` set to `Start` and five children +which override the cross axis alignment. + + +```kotlin +FlexBox( + config = { + alignItems(FlexAlignItems.Start) + } +) { + RedRoundedBox() + BlueRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Center) }) + GreenRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.End) }) + PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) }) + OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) }) +} +``` + +
+ +![Five children of varying sizes overriding the alignItems property.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/item-alignment.png) + +## Item order + +By default, `FlexBox` lays out items in the order that they are declared in +code. Override this behavior using `order`. + +The default value for `order` is zero, and `FlexBox` sorts items based on this +value in ascending order. Any items that have the same `order` value are +laid out in the same order they are declared in. Use negative and positive +`order` values to move items to the start or end of a layout without changing +where they are declared. + +The following example shows two child items. The first has the default `order` +of zero, and the second has an order of `-1`. After sorting, Child 1 appears +after Child 2. + + +```kotlin +FlexBox { + // Declared first, but will be placed after visually + RedRoundedBox( + title = "World" + ) + + // Declared second, but will be placed first visually + BlueRoundedBox( + title = "Hello", + modifier = Modifier.flex { + order(-1) + } + ) +} +``` + +
+ +![Two rounded boxes, with the first containing the text Hello and the second containing the text World.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/itemorder.png) \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md new file mode 100644 index 0000000..33b37a2 --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md @@ -0,0 +1,320 @@ +You can define a Grid container configuration to create flexible layouts +that respond to different screen sizes and content types. +This page describes how to do the following: + +- [Define a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-definition): Set up the basic structure of rows and columns. +- [Place items in a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#item-placement): Understand how items are placed into grid cells and how to change flow direction. +- [Manage track sizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-track-size): Use fixed, percentage, flexible, and intrinsic sizing to set track sizes. +- [Set gaps](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-gap): Manage the "gutters" between rows and columns. + +## Define a grid + +A grid consists of columns and rows. +The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter +that accepts a lambda to define the columns and rows +within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope). +The following example defines a grid that has three rows and two columns, +each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp): + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + } +) { +} +``` + +
+ +## Place items in a grid + +`Grid` takes the UI elements +in the `content` lambda and places them into grid cells. +The grid lays out items regardless of +whether you have explicitly defined the rows and columns. +By default, +`Grid` tries to place a UI element in the available grid cell in the row; +if it can't, it places it in an available grid cell in the next row. +If there are no empty cells, `Grid` creates a new row. + +In the following example, the grid has six grid cells +and places a card into each one (Figure 1). +Each grid cell is `160dp` x `90dp`, +making the total grid size `320dp` x `270dp`. + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + } +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
+ +![Six cards are placed in a grid that has three rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/placement.png) **Figure 1**. Six cards are placed in a grid that has three rows and two columns. + +To change this default behavior to filling by column, +set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) + flow = GridFlow.Column // Grid tries to place items to fill the column + }, +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
+ +![The flow function changes the direction to place items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-flow.png) **Figure 2** . `GridFlow.Row` (left) and `GridFlow.Column` (right). + +## Manage track sizing + +Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track). +You can specify the size of a grid track using one of the following methods: + +- **Fixed** (`Dp`): Allocates a specific size (e.g., `column(180.dp)`). +- **Percentage** (`Float`): Allocates a percentage of the total available space from `0.0f` to `1.0f` (e.g., `row(0.5f)` for 50%). +- **Flexible** ([`Fr`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Fr)): Distributes remaining space proportionally after fixed and percentage tracks are calculated. For example, if two rows are set to `1.fr` and `3.fr`, the latter receives 75% of the remaining height. +- **Intrinsic** : Sizes the track based on the content inside it. For more information, see [Determine grid track size intrinsically](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#intrinsic-grid-track-size). + +The following example uses the different track sizing options +to define the row heights: + + +```kotlin +Grid( + config = { + column(1f) + + row(100.dp) + row(0.2f) + row(1.fr) + row(GridTrackSize.Auto) + }, + modifier = Modifier.height(480.dp) +) { + PastelRedCard("Fixed(100.dp)") + PastelGreenCard("Percentage(0.2f)") + PastelBlueCard("Flex(1.fr)") + PastelYellowCard("Auto") + +} +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-sizes.png) **Figure 3** . Row heights defined using the four primary track sizing options in `Grid`. + +### Set the minimum size for flexible grid tracks + +When a grid container has no remaining space, +a standard flexible track can shrink to `0.dp`. +To prevent this and ensure content isn't crushed, +use [`GridTrackSize.MinMax`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinMax(androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.Fr)) +to enforce an explicit minimum size while keeping the track flexible. + +The following example allocates at least `100.dp` to the first row: + + +```kotlin +Grid( + config = { + column(1f) + // The first row has a minimum height of 100.dp and can expand to + // the half of the remaining space. + row(GridTrackSize.MinMax(100.dp, 1.fr)) + // The second row takes the half of the remaining space. + row(1.fr) + // The third row has a fixed height of 200.dp. + row(200.dp) + }, + modifier = Modifier.size(360.dp) // Total grid height is 360.dp +) { + PastelRedCard("MinMax(100.dp, 1.fr)") + PastelGreenCard("Flex(1.fr)") + PastelBlueCard("Fixed(200.dp)") +} +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-size-minmax.png) **Figure 4** . The first row has at least `100.dp` height. + +### Set the minimum grid track size to place lazy lists + +Standard flexible tracks automatically query the intrinsic sizes of +their children to establish a base size. +However, Jetpack Compose prohibits querying the intrinsic sizes of +[`SubcomposeLayout`](https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/SubcomposeLayout.composable#SubcomposeLayout(androidx.compose.ui.Modifier,kotlin.Function2)), which backs components, +such as [`LazyColumn`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable) and [`LazyRow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyRow.composable). + +Placing a lazy list inside a standard flexible track causes +an [`IllegalStateException`](https://developer.android.com/reference/java/lang/IllegalStateException) crash. +To safely place lazy lists inside a flexible grid track, +use `MinMax` with an explicit minimum size (such as `0.dp`) +to bypass the intrinsic measurement pass. + + +```kotlin +Grid( + config = { + column(1f) + // The first row's height is determined by the height of the Text composable. + row(GridTrackSize.Auto) + // The second row occupies the remaining space, allowing the LazyColumn to scroll. + row(GridTrackSize.MinMax(0.dp, 1.fr)) + + gap(8.dp) + }, + modifier = Modifier.size(width = 170.dp, height = 240.dp) +) { + Text("Lazy column in a Grid") + // The LazyColumn is placed in the second row, filling the remaining space. + LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) { + items(100) { number -> + PastelGreenCard("Card $number") + } + } +} +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/lazy-column-in-grid.png) **Figure 5** . `LazyColumn` in a grid cell. + +### Determine grid track size intrinsically + +You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` +when you want the layout to adapt to the content, +rather than forcing it into a fixed container. +The grid track size is determined with the following values: + +- [`GridTrackSize.MaxContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MaxContent()): Use the content's maximum intrinsic size (e.g., the width is determined by the full length of the text in a text block with no wrapping). +- [`GridTrackSize.MinContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinContent()): Use the content's minimum intrinsic size (e.g., the width is determined by the longest single word in a text block). +- [`GridTrackSize.Auto`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#Auto()): Use a flexible size for a track that adapts based on available space. It behaves like `MaxContent` by default, but shrinks and wraps its content to fit within the parent container. + +The following example places two texts side by side. +The column size for the first text is determined +by the required minimum width to display the text, +and the second column width depends on the required maximum width of the text. + + +```kotlin +Grid( + config = { + column(GridTrackSize.MinContent) + column(GridTrackSize.MaxContent) + row(1.0f) + }, + modifier = Modifier.width(480.dp) +) { + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") +} +``` + +
+ +![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 5**. Intrinsic sizes specified in the columns. + +## Set gaps between rows and columns + +Once your grid tracks are sized, +you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks. +You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function, +and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example, +there is a `16dp` gap between each row, +and an `8dp` gap between each column (Figure 5). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(16.dp) + columnGap(8.dp) + } +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
+ +![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 6**. Gaps between rows and columns. + +You can also use the convenience function [`gap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#gap(androidx.compose.ui.unit.Dp)) +to define gaps of the same column and row size, +and to define column and gap sizes separately using a single function. +The following code adds `8dp` gaps to the grid: + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) // Equivalent to columnGap(8.dp) and rowGap(8.dp) + } +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
\ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md new file mode 100644 index 0000000..bb78a97 --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md @@ -0,0 +1,51 @@ +This page describes how to implement basic [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) layouts. + +## Set up project + +1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's + `lib.versions.toml`. + + [versions] + compose = "1.12.0-beta02" + + [libraries] + androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } + +2. Add the library dependency to your app's `build.gradle.kts`. + + dependencies { + implementation(libs.androidx.compose.foundation.layout) + } + +## Create a basic grid + +The following example creates a basic 2x3 grid, +with the columns and rows having a fixed size of `100.dp`. + + +```kotlin +Grid( + config = { + repeat(2) { + column(100.dp) + } + repeat(3) { + row(100.dp) + } + } +) { + Card1(containerColor = PastelRed) + Card2(containerColor = PastelGreen) + Card3(containerColor = PastelBlue) + Card4(containerColor = PastelPink) + Card5(containerColor = PastelOrange) + Card6(containerColor = PastelYellow) +} +``` + +
+ +![A basic grid consists of rows and columns with fixed size.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/six-cards-in-grid.png) **Figure 1**. A basic grid consists of rows and columns with fixed size. + +To learn how to implement more advanced grids, +see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties). \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md new file mode 100644 index 0000000..316b046 --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md @@ -0,0 +1,73 @@ +> [!NOTE] +> **Note:** `Grid` is an experimental API and is subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&template=1424126). + +[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API +that lets you flexibly implement a two-dimensional layout. +With this API, you can display items in multi-column +or multi-row layouts that adapt to the available container size. +![A flexible and adaptive two-dimensional layout with Grid](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/example.png) **Figure 1.** A flexible and adaptive two-dimensional layout with `Grid`. + +## How is Grid different from similar composables? + +Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)). +These components are mainly for visualization of large, homogeneous data sets--- +for example, displaying a content catalog in a video streaming app. +These components are NOT designed +for the structural layout of a screen or complex component. + +You can also implement a two-dimensional layout +by combining multiple `Row` and `Column` composables. +However, this approach has some downsides, +such as deep hierarchies and difficulties in adaptability. + +The following table provides an overview +of which layouts are suitable for each API: + +| Component | Purpose | +|---|---| +| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that require lazy loading. | +| `Row`, `Column`, `FlexBox` | One-dimensional layout | +| `Grid` | Two-dimensional layout | + +> [!NOTE] +> **Note:** `Grid` doesn't support lazy loading. + +## Terminology + +Familiarize yourself with the following terminology +to understand how `Grid` works. + +### Grid line + +A grid is made up of lines, which run horizontally and vertically. +If your grid has three rows, it has four horizontal lines, +including the one after the last row. +In the following image, each dotted line represents a grid line: +![The grid consists of four horizontal lines and three vertical lines.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-line.png) **Figure 2**. The grid consists of four horizontal lines and three vertical lines. + +### Grid track + +A grid track is the space between two grid lines. +A row track is between two horizontal lines, +and a column track is between two vertical lines. +To define the size of these tracks, +assign a size to them when you create the grid. +![A grid track for the first row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-track.png) **Figure 3**. A grid track for the first row. + +### Grid cell + +A grid cell is the intersection of a row and column track. +![A grid cell that is an intersection of the second row and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-cell.png) **Figure 4**. A grid cell that is an intersection of the second row and the second column. + +### Grid area + +A grid area consists of several grid cells. +You can define a grid area by making an item span multiple tracks. +![A grid area that consists of four grid cells.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-area.png) **Figure 5**. A grid area that consists of four grid cells. + +### Grid gap + +A grid gap is the gutter between grid tracks. +You can't place a UI element into a gap, +but you can span a UI element across it. +![A grid gap between the first column and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-gap.png) **Figure 6**. A grid gap between the first column and the second column. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md new file mode 100644 index 0000000..9f0500e --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md @@ -0,0 +1,168 @@ +While the `Grid` config defines the overall structure, +you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning, +and alignment of items within that structure. + +## Set the item position + +Place an item into a specific track or cell +with the `row` and `column` parameters. + +The `row` and `column` parameters specify the row and column track indexes +that the item is placed in. +Track indexes are 1-based---they start at one. +Specifying only `row` or `column` (not both) places the item +in the next available space in that track. +Specifying both places the item into that cell. + +Use a positive integer to specify the track index from the start. +For example, to place an item in the first row and column, +use `gridItem(row = 1, column = 1)`. + +Use a negative integer to specify the track relative to the end. +For example, to place an item in the second-to-last row and column, use +`gridItem(row = -2, column = -2)`. + +In the following example, Card **#2** is placed +in the second row and the second column. +Card **#3** is assigned to the last row (indexed by -1), +where it automatically occupies +the first available column in that track (Figure 1). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) + } +) { + Card1() + Card2(modifier = Modifier.gridItem(row = 2, column = 2)) + Card3(modifier = Modifier.gridItem(row = -1, column = -2)) +} +``` + +
+ +![Card #2 is placed in the grid cell +in the second row and the second column, +and Card #3 is placed in the first column in the third row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/position.png) **Figure 1** . Card **#2** is placed in the grid cell in the second row and the second column, and Card **#3** is placed in the first column in the third row. + +## Span rows and columns + +Use the `rowSpan` and `columnSpan` parameters +to span an item over multiple cells. +You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area), +which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell). +The `gridItem` modifier lets you specify the grid area +with the `rowSpan` and `columnSpan` parameters. +In the following example, +Card **#1** is placed in the area consisting of two rows and two columns +(Figure 2). + + +```kotlin +Grid( + config = { + repeat(3) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(8.dp) + columnGap(8.dp) + } +) { + Card1(modifier = Modifier.gridItem(rowSpan = 2, columnSpan = 2)) + Card2() + Card3() + Card4(modifier = Modifier.gridItem(columnSpan = 3)) +} +``` + +
+ +![Card #4 spans three columns](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/spanning.png) **Figure 2** . Card **#4** spans three columns. + +## Set the alignment in a grid area + +You can set the alignment of the UI element in a grid area +by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier. +In the following example, **#1** is placed in the center of the grid area +consisting of two columns and two rows. + + +```kotlin +Grid( + config = { + repeat(3) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(8.dp) + columnGap(8.dp) + }, +) { + Text( + text = "#1", + modifier = Modifier + .gridItem( + rowSpan = 2, + columnSpan = 2, + alignment = Alignment.Center + ), + ) + Card2() + Card3() + Card4(modifier = Modifier.gridItem(columnSpan = 3)) +} +``` + +
+ +![The Text with #1 is placed in the center of the grid area +consisting of two rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/alignment.png) **Figure 3** . The Text with **#1** is placed in the center of the grid area consisting of two rows and two columns. + +## Auto-placement mixed with placed items + +A UI element in `Grid` +that has no position specification undergoes auto-placement. +This example shows how you can mix auto-placed elements +and the UI elements with specified grid cells. +Card **#2** and Card **#4** are placed in specified grid cells, +and the other items are auto-placed. + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(16.dp) + columnGap(8.dp) + } +) { + Card1() + Card2(modifier = Modifier.gridItem(row = 2, column = 2)) + Card3() + Card4(modifier = Modifier.gridItem(row = 3, column = 1)) + Card5() + Card6() +} +``` + +
+ +![Card #3 is placed next to Card #1, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to Card **#1**, as it is an auto-placement. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md new file mode 100644 index 0000000..4cd2564 --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md @@ -0,0 +1,329 @@ +> [!NOTE] +> **Note:** The `mediaQuery` function and the related data types are experimental and subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues?q=componentid:1876021). + +You need various types of information, such as device capability +and app status, to update your app layout. +Window width and height are the most commonly used information. +In addition to that, you can refer to the following information: + +- Window posture +- Pointing devices precision +- Keyboard type +- Whether the camera and microphone are supported by the device +- The distance between a user and the device display + +Because the information is updated dynamically, +you need to monitor it and trigger recomposition when any update happens. +The [`mediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/mediaQuery.composable#mediaQuery(kotlin.Function1)) function abstracts the details of the information retrieval +and lets you focus on defining the condition to trigger the layout updates. +The following example switches the layout to `TabletopLayout` +when the foldable posture is tabletop: + + +```kotlin +@Composable +fun VideoPlayer( + // ... +) { + // ... + if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { + TabletopLayout() + } else { + FlatLayout() + } + // ... +} +``` + +
+ +## Enable the `mediaQuery` function + +To enable the `mediaQuery` function, +set the `isMediaQueryIntegrationEnabled` attribute of +the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`: + + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + ComposeUiFlags.isMediaQueryIntegrationEnabled = true + super.onCreate() + } +} +``` + +
+ +## Define a condition with parameters + +You can define a condition as a lambda +that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope). +The `mediaQuery` function evaluates the condition according to +the current status and the device capabilities. +The function returns a boolean value, +so you can determine the layout with conditional branches +like an `if` expression. +Table 1 describes the parameters available in `UiMediaScope`. + +| Parameter | Value type | Description | +|---|---|---| +| `windowWidth` | [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp) | The current window width in dp. | +| `windowHeight` | `Dp` | The current window height in dp. | +| `windowPosture` | [`UiMediaScope.Posture`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture) | The current posture of the application window. | +| `pointerPrecision` | [`UiMediaScope.PointerPrecision`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision) | The highest precision of the available pointing devices. | +| `keyboardKind` | [`UiMediaScope.KeyboardKind`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind) | The type of keyboard available or connected. | +| `hasCamera` | `Boolean` | Whether the camera is supported on the device. | +| `hasMicrophone` | `Boolean` | Whether the microphone is supported on the device. | +| `viewingDistance` | [`UiMediaScope.ViewingDistance`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance) | The typical distance between the user and the device screen. | + +A `UiMediaScope` object resolves the values of the parameters. +The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope()) +to access the `UiMediaScope` object, +which represents the current device capabilities and context. +This object is dynamically updated when any changes are made, +such as when the user changes the device posture. +The `mediaQuery` function then evaluates the `query` lambda +with the updated `UiMediaScope` object and returns a boolean value. +For example, the following snippet chooses between `TabletopLayout` +and `FlatLayout` based on the `windowPosture` parameter value. + + +```kotlin +@Composable +fun VideoPlayer( + // ... +) { + // ... + if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { + TabletopLayout() + } else { + FlatLayout() + } + // ... +} +``` + +
+ +### Make a decision based on the window size + +[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints +that help you design, develop, and test adaptive layouts. +You can compare the two parameters representing the current window size +with the threshold defined in the window size classes. +The following example changes the number of panes according to the window width. +[`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds +of window size classes (Figure 1). + +The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda +and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof). +Because `windowWidth` and `windowHeight` can update frequently, +call the `derivedMediaQuery` function instead of the `mediaQuery` function +when you refer to those parameters in the `query` lambda. + + +```kotlin +val narrowerThanMedium by derivedMediaQuery { + windowWidth < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND.dp +} +val narrowerThanExpanded by derivedMediaQuery { + windowWidth < WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND.dp +} +when { + narrowerThanMedium -> SinglePaneLayout() + narrowerThanExpanded -> TwoPaneLayout() + else -> ThreePaneLayout() +} +``` + +
+ +**Figure 1**. Layout is updated according to the window width. + +### Update layout according to the window posture + +The `windowPosture` parameter describes the current window posture +as a `UiMediaScope.Posture` object. +You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter +with the values defined in the `UiMediaScope.Posture` class. +The following example switches layout according to the window posture: + + +```kotlin +when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() +} +``` + +
+ +### Check the precision of the available pointing device + +A high precision pointing device helps users to point a UI element precisely. +The precision of a pointing device depends on the device type. + +The `pointerPrecision` parameter describes the precision +of the available pointing devices, such as a mouse and touchscreen. +There are four values defined in the `UiMediaScope.PointerPrecision` class: +[`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()). +`None` means that no pointing device is available. +The precision ranges from highest to lowest in this order: +`Fine`, `Coarse`, and `Blunt`. + +If multiple pointing devices are available and their precisions are different, +the parameter is resolved with the highest one. +For example, if there are two pointing devices --- a `Fine` precision device and +a `Blunt` precision device --- +`Fine` is the value of the `pointerPrecision` parameter. + +The following example shows a larger button +when the user is using a pointing device with low precision: + + +```kotlin +if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) { + LargeSizeButton() +} else { + NormalSizeButton() +} +``` + +
+ +### Check the available keyboard type + +The `keyboardKind` parameter represents the type of the available keyboards: +[`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()). +If an on-screen keyboard is displayed and +a hardware keyboard is available at the same time, +the parameter is resolved as `Physical`. +If neither is detected, `None` is the value of the parameter. +The following example shows a message suggesting that users connect a keyboard +when no keyboard is detected: + + +```kotlin +if (mediaQuery { keyboardKind == UiMediaScope.KeyboardKind.None }) { + SuggestKeyboardConnect() +} +``` + +
+ +### Check if the device supports camera and microphone + +Some devices don't support cameras or microphones. +You can check if the device supports a camera and a microphone +with the `hasCamera` parameter and the `hasMicrophone` parameter. +The following example shows buttons to use with camera and microphone +when the device supports them: + + +```kotlin +Row { + OutlinedTextField(state = rememberTextFieldState()) + // Show the MicButton when the device supports a microphone. + if (mediaQuery { hasMicrophone }) { + MicButton() + } + // Show the CameraButton when the device supports a camera. + if (mediaQuery { hasCamera }) { + CameraButton() + } +} +``` + +
+ +### Adjust UI with the estimated viewing distance + +Viewing distance is a factor that helps determine layout. +If the user is using the app from a distance, +they would expect the text and UI elements to be bigger. +The `viewingDistance` parameter provides an estimate of the viewing distance +based on the device type and its typical usage context. + +There are three values defined in the `UiMediaScope.ViewingDistance` class: +[`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()). +`Near` means that the screen is in close range, +and `Far` means that the device is viewed from a distance. +The following example increases the font size when the viewing distance is +`Far` or `Medium`: + + +```kotlin +val fontSize = when { + mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Far } -> 20.sp + mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Medium } -> 18.sp + else -> 16.sp +} +``` + +
+ +## Preview a UI component + +You can call the `mediaQuery` and `derivedMediaQuery` functions in the +composable functions to preview UI components. +The following snippet chooses between `TabletopLayout` +and `FlatLayout` based on the `windowPosture` parameter value. +To preview the `TabletopLayout`, the `windowPosture` parameter should be +[`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()). + + +```kotlin +when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() +} +``` + +
+ +The `mediaQuery` and `derivedMediaQuery` functions evaluate +the given `query` lambda within a `UiMediaScope` object, +which is provided as `LocalUiMediaScope.current`. +You can override it with the following steps: + +1. Enable the `mediaQuery` function. +2. Define a custom object that implements the `UiMediaScope` interface. +3. Set the custom object to the `LocalUiMediaScope` with the [`CompositionLocalProvider`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocalProvider.composable#CompositionLocalProvider(androidx.compose.runtime.CompositionLocalContext,kotlin.Function0)) function. +4. Call the composable to preview in the content lambda of the `CompositionLocalProvider` function. + +You can preview the `TabletopLayout` with the following example: + + +```kotlin +@Preview +@Composable +fun PreviewLayoutForTabletop() { + // Step 1: Enable the mediaQuery function + ComposeUiFlags.isMediaQueryIntegrationEnabled = true + + val currentUiMediaScope = LocalUiMediaScope.current + // Step 2: Define a custom object implementing the UiMediaScope interface. + // The object overrides the windowPosture parameter. + // The resolution of the remaining parameters is deferred to the currentUiMediaScope object. + val uiMediaScope = remember(currentUiMediaScope) { + object : UiMediaScope by currentUiMediaScope { + override val windowPosture: UiMediaScope.Posture = UiMediaScope.Posture.Tabletop + } + } + + // Step 3: Set the object to the LocalUiMediaScope. + CompositionLocalProvider(LocalUiMediaScope provides uiMediaScope) { + // Step 4: Call the composable to preview. + when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() + } + } +} +``` + +
\ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md b/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md new file mode 100644 index 0000000..023836f --- /dev/null +++ b/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md @@ -0,0 +1,114 @@ +Tools for debugging your Compose UI are available in Android Studio. + +## Layout Inspector + +Layout Inspector lets you inspect a Compose layout inside a running app in an +emulator or physical device. You can use the Layout Inspector to check how often +a composable is recomposed or skipped, which can help identify issues with your +app. For example, some coding errors might force your UI to recompose +excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance). +Some coding errors can prevent your UI from recomposing and, therefore, +prevent your UI changes from showing up on the screen. If you're new to +Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to +run it. + +> [!NOTE] +> **Note:** If you're not seeing Compose components in layout inspector, make sure you are not removing `META-INF/androidx.compose.*.version` files from the APK. These are required for layout inspector to work. + +### Get recomposition counts + +When debugging your Compose layouts, knowing when composables +[recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in +understanding whether your UI is implemented properly. For example, if it's +recomposing too many times, your app might be doing more work than is necessary. +On the other hand, components that don't recompose when you anticipate them to +can lead to unexpected behaviors. + +The Layout Inspector shows you when discrete composables in your layout +hierarchy have either recomposed or skipped, as you interact with your app. In +Android Studio, your recompositions are highlighted to help you determine +where in the UI your composables are recomposing. + +**Figure 1.** Recompositions are highlighted in Layout Inspector. + +The highlighted portion shows a gradient overlay of the composable in the image +section of the Layout Inspector, and gradually disappears so that you can get an +idea of where in the UI the composable with the highest recompositions can be +found. If one composable is recomposing at a higher rate than another +composable, then the first composable receives a stronger gradient overlay +color. If you double-click a composable in the layout inspector, you're taken to +the corresponding code for analysis. + +> [!NOTE] +> **Note:** To view recomposition counts, make sure your app is using an API level of 29 or higher, and `Compose 1.2.0` or higher. Then, deploy your app as you normally would. + +![](https://developer.android.com/static/develop/ui/compose/images/li-recomposition-counts.png) **Figure 2.**The composition and skip counter in Layout Inspector. + +Open the **Layout Inspector** window and connect to your app process. In the +**Component Tree** , there are two columns that appear next to the layout +hierarchy. The first column shows the number of compositions for each node and +the second column displays the number of skips for each node. Selecting a +composable node shows the dimensions and parameters of the composable, unless +it's an inline function, in which case the parameters can't be shown. You can +also see similar information in the **Attributes** pane when you select a +composable from the **Component Tree** or the **Layout Display**. + +Resetting the count can help you understand recompositions or skips during a +specific interaction with your app. If you want to reset the count, click +**Reset** near the top of the **Component Tree** pane. + +> [!NOTE] +> **Note:** If you don't see the new columns in the **Component Tree** pane, you can view them by selecting **Show Recomposition Counts** from the **View Options** menu ![Layout Inspector View Options +> icon](https://developer.android.com/static/studio/images/buttons/live-layout-inspector-view-options-icon.png) near the top of the **Component Tree** pane, as shown in the following image. + +![Enable the composition and skip counter in Layout +Inspector](https://developer.android.com/static/develop/ui/compose/images/li-show-recomposition-counts.png) + +**Figure 3**. Enable the composition and skip counter in Layout Inspector. + +### Compose semantics + +In Compose, [Semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics) describe your UI in an +alternative manner that is understandable for +[Accessibility](https://developer.android.com/develop/ui/compose/accessibility) services and for the +[Testing](https://developer.android.com/develop/ui/compose/testing) framework. You can use the Layout Inspector +to inspect semantic information in your Compose layouts. +![Semantic information displayed using the Layout Inspector.](https://developer.android.com/static/develop/ui/compose/images/layout_inspector_semantics_new.png) **Figure 4.** Semantic information displayed using the Layout Inspector. + +When selecting a Compose node, use the **Attributes** pane to check whether it +declares semantic information directly, merges semantics from its children, or +both. To quickly identify which nodes include semantics, either declared or +merged, use select the **View options** drop-down in the **Component Tree** pane +and select **Highlight Semantics Layers**. This highlights only the nodes in the +tree that include semantics, and you can use your keyboard to quickly navigate +between them. + +## Compose UI Check + +To help you build more adaptive and accessible UIs in Jetpack Compose, Android +Studio provides a UI Check mode in Compose Preview. This feature is similar +to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner) +for views. + +When you activate Compose UI check mode on a Compose Preview, Android Studio +automatically audits your Compose UI and suggests improvements to make your UI +more accessible and adaptive. Android Studio checks that your UI works across +different screen sizes. In the **Problems** panel, the tool shows the issues +that it detects, such as text stretched on large screens or low color contrast. + +To access this feature, click the UI Check icon on Compose Preview: +![](https://developer.android.com/static/studio/images/design/compose-ui-check-entry.png) **Figure 5.** Entry point to UI check mode. + +UI check automatically previews your UI in different configurations and +highlights issues found in different configurations. In the **Problems** panel, +when you click an issue, you can see the details of the issue, suggested fixes, +and the renderings that highlight the area of the issue. +![](https://developer.android.com/static/studio/images/design/compose-ui-check.png) **Figure 6.** UI check mode in action. + +### Fix with AI + +For issues detected in UI Check mode, you can use the AI agent to propose and +apply code fixes. Click the **Fix with AI** button on an issue in the +**Problems** panel. The agent analyzes the problem and your code to suggest +changes that resolve the accessibility or adaptive issue. +![](https://developer.android.com/static/studio/preview/features/images/ui-check-mode-single-fix.png) **Figure 7.** The agent fixes UI issues in UI Check mode. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md new file mode 100644 index 0000000..fbaae79 --- /dev/null +++ b/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md @@ -0,0 +1,141 @@ +# Material List-Detail Recipe + +This recipe demonstrates how to create an adaptive list-detail layout using the `ListDetailSceneStrategy` from the Material 3 Adaptive library. This layout automatically adjusts to show one, two, or three panes depending on the available screen width. + +## How it works + +This example has three destinations: `ConversationList`, `ConversationDetail`, and `Profile`. + +### `ListDetailSceneStrategy` + +The key to this recipe is the `rememberListDetailSceneStrategy`, which provides the logic for the adaptive layout. + +- **Pane Roles**: Each destination is assigned a role using metadata: + + - `ListDetailSceneStrategy.listPane()`: For the primary (list) content. This pane is always visible. A placeholder can be provided to be shown in the detail pane area when no detail content is selected. + - `ListDetailSceneStrategy.detailPane()`: For the secondary (detail) content. + - `ListDetailSceneStrategy.extraPane()`: For tertiary content. +- **Adaptive Layout** : The `ListDetailSceneStrategy` automatically handles the layout. On smaller screens, only one pane is shown at a time. On wider screens, it will show the list and detail panes side-by-side. On very wide screens, it can show all three panes: list, detail, and extra. + +- **Navigation** : Navigation between the panes is handled by adding and removing destinations from the back stack as usual. The `ListDetailSceneStrategy` observes the back stack and adjusts the layout accordingly. + +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/listdetail) + +``` +package com.example.nav3recipes.material.listdetail + +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.content.ContentYellow +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private object ConversationList : NavKey + +@Serializable +private data class ConversationDetail(val id: String) : NavKey + +@Serializable +private data object Profile : NavKey + +class MaterialListDetailActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + + val backStack = rememberNavBackStack(ConversationList) + + // Override the defaults so that there isn't a horizontal space between the panes. + // See b/418201867 + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() + val directive = remember(windowAdaptiveInfo) { + calculatePaneScaffoldDirective(windowAdaptiveInfo) + .copy(horizontalPartitionSpacerSize = 0.dp) + } + val listDetailStrategy = rememberListDetailSceneStrategy(directive = directive) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(listDetailStrategy), + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + detailPlaceholder = { + ContentYellow("Choose a conversation from the list") + } + ) + ) { + ContentRed("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(ConversationDetail("ABC")) + }) { + Text("View conversation") + } + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { conversation -> + ContentBlue("Conversation ${conversation.id} ") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed { + backStack.add(Profile) + }) { + Text("View profile") + } + } + } + } + entry( + metadata = ListDetailSceneStrategy.extraPane() + ) { + ContentGreen("Profile") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/SKILL.md b/.agents/skills/agp-9-upgrade/SKILL.md new file mode 100644 index 0000000..280347a --- /dev/null +++ b/.agents/skills/agp-9-upgrade/SKILL.md @@ -0,0 +1,103 @@ +--- +name: agp-9-upgrade +description: Upgrades, or migrates, an Android project to use Android Gradle Plugin + (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) + projects. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-06-25' + 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 to update to the latest stable version of +AGP, and confirm when done. The user may also request that this requirement be +skipped; if this is the case, you should update the version of AGP to the latest +stable version as part of the AGP 9 migration. See the +[AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this. + +Each version of AGP has its own set of compatibilities with other tools, such as +Gradle, JDK, and Kotlin. The release notes for each of these versions will +include a **Compatibility** table indicating the minimum versions for these +tools. + +Do not use this skill for KMP projects, as they are unsupported. + +## Steps + +If AGP is already at 9 or higher, then do the following: + +### Step 1: Update dependencies + +If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on +version 2.3.6 or higher. + +If Hilt is used in the project, ensure it is on version 2.59.2 or higher. + +### Step 2: Migrate to built-in Kotlin + +See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information. + +### Step 3. 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 4. Migrate kapt to KSP or legacy-kapt + +If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are +used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration +steps. + +### Step 5. BuildConfig + +If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md) +for detailed information. + +### Step 6. 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. diff --git a/.agents/skills/agp-9-upgrade/references/buildconfig.md b/.agents/skills/agp-9-upgrade/references/buildconfig.md new file mode 100644 index 0000000..273f876 --- /dev/null +++ b/.agents/skills/agp-9-upgrade/references/buildconfig.md @@ -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 { + buildFeatures { + buildConfig = true + } + } + +In custom build-logic for a library module: + + extensions.configure { + buildFeatures { + buildConfig = true + } + } + +In custom build-logic using `CommonExtension`: + + extensions.configure { + 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. \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/ksp-kapt.md b/.agents/skills/agp-9-upgrade/references/ksp-kapt.md new file mode 100644 index 0000000..e8a5d33 --- /dev/null +++ b/.agents/skills/agp-9-upgrade/references/ksp-kapt.md @@ -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. \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md b/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md new file mode 100644 index 0000000..a20da2a --- /dev/null +++ b/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md @@ -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().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 + } \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/recipes.md b/.agents/skills/agp-9-upgrade/references/recipes.md new file mode 100644 index 0000000..5cf4a2d --- /dev/null +++ b/.agents/skills/agp-9-upgrade/references/recipes.md @@ -0,0 +1,62 @@ +When migrating to AGP's new DSL, any Gradle code (plugins or logic in build +scripts) that relied on the old DSL will stop working. Such code must be +migrated. + +## Guidelines + +- **DO NOT** search the web for examples of how to do this. Use the **gradle-recipes** repository examples **only**. +- **DO NOT** use AGP internals in migrated code. +- **DO** use only public APIs in migrated code. + +In some cases, there is a one-to-one replacement for the old code. Some examples +are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes). + +In 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. \ No newline at end of file diff --git a/.agents/skills/edge-to-edge/SKILL.md b/.agents/skills/edge-to-edge/SKILL.md new file mode 100644 index 0000000..f618ab2 --- /dev/null +++ b/.agents/skills/edge-to-edge/SKILL.md @@ -0,0 +1,426 @@ +--- +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 + last-updated: '2026-04-01' + 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 */ } + } + ``` + +
+ + 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") + } + } + ``` + +
+ + 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 */ } +} +``` + +
+ +*** ** * ** *** + +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 */ } +} +``` + +
+ +*** ** * ** *** + +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 */ } +} +``` + +
+ +#### 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 */ } +} +``` + +
+ +*** ** * ** *** + +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 */ } +} +``` + +
+ +### 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 */ } +} +``` + +
+ +*** ** * ** *** + + +```kotlin +// RIGHT +Box( + // Insets consumed + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures +) { + Column( + modifier = Modifier.imePadding() + ) { /* Content */ } +} +``` + +
+ +*** ** * ** *** + + +```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 */ } +} +``` + +
+ +#### 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 */ } +} +``` + +
+ +## 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) + } + ``` + +
+ +- 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 + ) + ) + ) + ) +} +``` + +
+ +## 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 */ } +``` + +
+ +## 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. diff --git a/.agents/skills/jetpack-compose-m3/SKILL.md b/.agents/skills/jetpack-compose-m3/SKILL.md new file mode 100644 index 0000000..84b8be2 --- /dev/null +++ b/.agents/skills/jetpack-compose-m3/SKILL.md @@ -0,0 +1,281 @@ +--- +name: jetpack-compose-m3 +description: Expert guidance for working with Wear OS Compose Material3. Use this + skill when creating, updating or migrating Wear OS projects. This includes the androidx.wear.compose.material3, + androidx.wear.compose.foundation and androidx.wear.compose.navigation3 libraries. + Also working with core components such as AppScaffold, ScreenScaffold and TransformingLazyColumn. + Migration from earlier versions such as Material 2.5 and Horologist. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-06-06' + keywords: + - Wear OS + - Compose + - Material3 + - Horologist + - TransformingLazyColumn + - AppScaffold + - ScreenScaffold +--- + +## Prerequisites and compatibility + +1. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. + - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`). +2. **Strict compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. +3. **Kotlin version:** For Wear Compose Material3, use Kotlin **2.0.0 or + higher**. +4. **Compose compiler:** + - If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin. + - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). +5. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). +6. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You MUST NOT propose code changes until the samples in Capability 2 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure. + +## Gotchas + +1. **Mandatory sync and validation:** After updating versions in `libs.versions.toml` or `build.gradle.kts`, you **must** perform a Gradle sync before refactoring any code. This ensures the environment has resolved the libraries correctly. +2. **Prohibition of guessing (error protocol):** If you encounter an 'Unresolved Reference' or API mismatch after a successful sync, do not attempt to 'fix' it by downgrading the library version. + +## Capabilities and tools + +### Capability 1: Migration + +Use this guidance when migrating from an older version of Wear OS Compose or +Horologist. + +1. Unless otherwise indicated by the developer, use the latest stable version of Wear Compose Material3 from `{VERSION}`. +2. Read the [migration guide](references/android/training/wearables/compose/migrate-to-material3.md). +3. Use the official component mappings from the migration guide. +4. Before refactoring any component (for example, `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. +5. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. +6. **Always** check against the component guidance in Capability 3. +7. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. + +### Capability 2: Component samples + +Wear Compose includes individual component samples for each and every component, +within the `--samples-sources.jar` file. Gradle automatically +downloads these JAR files along with the main library JAR when using any of +`compose-material3`, `compose-foundation` or `compose-navigation3`. + +Use the canonical component samples whenever adding or adjusting a Wear Compose +Material3 component. + +STRICT COMPLIANCE: Extraction is NOT optional. You are FORBIDDEN from +implementing any code until samples are extracted and read. Bypassing this step +with alternative search tools or by assuming library documentation is sufficient +is a protocol breach. You MUST verify the local cache by reading a sample file +before proceeding. + +#### Step 1: Prepare + +1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. +2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. + +#### Step 2: Check the local cache + +1. Define the cache directory path: `/samples/{VERSION}/`. Do NOT choose your own different location. +2. Check if this directory exists and contains subdirectories with `.kt` files. + - **IF YES (cache hit):** Proceed to **Step 4**. + - **IF NO (cache miss):** Proceed to **Step 3**. + +#### Step 3: Check the Gradle cache + +1. Sample sources are stored in the Gradle cache. To avoid slow, brute-force searches: + - Determine the Gradle user home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). + - The cache root is `/caches`. Call this ``. +2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include "navigation3" in the list if the `androidx.wear.compose.navigation3` library is being used. +3. For each `{ARTIFACT}` in the list: + + - Construct the expected relative path segment for the library: `androidx.wear.compose/compose-{ARTIFACT}/{VERSION}`. + - Run a targeted `find` command. Here is an example which is constructed + for efficiency: + + find /modules-2/files-2.1/androidx.wear.compose/compose-{ARTIFACT}/{VERSION}/ \ + -name "*samples-sources.jar" + +4. Use this JAR as the official sample sources. + +5. Extract the contents of each JAR to + `/samples/{VERSION}/{ARTIFACT}/` using `unzip -j` to flatten the + structure. + +6. Proceed **directly to step 4**. + +#### Step 4: Read samples and implement + +1. Read the relevant `.kt` sample files. +2. Use these official, version-matched samples as the source of truth for: + - Required parameters and slot names. + - Default styling and typography tokens. + - Interactive behaviors (for example: `onClick`, `onLongClick`). + - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). + +### Capability 3: Component guidance + +**Mandatory**: Use this capability as a checklist against any component use. It +provides more holistic guidance on how to use each component in practice, beyond +the component syntax. + +1. `AppScaffold` and `ScreenScaffold` + - \[ \] Use `AppScaffold` as the outer container, with `ScreenScaffold` children. + - \[ \] Use only **ONE** `AppScaffold` and any number of `ScreenScaffold`. +2. `ScalingLazyColumn` - Use `TransformingLazyColumn` instead. +3. `TransformingLazyColumn` - You will need the following imports: + + + ```kotlin + import androidx.wear.compose.foundation.lazy.TransformingLazyColumn + import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults + import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState + // ... + import androidx.wear.compose.material3.lazy.rememberTransformationSpec + import androidx.wear.compose.material3.lazy.transformedHeight + ``` + +
+ + **Canonical example**: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + ScreenScaffold( + scrollState = columnState + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + ``` + +
+ + - \[ \] Use `TransformingLazyColumn` instead of `ScalingLazyColumn`. + - \[ \] You must pass the `contentPadding` parameter from `ScreenScaffold` to the `TransformingLazyColumn`. + - \[ \] Use the `minimumVerticalContentPadding` modifier to achieve required padding top and bottom. + - This expects a value from defaults, such as `ButtonDefaults`, `CardDefaults`, \`ListHeaderDefaults. + - Note: This is a scoped modifier available within `TransformingLazyColumnItemScope`. + - \[ \] Ensure the list morphs and scales. + - \[ \] Use `transformedHeight` modifier. + - \[ \] Use `transform = SurfaceTransform(...)`. + - \[ \] If configuring a list for snapping, use `flingBehavior` and `rotaryScrollableBehavior` **together**: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + ScreenScaffold(scrollState = columnState) { contentPadding -> + TransformingLazyColumn( + state = columnState, + flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), + rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) + ) { + // ... + // ... + } + } + ``` + +
+ +4. `ScreenScaffold` + + - \[ \] Guard the `scrollIndicator` with `!LocalScrollCaptureInProgress.current`. +5. `EdgeButton` + + - \[ \] Do **NOT** use as the final item within a `TransformingLazyColumn`. Instead, use the slot in `ScreenScaffold`. + - \[ \] When used in a `TransformingLazyColumn`, add the required overscroll behavior: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollState = columnState, + edgeButton = { + EdgeButton( + onClick = { /* TODO */ }, + modifier = Modifier.scrollable( + columnState, + orientation = Orientation.Vertical, + reverseDirection = true, + // Apply overscroll to the EdgeButton for proper scrolling behavior. + overscrollEffect = rememberOverscrollEffect(), + ) + ) { + Text("More") + } + } + ) { contentPadding -> + TransformingLazyColumn( + contentPadding = contentPadding, + state = columnState, + ) { + // ... + // ... + } + } + ``` + +
+ +6. `Column` + + - \[ \] USE as a direct child of `ScreenScaffold` *if* the screen is will **never** scroll, even with the largest system font. + - \[ \] Use `TransformingLazyColumn` instead for all other cases. +7. Styles + + - \[ \] Do **NOT** hard-code text sizes, use `typography` from `MaterialTheme`. + - \[ \] Do **NOT** hard-code colors, use `colorScheme` from `MaterialTheme`. +8. Use component defaults: + + - \[ \] Components such as `Button` have a corresponding `ButtonDefaults` object. + - Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. +9. Use Wear specific previews: + + - \[ \] `WearPreviewDevices` + - \[ \] `WearPreviewFontScales` +10. Ambient mode + + - \[ \] Use `LocalAmbientModeManager` instead of `AmbientLifecycleObserver`. +11. Navigation + + - \[ \] When adding navigation fresh, use Navigation3. + - \[ \] For Navigation3 in Wear OS, use `SwipeDismissableSceneStrategy()` from the Wear Compose `compose-navigation3` library. diff --git a/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md new file mode 100644 index 0000000..1969a7d --- /dev/null +++ b/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -0,0 +1,677 @@ +[Material 3 Expressive](https://developer.android.com/design/ui/wear/guides/get-started) is the next evolution of Material Design. It includes +updated theming, components, and personalization features like dynamic color. + +This guide focuses on migrating from the [Wear Compose Material 2.5 +(androidx.wear.compose)](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2) Jetpack library to the [Wear Compose Material 3 +(androidx.wear.compose.material3)](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3) Jetpack library for apps. + +> [!NOTE] +> **Note:** This guide uses abbreviation "M3" to refer to the interchangeable terms of "Material 3 Expressive" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material3). The abbreviation "M2.5" is used to refer to the interchangeable terms of "Material 2.5" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material). + +## Approaches + +For migrating your app code from M2.5 to M3, follow the same approach described +in the [Compose Material migration phone guidance](https://developer.android.com/develop/ui/compose/designsystems/material2-material3), in particular: + +- You shouldn't use both [M2.5 and M3 in a single app long-term](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#approaches). +- You should no longer use the Horologist Composables, Compose Layout, or Compose Material libraries. Instead, use the components in M3. +- Adopt a [phased approach](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#phased-approach). + +## Dependencies + +M3 has a separate package and version to M2.5: + +### M2.5 + + implementation("androidx.wear.compose:compose-material:1.4.0") + +### M3 + + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha04") + +See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). + +Wear Compose Foundation library version 1.7.0-alpha04 introduced +some new components that are designed to work with Material 3 components. +Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an +updated animation when running on Wear OS 6 (API level 36) or higher. When +updating to Wear Compose Material 3 version, we suggest to also update the Wear +Compose Foundation and Navigation libraries: + + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha04") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha04") + +## Theme + +In both M2.5 and M3, the theme composable is named [`MaterialTheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)), but the +import packages and parameters differ. In M3, the `Colors` parameter has been +renamed to `ColorScheme` and `MotionScheme` has been introduced for implementing +transitions. + +### M2.5 + + import androidx.wear.compose.material.MaterialTheme + + MaterialTheme( + colors = AppColors, + typography = AppTypography, + shapes = AppShapes, + content = content + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.MaterialTheme +// ... + MaterialTheme( + colorScheme = ColorScheme(), + typography = Typography(), + shapes = Shapes(), + motionScheme = MotionScheme.standard(), + content = { /*content here*/ } + ) +``` + +
+ +### Color + +The color system in M3 is significantly different from M2.5. The number of color +parameters has increased, they have different names, and they map differently to +M3 components. In Compose, this applies to the M2.5 [`Colors`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Colors) class, the M3 +[`ColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/ColorScheme) class, and related functions: + +### M2.5 + + import androidx.wear.compose.material.Colors + + val appColorScheme: Colors = Colors( + // M2.5 Color parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.ColorScheme +// ... + val appColorScheme: ColorScheme = ColorScheme( + // M3 ColorScheme parameters + ) +``` + +
+ +The following table describes the key differences between M2.5 and M3: + +| M2.5 | M3 | +|---|---| +| `Color` | Has been renamed to `ColorScheme` | +| 13 colors | 28 colors | +| N/A | New dynamic color theming | +| N/A | New tertiary colors for more expression | + +#### Dynamic color theming + +A new feature in M3 is [dynamic color theming](https://m3.material.io/styles/color/dynamic-color/overview). If users change +the watch face colors, the colors in the UI change to match. + +Use the [`dynamicColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#dynamicColorScheme(android.content.Context)) function to implement dynamic color scheme +and provide a `defaultColorScheme` as a fallback in case dynamic color scheme is +not available. + + +```kotlin +@Composable +fun myApp() { + val dynamicColorScheme = dynamicColorScheme(LocalContext.current) + MaterialTheme(colorScheme = dynamicColorScheme ?: myBrandColors) {} +} + +internal val myBrandColors: ColorScheme = ColorScheme( /* Specify colors here */) +``` + +
+ +### Typography + +The [typography system](https://m3.material.io/styles/typography/overview) in M3 is different from M2.5 and it includes +the following features: + +- Nine new [text styles](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/Typography#public-properties_1) +- Flex fonts, which allow for customization of the type scales for different weights, widths, and roundness +- `AnimatedText`, which uses flex fonts + +### M2.5 + + import androidx.wear.compose.material.Typography + + val Typography = Typography( + // M2.5 TextStyle parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.Typography + +val Typography = Typography( + // M3 TextStyle parameters +) +``` + +
+ +#### Flex fonts + +Flex Fonts allow designers to specify the type width and weight for specific +sizes. + +#### Text styles + +The following [TextStyles](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt;l=115?q=displayLarge&ss=androidx/platform/frameworks/support) are available in M3. These are +employed by default by various M3 components. + +| Typography | TextStyle | +|---|---| +| Display | displayLarge, displayMedium, displaySmall | +| Title | titleLarge, titleMedium, titleSmall | +| Label | labelLarge, labelMedium, labelSmall | +| Body | bodyLarge, bodyMedium, bodySmall, bodyExtraSmall | +| Numeral | numeralExtraLarge, numeralLarge, numeralMedium, numeralSmall, numeralExtraSmall | +| Arc | arcLarge, arcMedium, arcSmall | + +### Shape + +The [shape system](https://m3.material.io/styles/shape/overview) in M3 is different from M2.5. The number of shape +parameters has increased, they're named differently, and they map differently to +M3 components. The following shape sizes are available: + +- Extra-small +- Small +- Medium +- Large +- Extra-large + +In Compose, this applies to the M2 [`Shapes`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Shapes) class and the M3 +[`Shapes`](https://developer.android.com/reference/kotlin/androidx/compose/material3/Shapes) class: + +### M2.5 + + import androidx.wear.compose.material.Shapes + + val Shapes = Shapes( + // M2.5 Shapes parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.Shapes + +val Shapes = Shapes( + // M3 Shapes parameters +) +``` + +
+ +> [!NOTE] +> **Note:** For shapes, we generally recommend using the default Material 3 Wear shapes which are optimized for round devices. + +Use the Shapes parameter mapping from [Migrate from Material 2 to Material 3 in +Compose](https://developer.android.com/training/wearables/compose/migrate-to-material3#shape) as a starting point. + +### Shape morphing + +M3 introduces Shape Morphing: shapes now morph in response to interactions. + +Shape Morphing behavior is available as a variation on a number of round +buttons, see the following list of buttons that support Shape Morphing: + +| Buttons | Shape morphing function | +|---|---| +| `IconButton` | [IconButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon button on press | +| `IconToggleButton` | [IconToggleButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon toggle button on press and [IconToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#variantAnimatedShapes()) animates the icon toggle button on press and check/uncheck | +| `TextButton` | [TextButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text button on press | +| `TextToggleButton` | [TextToggleButtonDefaults.animatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text toggle on press and [TextToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#variantAnimatedShapes()) animates the text toggle on press and check/uncheck | + +## Components and Layout + +Most components and layouts from M2.5 are available in M3. However, some M3 +components and layouts didn't exist in M2.5. Furthermore, some M3 components +have more variations than their equivalents in M2.5. + +While some components require special considerations, the following function +mappings are recommended as a starting point: + +| Material 2.5 | Material 3 | +|---|---| +| [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | +| [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function0,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0)) | +| [androidx.wear.compose.material.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Checkbox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Checkbox(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.CheckboxColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.OutlinedButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OutlinedButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) or [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) | +| [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | +| [androidx.wear.compose.material.LocalContentAlpha()](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#LocalContentAlpha()) | Has been removed as not used by `Text` or `Icon` in Material 3 | +| [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | +| [androidx.wear.compose.material.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#RadioButton(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.RadioButtonColors,kotlin.Boolean,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | +| [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | +| [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) | [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) and [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Switch](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Switch(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.SwitchColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) or [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | +| [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Vignette](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Vignette(androidx.wear.compose.material.VignettePosition,androidx.compose.ui.Modifier)) | Removed as not included in Material 3 Expressive design for Wear OS | + +Here is a full list of all the Material 3 components: + +| Material 3 | Material 2.5 equivalent component (if not new in M3) | +|---|---| +| [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | +| [androidx.wear.compose.material3.AnimatedPage](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedPage(kotlin.Int,androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.graphics.Color,kotlin.Function0)) | New | +| [androidx.wear.compose.material3.AnimatedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedText(kotlin.String,androidx.wear.compose.material3.AnimatedTextFontRegistry,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment)) | New | +| [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) ) | +| [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.ButtonGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ButtonGroup(androidx.compose.ui.Modifier,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.Alignment.Vertical,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a checkbox toggle control | +| [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) (only when no background is required) | +| [androidx.wear.compose.material3.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) | [androidx.wear.compose.material.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp)) | +| [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | +| [androidx.wear.compose.material3.ConfirmationDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ConfirmationDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ConfirmationDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Confirmation](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Confirmation(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,kotlin.Long,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | +| [androidx.wear.compose.material3.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | [androidx.wear.compose.material.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | +| [androidx.wear.compose.material3.DatePicker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#DatePicker(java.time.LocalDate,kotlin.Function1,androidx.compose.ui.Modifier,java.time.LocalDate,java.time.LocalDate,androidx.wear.compose.material3.DatePickerType,androidx.wear.compose.material3.DatePickerColors)) | New | +| [androidx.wear.compose.material3.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | +| [androidx.wear.compose.material3.EdgeButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.FadingExpandingLabel](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FadingExpandingLabel(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,kotlin.Boolean,kotlin.Int,kotlin.Int,androidx.compose.ui.text.TextStyle,androidx.compose.animation.core.FiniteAnimationSpec)) | New | +| [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) when a tonal button background is required | +| [androidx.wear.compose.material3.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPageIndicator(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.HorizontalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | +| [androidx.wear.compose.material3.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | +| [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | +| [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.LevelIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LevelIndicator(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.ranges.ClosedFloatingPointRange,kotlin.Boolean,androidx.wear.compose.material3.LevelIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Float,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.LinearProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LinearProgressIndicator(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | +| [androidx.wear.compose.material3.ListSubHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListSubHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)) | [androidx.wear.compose.material.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#MaterialTheme(androidx.wear.compose.material.Colors,androidx.wear.compose.material.Typography,androidx.wear.compose.material.Shapes,kotlin.Function0)) | +| [androidx.wear.compose.material3.OpenOnPhoneDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OpenOnPhoneDialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material3.OpenOnPhoneDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Picker(androidx.wear.compose.material3.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | [androidx.wear.compose.material.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Picker(androidx.wear.compose.material.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | +| [androidx.wear.compose.material3.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#PickerGroup(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Boolean,kotlin.Function1)) | [androidx.wear.compose.material.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PickerGroup(kotlin.Array,androidx.compose.ui.Modifier,androidx.wear.compose.material.PickerGroupState,kotlin.Function1,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.TouchExplorationStateProvider,kotlin.Function1)) | +| [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a radio button toggle control | +| [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1))) | +| [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | +| [androidx.wear.compose.material3.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function0)) | [androidx.wear.compose.material.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.compose.foundation.lazy.LazyListState,kotlin.Int,androidx.compose.ui.unit.Dp)) | +| [androidx.wear.compose.material3.SegmentedCircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SegmentedCircularProgressIndicator(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Float,kotlin.Float,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | +| [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Boolean,kotlin.Function1)) | +| [androidx.wear.compose.material3.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | [androidx.wear.compose.material.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | +| [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) and [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | +| [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a switch toggle control | +| [androidx.wear.compose.material3.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | [androidx.wear.compose.material.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | +| [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | +| [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.wear.compose.material3.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.material.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1)) | +| [androidx.wear.compose.material3.VerticalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | + +And finally a list of some relevant components from Wear Compose Foundation +library: + +| Wear Compose Foundation 1.7.0-alpha04 | | +|---|---| +| [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | +| [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | +| [androidx.wear.compose.foundation.pager.VerticalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#VerticalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A vertically scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | +| [androidx.wear.compose.foundation.lazy.TransformingLazyColumn](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#TransformingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.TransformingLazyColumnState,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) | Can be used instead of [`ScalingLazyColumn`](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#ScalingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType,androidx.wear.compose.foundation.lazy.AutoCenteringParams,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) to add scroll transform effects to each item. | +| | | + +### Buttons + +Buttons in M3 are different from M2.5. The M2.5 Chip has been replaced by +Button. [`Button`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) implementation provides default values for `Text` +`maxLines` and `textAlign`. Those default values can be overridden in the `Text` +element. + +### M2.5 + + import androidx.wear.compose.material.Chip + + //M2.5 Buttons + Chip(...) + CompactChip(...) + Button(...) + +### M3 + + +```kotlin +//M3 Buttons +Button(onClick = { }){} +CompactButton(onClick = { }){} +IconButton(onClick = { }){} +TextButton(onClick = { }){} +``` + +
+ +M3 also includes new button variations. Check them out on the [Compose Material +3 API reference overview](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2). + +M3 introduces a new button: [`EdgeButton`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)). `EdgeButton` is available in 4 +different sizes: extra small, small, medium, and large. `EdgeButton` +implementation provide a default value for `maxLines` depending on the size +which can be customized. + +If you are using `TransformingLazyColumn` or `ScalingLazyColumn`, pass the +`EdgeButton` into the `ScreenScaffold` so that it morphs, changing its shape +with scrolling, instead of adding an `EdgeButton` as the final list item. See +the following code to check how to use `EdgeButton` with `ScreenScaffold` and +`TransformingLazyColumn`. + + +```kotlin +val state = rememberTransformingLazyColumnState() +ScreenScaffold( + scrollState = state, + contentPadding = + rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader + ), + edgeButton = { + EdgeButton( + onClick = { } + ) { + Text(stringResource(R.string.show)) + } + } +){ contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding,){ + // additional code here + } +} +``` + +
+ +### Scaffold + +Scaffold in M3 is different from M2.5. In M3, `AppScaffold` and the new +`ScreenScaffold` composable have replaced Scaffold. `AppScaffold` and +`ScreenScaffold` lay out the structure of a screen and coordinate transitions of +the `ScrollIndicator` and `TimeText` components. + +`AppScaffold` allows static screen elements such as `TimeText` to remain visible +during in-app transitions such as swipe-to-dismiss. ​​It provides a slot for the +main application content, which will usually be supplied by a navigation +component such as `SwipeDismissableNavHost` + +You declare one `AppScaffold` for Activity and use a `ScreenScaffold` for each +Screen. +`AppScaffold` adds a default `TimeText`component to the screens. You can +override it if you want to customize it by using the `timeText` parameter. + +### M2.5 + + import androidx.wear.compose.material.Scaffold + + Scaffold {...} + +### M3 + + +```kotlin + AppScaffold { + val navController = rememberSwipeDismissableNavController() + SwipeDismissableNavHost( + navController = navController, + startDestination = "message_list" + ) { + composable("message_list") { + MessageList(onMessageClick = { id -> + navController.navigate("message_detail/$id") + }) + } + composable("message_detail/{id}") { + MessageDetail(id = it.arguments?.getString("id")!!) + } + } + } +} + +// Implementation of one of the screens in the navigation +@Composable +fun MessageDetail(id: String) { + // .. Screen level content goes here + val scrollState = rememberTransformingLazyColumnState() + + val padding = rememberResponsiveColumnPadding( + first = ColumnItemType.BodyText + ) + + ScreenScaffold( + scrollState = scrollState, + contentPadding = padding + ) { scaffoldPaddingValues -> + // Screen content goes here + // ... +``` + +
+ +> [!NOTE] +> **Note:** `AppScaffold` and `ScreenScaffold` from [Horologist](https://github.com/google/horologist) haven't been migrated to M3. To maintain correct scrolling behavior and `TimeText` elements, migrate to the `AppScaffold` and `ScreenScaffold` from M3. + +If you are using a `HorizontalPager` with [HorizontalPagerIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)), you +can migrate to `HorizontalPagerScaffold`. [`HorizontalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) is +placed within an `AppScaffold`. `AppScaffold` and `HorizontalPagerScaffold` lay +out the structure of a Pager and coordinate transitions of the +`HorizontalPageIndicator` and `TimeText` components. + +`HorizontalPagerScaffold` displays the `HorizontalPageIndicator` at the +center-end of the screen by default and coordinates showing and hiding +`TimeText` and `HorizontalPageIndicator` according to whether the `Pager` is +being paged, this is determined by the `PagerState`. + +There's also a new `AnimatedPage` component, which animates a page within a +Pager with a scaling and scrim effect based on its position. + + +```kotlin +AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + val columnState = rememberTransformingLazyColumnState() + val contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader, + last = ColumnItemType.BodyText, + ) + HorizontalPagerScaffold(pagerState = pagerState) { + HorizontalPager( + state = pagerState, + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold( + scrollState = columnState, + contentPadding = contentPadding + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Pager sample") + } + } + item { + if (page == 0) { + Text(text = "Page #$page. Swipe right") + } + else{ + Text(text = "Page #$page. Swipe left and right") + } + } + } + } + + } + } + } +} +``` + +
+ +Finally, M3 introduces a [`VerticalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) which follows the same +pattern as the `HorizontalPagerScaffold`: + + +```kotlin +AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + + VerticalPagerScaffold(pagerState = pagerState) { + VerticalPager( + state = pagerState + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + ///... + } + } + } + } +} +``` + +
+ +### Placeholder + +There are some API changes between M2.5 and M3. +`Placeholder.PlaceholderDefaults` now provides two modifiers: + +- [`Modifier.placeholder`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholder(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)), which is drawn instead of content that is not yet loaded +- A placeholder shimmer effect [`Modifier.placeholderShimmer`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholderShimmer(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)) which provides a placeholder shimmer effect which runs in an animation loop while waiting for the data to load. + +See the following table for additional changes to the `Placeholder` component. + +| M2.5 | M3 | +|---|---| +| `PlaceholderState.startPlaceholderAnimation` | Has been removed | +| `PlaceholderState.placeholderProgression` | Has been removed | +| `PlaceholderState.isShowContent` | Has been renamed to `!PlaceholderState.isVisible` | +| `PlaceholderState.isWipeOff` | Has been removed | +| `PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush` | Has been removed | +| `PlaceholderDefaults.placeholderBackgroundBrush` | Has been removed | +| `PlaceholderDefaults.placeholderChipColors` | Has been removed | + +### SwipeDismissableNavHost + +`SwipeDismissableNavHost` is part of `wear.compose.navigation`. When this +component is used with M3, the M3 MaterialTheme updates the +`LocalSwipeToDismissBackgroundScrimColor` and +`LocalSwipeToDismissContentScrimColor`. + +### TransformingLazyColumn + +`TransformingLazyColumn` is part of `wear.compose.lazy.foundation` and adds +support for scaling and morphing animations on list items during scrolling , +enhancing the user experience. It is strongly recommended that apps migrate from +`ScalingLazyColumn` to `TransformingLazyColumn` + +Similarly to `ScalingLazyColumn`, it provides +`rememberTransformingLazyColumnState()` to create a +`TransformingLazyColumnState` that is remembered across compositions. + +For adding scaling and morphing animations, add the following to each list item: + +- `Modifier.transformedHeight`, which lets you calculate transformed height of the items using a `TransformationSpec`, you can use `rememberTransformationSpec()` unless you need further customization. +- A `SurfaceTransformation` + +To verify that the padding is correct at the top and bottom of the list, use the +`minimumVerticalContentPadding` modifier. + + +```kotlin +val columnState = rememberTransformingLazyColumnState() +val transformationSpec = rememberTransformationSpec() +ScreenScaffold( + scrollState = columnState +) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} +``` + +
+ +## Useful links + +To learn more about migrating from M2.5 to M3 in Compose, consult the following +additional resources. + +### Samples + +- [Wear OS samples on GitHub](https://github.com/android/wear-os-samples/) +- [Compose for Wear OS codelab](https://developer.android.com/codelabs/compose-for-wear-os#0) +- [Jetcaster sample](https://github.com/android/compose-samples/tree/main/Jetcaster) + +### API reference and source code + +- [Compose Material 3 API reference](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary) +- [Compose Material 3 samples in source code](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/) + +### Design + +- [Design guidance](https://developer.android.com/design/ui/wear/guides/get-started) \ No newline at end of file diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md new file mode 100644 index 0000000..ea25f8e --- /dev/null +++ b/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md @@ -0,0 +1,124 @@ +--- +name: migrate-xml-views-to-jetpack-compose +description: Provides a structured workflow for migrating an Android XML View to Jetpack + Compose. This skill details the step-by-step process, from planning and dependency + setup, to theming and layout migration, validation and XML cleanup. Use this skill + when you need to migrate an XML View to Jetpack Compose in an Android project. It + solves the problem of converting the UI of a legacy XML View into modern, declarative + Compose components while maintaining interoperability. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-02' + keywords: + - Jetpack Compose + - migration + - XML + - Views + - interoperability + - incremental adoption + - UI development +--- + +This skill guides through the process of migrating an existing Android XML View +to Jetpack Compose. It performs a stable, safe and visually consistent +transition by following a structured, 10-step methodology. This skill migrates +UI (XML to Jetpack Compose) only. + +## Objective + +To systematically convert a single legacy XML layout into modern, declarative +Jetpack Compose UI while maintaining pixel-perfect visual parity and functional +integrity. + +## Summary of the 10-step migration process + +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** + +## Detailed steps + +### Step 1: Identify the optimal XML candidate for migration + +If the user has explicitly specified a target XML layout, proceed to Step 2. +Otherwise, analyze the codebase to identify the best candidate for migration by +following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md). + +### Step 2: Analyze the project and layout + +Analyze the identified XML View's structure, hierarchy, and implementation +details. +Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to +guide your technical audit of the layout and surrounding project context. + +### Step 3: Create a plan + +Using the outputs and analysis done in the Step 1 and 2, generate a +step-by-step plan for the migration. If you support user interaction, present +to the user and ask for approval before proceeding. If user interaction is not +supported, proceed to Step 4 following the generated plan. + +### 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. diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md new file mode 100644 index 0000000..3e2f0c3 --- /dev/null +++ b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md @@ -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. \ No newline at end of file diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md new file mode 100644 index 0000000..60b7968 --- /dev/null +++ b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md @@ -0,0 +1,171 @@ +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. | +| `Shapes` | 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`. | +| `ColorScheme` | 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 `Shapes` object to replicate your XML shape definitions. + 2. Provide this `Shapes` 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 or modifiers to composables, using the +[new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating +layered, reusable composable variations with the defined style. + +Provide separate @Composable functions named according to the style and the +base component, to signify the difference in styling and use cases for those +components. + +- **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:* ` + +
+ +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](https://developer.android.com/guide/navigation). +- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in + Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose). +- Routes are strongly typed. If you use string-based routes, [migrate to + type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)). +- *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](https://developer.android.com/guide/navigation/design/kotlin-dsl#custom) +- Deep links + +**AI Agent**: Before changing any code, check if the project contains any of the +unsupported features. If it does, do not proceed. Inform the user of the +unsupported feature and ask for further instructions. + +## Step 1: Add Navigation 3 dependencies + +Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your +project. The core dependencies are provided for you to copy. + +**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](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey` +interface. This lets you use `rememberNavBackStack` to assist with [saving your +navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state). + +Before: + + @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](https://developer.android.com/guide/navigation/navigation-3/get-started#project-setup). + +## 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 + ): 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, + val backStacks: Map> + ) { + var topLevelRoute: NavKey by topLevelRoute + val stacksInUse: List + get() = if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) + } + } + + /** + * Convert NavigationState into NavEntries. + */ + @Composable + fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry + ): SnapshotStateList> { + + val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + ) + 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](https://developer.android.com/topic/architecture): +> +> - 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 = , + topLevelRoutes = + ) + + 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](https://developer.android.com/guide/navigation/design#compose) +using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder), +usually inside `NavHost`'s trailing lambda. It is common to use extension +functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate). + +In Navigation 3, you define your destinations using an `entryProvider`. This +`entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the +`entryProvider` does not define parent-child relationships between entries. + +In 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](https://developer.android.com/guide/navigation/navigation-3/basics#entry-provider-DSL) at the same scope as the +`NavigationState`. + + val entryProvider = entryProvider { + + } + +## Step 5.2: Move destinations into the `entryProvider` + +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`: Move it into `entryProvider` and rename it to `entry`, retaining the type parameter. For example, `composable` becomes `entry`. +- `dialog`: Do the same as `composable`, but add metadata to the entry as follows: `entry(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{ composable{ ... } }`, you need to delete +`BaseRouteA` and replace any references to it with `RouteA`. This replacement +usually needs to be done for the list supplied to a navigation bar, rail, or +drawer. + +You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to +`EntryProviderScope` 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{ + val id = entry.toRoute().id + ScreenA(title = "Screen has ID: $id") + } + featureBSection() + dialog{ ScreenD() } + } + + fun NavGraphBuilder.featureBSection() { + navigation(startDestination = RouteB) { + composable { 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{ key -> ScreenA(title = "Screen has ID: ${key.id}") } + featureBSection() + entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } + } + + fun EntryProviderScope.featureBSection() { + entry { 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 `sceneStrategies` parameter. + +For example: + + import androidx.navigation3.ui.NavDisplay + + NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + sceneStrategies = remember { listOf(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). \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md new file mode 100644 index 0000000..5e15bd4 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md @@ -0,0 +1,147 @@ +# Animations Recipe + +This recipe shows how to override the default animations at the `NavDisplay` level, and at the individual destination level. + +## How it works + +The `NavDisplay` composable takes `transitionSpec`, `popTransitionSpec`, and `predictivePopTransitionSpec` parameters to define the animations for forward, backward, and predictive back navigation respectively. These animations will be applied to all destinations by default. + +In this example, we use `slideInHorizontally` and `slideOutHorizontally` to create a sliding animation for forward and backward navigation. + +It is also possible to override these animations for a specific destination by providing a different `transitionSpec` and `popTransitionSpec` to the `entry` composable. In this recipe, `ScreenC` has a custom vertical slide animation. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/animations) + +``` +package com.example.nav3recipes.animations + + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.material3.Button +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 +import com.example.nav3recipes.content.ContentMauve +import com.example.nav3recipes.content.ContentOrange +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + + +@Serializable +private data object ScreenA : NavKey + +@Serializable +private data object ScreenB : NavKey + +@Serializable +private data object ScreenC : NavKey + + +class AnimatedActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + + val backStack = rememberNavBackStack(ScreenA) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { + ContentOrange("This is Screen A") { + Button(onClick = dropUnlessResumed { backStack.add(ScreenB) }) { + Text("Go to Screen B") + } + } + } + entry { + ContentMauve("This is Screen B") { + Button(onClick = dropUnlessResumed { backStack.add(ScreenC) }) { + Text("Go to Screen C") + } + } + } + entry( + metadata = metadata { + // Slide new content up, keeping the old content in place underneath + put(NavDisplay.TransitionKey) { + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(1000) + ) togetherWith ExitTransition.KeepUntilTransitionsFinished + } + + // Slide old content down, revealing the new content in place underneath + put(NavDisplay.PopTransitionKey) { + EnterTransition.None togetherWith + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) + } + + // Slide old content down, revealing the new content in place underneath + put(NavDisplay.PredictivePopTransitionKey) { + EnterTransition.None togetherWith + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) + } + } + ) { + ContentGreen("This is Screen C") + } + }, + transitionSpec = { + // Slide in from right when navigating forward + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(1000) + ) togetherWith slideOutHorizontally( + targetOffsetX = { -it }, + animationSpec = tween(1000) + ) + }, + popTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = tween(1000) + ) togetherWith slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(1000) + ) + }, + predictivePopTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = tween(1000) + ) togetherWith slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(1000) + ) + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md new file mode 100644 index 0000000..087d5cf --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md @@ -0,0 +1,89 @@ +# Basic Recipe + +This recipe shows a basic example of how to use the Navigation 3 API with two screens. + +## How it works + +This example defines two routes: `RouteA` and `RouteB`. `RouteA` is a `data object` representing a simple screen, while `RouteB` is a `data class` that takes an `id` as a parameter. + +A `mutableStateListOf` is used to manage the navigation back stack. + +The `NavDisplay` composable is used to display the current screen. Its `entryProvider` parameter is a lambda that takes a route from the back stack and returns a `NavEntry`. Inside the `entryProvider`, a `when` statement is used to determine which composable to display based on the route. + +To navigate from `RouteA` to `RouteB`, we simply add a `RouteB` instance to the back stack. The `id` is passed as an argument to the `RouteB` data class. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basic) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.basic + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +private data object RouteA + +private data class RouteB(val id: String) + +class BasicActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = remember { mutableStateListOf(RouteA) } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = { key -> + when (key) { + is RouteA -> NavEntry(key) { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to navigate") + } + } + } + + is RouteB -> NavEntry(key) { + ContentBlue("Route id: ${key.id} ") + } + + else -> { + error("Unknown route: $key") + } + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md new file mode 100644 index 0000000..2067c08 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md @@ -0,0 +1,85 @@ +# Basic DSL Recipe + +This recipe shows a basic example of how to use the Navigation 3 API with two screens, using the `entryProvider` DSL and a persistent back stack. + +## How it works + +This example is similar to the basic recipe, but with a few key differences: + +1. **Persistent Back Stack** : It uses `rememberNavBackStack(RouteA)` to create and remember the back stack. This makes the back stack persistent across configuration changes (e.g., screen rotation). To use `rememberNavBackStack`, the navigation keys must be serializable, which is why `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. + +2. **`entryProvider` DSL** : Instead of a `when` statement, this example uses the `entryProvider` DSL to define the content for each route. The `entry` function is used to associate a route type with its composable content. + +The navigation logic remains the same: to navigate from `RouteA` to `RouteB`, we add a `RouteB` instance to the back stack. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basicdsl) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.basicdsl + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data class RouteB(val id: String) : NavKey + +class BasicDslActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to navigate") + } + } + } + entry { key -> + ContentBlue("Route id: ${key.id} ") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md new file mode 100644 index 0000000..fd2a72d --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md @@ -0,0 +1,90 @@ +# Basic Saveable Recipe + +This recipe shows a basic example of how to create a persistent back stack that survives configuration changes. + +## How it works + +To make the back stack persistent, we use the `rememberNavBackStack` function. This function creates and remembers the back stack across configuration changes (e.g., screen rotation). + +A requirement for using `rememberNavBackStack` is that the navigation keys (routes) must be serializable. In this example, `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. + +This example uses a `when` statement within the `entryProvider` to map routes to their corresponding composables, but it could also be used with the `entryProvider` DSL. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basicsaveable) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.basicsaveable + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data class RouteB(val id: String) : NavKey + +class BasicSaveableActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = { key -> + when (key) { + is RouteA -> NavEntry(key) { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to navigate") + } + } + } + + is RouteB -> NavEntry(key) { + ContentBlue("Route id: ${key.id} ") + } + + else -> { + error("Unknown route: $key") + } + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md new file mode 100644 index 0000000..aa6b456 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md @@ -0,0 +1,195 @@ +# Bottom Sheet Recipe + +This recipe demonstrates how to display a destination as a modal bottom sheet. + +## How it works + +To show a destination as a bottom sheet, you need to do two things: + +1. **Use `BottomSheetSceneStrategy`** : Create an instance of `BottomSheetSceneStrategy` and pass it to the `sceneStrategy` parameter of the `NavDisplay` composable. + +2. **Add metadata to the destination** : For the destination that you want to display as a bottom sheet, add `BottomSheetSceneStrategy.bottomSheet()` to its metadata. This is done in the `entry` function. + +In this example, `RouteB` is configured to be a bottom sheet. When you navigate from `RouteA` to `RouteB`, `RouteB` will be displayed in a modal bottom sheet that slides up from the bottom of the screen. + +The content of the bottom sheet can be styled as needed. In this recipe, the content is clipped to have rounded corners. + +For more information, see the official documentation on [custom layouts](https://developer.android.com/guide/navigation/navigation-3/custom-layouts). +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.bottomsheet + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data class RouteB(val id: String) : NavKey + +class BottomSheetActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + val bottomSheetStrategy = remember { BottomSheetSceneStrategy() } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(bottomSheetStrategy), + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to open bottom sheet") + } + } + } + entry( + metadata = BottomSheetSceneStrategy.bottomSheet() + ) { key -> + ContentBlue( + title = "Route id: ${key.id}", + modifier = Modifier.clip( + shape = RoundedCornerShape(16.dp) + ) + ) + } + } + ) + } + } +} +``` + +``` +package com.example.nav3recipes.bottomsheet + +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 data class BottomSheetScene( + override val key: T, + override val previousEntries: List>, + override val overlaidEntries: List>, + private val entry: NavEntry, + private val modalBottomSheetProperties: ModalBottomSheetProperties, + private val onBack: () -> Unit, +) : OverlayScene { + + override val entries: List> = listOf(entry) + + override val content: @Composable (() -> Unit) = { + val lifecycleOwner = rememberLifecycleOwner() + ModalBottomSheet( + onDismissRequest = onBack, + properties = modalBottomSheetProperties, + ) { + CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { + entry.Content() + } + } + } +} + +/** + * A [SceneStrategy] that displays entries that have added [bottomSheet] to their [NavEntry.metadata] + * within a [ModalBottomSheet] instance. + * + * This strategy should always be added before any non-overlay scene strategies. + */ +@OptIn(ExperimentalMaterial3Api::class) +class BottomSheetSceneStrategy : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + 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, + previousEntries = entries.dropLast(1), + overlaidEntries = entries.dropLast(1), + entry = lastEntry, + modalBottomSheetProperties = properties, + onBack = onBack + ) + } + } + + companion object { + /** + * Function to be called on the [NavEntry.metadata] to mark this entry as something that + * should be displayed within a [ModalBottomSheet]. + * + * @param modalBottomSheetProperties properties that should be passed to the containing + * [ModalBottomSheet]. + */ + fun bottomSheet(modalBottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()) = + metadata { + put(BottomSheetKey, modalBottomSheetProperties) + } + + object BottomSheetKey : NavMetadataKey + } + +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md new file mode 100644 index 0000000..d49b399 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md @@ -0,0 +1,200 @@ +# Common UI Recipe + +This recipe 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. + +## How it works + +This example has three top-level destinations: `Home`, `ChatList`, and `Camera`. The `ChatList` destination also has a sub-route, `ChatDetail`. + +### `TopLevelBackStack` + +The core of this recipe is the `TopLevelBackStack` class, which is responsible for managing the navigation state. It works as follows: + +- It maintains a separate back stack for each top-level destination (tab). +- It keeps track of the currently selected top-level destination. +- It provides a single, flattened back stack that can be used by the `NavDisplay` composable. This flattened back stack is a combination of the individual back stacks of all the tabs. + +### UI Structure + +The UI is built using a `Scaffold` composable, with a `NavigationBar` as the `bottomBar`. + +- The `NavigationBar` displays an item for each top-level destination. When an item is clicked, it calls `topLevelBackStack.addTopLevel` to switch to the corresponding tab, preserving the navigation history of each tab. +- The `NavDisplay` composable is placed in the content area of the `Scaffold`. It is responsible for displaying the current screen based on the flattened back stack provided by `TopLevelBackStack`. + +This approach allows for a common navigation pattern where users can switch between different sections of the app, and each section maintains its own navigation history. + +### State Preservation + +It's important to note how the navigation state is managed in this recipe. When a user navigates away from a top-level destination (e.g., by pressing the back button until they return to a previous tab), the entire navigation history for that destination is cleared. The state is not saved. When the user returns to that tab later, they will start from its initial screen. + +**Note** : In this example, the `Home` route can move above the `ChatList` and `Camera` routes, meaning navigating back from `Home` doesn't necessarily leave the app. The app will exit when the user goes back from a single remaining top level route in the back stack. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/commonui) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.commonui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentPurple +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +private sealed interface TopLevelRoute { + val icon: ImageVector +} +private data object Home : TopLevelRoute { override val icon = Icons.Default.Home } +private data object ChatList : TopLevelRoute { override val icon = Icons.Default.Face } +private data object ChatDetail +private data object Camera : TopLevelRoute { override val icon = Icons.Default.PlayArrow } + +private val TOP_LEVEL_ROUTES : List = listOf(Home, ChatList, Camera) + +class CommonUiActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val topLevelBackStack = remember { TopLevelBackStack(Home) } + + Scaffold( + bottomBar = { + NavigationBar { + TOP_LEVEL_ROUTES.forEach { topLevelRoute -> + + val isSelected = topLevelRoute == topLevelBackStack.topLevelKey + NavigationBarItem( + selected = isSelected, + onClick = { + topLevelBackStack.addTopLevel(topLevelRoute) + }, + icon = { + Icon( + imageVector = topLevelRoute.icon, + contentDescription = null + ) + } + ) + } + } + } + ) { _ -> + NavDisplay( + backStack = topLevelBackStack.backStack, + onBack = { topLevelBackStack.removeLast() }, + entryProvider = entryProvider { + entry{ + ContentRed("Home screen") + } + entry{ + ContentGreen("Chat list screen"){ + Button(onClick = dropUnlessResumed { + topLevelBackStack.add(ChatDetail) + }) { + Text("Go to conversation") + } + } + } + entry{ + ContentBlue("Chat detail screen") + } + entry{ + ContentPurple("Camera screen") + } + }, + ) + } + } + } +} + +class TopLevelBackStack(startKey: T) { + + // Maintain a stack for each top level route + private var topLevelStacks : LinkedHashMap> = linkedMapOf( + startKey to mutableStateListOf(startKey) + ) + + // Expose the current top level route for consumers + var topLevelKey by mutableStateOf(startKey) + private set + + // Expose the back stack so it can be rendered by the NavDisplay + val backStack = mutableStateListOf(startKey) + + private fun updateBackStack() = + backStack.apply { + clear() + addAll(topLevelStacks.flatMap { it.value }) + } + + fun addTopLevel(key: T){ + + // If the top level doesn't exist, add it + if (topLevelStacks[key] == null){ + topLevelStacks.put(key, mutableStateListOf(key)) + } else { + // Otherwise just move it to the end of the stacks + topLevelStacks.apply { + remove(key)?.let { + put(key, it) + } + } + } + topLevelKey = key + updateBackStack() + } + + fun add(key: T){ + topLevelStacks[topLevelKey]?.add(key) + updateBackStack() + } + + fun removeLast(){ + val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() + // If the removed key was a top level key, remove the associated top level stack + topLevelStacks.remove(removedKey) + topLevelKey = topLevelStacks.keys.last() + updateBackStack() + } +} + +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md new file mode 100644 index 0000000..60848b3 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md @@ -0,0 +1,230 @@ +# Conditional Navigation Recipe + +This recipe demonstrates how to implement conditional navigation, where certain destinations are only accessible if a condition is met (in this case, if the user is logged in). + +## How it works + +This example has a `Profile` destination that requires the user to be logged in. If the user is not logged in and attempts to navigate to `Profile`, they are redirected to a `Login` screen. After a successful login, they are automatically navigated to the `Profile` screen. + +### `AppBackStack` + +The core of this recipe is the custom `AppBackStack` class, which encapsulates the logic for conditional navigation. + +- **`RequiresLogin` interface** : A marker interface, `RequiresLogin`, is used to identify destinations that require the user to be logged in. The `Profile` destination implements this interface. + +- **Redirecting to Login** : When the `add` function is called with a destination that implements `RequiresLogin` and the user is not logged in, `AppBackStack` stores the intended destination and adds the `Login` route to the back stack instead. + +- **Handling Login** : When the `login` function is called, it sets the user's status to logged in. If there is a stored destination that the user was trying to access, it adds that destination to the back stack and removes the `Login` screen. + +- **Handling Logout** : When the `logout` function is called, it sets the user's status to logged out and removes any destinations from the back stack that require the user to be logged in. + +This approach provides a clean way to handle conditional navigation by centralizing the logic in a custom back stack implementation. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/conditional) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.conditional + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSerializable +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.serialization.NavBackStackSerializer +import androidx.navigation3.runtime.serialization.NavKeySerializer +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentYellow +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + + +/** + * Class for representing navigation keys in the app. + * + * Note: We use a sealed class because KotlinX Serialization handles + * polymorphic serialization of sealed classes automatically. + * + * @param requiresLogin - true if the navigation key requires that the user is logged in + * to navigate to it + */ +@Serializable +sealed class ConditionalNavKey(val requiresLogin: Boolean = false) : NavKey + +/** + * Key representing home screen + */ +@Serializable +private data object Home : ConditionalNavKey() + +/** + * Key representing profile screen that is only accessible once the user has logged in + */ +@Serializable +private data object Profile : ConditionalNavKey(requiresLogin = true) + +/** + * Key representing login screen + * + * @param redirectToKey - navigation key to redirect to after successful login + */ +@Serializable +private data class Login( + val redirectToKey: ConditionalNavKey? = null +) : ConditionalNavKey() + +class ConditionalActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + + val backStack = rememberNavBackStack(Home) + var isLoggedIn by rememberSaveable { + mutableStateOf(false) + } + val navigator = remember { + Navigator( + backStack = backStack, + onNavigateToRestrictedKey = { redirectToKey -> Login(redirectToKey) }, + isLoggedIn = { isLoggedIn } + ) + } + + NavDisplay( + backStack = backStack, + onBack = { navigator.goBack() }, + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3. Logged in? ${isLoggedIn}") { + Column { + Button(onClick = dropUnlessResumed { navigator.navigate(Profile) }) { + Text("Profile") + } + Button(onClick = dropUnlessResumed { navigator.navigate(Login()) }) { + Text("Login") + } + } + } + } + entry { + ContentBlue("Profile screen (only accessible once logged in)") { + Button(onClick = dropUnlessResumed { + isLoggedIn = false + navigator.navigate(Home) + }) { + Text("Logout") + } + } + } + entry { key -> + ContentYellow("Login screen. Logged in? $isLoggedIn") { + Button(onClick = dropUnlessResumed { + isLoggedIn = true + key.redirectToKey?.let { targetKey -> + backStack.remove(key) + navigator.navigate(targetKey) + } + }) { + Text("Login") + } + } + } + } + ) + } + } +} + + +// An overload of `rememberNavBackStack` that returns a subtype of `NavKey`. +// See https://issuetracker.google.com/issues/463382671 for a discussion of this function +@Composable +fun rememberNavBackStack(vararg elements: T): NavBackStack { + return rememberSerializable( + serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()) + ) { + NavBackStack(*elements) + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.conditional + +import androidx.navigation3.runtime.NavBackStack + +/** + * Provides navigation events with built-in support for conditional access. If the user attempts to + * navigate to a [ConditionalNavKey] that requires login ([ConditionalNavKey.requiresLogin] is true) + * but is not currently logged in, the Navigator will redirect the user to a login key. + * + * @property backStack The back stack that is modified by this class + * @property onNavigateToRestrictedKey A lambda that is called when the user attempts to navigate + * to a key that requires login. This should return the key that represents the login screen. The + * user's target key is supplied as a parameter so that after successful login the user can be + * redirected to their target destination. + * @property isLoggedIn A lambda that returns whether the user is logged in. + */ +class Navigator( + private val backStack: NavBackStack, + private val onNavigateToRestrictedKey: (targetKey: ConditionalNavKey?) -> ConditionalNavKey, + private val isLoggedIn: () -> Boolean, +) { + fun navigate(key: ConditionalNavKey) { + if (key.requiresLogin && !isLoggedIn()) { + val loginKey = onNavigateToRestrictedKey(key) + backStack.add(loginKey) + } else { + backStack.add(key) + } + } + + fun goBack() = backStack.removeLastOrNull() +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md new file mode 100644 index 0000000..f241291 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md @@ -0,0 +1,155 @@ +# Deep Link Advanced Recipe + +This recipe demonstrates how to apply the principles of navigation in the context of deep links by +managing a synthetic backStack and Task stacks. + +# Recipe Structure + +This recipe simulates a real-world scenario where "App A" deeplinks +into "App B". + +"App A" is simulated by the module [com.example.nav3recipes.deeplink.advanced](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/advanced), which +contains the `CreateAdvancedDeepLinkActivity` that allows you to create a deeplink intent and +trigger that in either the existing Task, or in a new Task. + +"App B" is simulated by the module [advanceddeeplinkapp](https://developer.android.com/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced), which contains +the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack +and how to manage the Task stack properly in order to support both Back and Up buttons. + +# Core implementation + +The core helper functions for navigateUp and building synthetic backStack can be +found [here](https://developer.android.com/static/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt) + +# Further Read + +Check out the [deep link guide](https://developer.android.com/docs/deeplink-guide) for a +comprehensive guide on Deep linking principles and how to apply them in Navigation 3. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/advanced) + +``` +package com.example.nav3recipes.deeplink.advanced + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.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 +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com" + +/** + * The recipe entry point that allows users to create a deep link and make a request with it. + * + * **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deeplinks + * into "App B". + * + * "App A" is simulated by this current module [com.example.nav3recipes.deeplink.advanced], which + * contains the [AdvancedCreateDeepLinkActivity] that allows you to create a deeplink intent and + * trigger that in either the existing Task, or in a new Task. + * + * "App B" is simulated by the module [com.example.nav3recipes.deeplink.advanced], which contains + * the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack + * and how to manage the Task stack properly in order to support both Back and Up buttons. + * + * See the [README](README.md) file of current module for more info on advanced deep linking. + */ +class AdvancedCreateDeepLinkActivity: ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + EntryScreen("Sandbox - Build Your Deeplink Intent") { + val initFirstName = MENU_OPTIONS_FIRST_NAME.values.first().first() + val initLocation = MENU_OPTIONS_LOCATION.values.last().first() + val initTaskStack = MENU_OPTIONS_TASK_STACK.values.first().first() + var firstName by remember { mutableStateOf(initFirstName) } + var location by remember { mutableStateOf(initLocation) } + var taskStack by remember { mutableStateOf(initTaskStack) } + + // select first name + MenuDropDown( + menuOptions = MENU_OPTIONS_FIRST_NAME, + ) { _, selected -> + firstName = selected + } + + // select first name + MenuDropDown( + menuOptions = MENU_OPTIONS_LOCATION, + ) { _, selected -> + location = selected + } + + // select current task stack or build new task stack + MenuDropDown( + menuOptions = MENU_OPTIONS_TASK_STACK, + ) { _, selected -> + taskStack = selected + } + + // build final deeplink URL and Intent + val finalUrl = "${ADVANCED_PATH_BASE}/user/$firstName/$location" + + // display Intent info + val flagString = if (taskStack == TAG_NEW_TASK) { + "Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK" + } else "" + val intentString = """ + | Final Intent: + | data = "$finalUrl" + | action = Intent.ACTION_VIEW + | flags = $flagString + """.trimMargin() + + TextContent(intentString) + + // deeplink to target + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent().apply { + data = finalUrl.toUri() + action = Intent.ACTION_VIEW + if (taskStack == TAG_NEW_TASK) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + + startActivity(intent) + }) + } + } + } +} + +private const val TAG_FIRST_NAME = "firstName" +private const val TAG_LOCATION = "location" +private const val TAG_TASK_STACK = "Task stack" +private const val TAG_CURRENT_TASK = "Use Current Task Stack" +private const val TAG_NEW_TASK = "Start New Task Stack" + +private val MENU_OPTIONS_FIRST_NAME = mapOf( + TAG_FIRST_NAME to LIST_FIRST_NAMES +) + +private val MENU_OPTIONS_LOCATION = mapOf( + TAG_LOCATION to LIST_LOCATIONS +) + +private val MENU_OPTIONS_TASK_STACK = mapOf( + TAG_TASK_STACK to listOf(TAG_CURRENT_TASK, TAG_NEW_TASK), +) +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md new file mode 100644 index 0000000..66312de --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md @@ -0,0 +1,744 @@ +# Deep Link Basic Recipe + +This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. + +## How it works + +It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. + +## Demonstrated forms of deeplink + +The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: + +1. `HomeKey` - deeplink with an exact url (no deeplink arguments) +2. `UsersKey` - deeplink with path arguments +3. `SearchKey` - deeplink with query arguments + +See `MainActivity.deepLinkPatterns` for the actual url pattern of each. + +## Recipe structure + +This recipe consists of three main packages: + +1. `basic.deeplink` - Contains the two activities +2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc +3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks + +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/basic) + +``` +package com.example.nav3recipes.deeplink.basic + +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS +import kotlinx.serialization.Serializable + +internal interface NavRecipeKey: NavKey { + val name: String +} + +@Serializable +internal object HomeKey: NavRecipeKey { + override val name: String = STRING_LITERAL_HOME +} + +@Serializable +internal data class UsersKey( + val filter: String, +): NavRecipeKey { + override val name: String = STRING_LITERAL_USERS + companion object { + const val FILTER_KEY = STRING_LITERAL_FILTER + const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" + const val FILTER_OPTION_ALL = "all" + } +} + +@Serializable +internal data class SearchKey( + val firstName: String? = null, + val ageMin: Int? = null, + val ageMax: Int? = null, + val location: String? = null, +): NavRecipeKey { + override val name: String = STRING_LITERAL_SEARCH +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.net.toUri +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FriendsList +import com.example.nav3recipes.common.deeplink.LIST_USERS +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT +import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher +import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern +import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest +import com.example.nav3recipes.deeplink.basic.util.KeyDecoder +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * Parses a target deeplink into a NavKey. There are several crucial steps involved: + * + * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable + * format (see [DeepLinkPattern]) + * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) + * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each + * other to facilitate comparison and finding a match + * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match + * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result + * to backstack key, regardless of what the conversion method may be. + * STEP 4. Associate the match results with the correct backstack key + * + * This recipes provides an example for each of the above steps by way of kotlinx.serialization. + * + * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional + * deeplink considerations are not included in this scope** + * - Create synthetic backStack + * - Multi-modular setup + * - DI + * - Managing TaskStack + * - Up button ves Back Button + * + */ +class MainActivity : ComponentActivity() { + /** STEP 1. Parse supported deeplinks */ + // internal so that landing activity can link to this in the kdocs + internal val deepLinkPatterns: List> = listOf( + // "https://www.nav3recipes.com/home" + DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), + // "https://www.nav3recipes.com/users/with/{filter}" + DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), + // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" + DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), + ) + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + // retrieve the target Uri + val uri: Uri? = intent.data + // associate the target with the correct backstack key + val key: NavKey = uri?.let { + /** STEP 2. Parse requested deeplink */ + val request = DeepLinkRequest(uri) + /** STEP 3. Compared requested with supported deeplink to find match*/ + val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> + DeepLinkMatcher(request, pattern).match() + } + /** STEP 4. If match is found, associate match to the correct key*/ + match?.let { + //leverage kotlinx.serialization's Decoder to decode + // match result into a backstack key + KeyDecoder(match.args) + .decodeSerializableValue(match.serializer) + } + } ?: HomeKey // fallback if intent.uri is null or match is not found + + /** + * Then pass starting key to backstack + */ + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { key -> + EntryScreen(key.name) { + TextContent("") + } + } + entry { key -> + EntryScreen("${key.name} : ${key.filter}") { + TextContent("") + val list = when { + key.filter.isEmpty() -> LIST_USERS + key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS + else -> LIST_USERS.take(5) + } + FriendsList(list) + } + } + entry { search -> + EntryScreen(search.name) { + TextContent("") + val matchingUsers = LIST_USERS.filter { user -> + (search.firstName == null || user.firstName == search.firstName) && + (search.location == null || user.location == search.location) && + (search.ageMin == null || user.age >= search.ageMin) && + (search.ageMax == null || user.age <= search.ageMax) + } + FriendsList(matchingUsers) + } + } + } + ) + } + } +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.EMPTY +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE +import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY +import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM +import com.example.nav3recipes.common.deeplink.LOCATION_BC +import com.example.nav3recipes.common.deeplink.LOCATION_BR +import com.example.nav3recipes.common.deeplink.LOCATION_CA +import com.example.nav3recipes.common.deeplink.LOCATION_US +import com.example.nav3recipes.common.deeplink.MenuDropDown +import com.example.nav3recipes.common.deeplink.MenuTextInput +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE +import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE +import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * This activity allows the user to create a deep link and make a request with it. + * + * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct + * and trigger the deeplink request, and the [MainActivity] to show how an app can handle + * that request. + * + * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to + * demonstrate different types of supported deeplinks: + * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) + * 2. [UsersKey] - deeplink with path arguments + * 3. [SearchKey] - deeplink with query arguments + * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. + * + * **RECIPE STRUCTURE** This recipe consists of three main packages: + * 1. basic.deeplink - Contains the two activities + * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc + * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match + * the deeplinks + * + * See [MainActivity] for how the requested deeplink is handled. + */ +class CreateDeepLinkActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + /** + * UI for deeplink sandbox + */ + EntryScreen("Sandbox - Build Your Deeplink") { + TextContent("Base url:\n${PATH_BASE}/") + var showFilterOptions by remember { mutableStateOf(false) } + val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } + + var showQueryOptions by remember { mutableStateOf(false) } + var selectedFilter by remember { mutableStateOf("") } + val selectedSearchQuery = remember { mutableStateMapOf() } + + // manage path options + MenuDropDown( + menuOptions = MENU_OPTIONS_PATH, + ) { _, selection -> + selectedPath.value = selection + when (selection) { + PATH_SEARCH -> { + showQueryOptions = true + showFilterOptions = false + } + + PATH_INCLUDE -> { + showQueryOptions = false + showFilterOptions = true + } + + else -> { + showQueryOptions = false + showFilterOptions = false + } + } + } + + // manage path filter options, reset state if menu is closed + LaunchedEffect(showFilterOptions) { + selectedFilter = if (showFilterOptions) { + MENU_OPTIONS_FILTER.values.first().first() + } else { + "" + } + } + if (showFilterOptions) { + MenuDropDown( + menuOptions = MENU_OPTIONS_FILTER, + ) { _, selected -> + selectedFilter = selected + } + } + + // manage query options, reset state if menu is closed + LaunchedEffect(showQueryOptions) { + if (showQueryOptions) { + val initEntry = MENU_OPTIONS_SEARCH.entries.first() + selectedSearchQuery[initEntry.key] = initEntry.value.first() + } else { + selectedSearchQuery.clear() + } + } + if (showQueryOptions) { + MenuTextInput( + menuLabels = MENU_LABELS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + MenuDropDown( + menuOptions = MENU_OPTIONS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + } + + // form final deeplink url + val arguments = when (selectedPath.value) { + PATH_INCLUDE -> "/${selectedFilter}" + PATH_SEARCH -> { + buildString { + selectedSearchQuery.forEach { entry -> + if (entry.value.isNotEmpty()) { + val prefix = if (isEmpty()) "?" else "&" + append("$prefix${entry.key}=${entry.value}") + } + } + } + } + + else -> "" + } + val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" + TextContent("Final url:\n$finalUrl") + // deeplink to target + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent( + this@CreateDeepLinkActivity, + MainActivity::class.java + ) + // start activity with the url + intent.data = finalUrl.toUri() + startActivity(intent) + }) + } + } + } +} + +private const val KEY_PATH = "path" +private val MENU_OPTIONS_PATH = mapOf( + KEY_PATH to listOf( + STRING_LITERAL_HOME, + PATH_INCLUDE, + PATH_SEARCH, + ), +) + +private val MENU_OPTIONS_FILTER = mapOf( + UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), +) + +private val MENU_OPTIONS_SEARCH = mapOf( + SearchKey::firstName.name to listOf( + EMPTY, + FIRST_NAME_JOHN, + FIRST_NAME_TOM, + FIRST_NAME_MARY, + FIRST_NAME_JULIE + ), + SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) +) + +private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) + +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +/** + * Decodes the list of arguments into a a back stack key + * + * **IMPORTANT** This decoder assumes that all argument types are Primitives. + */ +@OptIn(ExperimentalSerializationApi::class) +internal class KeyDecoder( + private val arguments: Map, +) : AbstractDecoder() { + + override val serializersModule: SerializersModule = EmptySerializersModule() + private var elementIndex: Int = -1 + private var elementName: String = "" + + /** + * Decodes the index of the next element to be decoded. Index represents a position of the + * current element in the [descriptor] that can be found with [descriptor].getElementIndex. + * + * The returned index will trigger deserializer to call [decodeValue] on the argument at that + * index. + * + * The decoder continually calls this method to process the next available argument until this + * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more + * arguments to decode. + * + * This method should sequentially return the element index for every element that has its value + * available within [arguments]. + */ + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + var currentIndex = elementIndex + while (true) { + // proceed to next element + currentIndex++ + // if we have reached the end, let decoder know there are not more arguments to decode + if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE + val currentName = descriptor.getElementName(currentIndex) + // Check if bundle has argument value. If so, we tell decoder to process + // currentIndex. Otherwise, we skip this index and proceed to next index. + if (arguments.contains(currentName)) { + elementIndex = currentIndex + elementName = currentName + return elementIndex + } + } + } + + /** + * Returns argument value from the [arguments] for the argument at the index returned by + * [decodeElementIndex] + */ + override fun decodeValue(): Any { + val arg = arguments[elementName] + checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } + return arg + } + + override fun decodeNull(): Nothing? = null + + // we want to know if it is not null, so its !isNull + override fun decodeNotNullMark(): Boolean = arguments[elementName] != null +} +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri + +/** + * Parse the requested Uri and store it in a easily readable format + * + * @param uri the target deeplink uri to link to + */ +internal class DeepLinkRequest( + val uri: Uri +) { + /** + * A list of path segments + */ + val pathSegments: List = uri.pathSegments + + /** + * A map of query name to query value + */ + val queries = buildMap { + uri.queryParameterNames.forEach { argName -> + this[argName] = uri.getQueryParameter(argName)!! + } + } + + // TODO add parsing for other Uri components, i.e. fragments, mimeType, action +} +``` + +```` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.encoding.CompositeDecoder +import java.io.Serializable + +/** + * Parse a supported deeplink and stores its metadata as a easily readable format + * + * The following notes applies specifically to this particular sample implementation: + * + * The supported deeplink is expected to be built from a serializable backstack key [T] that + * supports deeplink. This means that if this deeplink contains any arguments (path or query), + * the argument name must match any of [T] member field name. + * + * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] + * supports two deeplink patterns: + * ``` + * val deeplink1 = www.nav3recipes.com/home + * val deeplink2 = www.nav3recipes.com/profile/{userId} + * ``` + * Then two [DeepLinkPattern] should be created + * ``` + * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) + * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) + * ``` + * + * This implementation assumes a few things: + * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match + * 2. all query arguments are optional by way of nullable/has default value + * + * @param T the backstack key type that supports the deeplinking of [uriPattern] + * @param serializer the serializer of [T] + * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" + */ +internal class DeepLinkPattern( + val serializer: KSerializer, + val uriPattern: Uri +) { + /** + * Help differentiate if a path segment is an argument or a static value + */ + private val regexPatternFillIn = Regex("\\{(.+?)\\}") + + // TODO make these lazy + /** + * parse the path into a list of [PathSegment] + * + * order matters here - path segments need to match in value and order when matching + * requested deeplink to supported deeplink + */ + val pathSegments: List = buildList { + uriPattern.pathSegments.forEach { segment -> + // first, check if it is a path arg + var result = regexPatternFillIn.find(segment) + if (result != null) { + // if so, extract the path arg name (the string value within the curly braces) + val argName = result.groups[1]!!.value + // from [T], read the primitive type of this argument to get the correct type parser + val elementIndex = serializer.descriptor.getElementIndex(argName) + if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { + throw IllegalArgumentException( + "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." + ) + } + + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + // finally, add the arg name and its respective type parser to the map + add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) + } else { + // if its not a path arg, then its just a static string path segment + add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) + } + } + } + + /** + * Parse supported queries into a map of queryParameterNames to [TypeParser] + * + * This will be used later on to parse a provided query value into the correct KType + */ + val queryValueParsers: Map = buildMap { + uriPattern.queryParameterNames.forEach { paramName -> + val elementIndex = serializer.descriptor.getElementIndex(paramName) + // Ignore static query parameters that are not in the Serializable class + if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + this[paramName] = getTypeParser(elementDescriptor.kind) + } + } + } + + /** + * Metadata about a supported path segment + */ + class PathSegment( + val stringValue: String, + val isParamArg: Boolean, + val typeParser: TypeParser + ) +} + +/** + * Parses a String into a Serializable Primitive + */ +private typealias TypeParser = (String) -> Serializable + +private fun getTypeParser(kind: SerialKind): TypeParser { + return when (kind) { + PrimitiveKind.STRING -> Any::toString + PrimitiveKind.INT -> String::toInt + PrimitiveKind.BOOLEAN -> String::toBoolean + PrimitiveKind.BYTE -> String::toByte + PrimitiveKind.CHAR -> String::toCharArray + PrimitiveKind.DOUBLE -> String::toDouble + PrimitiveKind.FLOAT -> String::toFloat + PrimitiveKind.LONG -> String::toLong + PrimitiveKind.SHORT -> String::toShort + else -> throw IllegalArgumentException( + "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." + ) + } +} +```` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.util.Log +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer + +internal class DeepLinkMatcher( + val request: DeepLinkRequest, + val deepLinkPattern: DeepLinkPattern +) { + /** + * Match a [DeepLinkRequest] to a [DeepLinkPattern]. + * + * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise + */ + fun match(): DeepLinkMatchResult? { + if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null + if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null + if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null + // exact match (url does not contain any arguments) + if (request.uri == deepLinkPattern.uriPattern) + return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) + + val args = mutableMapOf() + // match the path + request.pathSegments + .asSequence() + // zip to compare the two objects side by side, order matters here so we + // need to make sure the compared segments are at the same position within the url + .zip(deepLinkPattern.pathSegments.asSequence()) + .forEach { it -> + // retrieve the two path segments to compare + val requestedSegment = it.first + val candidateSegment = it.second + // if the potential match expects a path arg for this segment, try to parse the + // requested segment into the expected type + if (candidateSegment.isParamArg) { + val parsedValue = try { + candidateSegment.typeParser.invoke(requestedSegment) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) + return null + } + args[candidateSegment.stringValue] = parsedValue + } else if(requestedSegment != candidateSegment.stringValue){ + // if it's path arg is not the expected type, its not a match + return null + } + } + // match queries (if any) + request.queries.forEach { query -> + val name = query.key + // If the pattern does not define this query parameter, ignore it. + // This prevents a NullPointerException. + val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach + + val queryParsedValue = try { + queryStringParser.invoke(query.value) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) + return null + } + args[name] = queryParsedValue + } + // provide the serializer of the matching key and map of arg names to parsed arg values + return DeepLinkMatchResult(deepLinkPattern.serializer, args) + } +} + + +/** + * Created when a requested deeplink matches with a supported deeplink + * + * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink + * @param serializer serializer for [T] + * @param args The map of argument name to argument value. The value is expected to have already + * been parsed from the raw url string back into its proper KType as declared in [T]. + * Includes arguments for all parts of the uri - path, query, etc. + * */ +internal data class DeepLinkMatchResult( + val serializer: KSerializer, + val args: Map +) + +const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" +``` + +``` +package com.example.nav3recipes.deeplink.basic.ui + +import com.example.nav3recipes.deeplink.basic.SearchKey + +/** + * String resources + */ +internal const val STRING_LITERAL_FILTER = "filter" +internal const val STRING_LITERAL_HOME = "home" +internal const val STRING_LITERAL_USERS = "users" +internal const val STRING_LITERAL_SEARCH = "search" +internal const val STRING_LITERAL_INCLUDE = "include" +internal const val PATH_BASE = "https://www.nav3recipes.com" +internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" +internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" +internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" + +internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" +internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + + "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + + "&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" + + "&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" + + "&${SearchKey::location.name}={${SearchKey::location.name}}" +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md new file mode 100644 index 0000000..6ef8efe --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md @@ -0,0 +1,107 @@ +# Dialog Recipe + +This recipe demonstrates how to display a destination as a dialog. + +## How it works + +To show a destination as a dialog, you need to do two things: + +1. **Use `DialogSceneStrategy`** : Create an instance of `DialogSceneStrategy` and pass it to the `sceneStrategy` parameter of the `NavDisplay` composable. + +2. **Add metadata to the destination** : For the destination that you want to display as a dialog, add `DialogSceneStrategy.dialog()` to its metadata. This is done in the `entry` function. You can also pass a `DialogProperties` object to customize the dialog's behavior and appearance. + +In this example, `RouteB` is configured to be a dialog. When you navigate from `RouteA` to `RouteB`, `RouteB` will be displayed in a dialog window. + +The content of the dialog can be styled as needed. In this recipe, the content is clipped to have rounded corners. + +For more information, see the official documentation on [custom layouts](https://developer.android.com/guide/navigation/navigation-3/custom-layouts). +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/dialog) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.dialog + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data class RouteB(val id: String) : NavKey + +class DialogActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + val dialogStrategy = remember { DialogSceneStrategy() } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(dialogStrategy), + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to open dialog") + } + } + } + entry( + metadata = DialogSceneStrategy.dialog( + DialogProperties(windowTitle = "Route B dialog") + ) + ) { key -> + ContentBlue( + title = "Route id: ${key.id}", + modifier = Modifier.clip( + shape = RoundedCornerShape(16.dp) + ) + ) + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md new file mode 100644 index 0000000..fbaae79 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md @@ -0,0 +1,141 @@ +# Material List-Detail Recipe + +This recipe demonstrates how to create an adaptive list-detail layout using the `ListDetailSceneStrategy` from the Material 3 Adaptive library. This layout automatically adjusts to show one, two, or three panes depending on the available screen width. + +## How it works + +This example has three destinations: `ConversationList`, `ConversationDetail`, and `Profile`. + +### `ListDetailSceneStrategy` + +The key to this recipe is the `rememberListDetailSceneStrategy`, which provides the logic for the adaptive layout. + +- **Pane Roles**: Each destination is assigned a role using metadata: + + - `ListDetailSceneStrategy.listPane()`: For the primary (list) content. This pane is always visible. A placeholder can be provided to be shown in the detail pane area when no detail content is selected. + - `ListDetailSceneStrategy.detailPane()`: For the secondary (detail) content. + - `ListDetailSceneStrategy.extraPane()`: For tertiary content. +- **Adaptive Layout** : The `ListDetailSceneStrategy` automatically handles the layout. On smaller screens, only one pane is shown at a time. On wider screens, it will show the list and detail panes side-by-side. On very wide screens, it can show all three panes: list, detail, and extra. + +- **Navigation** : Navigation between the panes is handled by adding and removing destinations from the back stack as usual. The `ListDetailSceneStrategy` observes the back stack and adjusts the layout accordingly. + +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/listdetail) + +``` +package com.example.nav3recipes.material.listdetail + +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.content.ContentYellow +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private object ConversationList : NavKey + +@Serializable +private data class ConversationDetail(val id: String) : NavKey + +@Serializable +private data object Profile : NavKey + +class MaterialListDetailActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + + val backStack = rememberNavBackStack(ConversationList) + + // Override the defaults so that there isn't a horizontal space between the panes. + // See b/418201867 + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() + val directive = remember(windowAdaptiveInfo) { + calculatePaneScaffoldDirective(windowAdaptiveInfo) + .copy(horizontalPartitionSpacerSize = 0.dp) + } + val listDetailStrategy = rememberListDetailSceneStrategy(directive = directive) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(listDetailStrategy), + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + detailPlaceholder = { + ContentYellow("Choose a conversation from the list") + } + ) + ) { + ContentRed("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(ConversationDetail("ABC")) + }) { + Text("View conversation") + } + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { conversation -> + ContentBlue("Conversation ${conversation.id} ") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed { + backStack.add(Profile) + }) { + Text("View profile") + } + } + } + } + entry( + metadata = ListDetailSceneStrategy.extraPane() + ) { + ContentGreen("Profile") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md new file mode 100644 index 0000000..58a6a7c --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md @@ -0,0 +1,145 @@ +# Material Supporting Pane Recipe + +This recipe demonstrates how to create an adaptive layout with a main pane and a supporting pane using the `SupportingPaneSceneStrategy` from the Material 3 Adaptive library. This layout is useful for displaying supplementary content alongside the main content on larger screens. + +## How it works + +This example has three destinations: `MainVideo`, `RelatedVideos`, and `Profile`. + +### `SupportingPaneSceneStrategy` + +The `rememberSupportingPaneSceneStrategy` provides the logic for this adaptive layout. + +- **Pane Roles**: Each destination is assigned a role using metadata: + + - `SupportingPaneSceneStrategy.mainPane()`: For the primary content. This pane is always visible. + - `SupportingPaneSceneStrategy.supportingPane()`: For the supplementary content. This pane is shown alongside the main pane on larger screens. + - `SupportingPaneSceneStrategy.extraPane()`: For tertiary content that can be displayed alongside the supporting pane on even larger screens. +- **Adaptive Layout** : The `SupportingPaneSceneStrategy` automatically handles the layout. On smaller screens, only the main pane is shown. On larger screens, the supporting pane is shown next to the main pane. + +- **Back Navigation** : The `BackNavigationBehavior` is customized in this example to `PopUntilCurrentDestinationChange`. This means that when the user presses the back button, the supporting pane will be dismissed, revealing the main pane underneath. + +- **Navigation** : Navigation is handled by adding and removing destinations from the back stack. The `SupportingPaneSceneStrategy` observes these changes and adjusts the layout accordingly. + +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/supportingpane) + +``` +package com.example.nav3recipes.material.supportingpane + +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior +import androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberSupportingPaneSceneStrategy +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private object MainVideo : NavKey + +@Serializable +private data object RelatedVideos : NavKey + +@Serializable +private data object Profile : NavKey + +class MaterialSupportingPaneActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + + val backStack = rememberNavBackStack(MainVideo) + + // Override the defaults so that there isn't a horizontal or vertical space between the panes. + // See b/444438086 + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() + val directive = remember(windowAdaptiveInfo) { + calculatePaneScaffoldDirective(windowAdaptiveInfo) + .copy(horizontalPartitionSpacerSize = 0.dp, verticalPartitionSpacerSize = 0.dp) + } + + // Override the defaults so that the supporting pane can be dismissed by pressing back. + // See b/445826749 + val supportingPaneStrategy = rememberSupportingPaneSceneStrategy( + backNavigationBehavior = BackNavigationBehavior.PopUntilCurrentDestinationChange, + directive = directive + ) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(supportingPaneStrategy), + entryProvider = entryProvider { + entry( + metadata = SupportingPaneSceneStrategy.mainPane() + ) { + ContentRed("Video content") { + Button(onClick = dropUnlessResumed { + backStack.add(RelatedVideos) + }) { + Text("View related videos") + } + } + } + entry( + metadata = SupportingPaneSceneStrategy.supportingPane() + ) { + ContentBlue("Related videos") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed { + backStack.add(Profile) + }) { + Text("View profile") + } + } + } + } + entry( + metadata = SupportingPaneSceneStrategy.extraPane() + ) { + ContentGreen("Profile") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md new file mode 100644 index 0000000..fcc94a2 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md @@ -0,0 +1,283 @@ +# Modular Navigation Recipe (Hilt) + +This recipe demonstrates how to structure a multi-module application using Navigation 3 and Dagger/Hilt for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. + +## How it works + +The application is divided into several modules: + +- **`app` module** : This is the main application module. It initializes a common `Navigator` and injects a set of `EntryProviderInstaller`s from the feature modules. It then uses these installers to build the final `entryProvider` for the `NavDisplay`. + +- **`common` module**: This module contains the core navigation logic, including: + + - A `Navigator` class that manages the back stack. + - An `EntryProviderInstaller` type, which is a function that feature modules use to contribute their navigation entries to the application's `entryProvider`. +- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: + + - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. + - **`impl` module** : Provides the implementation of the feature, including its composables and an `EntryProviderInstaller` that maps the feature's routes to its composables. This installer is then provided to the `app` module using Dagger/Hilt. + +This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/hilt) + +``` +package com.example.nav3recipes.modular.hilt + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +// API +object Profile + +// IMPLEMENTATION +@Module +@InstallIn(ActivityRetainedComponent::class) +object ProfileModule { + + @IntoSet + @Provides + fun provideEntryProviderInstaller() : EntryProviderInstaller = { + entry{ + ProfileScreen() + } + } +} + +@Composable +private fun ProfileScreen() { + val profileColor = MaterialTheme.colorScheme.surfaceVariant + Column( + modifier = Modifier + .fillMaxSize() + .background(profileColor) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Profile Screen", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} +``` + +``` +package com.example.nav3recipes.modular.hilt + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class HiltModularActivity : ComponentActivity() { + + @Inject + lateinit var navigator: Navigator + + @Inject + lateinit var entryProviderScopes: Set<@JvmSuppressWildcards EntryProviderInstaller> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setEdgeToEdgeConfig() + setContent { + Scaffold { paddingValues -> + NavDisplay( + backStack = navigator.backStack, + modifier = Modifier.padding(paddingValues), + onBack = { navigator.goBack() }, + entryProvider = entryProvider { + entryProviderScopes.forEach { builder -> this.builder() } + } + ) + } + } + } +} +``` + +``` +package com.example.nav3recipes.modular.hilt + +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.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.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.ui.theme.colors +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +// API +object ConversationList +data class ConversationDetail(val id: Int) { + val color: Color + get() = colors[id % colors.size] +} + +// IMPL +@Module +@InstallIn(ActivityRetainedComponent::class) +object ConversationModule { + + @IntoSet + @Provides + fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = + { + entry { + ConversationListScreen( + onConversationClicked = { conversationDetail -> + navigator.goTo(conversationDetail) + } + ) + } + entry { key -> + ConversationDetailScreen(key) { navigator.goTo(Profile) } + } + } +} + +@Composable +private fun ConversationListScreen( + onConversationClicked: (ConversationDetail) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + items(10) { index -> + val conversationId = index + 1 + val conversationDetail = ConversationDetail(conversationId) + val backgroundColor = conversationDetail.color + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = dropUnlessResumed { + onConversationClicked(conversationDetail) + }), + headlineContent = { + Text( + text = "Conversation $conversationId", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + colors = ListItemDefaults.colors( + containerColor = backgroundColor // Set container color directly + ) + ) + } + } +} + +@Composable +private fun ConversationDetailScreen( + conversationDetail: ConversationDetail, + onProfileClicked: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(conversationDetail.color) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Conversation Detail Screen: ${conversationDetail.id}", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onProfileClicked)) { + Text("View Profile") + } + } +} +``` + +``` +package com.example.nav3recipes.modular.hilt + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.navigation3.runtime.EntryProviderScope +import dagger.hilt.android.scopes.ActivityRetainedScoped + + +typealias EntryProviderInstaller = EntryProviderScope.() -> Unit + +@ActivityRetainedScoped +class Navigator(startDestination: Any) { + val backStack : SnapshotStateList = mutableStateListOf(startDestination) + + fun goTo(destination: Any){ + backStack.add(destination) + } + + fun goBack(){ + backStack.removeLastOrNull() + } +} +``` + +``` +package com.example.nav3recipes.modular.hilt + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@Module +@InstallIn(ActivityRetainedComponent::class) +object AppModule { + + @Provides + @ActivityRetainedScoped + fun provideNavigator() : Navigator = Navigator(startDestination = ConversationList) +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md new file mode 100644 index 0000000..d1e7ad6 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md @@ -0,0 +1,287 @@ +# Modular Navigation Recipe (Koin) + +This recipe demonstrates how to structure a multi-module application using Navigation 3 and Koin for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. It relies on the [`koin-compose-navigation3`](https://insert-koin.io/docs/reference/koin-compose/navigation3) artifact. + +## How it works + +The application is divided into several Android modules: + +- **`app` module** : This is the main application module. It `includes()` the feature modules and initializes a common `Navigator`. + +- **`common` module** : This module contains the core navigation logic used by both the application module and the feature modules. Namely, it defines a `Navigator` class that manages the back stack. + +- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: + + - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. + - **`impl` module** : Provides the implementation of the feature, including its composables and Koin `Module`. The Koin module uses the [`navigation`](https://insert-koin.io/docs/reference/koin-compose/navigation3/#declaring-navigation-entries) DSL to define the entry provider installers for the feature module. + +This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/koin) + +``` +package com.example.nav3recipes.modular.koin + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.dsl.module +import org.koin.dsl.navigation3.navigation + +// API +object Profile + +// IMPL +@OptIn(KoinExperimentalAPI::class) +val profileModule = module { + activityRetainedScope { + navigation { ProfileScreen() } + } +} + +@Composable +private fun ProfileScreen() { + val profileColor = MaterialTheme.colorScheme.surfaceVariant + Column( + modifier = Modifier + .fillMaxSize() + .background(profileColor) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Profile Screen", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +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.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.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.ui.theme.colors +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.dsl.module +import org.koin.dsl.navigation3.navigation + +// API +object ConversationList +data class ConversationDetail(val id: Int) { + val color: Color + get() = colors[id % colors.size] +} + +// IMPL +@OptIn(KoinExperimentalAPI::class) +val conversationModule = module { + activityRetainedScope { + navigation { + ConversationListScreen( + onConversationClicked = { conversationDetail -> + get().goTo(conversationDetail) + } + ) + } + + navigation { key -> + ConversationDetailScreen(key) { + get().goTo(Profile) + } + } + } +} + +@Composable +private fun ConversationListScreen( + onConversationClicked: (ConversationDetail) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + items(10) { index -> + val conversationId = index + 1 + val conversationDetail = ConversationDetail(conversationId) + val backgroundColor = conversationDetail.color + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = dropUnlessResumed { + onConversationClicked(conversationDetail) + }), + headlineContent = { + Text( + text = "Conversation $conversationId", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + colors = ListItemDefaults.colors( + containerColor = backgroundColor // Set container color directly + ) + ) + } + } +} + +@Composable +private fun ConversationDetailScreen( + conversationDetail: ConversationDetail, + onProfileClicked: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(conversationDetail.color) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Conversation Detail Screen: ${conversationDetail.id}", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onProfileClicked)) { + Text("View Profile") + } + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList + +class Navigator(startDestination: Any) { + val backStack : SnapshotStateList = mutableStateListOf(startDestination) + + fun goTo(destination: Any){ + backStack.add(destination) + } + + fun goBack(){ + backStack.removeLastOrNull() + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.dsl.module + +val appModule = module { + includes(profileModule,conversationModule) + + activityRetainedScope { + scoped { + Navigator(startDestination = ConversationList) + } + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import org.koin.android.ext.android.inject +import org.koin.android.scope.AndroidScopeComponent +import org.koin.androidx.compose.navigation3.getEntryProvider +import org.koin.androidx.scope.activityRetainedScope +import org.koin.core.Koin +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.core.component.KoinComponent +import org.koin.core.scope.Scope +import org.koin.dsl.koinApplication + +/** + * This recipe demonstrates how to use a modular approach with Navigation 3, + * where different parts of the application are defined in separate modules and injected + * into the main app using Koin. + * + * Features (Conversation and Profile) are split into two modules: + * - api: defines the public facing routes for this feature + * - impl: defines the entryProviders for this feature, these are injected into the app's main activity + * The common module defines: + * - a common navigator class that exposes a back stack and methods to modify that back stack + * - a type that should be used by feature modules to inject entryProviders into the app's main activity + * The app module creates the navigator by supplying a start destination and provides this navigator + * to the rest of the app module (i.e. MainActivity) and the feature modules. + */ +@OptIn(KoinExperimentalAPI::class) +class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComponent { + // Local Koin Context Instance + companion object { + private val localKoin = koinApplication { + modules(appModule) + }.koin + } + // Override default Koin context to use the local one + override fun getKoin(): Koin = localKoin + override val scope : Scope by activityRetainedScope() + val navigator: Navigator by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setEdgeToEdgeConfig() + setContent { + Scaffold { paddingValues -> + NavDisplay( + backStack = navigator.backStack, + modifier = Modifier.padding(paddingValues), + onBack = { navigator.goBack() }, + entryProvider = getEntryProvider() + ) + } + } + } + +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md new file mode 100644 index 0000000..81cd794 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md @@ -0,0 +1,436 @@ +# Multiple back stacks recipe + +This recipe demonstrates how to create multiple back stacks. + +The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. The content for the sub routes is a counter that can be used to verify state retention through configuration changes and process death. + +The app's navigation state is held in the `NavigationState` class. The state itself is created using `rememberNavigationState`. + +Navigation events are handled by the `Navigator`. It updates the navigation state. + +The navigation state is converted into `NavEntry`s with `NavigationState.toDecoratedEntries`. These entries are then displayed by `NavDisplay`. + +Key behaviors: + +- This app follows the "exit through home" pattern where the user always exits through the starting back stack. This means that `RouteA`'s entries are *always* in the list of entries. +- Navigating to a top level route that is not the starting route *replaces* the other entries. For example, navigating A-\>B-\>C would result in entries for A+C, B's entries are removed. + +Important implementation details: + +- Each top level route has its own `SaveableStateHolderNavEntryDecorator`. This is the object responsible for managing the state for the entries in its back stack. + +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/multiplestacks) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.multiplestacks + +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() + } + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.multiplestacks + +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.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. + * + * @param startRoute - The top level route to start on. This should also be in `topLevelRoutes`. + * @param topLevelRoutes - The top level routes in the app. + */ +@Composable +fun rememberNavigationState( + startRoute: NavKey, + topLevelRoutes: Set +): NavigationState { + + val topLevelRoute = rememberSerializable( + startRoute, topLevelRoutes, + serializer = MutableStateSerializer(NavKeySerializer()) + ) { + mutableStateOf(startRoute) + } + + // Create a back stack for each top level route. + val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } + + return remember(startRoute, topLevelRoutes) { + NavigationState( + startRoute = startRoute, + topLevelRoute = topLevelRoute, + backStacks = backStacks + ) + } +} + +/** + * State holder for navigation state. This class does not modify its own state. It is designed + * to be modified using the `Navigator` class. + * + * @param startRoute - the start route. The user will exit the app through this route. + * @param topLevelRoute - the state object that backs the top level route. + * @param backStacks - the back stacks for each top level route. + */ +class NavigationState( + val startRoute: NavKey, + topLevelRoute: MutableState, + val backStacks: Map> +) { + + /** + * The top level route. + */ + var topLevelRoute: NavKey by topLevelRoute + + /** + * Convert the navigation state into `NavEntry`s that have been decorated with a + * `SaveableStateHolder`. + * + * @param entryProvider - the entry provider used to convert the keys in the + * back stacks to `NavEntry`s. + */ + @Composable + fun toDecoratedEntries( + entryProvider: (NavKey) -> NavEntry + ): List> { + + // For each back stack, create a `SaveableStateHolder` decorator and use it to decorate + // the entries from that stack. When backStacks changes, `rememberDecoratedNavEntries` will + // be recomposed and a new list of decorated entries is returned. + val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider + ) + } + + // Only return the entries for the stacks that are currently in use. + return getTopLevelRoutesInUse() + .flatMap { decoratedEntries[it] ?: emptyList() } + } + + /** + * Get the top level routes that are currently in use. The start route is always the first route + * in the list. This means the user will always exit the app through the starting route + * ("exit through home" pattern). The list will contain a maximum of one other route. This is a + * design decision. In your app, you may wish to allow more than two top level routes to be + * active. + * + * Note that even if a top level route is not in use its state is still retained. + * + * @return the current top level routes that are in use. + */ + private fun getTopLevelRoutesInUse() : List = + if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.multiplestacks + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + + +@Serializable +data object RouteA : NavKey + +@Serializable +data object RouteA1 : NavKey + +@Serializable +data object RouteB : NavKey + +@Serializable +data object RouteB1 : NavKey + +@Serializable +data object RouteC : NavKey + +@Serializable +data object RouteC1 : NavKey + +private val TOP_LEVEL_ROUTES = mapOf( + RouteA to NavBarItem(icon = Icons.Default.Home, description = "Route A"), + RouteB to NavBarItem(icon = Icons.Default.Face, description = "Route B"), + RouteC to NavBarItem(icon = Icons.Default.Camera, description = "Route C"), +) + +data class NavBarItem( + val icon: ImageVector, + val description: String +) + +class MultipleStacksActivity : ComponentActivity() { + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val navigationState = rememberNavigationState( + startRoute = RouteA, + topLevelRoutes = TOP_LEVEL_ROUTES.keys + ) + + val navigator = remember { Navigator(navigationState) } + + val entryProvider = entryProvider { + featureASection(onSubRouteClick = { navigator.navigate(RouteA1) }) + featureBSection(onSubRouteClick = { navigator.navigate(RouteB1) }) + featureCSection(onSubRouteClick = { navigator.navigate(RouteC1) }) + } + + Scaffold(bottomBar = { + NavigationBar { + TOP_LEVEL_ROUTES.forEach { (key, value) -> + val isSelected = key == navigationState.topLevelRoute + NavigationBarItem( + selected = isSelected, + onClick = { navigator.navigate(key) }, + icon = { + Icon( + imageVector = value.icon, + contentDescription = value.description + ) + }, + label = { Text(value.description) } + ) + } + } + }) { + NavDisplay( + entries = navigationState.toDecoratedEntries(entryProvider), + onBack = { navigator.goBack() } + ) + } + } + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.multiplestacks + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentMauve +import com.example.nav3recipes.content.ContentOrange +import com.example.nav3recipes.content.ContentPink +import com.example.nav3recipes.content.ContentPurple +import com.example.nav3recipes.content.ContentRed + +fun EntryProviderScope.featureASection( + onSubRouteClick: () -> Unit, +) { + entry { + ContentRed("Route A") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { + Text("Go to A1") + } + } + } + } + entry { + ContentPink("Route A1") { + var count by rememberSaveable { + mutableIntStateOf(0) + } + + Button(onClick = { count++ }) { + Text("Value: $count") + } + } + } +} + +fun EntryProviderScope.featureBSection( + onSubRouteClick: () -> Unit, +) { + entry { + ContentGreen("Route B") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { + Text("Go to B1") + } + } + } + } + entry { + ContentPurple("Route B1") { + var count by rememberSaveable { + mutableIntStateOf(0) + } + Button(onClick = { count++ }) { + Text("Value: $count") + } + } + } +} + +fun EntryProviderScope.featureCSection( + onSubRouteClick: () -> Unit, +) { + entry { + ContentMauve("Route C") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { + Text("Go to C1") + } + } + } + } + entry { + ContentOrange("Route C1") { + var count by rememberSaveable { + mutableIntStateOf(0) + } + + Button(onClick = { count++ }) { + Text("Value: $count") + } + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md new file mode 100644 index 0000000..2640a22 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md @@ -0,0 +1,371 @@ +# Passing Arguments to ViewModels (Hilt) + +This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using Hilt for dependency injection. + +## How it works + +This example uses Dagger/Hilt's assisted injection feature: + +1. The `ViewModel` is annotated with `@HiltViewModel` and its constructor uses `@AssistedInject` to receive the navigation key (which is annotated with `@Assisted`). +2. An `@AssistedFactory` interface is defined to create the `ViewModel`. +3. The `hiltViewModel` composable function is used to obtain the `ViewModel` instance. A `creationCallback` is provided to pass the navigation key to the factory, making it available to the `ViewModel`. + +**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/hilt) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.passingarguments.viewmodels.hilt + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.passingarguments.viewmodels.basic.RouteB +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel + +data object RouteA +data class RouteB(val id: String) + +@AndroidEntryPoint +class HiltViewModelsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = remember { mutableStateListOf(RouteA) } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + + // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) + // we also need to add the default `NavEntryDecorator`s as well. These provide + // extra information to the entry's content to enable it to display correctly + // and save its state. + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + LazyColumn { + items(10) { i -> + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("$i")) + }) { + Text("$i") + } + } + } + } + } + entry { key -> + val viewModel = hiltViewModel( + // Note: We need a new ViewModel for every new RouteB instance. Usually + // we would need to supply a `key` String that is unique to the + // instance, however, the ViewModelStoreNavEntryDecorator (supplied + // above) does this for us, using `NavEntry.contentKey` to uniquely + // identify the viewModel. + // + // tl;dr: Make sure you use rememberViewModelStoreNavEntryDecorator() + // if you want a new ViewModel for each new navigation key instance. + creationCallback = { factory -> + factory.create(key) + } + ) + ScreenB(viewModel = viewModel) + } + } + ) + } + } +} + +@Composable +fun ScreenB(viewModel: RouteBViewModel) { + ContentBlue("Route id: ${viewModel.navKey.id} ") +} + +@HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) +class RouteBViewModel @AssistedInject constructor( + @Assisted val navKey: RouteB +) : ViewModel() { + + @AssistedFactory + interface Factory { + fun create(navKey: RouteB): RouteBViewModel + } +} +``` + +# Passing Arguments to ViewModels (Basic) + +This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using a custom `ViewModelProvider.Factory`. + +## How it works + +1. A custom `ViewModelProvider.Factory` is created that takes the navigation key as a constructor parameter. +2. Inside the `entry` composable, `viewModel(factory = ...)` is used to create the `ViewModel` instance, passing the current navigation key to the factory. This makes the navigation key available to the `ViewModel`. + +**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/basic) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.passingarguments.viewmodels.basic + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +data object RouteA + +data class RouteB(val id: String) + +class BasicViewModelsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = remember { mutableStateListOf(RouteA) } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) + // we also need to add the default `NavEntryDecorator`s as well. These provide + // extra information to the entry's content to enable it to display correctly + // and save its state. + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + LazyColumn { + items(10) { i -> + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("$i")) + }) { + Text("$i") + } + } + } + } + } + entry { key -> + // Note: We need a new ViewModel for every new RouteB instance. Usually + // we would need to supply a `key` String that is unique to the + // instance, however, the ViewModelStoreNavEntryDecorator (supplied + // above) does this for us, using `NavEntry.contentKey` to uniquely + // identify the viewModel. + // + // tl;dr: Make sure you use rememberViewModelStoreNavEntryDecorator() + // if you want a new ViewModel for each new navigation key instance. + ScreenB(viewModel = viewModel(factory = RouteBViewModel.Factory(key))) + } + } + ) + } + } +} + +@Composable +fun ScreenB(viewModel: RouteBViewModel = viewModel()) { + ContentBlue("Route id: ${viewModel.key.id} ") +} + +class RouteBViewModel( + val key: RouteB +) : ViewModel() { + class Factory( + private val key: RouteB, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return RouteBViewModel(key) as T + } + } +} +``` + +# Passing Arguments to ViewModels (Koin) + +This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using Koin for dependency injection. + +## How it works + +1. A Koin module is defined that provides the `ViewModel`. +2. The `koinViewModel` composable function is used to get the `ViewModel` instance. +3. The navigation key is passed to the `ViewModel`'s constructor using `parametersOf(key)`. This makes the navigation key available to the `ViewModel`. + +**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/koin) + +``` +package com.example.nav3recipes.passingarguments.viewmodels.koin + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import org.koin.compose.KoinApplication +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.core.parameter.parametersOf +import org.koin.dsl.koinConfiguration +import org.koin.dsl.module + +data object RouteA +data class RouteB(val id: String) + +class KoinViewModelsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = remember { mutableStateListOf(RouteA) } + + // Koin Compose Entry point + KoinApplication( + configuration = koinConfiguration { + modules(appModule) + } + ) { + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + + // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) + // we also need to add the default `NavEntryDecorator`s as well. These provide + // extra information to the entry's content to enable it to display correctly + // and save its state. + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + LazyColumn { + items(10) { i -> + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("$i")) + }) { + Text("$i") + } + } + } + } + } + entry { key -> + val viewModel = koinViewModel { + parametersOf(key) + } + ScreenB(viewModel = viewModel) + } + } + ) + } + } + } +} + +// Local Koin Module +private val appModule = module { + viewModelOf(::RouteBViewModel) +} + +@Composable +fun ScreenB(viewModel: RouteBViewModel) { + ContentBlue("Route id: ${viewModel.navKey.id} ") +} + +class RouteBViewModel(val navKey: RouteB) : ViewModel() +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md new file mode 100644 index 0000000..a61736f --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md @@ -0,0 +1,272 @@ +# Returning a Result (Event-Based) + +This recipe demonstrates how to return a result from one screen to a previous screen using an event-based approach. + +## How it works + +This example uses a `ResultEventBus` to facilitate communication between the screens. + +1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. +2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. +3. **Sending the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back as a one-time event. +4. **Receiving the result** : The screen that needs the result uses a `ResultEffect` composable to listen for results of a specific type. When a result is received, the effect's lambda is triggered. + +This approach is useful for results that are transient and should be handled as one-time events. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/results/event) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { + var person by mutableStateOf(null) +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +data object Home : NavKey + +@Serializable +class PersonDetailsForm : NavKey +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Person(val name: String, val favoriteColor: String) : Parcelable +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen + +@Composable +fun HomeScreen( + person: Person?, + onNext: () -> Unit +) { + ContentBlue("Hello ${person?.name ?: "unknown person"}") { + + if (person != null) { + Text("Your favorite color is ${person.favoriteColor}") + } + + Spacer(Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onNext)) { + Text("Tell us about yourself") + } + } +} + +@Composable +fun PersonDetailsScreen( + onSubmit: (Person) -> Unit +) { + ContentGreen("About you") { + + val nameTextState = rememberTextFieldState() + OutlinedTextField( + state = nameTextState, + label = { Text("Please enter your name") } + ) + + val favoriteColorTextState = rememberTextFieldState() + OutlinedTextField( + state = favoriteColorTextState, + label = { Text("Please enter your favorite color") } + ) + + Button( + onClick = dropUnlessResumed { + val person = Person( + name = nameTextState.text.toString(), + favoriteColor = favoriteColorTextState.text.toString() + ) + onSubmit(person) + }, + enabled = nameTextState.text.isNotBlank() && + favoriteColorTextState.text.isNotBlank() + ) { + Text("Submit") + } + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.event + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.result.LocalResultEventBus +import androidx.navigation3.runtime.result.ResultEffect +import androidx.navigation3.runtime.result.ResultEventBus +import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.results.common.Home +import com.example.nav3recipes.results.common.HomeScreen +import com.example.nav3recipes.results.common.HomeViewModel +import com.example.nav3recipes.results.common.Person +import com.example.nav3recipes.results.common.PersonDetailsForm +import com.example.nav3recipes.results.common.PersonDetailsScreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +class ResultEventActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + Scaffold { paddingValues -> + + val backStack = rememberNavBackStack(Home) + + NavDisplay( + backStack = backStack, + modifier = Modifier.padding(paddingValues), + onBack = { backStack.removeLastOrNull() }, + entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), + entryProvider = entryProvider { + entry { + val viewModel = viewModel(key = Home.toString()) + ResultEffect { person -> + viewModel.person = person + } + + val person = viewModel.person + HomeScreen( + person = person, + onNext = { backStack.add(PersonDetailsForm()) } + ) + } + entry { + val resultBus = LocalResultEventBus.current + PersonDetailsScreen( + onSubmit = { person -> + resultBus.sendResult(result = person) + backStack.removeLastOrNull() + } + ) + } + } + ) + } + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md new file mode 100644 index 0000000..71c2ccf --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md @@ -0,0 +1,266 @@ +# Returning a Result (State-Based) + +This recipe demonstrates how to return a result from one screen to a previous screen using a state-based approach. + +## How it works + +This example uses a `ResultEventBus` to manage the result as state. + +1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. +2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. +3. **Setting the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back. +4. **Observing the result** : The screen that needs the result calls `resultBus.conflateAsState()` to get a `State` object representing the result. The UI then observes this state and recomposes whenever the result changes. + +This approach is suitable when only the latest result is required. The result state does not survive configuration change or process death. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/results/state) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { + var person by mutableStateOf(null) +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +data object Home : NavKey + +@Serializable +class PersonDetailsForm : NavKey +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Person(val name: String, val favoriteColor: String) : Parcelable +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.common + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen + +@Composable +fun HomeScreen( + person: Person?, + onNext: () -> Unit +) { + ContentBlue("Hello ${person?.name ?: "unknown person"}") { + + if (person != null) { + Text("Your favorite color is ${person.favoriteColor}") + } + + Spacer(Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onNext)) { + Text("Tell us about yourself") + } + } +} + +@Composable +fun PersonDetailsScreen( + onSubmit: (Person) -> Unit +) { + ContentGreen("About you") { + + val nameTextState = rememberTextFieldState() + OutlinedTextField( + state = nameTextState, + label = { Text("Please enter your name") } + ) + + val favoriteColorTextState = rememberTextFieldState() + OutlinedTextField( + state = favoriteColorTextState, + label = { Text("Please enter your favorite color") } + ) + + Button( + onClick = dropUnlessResumed { + val person = Person( + name = nameTextState.text.toString(), + favoriteColor = favoriteColorTextState.text.toString() + ) + onSubmit(person) + }, + enabled = nameTextState.text.isNotBlank() && + favoriteColorTextState.text.isNotBlank() + ) { + Text("Submit") + } + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.results.state + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.result.LocalResultEventBus +import androidx.navigation3.runtime.result.ResultEffect +import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.results.common.Home +import com.example.nav3recipes.results.common.HomeScreen +import com.example.nav3recipes.results.common.HomeViewModel +import com.example.nav3recipes.results.common.Person +import com.example.nav3recipes.results.common.PersonDetailsForm +import com.example.nav3recipes.results.common.PersonDetailsScreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +class ResultStateActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + Scaffold { paddingValues -> + val backStack = rememberNavBackStack(Home) + NavDisplay( + backStack = backStack, + modifier = Modifier.padding(paddingValues), + onBack = { backStack.removeLastOrNull() }, + entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), + entryProvider = entryProvider { + entry { + val resultState = LocalResultEventBus + .current + .conflateAsState(null) + val person = resultState.value + HomeScreen( + person = person, + onNext = { backStack.add(PersonDetailsForm()) } + ) + } + entry { + val resultBus = LocalResultEventBus.current + PersonDetailsScreen( + onSubmit = { person -> + resultBus.sendResult(result = person) + backStack.removeLastOrNull() + } + ) + } + } + ) + } + } + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md new file mode 100644 index 0000000..32063b3 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md @@ -0,0 +1,435 @@ +# List-Detail Scene Recipe + +This example shows how to create a list-detail layout using the Scenes API. + +A `ListDetailSceneStrategy` will return a `ListDetailScene` if: + +- the window width is over 600dp +- A `Detail` entry is the last item in the back stack +- A `List` entry is in the back stack + +The `ListDetailScene` provides a `CompositionLocal` named `LocalBackButtonVisibility` that can be used by the detail `NavEntry` to control whether it displays a back button. This is useful when the detail entry usually displays a back button but should not display it when being displayed in a `ListDetailScene`. See for more details on this use case. + +See `ListDetailScene.kt` for more implementation details. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/scenes/listdetail) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.scenes.listdetail + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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 + +/** + * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. + * + */ +data class ListDetailScene( + override val key: Any, + override val previousEntries: List>, + val listEntry: NavEntry, + val detailEntry: NavEntry, +) : Scene { + override val entries: List> = listOf(listEntry, detailEntry) + override val content: @Composable (() -> Unit) = { + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.weight(0.4f)) { + listEntry.Content() + } + + // Let the detail entry know not to display a back button. + CompositionLocalProvider(LocalBackButtonVisibility provides false) { + Column(modifier = Modifier.weight(0.6f)) { + AnimatedContent( + targetState = detailEntry, + contentKey = { entry -> entry.contentKey }, + transitionSpec = { + slideInHorizontally( + initialOffsetX = { it } + ) togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + } + ) { entry -> + entry.Content() + } + } + } + } + } + + companion object { + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * in the list pane of a [ListDetailScene]. + */ + 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() = metadata { + put(DetailKey, true) + } + } + + object ListKey : NavMetadataKey + object DetailKey : NavMetadataKey +} + +/** + * This `CompositionLocal` can be used by a detail `NavEntry` to decide whether to display + * 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 } + +@Composable +fun rememberListDetailSceneStrategy(): ListDetailSceneStrategy { + val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass + + return remember(windowSizeClass) { + ListDetailSceneStrategy(windowSizeClass) + } +} + + +/** + * A [SceneStrategy] that returns a [ListDetailScene] if: + * + * - the window width is over 600dp + * - A `Detail` entry is the last item in the back stack + * - A `List` entry is in the back stack + * + * Notably, when the detail entry changes the scene's key does not change. This allows the scene, + * rather than the NavDisplay, to handle animations when the detail entry changes. + */ +class ListDetailSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + + if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { + return null + } + + val detailEntry = + 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 + // having NavDisplay animate the whole scene out when the selected detail item changes. + val sceneKey = listEntry.contentKey + + return ListDetailScene( + key = sceneKey, + previousEntries = entries.dropLast(1), + listEntry = listEntry, + detailEntry = detailEntry + ) + } +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.scenes.listdetail + +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 +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +/** + * This example shows how to create a list-detail layout using the Scenes API. + * + * A `ListDetailScene` will render content in two panes if: + * + * - the window width is over 600dp + * - A `Detail` entry is the last item in the back stack + * - A `List` entry is in the back stack + * + * @see `ListDetailScene` + */ +@Serializable +data object ConversationList : NavKey + +@Serializable +data class ConversationDetail( + val id: Int, + val colorId: Int +) : NavKey + +@Serializable +data object Profile : NavKey + +class ListDetailActivity : ComponentActivity() { + + @OptIn(ExperimentalSharedTransitionApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + + Scaffold { paddingValues -> + + val backStack = rememberNavBackStack(ConversationList) + val listDetailStrategy = rememberListDetailSceneStrategy() + + SharedTransitionLayout { + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(listDetailStrategy), + sharedTransitionScope = this, + modifier = Modifier.padding(paddingValues), + entryProvider = entryProvider { + entry( + metadata = ListDetailScene.listPane() + ) { + ConversationListScreen( + onConversationClicked = { detailRoute -> + backStack.addDetail(detailRoute) + } + ) + } + entry( + metadata = ListDetailScene.detailPane() + ) { conversationDetail -> + ConversationDetailScreen( + conversationDetail = conversationDetail, + onBack = { backStack.removeLastOrNull() }, + onProfileClicked = { backStack.add(Profile) } + ) + } + entry { + ProfileScreen() + } + } + ) + } + } + } + } +} + +private fun NavBackStack.addDetail(detailRoute: ConversationDetail) { + + // Remove any existing detail routes before adding this detail route. + // In certain scenarios, such as when multiple detail panes can be shown at once, it may + // be desirable to keep existing detail routes on the back stack. + removeIf { it is ConversationDetail } + add(detailRoute) +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.scenes.listdetail + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.ui.theme.colors + +@Composable +fun ConversationListScreen( + onConversationClicked: (ConversationDetail) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), + ) { + items(10) { index -> + val conversationId = index + 1 + val conversationDetail = ConversationDetail( + id = conversationId, + colorId = conversationId % colors.size + ) + val backgroundColor = colors[conversationDetail.colorId] + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = dropUnlessResumed { + onConversationClicked(conversationDetail) + }), + headlineContent = { + Text( + text = "Conversation $conversationId", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + colors = ListItemDefaults.colors( + containerColor = backgroundColor // Set container color directly + ) + ) + } + } +} + +@Composable +fun ConversationDetailScreen( + conversationDetail: ConversationDetail, + onBack: () -> Unit, + onProfileClicked: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(colors[conversationDetail.colorId]) + .padding(16.dp) + ) { + if (LocalBackButtonVisibility.current) { + IconButton( + onClick = onBack, + modifier = Modifier.align(Alignment.TopStart) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Conversation Detail Screen: ${conversationDetail.id}", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onProfileClicked)) { + Text("View Profile") + } + } + } +} + +@Composable +fun ProfileScreen() { + val profileColor = MaterialTheme.colorScheme.surfaceVariant + Column( + modifier = Modifier + .fillMaxSize() + .background(profileColor) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Profile Screen", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md new file mode 100644 index 0000000..fed0bf9 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md @@ -0,0 +1,244 @@ +# Two-Pane Scene Recipe + +This example shows how to create a two pane layout using the Scenes API. + +A `TwoPaneSceneStrategy` will return a `TwoPaneScene` if: + +- the window width is over 600dp +- the last two nav entries on the back stack have indicated that they support being displayed in a `TwoPaneScene` in their metadata. + +See `TwoPaneScene.kt` for more implementation details. +[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/scenes/twopane) + +``` +package com.example.nav3recipes.scenes.twopane + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +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. + */ +data class TwoPaneScene( + override val key: Any, + override val previousEntries: List>, + val firstEntry: NavEntry, + val secondEntry: NavEntry +) : Scene { + override val entries: List> = listOf(firstEntry, secondEntry) + override val content: @Composable (() -> Unit) = { + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.weight(0.5f)) { + firstEntry.Content() + } + Column(modifier = Modifier.weight(0.5f)) { + secondEntry.Content() + } + } + } + + companion object { + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * in a two-pane layout. + */ + fun twoPane() = metadata { + put(TwoPaneKey, true) + } + } + + object TwoPaneKey : NavMetadataKey +} + +@Composable +fun rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy { + val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass + + return remember(windowSizeClass) { + TwoPaneSceneStrategy(windowSizeClass) + } +} + + +// --- TwoPaneSceneStrategy --- +/** + * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough + * and the top two back stack entries declare support for two-pane display. + */ +class TwoPaneSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + + // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. + // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). + if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { + return null + } + + val lastTwoEntries = entries.takeLast(2) + + // 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.contains(TwoPaneScene.TwoPaneKey) } + ) { + val firstEntry = lastTwoEntries.first() + val secondEntry = lastTwoEntries.last() + + // The scene key must uniquely represent the state of the scene. + // A Pair of the first and second entry keys ensures uniqueness. + val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) + + TwoPaneScene( + key = sceneKey, + // Where we go back to is a UX decision. In this case, we only remove the top + // entry from the back stack, despite displaying two entries in this scene. + // This is because in this app we only ever add one entry to the + // back stack at a time. It would therefore be confusing to the user to add one + // when navigating forward, but remove two when navigating back. + previousEntries = entries.dropLast(1), + firstEntry = firstEntry, + secondEntry = secondEntry + ) + + } else { + null + } + } + + +} +``` + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.scenes.twopane + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBase +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import com.example.nav3recipes.ui.theme.colors +import kotlinx.serialization.Serializable + +@Serializable +private object Home : NavKey + +@Serializable +private data class Product(val id: Int) : NavKey + +@Serializable +private data object Profile : NavKey + +class TwoPaneActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + val backStack = rememberNavBackStack(Home) + val twoPaneStrategy = rememberTwoPaneSceneStrategy() + + SharedTransitionLayout { + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(twoPaneStrategy), + sharedTransitionScope = this, + entryProvider = entryProvider { + entry( + metadata = TwoPaneScene.twoPane() + ) { + ContentRed("Welcome to Nav3") { + Button(onClick = { backStack.addProductRoute(1) }) { + Text("View the first product") + } + } + } + entry( + 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 { + ContentGreen("Profile (single pane only)") + } + } + ) + } + } + } +} + +private fun NavBackStack.addProductRoute(productId: Int) { + val productRoute = + Product(productId) + // Avoid adding the same product route to the back stack twice. + if (!contains(productRoute)) { + add(productRoute) + } +} +``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md b/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md new file mode 100644 index 0000000..c570000 --- /dev/null +++ b/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md @@ -0,0 +1,129 @@ +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 { + HomeScreen(...) + } + composable { 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() + 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( + typeMap = mapOf(typeOf() 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()` to check the current destination in a type-safe manner during UI tests \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/SKILL.md b/.agents/skills/r8-analyzer/SKILL.md new file mode 100644 index 0000000..7ad9e5e --- /dev/null +++ b/.agents/skills/r8-analyzer/SKILL.md @@ -0,0 +1,62 @@ +--- +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 + last-updated: '2026-06-09' + keywords: + - R8 + - proguard + - keep rules + - app size + - optimization +--- + +## Step 1. Setup and configuration check + +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. +- Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. +- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement + performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) +- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. + +## Step 2. Analysis path selection + +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and + `libs.versions.toml` to get the R8 version + +- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. + +- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. + +### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) + +- **Check requirements** : Python and `protobuf` package are mandatory. +- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. +- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. + +### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) + +*(Use ONLY if quantitative data generation is not possible)* + +- **Manual evaluation** : Inspect `proguard-rules.pro`. +- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. +- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). +- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. + +## Step 3. Report generation + +- **Format** : Follow [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) strictly. +- **Input**: Extract metrics (Scores, Impacts, Example Classes) directly from generated file analysis.txt if using Path A, or from manual findings if using Path B. +- **Output** : Output ONLY the raw Markdown report in the chat. Do NOT output conversational filler (for example, "Here is your report..."). Do NOT provide recommendations, next steps, or any other text outside of the sections defined in [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) Do NOT mention the path used for analysis of the configuration + +## Constraints + +- **Strict output limit**: The final output MUST strictly be the Markdown report and nothing else. +- **No code changes**: Research and suggest only; Do not modify files. +- **No redundancy**: Do not explain R8 benefits or reference skill internal files in the report. +- **Focus**: Omit sections (for example, Subsumed Rules, Configuration) if no issues or items are found. diff --git a/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md new file mode 100644 index 0000000..4039dcf --- /dev/null +++ b/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -0,0 +1,287 @@ +On each step, keep the user informed of the progress by displaying the output. + +### 1. Requirements + +- R8 Version: 9.3.7-dev or later + +### 2. Generate proto + +The report and files must be generated at `{project_root}/tmp/r8analysis`. If +the folder is not present, create it. For example: + + mkdir -p "$PWD/tmp/r8analysis" + +### 3. Remove existing files + +To make sure that this invocation doesn't source data from previous runs, remove +the intermediate files `keepruleradius.json` and `analysis_result.txt` and +remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example +bash commands: + + # Remove the intermediate JSON and the directory containing protobuf files + rm tmp/r8analysis/keepruleradius.json + rm tmp/r8analysis/*.pb + + # Copy the previous result to history before deleting the analysis + if [ -f tmp/r8analysis/analysis_result.txt ]; + then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && + rm tmp/r8analysis/analysis_result.txt; fi + +### 4. Generate the Configuration Analyzer report + +Run the R8 enabled build with the system property +"-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to +generate Configuration Analyzer report + + ./gradlew assembleRelease \ + -Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis + +### 5. Convert to JSON + +To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into +json, run the following script. The json must be generated in +`{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is +in the same directory. + + import sys + import os + import glob + from google.protobuf import json_format + import keep_radius_pb2 + + def convert_pb_to_json(input_pb_path, output_json_path): + bundle = keep_radius_pb2.BlastRadiusContainer() + + try: + with open(input_pb_path, "rb") as pb_file: + binary_data = pb_file.read() + except Exception as e: + print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) + return False + + try: + bundle.ParseFromString(binary_data) + except Exception as e: + print(f"Error parsing protobuf: {e}", file=sys.stderr) + return False + + try: + json_string = json_format.MessageToJson( + bundle, + always_print_fields_with_no_presence=True, + preserving_proto_field_name=True, + indent=4 + ) + with open(output_json_path, "w", encoding="utf-8") as json_file: + json_file.write(json_string) + return True + except Exception as e: + print(f"Error writing JSON: {e}", file=sys.stderr) + return False + + if __name__ == "__main__": + input_pb = sys.argv[1] if len(sys.argv) > 1 else None + if not input_pb: + pb_files = glob.glob("tmp/r8analysis/*.pb") + if not pb_files: + print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) + sys.exit(1) + input_pb = sorted(pb_files)[-1] # Use the most recent one + output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" + if not convert_pb_to_json(input_pb, output_json): + sys.exit(1) + +### 6. Analyze + +Run the following analysis script on the generated JSON to get the impact of the +keep rules and sort it. + + import json, sys + + def analyze(path): + try: + with open(path, 'r') as f: + d = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Build reference map + c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} + r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} + + tot_opt = tot_obf = tot_shr = tot_items = 0 + + # Tally constraints across all kept items + for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): + for i in d.get(tbl, []): + tot_items += 1 + kb = i.get('kept_by', []) + if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 + if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 + if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 + + # Find denominator + bi = d.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else tot_items + + # Check for globals + globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] + def score(cnt, flag): + if any(flag in src for src in globals_src): return 0.0 + return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) + + result = [ + f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", + f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", + f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" + ] + for line in result: + print(line) + with open("tmp/r8analysis/analysis_result.txt", "w") as f: + f.write("\n".join(result)) + + if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" + analyze(path) + +Outputs `analysis_result.txt` containing scores and rule impacts. + +### 7. Report impactful rules + +Identify the keep rules with the highest impact and the subsumed rules using the +following script. + + import json, sys + + def report(path): + try: + with open(path, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Calculate denominator for percentage + bi = data.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) + + processed = [] + for r in data.get('keep_rule_blast_radius_table', []): + br = r.get('blast_radius', {}) + c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) + impact = c + f + m + if impact == 0: + continue + impact_pct = (impact / denom * 100) if denom > 0 else 0.0 + processed.append({ + 'id': r.get('id'), + 'source': r.get('source'), + 'impact': impact, + 'impact_pct': f"{impact_pct:.2f}%", + 'classes': c, + 'fields': f, + 'methods': m, + 'subsumed_by': br.get('subsumed_by', []) + }) + + processed.sort(key=lambda x: x['impact'], reverse=True) + + # Output JSON for the agent to fetch and process + print(json.dumps({ + "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], + "subsumed": [r for r in processed if r['subsumed_by']] + }, indent=2)) + + if __name__ == "__main__": + report("tmp/r8analysis/keepruleradius.json") + +Add this data to the `analysis_result.txt` with the top impactful rules and +subsumed rules. + +### 8. Compare with previous report + +If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script +to compare the previous run. Use this to compare with the current values + +### 9. Remove generated files + +After the final report and analysis results are generated, remove the +intermediate files `keepruleradius.json` and `analysis_result.txt` and remove +the proto files in "{project_root}/tmp/r8analysis" folder + + rm tmp/r8analysis/keepruleradius.json + rm tmp/r8analysis/*.pb + +### 10. Protobuf Python bindings + +The following script `keep_radius_pb2.py` is required by the conversion script +in Step 5. + + from google.protobuf import descriptor as _descriptor + from google.protobuf import descriptor_pool as _descriptor_pool + from google.protobuf import runtime_version as _runtime_version + from google.protobuf import symbol_database as _symbol_database + from google.protobuf.internal import builder as _builder + _runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 4, + '', + 'keep_radius.proto' + ) + # @@protoc_insertion_point(imports) + + _sym_db = _symbol_database.Default() + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') + + _globals = globals() + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) + _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) + if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' + _globals['_KEEPRULETAG']._serialized_start = 3332 + _globals['_KEEPRULETAG']._serialized_end = 3363 + _globals['_KEEPCONSTRAINT']._serialized_start = 3365 + _globals['_KEEPCONSTRAINT']._serialized_end = 3437 + _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 + _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 + _globals['_BLASTRADIUS']._serialized_start = 351 + _globals['_BLASTRADIUS']._serialized_end = 470 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 + _globals['_KEEPCONSTRAINTS']._serialized_start = 601 + _globals['_KEEPCONSTRAINTS']._serialized_end = 707 + _globals['_KEPTCLASSINFO']._serialized_start = 709 + _globals['_KEPTCLASSINFO']._serialized_end = 805 + _globals['_KEPTFIELDINFO']._serialized_start = 807 + _globals['_KEPTFIELDINFO']._serialized_end = 903 + _globals['_KEPTMETHODINFO']._serialized_start = 905 + _globals['_KEPTMETHODINFO']._serialized_end = 1003 + _globals['_FIELDREFERENCE']._serialized_start = 1005 + _globals['_FIELDREFERENCE']._serialized_end = 1102 + _globals['_METHODREFERENCE']._serialized_start = 1104 + _globals['_METHODREFERENCE']._serialized_end = 1203 + _globals['_PROTOREFERENCE']._serialized_start = 1205 + _globals['_PROTOREFERENCE']._serialized_end = 1280 + _globals['_TYPEREFERENCE']._serialized_start = 1282 + _globals['_TYPEREFERENCE']._serialized_end = 1334 + _globals['_TYPEREFERENCELIST']._serialized_start = 1336 + _globals['_TYPEREFERENCELIST']._serialized_end = 1395 + _globals['_FILEORIGIN']._serialized_start = 1398 + _globals['_FILEORIGIN']._serialized_end = 1557 + _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 + _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 + _globals['_TEXTFILEORIGIN']._serialized_start = 1634 + _globals['_TEXTFILEORIGIN']._serialized_end = 1718 + _globals['_MAVENCOORDINATE']._serialized_start = 1720 + _globals['_MAVENCOORDINATE']._serialized_end = 1805 + _globals['_BUILDINFO']._serialized_start = 1808 + _globals['_BUILDINFO']._serialized_end = 1962 + _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 + _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 + # @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/CONFIGURATION.md b/.agents/skills/r8-analyzer/references/CONFIGURATION.md new file mode 100644 index 0000000..ae04947 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/CONFIGURATION.md @@ -0,0 +1,44 @@ +To achieve maximum utilization of R8, the codebase must be configured correctly +depending on the build script language (Kotlin DSL versus Groovy DSL). + +## 1. App Modules (`com.android.application`) + +The app's `build.gradle` or `build.gradle.kts` file must 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** : Explicitly enable 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 \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md b/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md new file mode 100644 index 0000000..2e07317 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md @@ -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 { ; } + -keepclasseswithmembers class * { native ; } \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md b/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md new file mode 100644 index 0000000..0021890 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md @@ -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.* ; }` + +### 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. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md b/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md new file mode 100644 index 0000000..8f23679 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md @@ -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 register(clazz: Class) { } + // 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 { + (); + }` + +### 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. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md b/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md new file mode 100644 index 0000000..629c777 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md @@ -0,0 +1,51 @@ +## 1. Configuration + +*(Optional section for the report, omit if no relevant findings are present.)* + +- **AGP Version**: \[Current\] -\> Upgrade to 9.0. +- **Full Mode** : Not enabled. Remove `android.enableR8.fullMode=false` from `gradle.properties`. + +## 2. Global disable rules + +*(Optional section for the report, omit if no relevant findings are present.)* + +- \[Rule\]: Disables R8 globally. **Action**: Remove. + +If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, +mention in this section + +## 3. Optimization summary + +- **Optimization score**: \[X\]% code is available for R8 optimizations (e.g., inlining, merging). \[100-X\]% of codebase can't be optimized by R8. +- **Shrinking score**: \[X\]% of code will be optimized by R8 by removing unused classes, fields and methods. \[100-X\]% of codebase contains redundant classes, fields and methods that can't be removed by R8. +- **Obfuscation score**: \[X\]% of the codebase is available for R8 to obfuscate. + +Increasing these scores increases the codebase available to R8 for +optimizations. + +## 4. Keep rules evaluation + +### \[Rule text\] + +- **Keeps**: \[X\] items or \[X\] % of the codebase from optimization. Classes: \[X\], Fields: \[X\], Methods: \[X\] are prevented from optimization due to this keep rule +- **Kept items**: \[Class1\], \[Class2\] +- **Action** : **Remove** (Library bundles rules) OR **Refine** (Too broad, use \[Surgical Rule\]). + +## 5. Subsumed keep rules + +*(Optional section for the report, omit if no relevant findings are present.)* + +### \[Redundant rules\] + +- **Subsumed By**: \[Broader Rule\] +- **Action** : **Remove**. + +## 6. Historical analysis summary + +*(Only include this section if a previous report existed. Summarize the changes +in optimization scores here to track progress. For example:)* The previous app +had scores: Optimization (XX%), Obfuscation (XX%), and Shrinking (XX%). The +current app has scores: Optimization (YY%), Obfuscation (YY%), and Shrinking +(YY%). +**Change**: Optimization improved by ZZ%, Obfuscation improved by ZZ%, and +Shrinking improved by ZZ%. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md b/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md new file mode 100644 index 0000000..69c4295 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md @@ -0,0 +1,198 @@ +For the best user experience, you should optimize your app to make it as small +and fast as possible. Our app optimizer, called R8, streamlines your app by +removing unused code and resources, rewriting code to optimize runtime +performance, and more. To your users, this means: + +- Faster startup time +- Reduced memory usage +- Improved rendering and runtime performance +- Fewer [ANRs](https://developer.android.com/topic/performance/anrs/keep-your-app-responsive) + +> [!IMPORTANT] +> **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the +> optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization). + +> [!IMPORTANT] +> **Important:** We released an agent skill that you can use to improve your app performance with R8. Try out the skill from the [Android skills repository](https://github.com/android/skills). + +## R8 optimization overview + +R8 uses a multi-phase process to optimize your app for size and speed. Key +operations include the following: + +- **Code shrinking (also known as tree shaking)** : R8 identifies and removes + unreachable code from your application and its library dependencies. By + analyzing the entry points of your app (such as `Activities` or `Services` + defined in the manifest), R8 builds a graph of referenced code and removes + anything that remains unreferenced. + +- **Logical optimizations**: R8 rewrites your code to improve execution + efficiency and reduce overhead. Key techniques include: + + - **Method inlining**: R8 replaces a method call site with the actual body + of the called method. This eliminates the overhead of a function call + and lets R8 conduct further optimizations. + + - **Class merging**: R8 combines sets of classes and interfaces into a + single class. This reduces the number of classes in the app, lowering + memory pressure and improving startup speed. + +- **Obfuscation (also known as minification)** : To reduce the size of the DEX + file, R8 shortens the names of classes, fields, and methods (for example, + `com.example.MyActivity` could become `a.b.a`). + +Since 8.12.0 version of Android Gradle Plugin (AGP), R8 also optimizes resources +as part of its optimization phases. For more information, see [Optimized +resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking). + +## Enable optimization + +To enable app optimization, set `isMinifyEnabled = true` (for code optimization) +and `isShrinkResources = true` (for resource optimization) in your [release +build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script as shown in the following code. We recommend +that you always enable both settings. We also recommend enabling app +optimization only in the final version of your app that you test before +publishing---usually your release build---because the optimizations increase the +build time of your project and can make debugging harder due to the way it +modifies code. + +### Kotlin + +```kotlin +android { + buildTypes { + release { + + // Enables code-related app optimization. + isMinifyEnabled = true + + // Enables resource shrinking. + isShrinkResources = true + + proguardFiles( + // Default file with automatically generated optimization rules. + getDefaultProguardFile("proguard-android-optimize.txt"), + + ... + ) + ... + } + } + ... +} +``` + +### Groovy + +```groovy +android { + buildTypes { + release { + + // Enables code-related app optimization. + minifyEnabled = true + + // Enables resource shrinking. + shrinkResources = true + + // Default file with automatically generated optimization rules. + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') + + ... + } + } +} +``` + +## Improve R8 optimization + +The performance benefits of R8 are directly correlated to how much of your +codebase R8 is able to optimize. To get the maximum benefits out of R8, follow +best practices: + +- Enable R8 in [full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) +- Enable [obfuscation, optimization, and shrinking](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally) +- Enable resource shrinking and [optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) +- [Refine keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-best-practices) to allow maximum optimization of classes, fields and methods. + +To help you refine keep rules, use the [R8 Configuration Analyzer](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). + +The R8 Configuration Analyzer lets you do the following: + +- Track and improve the overall R8 configuration quality by monitoring the metrics provided by the R8 Configuration Analyzer report. +- Find the broadest keep rules - those which prevent the most optimization +- and understand what optimization they prevent to refine them. + +The R8 Configuration Analyzer is available in AGP version 9.3.0-alpha05 or from +R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). + +## Optimize resource shrinking for even smaller apps + +The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource +shrinking, which aims to integrate resource and code optimization to create even +smaller and faster apps. + +Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2) +generated keep rules that effectively treating resource shrinking separately +from code, often retaining inaccessible code or resources that referenced each +other. + +With optimized resource shrinking, resources are considered like a part of +program code, forming the reference graph. When a collection of code or +resources is not referenced, it is not protected by a keep rule, and can be +removed. + +### Enable optimized resource shrinking + +To enable the new optimized resource shrinking pipeline for AGP 8.12 or 8.13, +add the following to your project's `gradle.properties` file: + + android.r8.optimizedResourceShrinking=true + +If you are using AGP 9.0.0 or a newer version, you don't need to set +`android.r8.optimizedResourceShrinking=true`. Optimized resource shrinking is +automatically applied when `isShrinkResources = true` is enabled in your build +configuration. + +## Verify and configure R8 optimization settings + +To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the +following line from your project's `gradle.properties` file, if it exists: + + android.enableR8.fullMode=false # Remove this line from your codebase. + +Note that enabling app optimization makes stack traces difficult to understand, +especially if R8 renames class or method names. To get stack traces that +correctly correspond to your source code, see [Recover the original stack +trace](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization#recover-original-stack-trace). + +If R8 is enabled, you should also [create Startup Profiles](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations) for even better +startup performance. + +If you enable app optimization and it causes errors, here are some strategies to +fix them: + +- [Add keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to keep some code untouched. +- [Adopt optimizations incrementally](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally). +- Update your code to [use libraries that are better suited for + optimization](https://developer.android.com/topic/performance/app-optimization/choose-libraries-wisely). + +> [!CAUTION] +> **Caution:** Tools that replace or modify R8's output can negatively impact runtime performance. R8 is careful about including and testing many optimizations at the code level, in [DEX layout](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations), and in correctly producing Baseline Profiles - other tools producing or modifying DEX files can break these optimizations, or otherwise regress performance. + +If you are interested in optimizing your build speed, see [Configure how R8 +runs](https://developer.android.com/build/r8-execution-profiles) for information on how to configure R8 based on your environment. + +## AGP and R8 version behavior changes + +The following table outlines the key features introduced in various versions of +the Android Gradle Plugin (AGP) and the R8 compiler. + +| AGP version | Features introduced | +|---|---| +| 9.1 | **Classes repackaged by default:** R8 repackages classes (moving them to the unnamed package, at the top level) to compact DEX further, eliminating the need to specify `-repackageclasses` option. For information about how this works and how to opt out, see [global options](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). | +| 9.0 | **Optimized resource shrinking:** Enabled by default (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline, leading to smaller, faster apps. By optimizing both code and resource references simultaneously, it identifies and removes resources referenced exclusively from unused code. This is a significant improvement over the previous separate optimization processes. This is especially useful for apps that share substantial resources and code across different form factor verticals, with measured improvements of over 50% in app size. The resulting size reduction leads to smaller downloads, faster installations, and a better user experience with faster startup, improved rendering, and fewer ANRs. **Library rule filtering:** Support for global options (for example, `-dontobfuscate`) in library consumer rules has been dropped, and apps will filter them out. For more information, see [Add global options](https://developer.android.com/topic/performance/app-optimization/global-options). **Kotlin null checks:** Optimized by default (controlled using `-processkotlinnullchecks`). This version also introduced significant improvements in build speed. For more information, see [Global options for additional optimization](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). **Optimize specific packages:** You can use `packageScope` to optimize specific packages. This is in experimental support. For more information, see [Optimize specified packages with `packageScope`](https://developer.android.com/topic/performance/app-optimization/optimize-specified-packages). **Optimized by default:** Support for `getDefaultProguardFile("proguard-android.txt")` has been dropped, because it includes `-dontoptimize`, which should be avoided. Instead, use `"proguard-android-optimize.txt"`. If you need to globally disable optimization in your app, [add the flag manually to a proguard file](https://developer.android.com/topic/performance/app-optimization/global-options#global-options-2). | +| 8.12 | **Optimized resource shrinking:** Initial support added (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline. You must manually enable it in this version of AGP. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). | +| 8.6 | **Improved retracing:** Includes filename and line number retracing by default for all `minSdk` levels (previously required `minSdk` 26+ in version 8.2). Updating R8 helps ensure that stack traces from obfuscated builds are readily and clearly readable. This version improves how line numbers and source files are mapped, making it easier for tools like the Android Studio Logcat to automatically retrace crashes to the original source code. | +| 8.0 | **Full mode by default:** [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) provides significantly more powerful optimization. It is enabled by default. You can opt out using `android.enableR8.fullMode=false`. | +| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. | \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md b/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md new file mode 100644 index 0000000..2b5b253 --- /dev/null +++ b/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md @@ -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`, 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)` | \ No newline at end of file diff --git a/.agents/skills/styles/SKILL.md b/.agents/skills/styles/SKILL.md new file mode 100644 index 0000000..daf7256 --- /dev/null +++ b/.agents/skills/styles/SKILL.md @@ -0,0 +1,226 @@ +--- +name: styles +description: Use this skill to integrate the Jetpack Compose Styles API into an Android + project. This skill guides you through upgrading dependencies, setting up component + themes, making custom components styleable, and migrating existing layout properties + to use unified styles. Migrate custom design system components, replace hard coded + parameters with Style attributes, and use Modifier.styleable for interaction states. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-02' + keywords: + - Jetpack Compose + - Styles + - Theming with Styles + - Migrate to Styles + - Modifier.styleable +--- + +## Limitations + +- Warn the user that this skill is EXPERIMENTAL and requires updating to alpha version of Compose and opting in to the Experimental APIs. +- This skill only supports custom UI components and custom themes. +- This skill does not support Material Design component Styles. + +## Prerequisites + +### 1. Upgrade dependencies + +- The project must use `compileSdk` version 37 or higher. +- The project must use `androidx.compose.foundation:foundation` version `1.12.0-alpha01` or higher. +- Alternatively, the project must use Compose BOM version `2026.04.01` or higher. +- The API requires this exact package: `import + androidx.compose.foundation.style.Style` + +### 2. Configure compiler options to enable experimental API + +You must opt-in to the experimental API at the project level. Add the following +block to your module's `build.gradle.kts`: + + kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget("17") + freeCompilerArgs.add("-opt-in=androidx.compose.foundation.style.ExperimentalFoundationStyleApi") + } + } + +## Core workflows and guides + +Refer to the official documentation to complete specific development tasks: + +- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals + Guide](references/android/develop/ui/compose/styles/fundamentals.md). +- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling + Guide](references/android/develop/ui/compose/styles/state-animations.md). +- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers + Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md). +- Theme Level Integration: To connect style definitions with custom themes, follow [Theming with Styles](references/android/develop/ui/compose/styles/theming.md) and [Custom Themes in Compose](references/android/develop/ui/compose/designsystems/custom.md). + +## Step-by-Step Migration Workflow + +### Step 1: Analyze theme structure + +1. Locate your central theme file (such as `Theme.kt`). +2. Identify design tokens. Note references for colors, typography, and shapes (for example, `LocalColorScheme`, `LocalTypography`, or `LocalShapes`). +3. If the project lacks Jetpack Compose dependencies, stop. Instruct the user to migrate to Jetpack Compose first. +4. If the project imports `androidx.compose.material.MaterialTheme`, recommend migrating to Material 3 before proceeding. + +### Step 2: Establish `ComponentStyles` + +1. Create a new file named `ComponentStyles.kt` in your theme directory. +2. Define a top-level data class to hold your component styles, for example, the Jetsnack one is called `JetsnackStyles`: + + + ```kotlin + object ExampleComponentStyles { + val customButtonStyle: Style = { + + } + val customTextFieldStyle: Style = { + + } + } + ``` + +
+ +3. Expose this class through your custom theme with a static reference, don't + use `CompositionLocals` here as it's not required. + + + ```kotlin + @Immutable + class JetsnackTheme( + // other Design system properties + ) { + companion object { + val colors: CustomThemingWithStyles.JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + // ... + + // add helper static reference + val styles: ComponentStyles = ComponentStyles + } + } + ``` + +
+ +4. Provide extensions on `StyleScope` to reference theme tokens directly if + they are exposed using `CompositionLocals`. For example: + + + ```kotlin + val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + + val StyleScope.shapes: Shapes + get() = LocalJetsnackTheme.currentValue.shapes + ``` + +
+ +### Step 3: Migrate a component to Styles API + +For each custom component (for example, `CustomButton`), complete the following +sequence: + +1. **Establish a visual baseline (If an emulator is available):** + - **If you CANNOT run an Android emulator:** Skip this step entirely and proceed to Step 2. + - **If you CAN run an Android emulator:** Perform the following to capture a baseline screenshot: + - **Option A:** Locate and run an existing screenshot test for the component. + - **Option B (If no test exists):** Create a test using the project's existing testing framework, then run it. + - **Option C (If no framework exists):** Create a minimal screenshot test using UI Automator or Espresso, then run it. +2. **Remove individual styling parameters** : Remove styling parameters such as `backgroundColor`, `shape`, `textStyle`, and `contentPadding` from the signature - anything that `StyleScope` supports. +3. **Add the style parameter** : Add `style: Style = Style` to the function signature. Always ensure the default value is exactly `Style` (e.g., `style: + Style = Style`) and not a specific style default like `ChipStyleDefault` or any other value. +4. **Declare state tracking** : If the component is interactable, create a `MutableStyleState` using the interaction source. Update state fields (such as `isEnabled`) inside the Composable to track the state correctly. +5. **Apply styleable modifier** : Replace specific layout modifiers on the root element with `Modifier.styleable()`. +6. **Move defaults to ComponentStyles** : Move hardcoded values from the component definition to a dedicated `Style` instance in `ComponentStyles.kt`. +7. **Validate component:** Compare the baseline screenshot image taken at the start 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. + +#### Migration example + +Before Migration: + + +```kotlin +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = JetsnackTheme.colors.brandLight, + disabledBackgroundColor: Color = JetsnackTheme.colors.brandSecondary, + shape: Shape = JetsnackTheme.shapes.extraLarge, + textStyle: TextStyle = JetsnackTheme.typography.labelLarge, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .background(if (enabled) backgroundColor else disabledBackgroundColor, shape) + .defaultMinSize(58.dp, 40.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +``` + +
+ +After Migration: + + +```kotlin +// Exposed via ComponentStyles.kt +object ComponentStyles { + val buttonStyle = Style { + background(colors.brandLight) + shape(shapes.extraLarge) + minWidth(58.dp) + minHeight(40.dp) + textStyle(typography.labelLarge) + disabled { + background(colors.brandSecondary) + } + } +} + +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +``` + +
+ +### Step 4: Validate Changes + +1. Build the project. Verify that there are no compilation errors. +2. Run your module's screenshot tests. +3. Compare visual outputs of the whole app between the previous and updated components. Verify that no visual layout regressions occur. diff --git a/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md b/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md new file mode 100644 index 0000000..59dbb25 --- /dev/null +++ b/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md @@ -0,0 +1,459 @@ +While Material is our recommended design system and Jetpack Compose ships an +implementation of Material, you are not forced to use it. Material is built +entirely on public APIs, so it's possible to create your own design system in +the same manner. + +There are several approaches you might take: + +- [Extend `MaterialTheme`](https://developer.android.com/develop/ui/compose/designsystems/custom#extending-material) with additional theming values. +- [Replace one or more Material systems](https://developer.android.com/develop/ui/compose/designsystems/custom#replacing-systems) --- `Colors`, `Typography`, or `Shapes` --- with custom implementations while keeping the others. +- [Implement a fully custom design system](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom) to replace `MaterialTheme`. + +You may also want to continue using Material components with a custom design +system. It's possible to do this but there are things to keep in mind to suit +the approach you've taken. + +To learn more about the lower-level constructs and APIs used by `MaterialTheme` +and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide. + +## Extend Material Theming + +Compose Material closely models +[Material Theming](https://m3.material.io/) +to make it straightforward and type-safe to follow the Material guidelines. +However, it's possible to extend the color, typography, and shape sets with +additional values. The simplest approach is to add extension properties: + + +```kotlin +// Use with MaterialTheme.colorScheme.snackbarAction +val ColorScheme.snackbarAction: Color + @Composable + get() = if (isSystemInDarkTheme()) Red300 else Red700 + +// Use with MaterialTheme.typography.textFieldInput +val Typography.textFieldInput: TextStyle + get() = TextStyle(/* ... */) + +// Use with MaterialTheme.shapes.card +val Shapes.card: Shape + get() = RoundedCornerShape(size = 20.dp) +``` + +
+ +This provides consistency with `MaterialTheme` usage APIs. An example of this +defined by Compose itself is +[`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)), +which determines the surface color that should be used depending on the +elevation. + +> [!NOTE] +> **Note:** This approach is only recommended for straightforward theming value additions, or for values that are the same in different themes. If you have multiple themes, it's better to define a class with new properties instead. + +Another approach is to define an extended theme that "wraps" `MaterialTheme` and +its values. + +Suppose you want to add two additional colors --- `caution` and `onCaution`, a +yellow color used for actions that are semi-dangerous --- whilst keeping the +existing Material colors: + + +```kotlin +@Immutable +data class ExtendedColors( + val caution: Color, + val onCaution: Color +) + +val LocalExtendedColors = staticCompositionLocalOf { + ExtendedColors( + caution = Color.Unspecified, + onCaution = Color.Unspecified + ) +} + +@Composable +fun ExtendedTheme( + /* ... */ + content: @Composable () -> Unit +) { + val extendedColors = ExtendedColors( + caution = Color(0xFFFFCC02), + onCaution = Color(0xFF2C2D30) + ) + CompositionLocalProvider(LocalExtendedColors provides extendedColors) { + MaterialTheme( + /* colors = ..., typography = ..., shapes = ... */ + content = content + ) + } +} + +// Use with eg. ExtendedTheme.colors.caution +object ExtendedTheme { + val colors: ExtendedColors + @Composable + get() = LocalExtendedColors.current +} +``` + +
+ +This is similar to `MaterialTheme` usage APIs. It also supports multiple themes +as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`. + +### Use Material components + +When extending Material Theming, existing `MaterialTheme` values are maintained +and Material components still have reasonable defaults. + +If you want to use extended values in components, wrap them in your own +composable functions, directly setting the values you want to alter, and +exposing others as parameters to the containing composable: + + +```kotlin +@Composable +fun ExtendedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = ExtendedTheme.colors.caution, + contentColor = ExtendedTheme.colors.onCaution + /* Other colors use values from MaterialTheme */ + ), + onClick = onClick, + modifier = modifier, + content = content + ) +} +``` + +
+ +You would then replace usages of `Button` with `ExtendedButton` where +appropriate. + + +```kotlin +@Composable +fun ExtendedApp() { + ExtendedTheme { + /*...*/ + ExtendedButton(onClick = { /* ... */ }) { + /* ... */ + } + } +} +``` + +
+ +## Replace Material subsystems + +Instead of extending Material Theming, you may want to replace one or more +systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation, +while maintaining the others. + +Suppose you want to replace the type and shape systems while keeping the color +system: + + +```kotlin +@Immutable +data class ReplacementTypography( + val body: TextStyle, + val title: TextStyle +) + +@Immutable +data class ReplacementShapes( + val component: Shape, + val surface: Shape +) + +val LocalReplacementTypography = staticCompositionLocalOf { + ReplacementTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) +} +val LocalReplacementShapes = staticCompositionLocalOf { + ReplacementShapes( + component = RoundedCornerShape(ZeroCornerSize), + surface = RoundedCornerShape(ZeroCornerSize) + ) +} + +@Composable +fun ReplacementTheme( + /* ... */ + content: @Composable () -> Unit +) { + val replacementTypography = ReplacementTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val replacementShapes = ReplacementShapes( + component = RoundedCornerShape(percent = 50), + surface = RoundedCornerShape(size = 40.dp) + ) + CompositionLocalProvider( + LocalReplacementTypography provides replacementTypography, + LocalReplacementShapes provides replacementShapes + ) { + MaterialTheme( + /* colors = ... */ + content = content + ) + } +} + +// Use with eg. ReplacementTheme.typography.body +object ReplacementTheme { + val typography: ReplacementTypography + @Composable + get() = LocalReplacementTypography.current + val shapes: ReplacementShapes + @Composable + get() = LocalReplacementShapes.current +} +``` + +
+ +### Use Material components + +When one or more systems of `MaterialTheme` have been replaced, using Material +components as-is may result in unwanted Material color, type, or shape values. + +If you want to use replacement values in components, wrap them in your own +composable functions, directly setting the values for the relevant system, and +exposing others as parameters to the containing composable. + +> [!NOTE] +> **Note:** Not all values may be exposed as parameters in Material composables, in particular with `CompositionLocal` composables (such as `LocalTextStyle`). In such cases you may need to wrap `content` lambdas in provider functions (like `ProvideTextStyle`). + + +```kotlin +@Composable +fun ReplacementButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + shape = ReplacementTheme.shapes.component, + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = ReplacementTheme.typography.body + ) { + content() + } + } + ) +} +``` + +
+ +You would then replace usages of `Button` with `ReplacementButton` where +appropriate. + + +```kotlin +@Composable +fun ReplacementApp() { + ReplacementTheme { + /*...*/ + ReplacementButton(onClick = { /* ... */ }) { + /* ... */ + } + } +} +``` + +
+ +## Implement a fully custom design system + +You may want to replace Material Theming with a fully custom design system. +Consider that `MaterialTheme` provides the following systems: + +- `Colors`, `Typography`, and `Shapes`: Material Theming systems +- `TextSelectionColors`: Colors used for text selection by `Text` and `TextField` +- `Ripple` and `RippleTheme`: Material implementation of `Indication` + +If you want to continue using Material components, you must replace some of +these systems in your custom themes or handle the systems in your +components to avoid unwanted behavior. + +However, design systems are not limited to the concepts Material relies on. You +can modify existing systems and introduce entirely new ones --- with new classes +and types --- to make other concepts compatible with themes. + +In the following code, we model a custom color system that includes gradients +(`List`), include a type system, introduce a new elevation system, +and exclude other systems provided by `MaterialTheme`: + +![Screenshot of a mobile app UI demonstrating a custom design system with elements using gradients for colors, custom typography, and elevation.](https://developer.android.com/static/develop/ui/compose/images/themes/custom-color-gradients.png) + + +```kotlin +@Immutable +data class CustomColors( + val content: Color, + val component: Color, + val background: List +) + +@Immutable +data class CustomTypography( + val body: TextStyle, + val title: TextStyle +) + +@Immutable +data class CustomElevation( + val default: Dp, + val pressed: Dp +) + +val LocalCustomColors = staticCompositionLocalOf { + CustomColors( + content = Color.Unspecified, + component = Color.Unspecified, + background = emptyList() + ) +} +val LocalCustomTypography = staticCompositionLocalOf { + CustomTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) +} +val LocalCustomElevation = staticCompositionLocalOf { + CustomElevation( + default = Dp.Unspecified, + pressed = Dp.Unspecified + ) +} + +@Composable +fun CustomTheme( + /* ... */ + content: @Composable () -> Unit +) { + val customColors = CustomColors( + content = Color(0xFFDD0D3C), + component = Color(0xFFC20029), + background = listOf(Color.White, Color(0xFFF8BBD0)) + ) + val customTypography = CustomTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val customElevation = CustomElevation( + default = 4.dp, + pressed = 8.dp + ) + CompositionLocalProvider( + LocalCustomColors provides customColors, + LocalCustomTypography provides customTypography, + LocalCustomElevation provides customElevation, + content = content + ) +} + +// Use with eg. CustomTheme.elevation.small +object CustomTheme { + val colors: CustomColors + @Composable + get() = LocalCustomColors.current + val typography: CustomTypography + @Composable + get() = LocalCustomTypography.current + val elevation: CustomElevation + @Composable + get() = LocalCustomElevation.current +} +``` + +
+ +### Use Material components + +When no `MaterialTheme` is present, using Material components as-is will result +in unwanted Material color, type, and shape values and indication behavior. + +If you want to use custom values in components, wrap them in your own composable +functions, directly setting the values for the relevant system, and exposing +others as parameters to the containing composable. + +We recommend that you access values you set from your custom theme. +Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or +other systems, you can hardcode them. + + +```kotlin +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = CustomTheme.colors.component, + contentColor = CustomTheme.colors.content, + disabledContainerColor = CustomTheme.colors.content + .copy(alpha = 0.12f) + .compositeOver(CustomTheme.colors.component), + disabledContentColor = CustomTheme.colors.content + .copy(alpha = 0.38f) + + ), + shape = ButtonShape, + elevation = ButtonDefaults.elevatedButtonElevation( + defaultElevation = CustomTheme.elevation.default, + pressedElevation = CustomTheme.elevation.pressed + /* disabledElevation = 0.dp */ + ), + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = CustomTheme.typography.body + ) { + content() + } + } + ) +} + +val ButtonShape = RoundedCornerShape(percent = 50) +``` + +
+ +> [!NOTE] +> **Note:** `Button` uses `rememberRipple()` internally to provide a `Ripple` `Indication`. It's a good idea to check the source code when implementing other custom components that wrap existing components. + +If you've introduced new class types --- such as `List` to represent +gradients --- then it may be better to implement components from scratch instead +of wrapping them. For an example, take a look at +[`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt) +from the Jetsnack sample. + +## Recommended for you + +- Note: link text is displayed when JavaScript is off +- [Material Design 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material3) +- [Migrate from Material 2 to Material 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material2-material3) +- [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) \ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md new file mode 100644 index 0000000..ab0d26d --- /dev/null +++ b/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md @@ -0,0 +1,421 @@ +There are three ways you can adopt Styles throughout your app: + +1. Use directly on existing components that expose a [`Style`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/Style) parameter. +2. Apply a style with [`Modifier.styleable`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/styleable.modifier#(androidx.compose.ui.Modifier).styleable(androidx.compose.foundation.style.StyleState,androidx.compose.foundation.style.Style)) on layout composables that don't accept a `Style` parameter. +3. In your own custom design system, use `Modifier.styleable{}` and expose a style parameter on your own components. + +## Available properties on Styles + +Styles support many of the same properties that modifiers support; however, not +everything that is a modifier can be replicated with a Style. You still need +modifiers for certain behaviors, like interactions, custom drawing, or stacking +of properties. + +| Grouping | Properties | Inherited by children | +|---|---|---| +| **Layout and sizing** | | | +| Content Padding (inner) | - `contentPadding(all: Dp)` - `contentPadding(horizontal: Dp, vertical: Dp)` - `contentPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `contentPaddingHorizontal(value: Dp)` / `contentPaddingVertical(value: Dp)` - `contentPaddingStart(value: Dp)` / `contentPaddingTop(value: Dp)` / `contentPaddingEnd(value: Dp)` / `contentPaddingBottom(value: Dp)` | No | +| External Padding (outer) | - `externalPadding(all: Dp)` - `externalPadding(horizontal: Dp, vertical: Dp)` - `externalPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `externalPaddingHorizontal(value: Dp)` / `externalPaddingVertical(value: Dp)` - `externalPaddingStart(value: Dp)` / `externalPaddingTop(value: Dp)` / `externalPaddingEnd(value: Dp)` / `externalPaddingBottom(value: Dp)` | No | +| Dimensions | `fillWidth()/fillHeight()/fillSize()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No | +| Positioning | `left/top/right/bottom` offsets. | No | +| **Visual Appearance** | | | +| Fills | `background` and `foreground` (supports `Color` or `Brush`). | No | +| Borders | `borderWidth`, `borderColor`, and `borderBrush`. | No | +| Shape | `shape` | No - but used in conjunction with other properties. `clip` and `border` use this defined shape. | +| Shadows | `dropShadow`, `innerShadow` | No | +| **Transformations** | | | +| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/scaleY`, `rotationX/rotationY/rotationZ` | No | +| Control | `alpha`, `zIndex` (stacking order), and `transformOrigin` (pivot point) | No | +| **Typography** | | | +| Styling | `textStyle`, `fontSize`, `fontWeight`, `fontStyle`, and `fontFamily` | Yes | +| Coloration | `contentColor` and `contentBrush`. This is also used for Icons styling. | Yes | +| Paragraph | `lineHeight`, `letterSpacing`, `textAlign`, `textDirection`, `lineBreak`, and `hyphens`. | Yes | +| Decoration | `textDecoration`, `textIndent`, and `baselineShift`. | Yes | + +## Use Styles directly on components with Style parameters + +Components that expose a `Style` parameter allow you to set their styling: + + +```kotlin +BaseButton( + onClick = { }, + style = { } +) { + BaseText("Click me") +} +``` + +
+ +Within the style lambda, you can set various properties, such as `externalPadding` +or `background`: + + +```kotlin +BaseButton( + onClick = { }, + style = { background(Color.Blue) } +) { + BaseText("Click me") +} +``` + +
+ +For the full list of supported properties, see [Available properties on +Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles). + +## Apply Styles using modifiers for components with no existing parameter + +For components that lack a built-in style parameter, you can still apply styles +with the `styleable` modifier. This approach is also useful when developing your +own custom components. + + +```kotlin +Row( + modifier = Modifier.styleable { } +) { + BaseText("Content") +} +``` + +
+ +Similar to the `style` parameter, you can include properties like `background`, +`contentPadding`, or `externalPadding` inside the lambda. + + +```kotlin +Row( + modifier = Modifier.styleable { + background(Color.Blue) + } +) { + BaseText("Content") +} +``` + +
+ +> [!NOTE] +> **Note:** When using `Modifier.styleable`, the child composables won't have those properties applied to them, unless they are inherited properties. Only the container with the `styleable` modifier has those properties applied. + +Multiple chained `Modifier.styleable` modifiers are additive with non-inherited +properties on the applied composable, behaving similarly to multiple modifiers +defining the same properties. For inherited properties, these are overridden, +and the last `styleable` modifier in the chain sets the values. + +When using `Modifier.styleable`, you may also want to create and supply a +`StyleState` to be used with the modifier to apply state-based styling. For more +details, see [State and animations with +Styles](https://developer.android.com/develop/ui/compose/styles/state-animations). + +## Define a standalone Style + +You can define a standalone Style for reusability purposes: + + +```kotlin +val style = Style { background(Color.Blue) } +``` + +
+ +You can then pass that defined style into a composable's style parameter or with +`Modifier.styleable`. When using `Modifier.styleable`, you also need to create a +`StyleState` object. `StyleState` is covered in detail in the [State and +animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation. + +The following example shows how you can apply a Style either directly through a +component's built-in parameters, or through a `Modifier.styleable`: + + +```kotlin +val style = Style { background(Color.Blue) } + +// built in parameter +BaseButton(onClick = { }, style = style) { + BaseText("Button") +} + +// modifier styleable +val styleState = remember { MutableStyleState(null) } +Column( + Modifier.styleable(styleState, style) +) { + BaseText("Column content") +} +``` + +
+ +You can also pass that Style into multiple components: + + +```kotlin +val style = Style { background(Color.Blue) } + +// built in parameter +BaseButton(onClick = { }, style = style) { + BaseText("Button") +} +BaseText("Different text that uses the same style parameter", style = style) + +// modifier styleable +val columnStyleState = remember { MutableStyleState(null) } +Column( + Modifier.styleable(columnStyleState, style) +) { + BaseText("Column") +} +val rowStyleState = remember { MutableStyleState(null) } +Row( + Modifier.styleable(rowStyleState, style) +) { + BaseText("Row") +} +``` + +
+ +## Add multiple Style properties + +You can add multiple Style properties by setting different properties on each +line: + + +```kotlin +BaseButton( + onClick = { }, + style = { + background(Color.Blue) + contentPaddingStart(16.dp) + } +) { + BaseText("Button") +} +``` + +
+ +> [!IMPORTANT] +> **Important:** Unlike modifier-based styling, properties in Styles override one another; the last property defined takes precedence. + +Properties in Styles are not additive, unlike modifier-based styling. Styles +take the last set value in the list of properties within one style block. In the +following example, with the background set twice, the `TealColor` is the applied +background. For padding, `contentPaddingTop` overrides the top +padding set by `contentPadding` and does not combine the values. + + +```kotlin +BaseButton( + style = { + background(Color.Red) + // Background of Red is now overridden with TealColor instead + background(TealColor) + // All directions of padding are set to 64.dp (top, start, end, bottom) + contentPadding(64.dp) + // Top padding is now set to 16.dp, all other paddings remain at 64.dp + contentPaddingTop(16.dp) + }, + onClick = { + // + } +) { + BaseText("Click me!") +} +``` + +
+ +![Button with two background colors set, and two contentPadding +overrides](https://developer.android.com/static/develop/ui/compose/styles/images/basic_style_button.png) **Figure 1.** Button with two background colors set and two `contentPadding` overrides. + +## Merge multiple style objects + +You can create multiple Style objects and pass them into the style parameter of +your composable. + + +```kotlin +val style1 = Style { background(TealColor) } +val style2 = Style { contentPaddingTop(16.dp) } + +BaseButton( + style = style1 then style2, + onClick = { + + }, +) { + BaseText("Click me!") +} +``` + +
+ +![Button with background color and contentPaddingTop +set](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_top.png) **Figure 2.** Button with background color and `contentPaddingTop` set. + +When multiple Styles specify the same property, the last set +property is chosen. Because properties are not additive in Styles, the last +padding passed in overrides the `contentPaddingHorizontal` set by the initial +`contentPadding`. Additionally, the last background color overrides the +background color set by the initial style passed in. + + +```kotlin +val style1 = Style { + background(Color.Red) + contentPadding(32.dp) +} + +val style2 = Style { + contentPaddingHorizontal(8.dp) + background(Color.LightGray) +} + +BaseButton( + style = style1 then style2, + onClick = { + + }, +) { + BaseText("Click me!") +} +``` + +
+ +In this case, the styling applied has a light gray background and `32.dp` padding, +except for the left and right padding, which has a value of `8.dp`. +![Button with contentPadding that's overridden by different +Styles](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_overrides.png) **Figure 3.** Button with `contentPadding` that's overridden by different Styles. + +## Style inheritance + +> [!NOTE] +> **Note:** While the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`. + +Certain style properties, such as `contentColor` and text style-related +properties, propagate to the child composables. A style set on a child +composable overrides the inherited parent styling for that specific child. +![Style propagation with Style, styleable, and direct +parameters](https://developer.android.com/static/develop/ui/compose/styles/images/styles_modifiers_precedence_ordering.png) **Figure 4.** Style propagation with `Style`, `styleable`, and direct parameters. + +| Priority | Method | Effect | +|---|---|---| +| 1 (Highest) | Direct arguments on a composable | Overrides everything; for example, `Text(color = Color.Red)` | +| 2 | Style parameter | Local style overrides `Text(style = Style { contentColor(Color.Red)}` | +| 3 | Modifier chain | `Modifier.styleable{ contentColor(Color.Red)` on the component itself. | +| 4 (Lowest) | Parent styles | For properties that can be inherited (Typography/Color) passed down from the parent. | + +> [!NOTE] +> **Note:** Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, similar to having multiple modifiers defining the same properties. For inherited properties, these are overridden; the last `styleable` modifier in the chain sets the values. + +### Parent styling + +You can set text properties (such as `contentColor`) from the parent composable, +and they propagate to all child `Text` composables. + + +```kotlin +val styleState = remember { MutableStyleState(null) } +Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, +) { + BaseText("Children inherit", style = { width(60.dp) }) + BaseText("certain properties") + BaseText("from their parents") +} +``` + +
+ +![Child composables' property +inheritance](https://developer.android.com/static/develop/ui/compose/styles/images/children_inherit_styles_parents.png) **Figure 5.** Child composables' property inheritance. + +### Child override of properties + +You can also set styling on a specific `Text` composable. If the parent composable +has styling set, the styling set on the child composable overrides the +parent composable's styling. + + +```kotlin +val styleState = remember { MutableStyleState(null) } +Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, +) { + BaseText("Children can ", style = { + contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue))) + }) + BaseText("override properties") + BaseText("set by their parents") +} +``` + +
+ +![Child composables override parent +properties](https://developer.android.com/static/develop/ui/compose/styles/images/children_override_styles.png) **Figure 6.** Child composables override parent properties. + +## Implement custom Style properties + +You can create custom properties that map to existing Style definitions by using +extension functions on the `StyleScope`, as shown in the following example: + + +```kotlin +fun StyleScope.outlinedBackground(color: Color) { + border(1.dp, color) + background(color) +} +``` + +
+ +Apply this new property within a Style definition: + + +```kotlin +val customExtensionStyle = Style { + outlinedBackground(Color.Blue) +} +``` + +
+ +Creating new styleable properties is unsupported. If your use case +requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128). + +## Read `CompositionLocal` values + +It's a common pattern to store design system tokens within a `CompositionLocal`, +to access the variables without needing to pass them as parameters. Styles +can access `CompositionLocal`s to retrieve system-wide values within a style: + + +```kotlin +val buttonStyle = Style { + contentPadding(12.dp) + shape(RoundedCornerShape(50)) + background(Brush.verticalGradient(LocalCustomColors.currentValue.background)) +} +``` + +
\ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md new file mode 100644 index 0000000..01683bd --- /dev/null +++ b/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md @@ -0,0 +1,461 @@ +
+ +The Styles API offers a declarative and streamlined approach to managing UI +changes during interaction states like `hovered`, `focused`, and `pressed`. With +this API, you can significantly decrease the boilerplate code typically required +when using modifiers. + +To facilitate reactive styling, `StyleState` acts as a stable, read-only +interface that tracks the active state of an element (such as its enabled, +pressed, or focused status). Within a `StyleScope`, you can access this through +the `state` property to implement conditional logic directly in your Style +definitions. + +## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled + +Styles come with built-in support for common interactions: + +- Pressed +- Hovered +- Selected +- Enabled +- Toggled + +It's also possible to support custom states. See the [Custom State Styling with +StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information. + +### Handle interaction states with Style parameters + +The following example demonstrates modifying the `background` and `borderColor` +in response to interaction states, specifically switching to purple when hovered +and blue when focused: + + +```kotlin +@Preview +@Composable +private fun OpenButton() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + background(lightPurple) + border(2.dp, lightPurple) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} +``` + +
+ +**Figure 1.** Changing background color based on hovered and focused states. + +You can also create nested state definitions. For example, you can define a +specific style for when a button is being both pressed and hovered +simultaneously: + + +```kotlin +@Composable +private fun OpenButton_CombinedStates() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + // light purple + background(lightPurple) + pressed { + // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked + background(lightOrange) + } + } + pressed { + // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only + background(lightRed) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} +``` + +
+ +**Figure 2.** Hovered and pressed state together on a button. + +### Custom composables with Modifier.styleable + +When creating your own `styleable` components, you must connect an +`interactionSource` to a `styleState`. Then, pass this state into +`Modifier.styleable` to utilize it. + +Consider a scenario where your design system includes a `GradientButton`. You +may want to create a `LoginButton` that inherits from `GradientButton`, but +alters its colors during interactions, like being pressed. + +- To enable `interactionSource` style updates, include an `interactionSource` as a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a new `MutableInteractionSource`. +- Initialize the `styleState` by providing the `interactionSource`. Make sure the `styleState`'s enabled status reflects the value of the provided enabled parameter. +- Assign the `interactionSource` to the `focusable` and `clickable` modifiers. Finally, apply the `styleState` to the modifier's `styleable` parameter. + + +```kotlin +@Composable +private fun GradientButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier = + modifier + .clickable( + onClick = onClick, + enabled = enabled, + interactionSource = interactionSource, + indication = null, + ) + .styleable(styleState, baseGradientButtonStyle then style), + content = content, + ) +} +``` + +
+ +You can now use the `interactionSource` state to drive style modifications with +the pressed, focused, and hovered options inside the style block: + + +```kotlin +@Preview +@Composable +fun LoginButton() { + val loginButtonStyle = Style { + pressed { + background( + Brush.linearGradient( + listOf(Color.Magenta, Color.Red) + ) + ) + } + } + GradientButton(onClick = { + // Login logic + }, style = loginButtonStyle) { + BaseText("Login") + } +} +``` + +
+ +**Figure 3.** Changing a custom composable state based on `interactionSource`. + +## Animate style changes + +Styles state changes come with built-in animation support. You can wrap the new +property within any state change block with `animate` to automatically add +animations between different states. This is similar to the `animate*AsState` +APIs. The following example animates the `borderColor` from black to blue when +the state changes to focused: + + +```kotlin +val animatingStyle = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + } +} + +@Preview +@Composable +private fun AnimatingStyleChanges() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyle)) { + + } +} +``` + +
+ +**Figure 4.** Animating color changes on press. + +The `animate` API accepts an `animationSpec` to change the duration or shape of +the animation curve. The following example animates the size of the box with a +`spring` spec: + + +```kotlin +val animatingStyleSpec = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + transformOrigin(TransformOrigin.Center) + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { + scale(1.2f) + } + } +} + +@Preview(showBackground = true) +@Composable +fun AnimatingStyleChangesSpec() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyleSpec)) +} +``` + +
+ +**Figure 5.** Animating size and color changes on press. + +## Custom state styling with StyleState + +Depending on your composable use case, you may have different styles that are +backed by custom states. For example, if you have a media app, you may want to +have different styling for the buttons in your `MediaPlayer` composable +depending on the playback state of the player. Follow these steps to create and +use your own custom state: + +1. Define custom key +2. Create `StyleState` extension +3. Link to custom state + +### Define custom key + +To create a custom state-based style, first create a +[`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the +app launches, the media player is in the `Stopped` state, so it's initialized in +this way: + + +```kotlin +enum class PlayerState { + Stopped, + Playing, + Paused +} + +val playerStateKey = StyleStateKey(PlayerState.Stopped) +``` + +
+ +### Create StyleState extension functions + +Define an extension function on `StyleState` to query the current `playState`. +Then, create extension functions on `StyleScope` with your custom states passing +in the `playStateKey`, a lambda with the specific state, and the style. + + +```kotlin +// Extension Function on MutableStyleState to query and set the current playState +var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + +fun StyleScope.playerPlaying(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) +} +fun StyleScope.playerPaused(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) +} +``` + +
+ +### Link to custom state + +Define the `styleState` in your composable and set the `styleState.playState` +equal to incoming state. Pass `styleState` into the `styleable` function on the +modifier. + + +```kotlin +@Composable +fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } +) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, style)) { + ///.. + } +} +``` + +
+ +Within the `style` lambda, you can apply state-based styling for custom states, +using the previously defined extension functions. + + +```kotlin +@Composable +fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + style = style, + state = PlayerState.Stopped) +} +``` + +
+ +The following code is the full snippet for this example: + + +```kotlin +enum class PlayerState { + Stopped, + Playing, + Paused +} +val playerStateKey = StyleStateKey(PlayerState.Stopped) +var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + +fun StyleScope.playerPlaying(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) +} +fun StyleScope.playerPaused(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) + +} + +@Composable +fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } +) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, Style { + size(100.dp) + border(2.dp, Color.Red) + + }, style, )) { + + ///.. + } +} +@Composable +fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + style = style, + state = PlayerState.Stopped) +} +``` + +
\ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md new file mode 100644 index 0000000..cd9bc50 --- /dev/null +++ b/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md @@ -0,0 +1,48 @@ +Styles differ from modifiers by design. Styles don't replace modifiers; instead, +the two systems coexist with different goals. Internally, a Style is a modifier. +You can do everything Styles can do with modifiers, but not all functionality in +modifiers is available in Styles. +**Important:** + +- **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define a theme-wide set of properties for a component. +- **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties. + +The following is a comparison between Styles versus modifiers: + +| Feature | Modifiers | Styles | +|---|---|---| +| **Primary Goal** | Define behaviors, semantics, and complex layouts. Modifiers manipulate individual elements on the fly for a particular composable and don't trickle down from the theme. | Define visual appearance, individual item sizing and themeable properties. Styles operate at a theme level and are over-writeable at a component level. They trickle down and apply styling across different composables. | +| **Logic** | Additive - the modifiers combine together to form a new result. | Over-writable - the last property set in the Style wins. Styles act as a single layer of properties that override each other based on a defined precedence hierarchy. | +| **Theming** | Challenging to lift into a theme, normally used individually. | By design, Styles are themeable (they can access `CompositionLocal`s) and can be defined once and used across components. | +| **Performance** | Updates often require all three phases of Compose: composition, layout and draw. Achieving good animation performance of modifiers often requires writing lambda-based versions. | Skips composition phase, only active in layout and draw phase, reducing recompositions. Requires less object allocation. | +| **Animations** | Requires using separate animation primitives like `animate*AsState` | Features built-in `animate { }` API that handles some animations for you. | + +## Limitations of modifiers + +Modifiers have many benefits in the current Compose landscape. However, Styles +address some limitations of modifiers, which the following list describes: + +- Modifiers are typically created in the Composition phase. Updates can force a full rerun of Composition, Layout, and Draw, even for small visual changes like color, unless you create lambda-based modifiers. +- Conditional modifiers require disruptive if-else logic within fluent chains. Animating them requires manual state boilerplate and lacks a high-performance "auto-animate" mechanism. +- Modifiers stack rather than replace. You can't override a component's default border; you can only draw a second one on top. +- Modifiers are difficult to abstract into global themes. Consequently, themes usually store raw values instead of reusable modifier configurations. + +## Limitations of Styles + +While Styles can fill in some of the gaps that modifiers have, they also have +some limitations, which show how they cannot entirely replace modifiers: + +- Styles are specialized Modifiers. While a modifier can do anything a Style does, the reverse is not true. Consequently, Styles can supplement, but cannot replace, modifiers. +- Styles are limited to visual configuration (backgrounds, padding, borders). They cannot handle behaviors like click logic, gesture detection, or accessibility semantics. +- Resolving a Style into its final state is *more expensive than applying a + single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this. + +## When to use Styles over modifiers + +While the choice to use Styles is largely dependent on your app and use cases, +the following guidance helps determine when to prefer a style over a modifier: + +- **To achieve theme-wide consistency:** Styles are designed to be "lifted" into a global theme. Instead of passing repetitive Modifiers to every component, you can define a single Style in your theme to create a unified look across the entire app. +- **When performing frequent animations:** Styles evaluate during the Layout and Draw phases, allowing properties like color or scale to animate while bypassing the Composition phase entirely. This significantly reduces performance overhead. Use a Style instead of a modifier when doing visual property animations. +- **Overriding vs. stacking:** Use Styles when you need to replace a default property. Modifiers are additive (adding a border stacks a second one), whereas Styles use "last-write-wins" logic, making it easier to swap out backgrounds or padding without visual clutter. +- **Customizing Material components:** If a Material component provides a Style parameter, it is the suggested approach for customization. These styles allow you to access and modify specific properties within the composable's internal structure that might otherwise be inaccessible. \ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md new file mode 100644 index 0000000..c8a373b --- /dev/null +++ b/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md @@ -0,0 +1,257 @@ +> [!NOTE] +> **Note:** Styles are `@Experimental` and likely to change in upcoming releases, with Material support for Styles added in future releases. If you have any feedback, [file Styles issues](https://issuetracker.google.com/issues/new?component=612128). + +There are several ways you can build out your apps using Styles. What you choose +depends on where your app sits in relation to its adoption of Material Design: + +1. Fully custom design system, not using Material Design + - **Recommendation**: Define component styles that consume values from the theme, and expose style parameters on design system components. +2. Using Material Design + - **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible. + +## The Style layer + +In the traditional Compose model, customization often relies heavily on +overriding global tokens (colors and typography) provided by `MaterialTheme`, or +wrapping and overriding properties of a design system composable where possible. +Sometimes, there are properties within the Material layer that are not exposed +through the subsystems or parameters, but are hardcoded defaults on the +component itself. + +With the Styles API, there's a new layer of abstraction that's a bridge between +subsystems and components: **Styles**. + +| Layer | Responsibility | Example | +|---|---|---| +| **Subsystem values** | Named values | `val Primary = Color(0xFF34A85E)` | +| **Atomic Styles** | Style that does exactly one property change | `val largeSizeAtomic = Style { size(100.dp, 40.dp) }` | +| **Component Styles** | Component-specific configurations | A Button with Primary background and 16dp padding. `val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }` | +| **Components** | The functional UI element that consumes a Style. | `Button(style = buttonStyle) { ... }` | + +![Diagram showing Theming with Styles with the new layer introduction](https://developer.android.com/static/develop/ui/compose/styles/images/theming_styles_layer.png) **Figure 1.** An example of a component and how it accesses styles from a theme. + +### Atomic versus monolithic Styles + +With the Styles API, you can break down a Style into separate atomic styles. +Instead of defining complex, component-specific styles like `baseButtonStyle`, +you can also create small, single-purpose utility styles. These act as your +"atoms". + + +```kotlin +// Define single-purpose "atomic" styles +val paddingAtomic = Style { + contentPadding(16.dp) +} +val roundedCornerShapeAtomic = Style { + shape(RoundedCornerShape(8.dp)) +} +val primaryBackgroundAtomic = Style { + background(Color.Blue) +} +val largeSizeAtomic = Style { + size(100.dp, 40.dp) +} +val interactiveShadowAtomic = Style { + hovered { + animate { + dropShadow( + Shadow( + offset = DpOffset( + 0.dp, + 0.dp + ), + radius = 2.dp, + spread = 0.dp, + color = Color.Blue, + ) + ) + } + } +} +``` + +
+ +#### Composition using "then" + +One of the powerful features of the new Styles API is the `then` operator, which +lets you merge multiple `Style` objects. This lets you build a component using +atomic utility classes. + +**Traditional (non-atomic)**: + + +```kotlin +// One large monolithic style +val buttonStyle = Style { + contentPadding(16.dp) + shape(RoundedCornerShape(8.dp)) + background(Color.Blue) +} +``` + +
+ +**Atomic refactor**: + + +```kotlin +// Combine atoms to create the final appearance +val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic +``` + +
+ +## Adopt Styles in your design system + +Consider the following options when adopting Styles within your design system, +depending on where in the spectrum your design system lies. + +### Custom design system with Styles + +***Consider when**: You've been handed an extensive brand guide that is not +based on Material Design, and you are not planning to use Material Design*. + +***Strategy**: Implement a fully custom design system, and expose styles as part +of the theme*. + +This option is the custom path if you don't use Material as your main design +system language. You bypass `MaterialTheme` entirely for visual definitions and +have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that +acts as a container for your Styles. + +- **How it works** : Create a `CompanyTheme` object that holds `Style` objects for every component in your system. Your components (either wrappers around Material logic or custom `Box` or `Layout` implementations) consume these styles directly, and expose a `Style` parameter for consumers of your design system. +- **The Style layer**: Styles are the primary definition of your design system. Tokens are named variables fed into these styles. This allows for deep customization, such as defining unique animations for state changes (for example, animating scale and color on press). + +If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and +want to adopt styles, add your list of styles to your Theme. This lets you +access your base styles from anywhere in your project. + +1. Create a `Styles` class that stores the various styles in your application + and create the defaults. For example, in the Jetsnack app - the class is + named `JetsnackStyles`: + + + ```kotlin + object JetsnackStyles{ + val buttonStyle: Style = Style { + shape(shapes.medium) + background(colors.brand) + contentColor(colors.textPrimary) + contentPaddingVertical(8.dp) + contentPaddingHorizontal(24.dp) + textStyle(typography.labelLarge) + disabled { + animate { + background(colors.brandSecondary) + } + } + } + val cardStyle: Style = Style { + shape(shapes.medium) + background(colors.uiBackground) + contentColor(colors.textPrimary) + } + } + ``` + +
+ +2. Provide `Styles` as part of your overall theme, and expose helper extension + functions on `StyleScope` to access the subsystems: + + + ```kotlin + @Immutable + class JetsnackTheme( + val colors: JetsnackColors = LightJetsnackColors, + val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), + val shapes: Shapes = Shapes() + ) { + companion object { + val colors: JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + + val typography: androidx.compose.material3.Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + + val styles: JetsnackStyles = JetsnackStyles + + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + } + } + + val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + + val StyleScope.shapes: Shapes + get() = LocalJetsnackTheme.currentValue.shapes + + internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } + + @Composable + fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors + val theme = JetsnackTheme(colors = colors) + + CompositionLocalProvider( + LocalJetsnackTheme provides theme, + ) { + MaterialTheme( + typography = LocalJetsnackTheme.current.typography, + shapes = LocalJetsnackTheme.current.shapes, + content = content, + ) + } + } + ``` + +
+ +3. Access `JetsnackStyles` within your composable: + + + ```kotlin + @Composable + fun CustomButton(modifier: Modifier, + style: Style = Style, + text: String) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + + // Apply style to top level container in combination with incoming style from parameter. + Box(modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + enabled = true, + role = Role.Button, + onClick = { + + }, + ) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { + Text(text) + } + } + ``` + +
+ +Beyond global theme adoption, there are alternative strategies for incorporating +`Styles` into your apps. You can leverage `Styles` inline for specific call +sites or use static definitions when full theming capabilities are unnecessary. +`Styles` shouldn't be swapped conditionally unless the whole style is +fundamentally different. You should prefer accessing dynamic tokens inside a +visual definition rather than switching between distinct style objects. \ No newline at end of file diff --git a/prompt_ai/MIGRATE_COMPOSE.md b/prompt_ai/MIGRATE_COMPOSE.md index d0c1f1e..8baf84a 100644 --- a/prompt_ai/MIGRATE_COMPOSE.md +++ b/prompt_ai/MIGRATE_COMPOSE.md @@ -4,12 +4,17 @@ Anda adalah seorang AI Agent Android Developer Senior yang memiliki spesialisasi # TUJUAN -Tugas Anda adalah mengonversi kode layout XML dan logika View terkait (Activity/Fragment) yang saya berikan menjadi Jetpack Compose Composable Functions yang bersih, deklaratif, dan siap pakai.# KONTEKS & TUJUAN - -Saya ingin memigrasikan komponen UI Android dari View System (XML) ke Jetpack Compose menggunakan skill/tools yang tersedia di sistem ini. Migrasi ini harus mengikuti standar arsitektur modern (MVI/MVVM), menggunakan Material Design 3, dan memastikan state management terpisah dari UI. +Saya ingin memigrasikan komponen UI Android dari View System (XML) ke Jetpack Compose menggunakan skill/tools yang tersedia di sistem ini, dengan mengikuti standar arsitektur modern (MVI/MVVM), Material Design 3, serta memastikan state management terpisah dari UI. # INPUT DATA +Skill yang wajib di gunakan adalah sebagai berikut: + +- .agents\skills\jetpack-compose-m3 +- .agents\skills\migrate-xml-views-to-jetpack-compose +- .agents\skills\styles +- .agents\skills\edge-to-edge + Gunakan skill pembaca file / workspace untuk mengambil source code berikut: 1. File Layout XML: [PATH_KE_FILE_XML_ANDA, contoh: res/layout/activity_main.xml] @@ -50,9 +55,60 @@ Berikan output berupa full code untuk file Composable baru (`.kt`), serta berika --- -# INPUT DATA - -## 1. File XML (Layout Asli) - -```xml -[TEMPELKAN KODE XML DI SINI] +# STRUCTURE FOLDER + +com.example.myapp/ +│ +├── data/ # Data Layer (Agnostik terhadap UI/Compose) +│ ├── model/ # Data models (DTO, Entity) +│ ├── repository/ # Implementasi Repository +│ └── source/ # Local (Room) & Remote (Retrofit/Ktor) data sources +│ +├── domain/ # Domain Layer (Opsional, untuk business logic kompleks) +│ ├── model/ # Domain/Business models +│ └── usecase/ # Use Cases / Interactors +│ +├── ui/ # UI Layer (Tempat Jetpack Compose berada) +│ ├── components/ # Global/Shared Composables (Reusable UI) +│ │ ├── CustomButton.kt +│ │ └── LoadingScreen.kt +│ │ +│ ├── theme/ # Design System Tokens (Aksesibilitas global) +│ │ ├── Color.kt +│ │ ├── Theme.kt +│ │ ├── Type.kt +│ │ └── Shape.kt +│ │ +│ ├── features/ # Fitur Utama Aplikasi (Feature-by-Package) +│ │ ├── home/ +│ │ │ ├── HomeScreen.kt # Composable utama untuk fitur Home +│ │ │ ├── HomeViewModel.kt # State holder untuk Home +│ │ │ ├── HomeUiState.kt # Data class / Sealed interface penampung State +│ │ │ └── components/ # Composable lokal yang hanya dipakai di Home +│ │ │ └── HomeHeader.kt +│ │ │ +│ │ └── detail/ +│ │ ├── DetailScreen.kt +│ │ └── DetailViewModel.kt +│ │ +│ └── navigation/ # Navigasi Aplikasi (Compose Navigation) +│ ├── NavGraph.kt # Setup NavHost dan composable destinations +│ └── Destinations.kt # Definisi rute/screen (bisa menggunakan Type-Safe Navigation) +│ +└── MainActivity.kt # Entry point aplikasi (Entry Point untuk Scaffold/NavHost) + +# ANDROID APP ARCHITECTURE + +1. File State, ViewModel, dan Screen Berdampingan +Di Jetpack Compose, UI digerakkan oleh State (UiState). Menyatukan HomeScreen.kt, HomeViewModel.kt, dan HomeUiState.kt di dalam folder fitur yang sama (ui/features/home/) membuat kode jauh lebih mudah dirawat (highly cohesive). Saat Anda mengerjakan fitur Home, Anda tidak perlu melompat-lompat folder dari ujung atas ke ujung bawah proyek. + +2. Pemisahan Komponen Global vs Lokal +ui/components/: Berisi komponen UI generik yang digunakan di banyak layar, seperti custom button, loading spinner, atau error dialog. + +ui/features/[nama_fitur]/components/: Berisi komponen yang sangat spesifik dan hanya masuk akal jika berada di layar tersebut. Ini mencegah folder global menjadi terlalu penuh. + +1. Paket theme/ yang Sentralized +Saat Anda membuat proyek baru di Android Studio dengan template Compose, folder ui/theme otomatis dibuat. Tetap pertahankan folder ini karena ia menyimpan konfigurasi MaterialTheme (Warna, Tipografi, dan Bentuk) yang membungkus seluruh aplikasi Anda di MainActivity. + +2. Layer Data dan Domain Tetap Bersih dari Compose +Perlu diingat bahwa Jetpack Compose hanyalah toolkit UI. Folder data/ dan domain/ Anda sama sekali tidak boleh mengimpor library Compose (androidx.compose.*). Mereka murni berisi Kotlin standard/coroutine agar kode backend aplikasi Anda tetap bisa diuji (testable) secara independen. From cf418b934f67cfc81971974c7e79fe71498f13e7 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 11:20:54 +0700 Subject: [PATCH 05/14] docs: add migration guide for XML to Jetpack Compose conversion --- prompt_ai/{ => template}/MIGRATE_COMPOSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename prompt_ai/{ => template}/MIGRATE_COMPOSE.md (100%) diff --git a/prompt_ai/MIGRATE_COMPOSE.md b/prompt_ai/template/MIGRATE_COMPOSE.md similarity index 100% rename from prompt_ai/MIGRATE_COMPOSE.md rename to prompt_ai/template/MIGRATE_COMPOSE.md From 10f5b9f8ea19daa174bd83ef85f88c9504e1a68d Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 13:45:41 +0700 Subject: [PATCH 06/14] feat: integrate Jetpack Compose and setup project architecture with Hilt and feature-based modules --- .../ui/features/favorite/FavoriteScreen.kt | 99 +++++++++ .../androidapp/ui/features/main/MainScreen.kt | 190 ++++++++++++++++++ .../ui/features/main/MainUiState.kt | 9 + .../ui/features/main/MainViewModel.kt | 37 ++++ prompt_ai/main-to-compose.md | 122 +++++++++++ 5 files changed, 457 insertions(+) create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainUiState.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainViewModel.kt create mode 100644 prompt_ai/main-to-compose.md diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt new file mode 100644 index 0000000..ca3bab3 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt @@ -0,0 +1,99 @@ +package io.github.amirisback.androidapp.ui.features.favorite + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import io.github.amirisback.androidapp.R +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.favorite.FavoriteViewModel +import io.github.amirisback.androidapp.ui.features.main.MealCard + +@Composable +fun FavoriteScreen( + viewModel: FavoriteViewModel, + onItemClick: (MealModel) -> Unit, + modifier: Modifier = Modifier +) { + // Refresh favorite list when entering screen + LaunchedEffect(Unit) { + viewModel.getData() + } + + val resourceState = viewModel.mealsState.observeAsState(initial = Resource.Loading()) + + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (val state = resourceState.value) { + is Resource.Loading -> { + CircularProgressIndicator() + } + is Resource.Error -> { + Text( + text = state.message ?: stringResource(id = R.string.frogo_is_empty_data), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp) + ) + } + is Resource.Success -> { + val meals = state.data ?: emptyList() + if (meals.isEmpty()) { + Text( + text = stringResource(id = R.string.frogo_is_empty_data), + style = MaterialTheme.typography.bodyLarge + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items( + items = meals, + key = { it.idMeal ?: "" } + ) { meal -> + MealCard( + meal = meal, + onClick = { onItemClick(meal) } + ) + } + } + } + } + } + } +} + +/** + * Extension helper to observe LiveData in Compose without adding extra dependency + */ +@Composable +fun LiveData.observeAsState(initial: T): State { + val state = remember { mutableStateOf(initial) } + DisposableEffect(this) { + val observer = Observer { state.value = it } + observeForever(observer) + onDispose { + removeObserver(observer) + } + } + return state +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt new file mode 100644 index 0000000..1df6e9a --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt @@ -0,0 +1,190 @@ +package io.github.amirisback.androidapp.ui.features.main + +import androidx.compose.foundation.clickable +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.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.init.ui.theme.InitTheme + +@Composable +fun MainScreen( + viewModel: MainViewModel, + onItemClick: (MealModel) -> Unit, + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + MainContent( + uiState = uiState, + onItemClick = onItemClick, + modifier = modifier + ) +} + +@Composable +fun MainContent( + uiState: MainUiState, + onItemClick: (MealModel) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (uiState) { + is MainUiState.Loading -> { + CircularProgressIndicator() + } + is MainUiState.Error -> { + Text( + text = uiState.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp) + ) + } + is MainUiState.Success -> { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items( + items = uiState.meals, + key = { it.idMeal ?: "" } + ) { meal -> + MealCard( + meal = meal, + onClick = { onItemClick(meal) } + ) + } + } + } + } + } +} + +@Composable +fun MealCard( + meal: MealModel, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { onClick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(meal.strMealThumb) + .crossfade(true) + .build(), + contentDescription = meal.strMeal, + modifier = Modifier + .fillMaxWidth() + .height(128.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strMeal ?: "", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = meal.strArea ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strCategory ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MealCardPreview() { + InitTheme { + MealCard( + meal = MealModel( + idMeal = "1", + strMeal = "Creamy Chicken Pasta", + strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", + strCategory = "Pasta description here. This is a very delicious and easy meal to make.", + strArea = "Italian" + ), + onClick = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun MainContentSuccessPreview() { + InitTheme { + MainContent( + uiState = MainUiState.Success( + meals = listOf( + MealModel( + idMeal = "1", + strMeal = "Creamy Chicken Pasta", + strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", + strCategory = "Pasta description here. This is a very delicious and easy meal to make.", + strArea = "Italian" + ), + MealModel( + idMeal = "2", + strMeal = "Beef Wellington", + strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", + strCategory = "Beef description here.", + strArea = "British" + ) + ) + ), + onItemClick = {} + ) + } +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainUiState.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainUiState.kt new file mode 100644 index 0000000..72b6d84 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainUiState.kt @@ -0,0 +1,9 @@ +package io.github.amirisback.androidapp.ui.features.main + +import io.github.amirisback.androidapp.domain.model.MealModel + +sealed interface MainUiState { + object Loading : MainUiState + data class Success(val meals: List) : MainUiState + data class Error(val message: String) : MainUiState +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainViewModel.kt new file mode 100644 index 0000000..39c8056 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainViewModel.kt @@ -0,0 +1,37 @@ +package io.github.amirisback.androidapp.ui.features.main + +import androidx.lifecycle.viewModelScope +import io.github.amirisback.androidapp.common.base.BaseViewModel +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + private val useCase: MealUseCase, +) : BaseViewModel() { + + private val _uiState = MutableStateFlow(MainUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + fun searchMeal(name: String = "") { + viewModelScope.launch { + useCase.searchMeal( + nameMeal = name + ).onEach { resource -> + _uiState.value = when (resource) { + is Resource.Loading -> MainUiState.Loading + is Resource.Success -> MainUiState.Success(resource.data ?: emptyList()) + is Resource.Error -> MainUiState.Error(resource.message ?: "Unknown Error") + } + }.launchIn(viewModelScope) + } + } +} diff --git a/prompt_ai/main-to-compose.md b/prompt_ai/main-to-compose.md new file mode 100644 index 0000000..1dbf346 --- /dev/null +++ b/prompt_ai/main-to-compose.md @@ -0,0 +1,122 @@ +# ROLE & SKILL + +Anda adalah seorang AI Agent Android Developer Senior yang memiliki spesialisasi (skill) dalam memigrasikan sistem UI lama (XML/View-based) ke Jetpack Compose modern. Anda menguasai best practices Android, arsitektur MVVM/MVI, State Management, Kotlin Coroutines, dan optimasi performa UI. + +# TUJUAN + +Saya ingin memigrasikan komponen UI Android dari View System (XML) ke Jetpack Compose menggunakan skill/tools yang tersedia di sistem ini, dengan mengikuti standar arsitektur modern (MVI/MVVM), Material Design 3, serta memastikan state management terpisah dari UI. + +# INPUT DATA + +Skill yang wajib di gunakan adalah sebagai berikut: + +- .agents\skills\jetpack-compose-m3 +- .agents\skills\migrate-xml-views-to-jetpack-compose +- .agents\skills\styles +- .agents\skills\edge-to-edge + +Gunakan skill pembaca file / workspace untuk mengambil source code berikut: + +1. File Layout XML: + +- app\src\main\res\layout\activity_main.xml +- app\src\main\res\layout\fragment_main.xml + +1. File Activity/Fragment Terkait: + +- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainActivity.kt +- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainAdapter.kt +- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainFragment.kt +- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainViewModel.kt + +# INSTRUKSI MIGRASI + +Mohon proses file di atas dan buatkan kode Jetpack Compose dengan ketentuan sebagai berikut: + +1. Komponen UI & Layouting: + - Konversikan ViewGroup (ConstraintLayout, LinearLayout, RelativeLayout) ke Composable yang setara (Box, Column, Row, LazyColumn, atau ConstraintLayout Compose jika sangat kompleks). + - Gunakan komponen Material Design 3 (Button, OutlinedTextField, Card, Text, dll.). + - ConstraintLayout XML -> ConstraintLayout Compose (hanya jika kompleks) atau optimalkan menggunakan Row/Column/Box standar. + - RecyclerView -> LazyColumn / LazyRow. + - ImageView -> AsyncImage (Coil) jika memuat gambar dari URL. + +2. Styling & Resources: + - Gunakan `stringResource()`, `painterResource()`, dan `dimensionResource()` untuk menjaga modularitas resource. + - Sesuaikan warna dan tipografi menggunakan objek `MaterialTheme`. + - Styling & Themes: Gunakan token dari `MaterialTheme` (color, typography, shapes) alih-alih hardcoded colors/dimens dari XML, kecuali jika saya sebutkan lain. + +3. State & Event Handling: + - UI harus bersifat Stateless. Pisahkan State dan Event. + - Buat parameter lambda untuk event handling (misal: `onButtonClicked: () -> Unit`). + - Integrasikan dengan StateFlow/LiveData dari ViewModel yang ada di file Activity/Fragment asal (gunakan `collectAsStateWithLifecycle()`). + - State Management: Ubah semua state UI yang sebelumnya diatur manual (misal: setText(), setVisibility()) menggunakan `MutableState`, `remember`, atau `collectAsStateWithLifecycle()` dari ViewModel jika ada. + - Unidirectional Data Flow (UDF): Pastikan Composable bersifat stateless (menggunakan State Hoisting) di mana event dikirim ke atas (callbacks) dan data mengalir ke bawah. + +4. Preview: + - Sediakan `@Preview` fungsi Composable, lengkap dengan `ShowBackground = true` dan tema default-nya. + +5. Performa: Hindari recomposition yang tidak perlu. Gunakan `remember` untuk objek yang berat dan `derivedStateOf` jika ada kalkulasi state turunan. + +# OUTPUT YANG DIHARAPKAN + +Berikan output berupa full code untuk file Composable baru (`.kt`), serta berikan panduan singkat jika ada dependensi Gradle baru yang perlu ditambahkan atau perubahan minor yang harus saya lakukan di sisi ViewModel/Activity. + +--- + +# STRUCTURE FOLDER + +com.example.myapp/ +│ +├── data/ # Data Layer (Agnostik terhadap UI/Compose) +│ ├── model/ # Data models (DTO, Entity) +│ ├── repository/ # Implementasi Repository +│ └── source/ # Local (Room) & Remote (Retrofit/Ktor) data sources +│ +├── domain/ # Domain Layer (Opsional, untuk business logic kompleks) +│ ├── model/ # Domain/Business models +│ └── usecase/ # Use Cases / Interactors +│ +├── ui/ # UI Layer (Tempat Jetpack Compose berada) +│ ├── components/ # Global/Shared Composables (Reusable UI) +│ │ ├── CustomButton.kt +│ │ └── LoadingScreen.kt +│ │ +│ ├── theme/ # Design System Tokens (Aksesibilitas global) +│ │ ├── Color.kt +│ │ ├── Theme.kt +│ │ ├── Type.kt +│ │ └── Shape.kt +│ │ +│ ├── features/ # Fitur Utama Aplikasi (Feature-by-Package) +│ │ ├── home/ +│ │ │ ├── HomeScreen.kt # Composable utama untuk fitur Home +│ │ │ ├── HomeViewModel.kt # State holder untuk Home +│ │ │ ├── HomeUiState.kt # Data class / Sealed interface penampung State +│ │ │ └── components/ # Composable lokal yang hanya dipakai di Home +│ │ │ └── HomeHeader.kt +│ │ │ +│ │ └── detail/ +│ │ ├── DetailScreen.kt +│ │ └── DetailViewModel.kt +│ │ +│ └── navigation/ # Navigasi Aplikasi (Compose Navigation) +│ ├── NavGraph.kt # Setup NavHost dan composable destinations +│ └── Destinations.kt # Definisi rute/screen (bisa menggunakan Type-Safe Navigation) +│ +└── MainActivity.kt # Entry point aplikasi (Entry Point untuk Scaffold/NavHost) + +# ANDROID APP ARCHITECTURE + +1. File State, ViewModel, dan Screen Berdampingan +Di Jetpack Compose, UI digerakkan oleh State (UiState). Menyatukan HomeScreen.kt, HomeViewModel.kt, dan HomeUiState.kt di dalam folder fitur yang sama (ui/features/home/) membuat kode jauh lebih mudah dirawat (highly cohesive). Saat Anda mengerjakan fitur Home, Anda tidak perlu melompat-lompat folder dari ujung atas ke ujung bawah proyek. + +2. Pemisahan Komponen Global vs Lokal +ui/components/: Berisi komponen UI generik yang digunakan di banyak layar, seperti custom button, loading spinner, atau error dialog. + +ui/features/[nama_fitur]/components/: Berisi komponen yang sangat spesifik dan hanya masuk akal jika berada di layar tersebut. Ini mencegah folder global menjadi terlalu penuh. + +1. Paket theme/ yang Sentralized +Saat Anda membuat proyek baru di Android Studio dengan template Compose, folder ui/theme otomatis dibuat. Tetap pertahankan folder ini karena ia menyimpan konfigurasi MaterialTheme (Warna, Tipografi, dan Bentuk) yang membungkus seluruh aplikasi Anda di MainActivity. + +2. Layer Data dan Domain Tetap Bersih dari Compose +Perlu diingat bahwa Jetpack Compose hanyalah toolkit UI. Folder data/ dan domain/ Anda sama sekali tidak boleh mengimpor library Compose (androidx.compose.*). Mereka murni berisi Kotlin standard/coroutine agar kode backend aplikasi Anda tetap bisa diuji (testable) secara independen. From 633dbc701cd8965a874ec4bd170bf66b6b573824 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 13:45:45 +0700 Subject: [PATCH 07/14] feat: implement MainActivity with Jetpack Compose navigation and base configuration --- app/build.gradle.kts | 1 + .../ui/favorite/FavoriteFragment.kt | 94 ----------- .../androidapp/ui/main/MainActivity.kt | 154 ++++++++++++------ .../androidapp/ui/main/MainAdapter.kt | 77 --------- .../androidapp/ui/main/MainFragment.kt | 87 ---------- .../androidapp/ui/main/MainViewModel.kt | 48 ------ app/src/main/res/layout/activity_main.xml | 21 +-- .../res/layout/content_article_vertical.xml | 65 -------- app/src/main/res/layout/fragment_favorite.xml | 42 ----- app/src/main/res/layout/fragment_main.xml | 22 --- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 + 12 files changed, 110 insertions(+), 504 deletions(-) delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt delete mode 100644 app/src/main/res/layout/content_article_vertical.xml delete mode 100644 app/src/main/res/layout/fragment_favorite.xml delete mode 100644 app/src/main/res/layout/fragment_main.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a48f5ee..1c96b14 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,6 +150,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.coil.compose) debugImplementation(libs.androidx.compose.ui.tooling) } \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt deleted file mode 100644 index bebb95a..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/favorite/FavoriteFragment.kt +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.amirisback.androidapp.ui.favorite - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.LinearLayoutManager -import io.github.amirisback.androidapp.common.base.BaseFragment -import io.github.amirisback.androidapp.common.callback.OnItemClickCallback -import io.github.amirisback.androidapp.common.callback.Resource -import io.github.amirisback.androidapp.databinding.FragmentFavoriteBinding -import io.github.amirisback.androidapp.domain.model.MealModel -import io.github.amirisback.androidapp.ui.detail.DetailActivity -import io.github.amirisback.androidapp.ui.main.MainAdapter -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.showToast -import com.frogobox.sdk.ext.visible -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class FavoriteFragment : BaseFragment() { - - private val viewModel : FavoriteViewModel by activityViewModels() - - private val mainAdapter: MainAdapter by lazy { - MainAdapter() - } - - override fun setupViewBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentFavoriteBinding { - return FragmentFavoriteBinding.inflate(inflater, container, false) - } - - override fun setupViewModel() { - viewModel.mealsState.observe(this) { - when (it) { - is Resource.Error -> { - binding.progressView.gone() - requireContext().showToast(it.message.toString()) - } - - is Resource.Loading -> { - binding.progressView.visible() - } - - is Resource.Success -> { - binding.progressView.gone() - it.data?.let { items -> - if (items.isEmpty()) { - binding.tvEmpty.visible() - } else { - binding.tvEmpty.gone() - } - mainAdapter.setItem(items) - } - } - } - } - } - - override fun onViewCreatedExt(view: View, savedInstanceState: Bundle?) { - super.onViewCreatedExt(view, savedInstanceState) - viewModel.getData() - binding.apply { - rv.adapter = mainAdapter - rv.layoutManager = - LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) - - - mainAdapter.setOnItemCallBack(object : OnItemClickCallback { - override fun onItemClick( - view: View, - objects: Any, - position: Int?, - ) { - (objects as MealModel).let { - mActivity.startActivityResultExt(DetailActivity.createIntent(requireContext(), it)) - } - } - }) - - } - } - - - override fun onDestroy() { - super.onDestroy() - viewModel.onClearDisposable() - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt index 1061c13..f6fc243 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt @@ -1,21 +1,51 @@ package io.github.amirisback.androidapp.ui.main -import android.content.res.ColorStateList import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResult import androidx.activity.viewModels +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import io.github.amirisback.androidapp.R import io.github.amirisback.androidapp.common.base.BaseActivity import io.github.amirisback.androidapp.databinding.ActivityMainBinding -import io.github.amirisback.androidapp.ui.favorite.FavoriteFragment +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.detail.DetailActivity import io.github.amirisback.androidapp.ui.favorite.FavoriteViewModel -import com.frogobox.sdk.ext.getColorExt +import io.github.amirisback.androidapp.ui.features.favorite.FavoriteScreen +import io.github.amirisback.androidapp.ui.features.main.MainScreen +import io.github.amirisback.androidapp.ui.features.main.MainViewModel +import io.github.amirisback.init.ui.theme.InitTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : BaseActivity() { - private val favoriteViewModel : FavoriteViewModel by viewModels() + private val favoriteViewModel: FavoriteViewModel by viewModels() + private val mainViewModel: MainViewModel by viewModels() + + enum class Tab(val titleRes: Int, val iconRes: Int) { + MAIN(R.string.title_main, R.drawable.ic_tv), + FAVORITE(R.string.title_fav, R.drawable.ic_favorite) + } override fun setupViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -30,64 +60,84 @@ class MainActivity : BaseActivity() { override fun onCreateExt(savedInstanceState: Bundle?) { super.onCreateExt(savedInstanceState) + enableEdgeToEdge() setupToolbar() - setupBottomNav(binding.framelayoutMainContainer.id) - setupFragment(savedInstanceState) + mainViewModel.searchMeal("Cream") // Trigger search for meals inside MainViewModel + + binding.composeView.setContent { + InitTheme { + MainActivityScreen( + mainViewModel = mainViewModel, + favoriteViewModel = favoriteViewModel, + onItemClick = { meal -> + startActivityResultExt(DetailActivity.createIntent(this, meal)) + } + ) + } + } } private fun setupToolbar() { - supportActionBar?.elevation = 0f + supportActionBar?.hide() } +} - private fun setupFragment(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - binding.bottomNavMainMenu.selectedItemId = R.id.bottom_menu_main - } - } - - private fun setupBottomNav(frameLayout: Int) { - binding.bottomNavMainMenu.apply { - clearAnimation() - - val iconColorStates = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_checked), - intArrayOf(android.R.attr.state_checked) - ), intArrayOf( - getColorExt(R.color.colorTextTitle), - getColorExt(R.color.colorPrimary), +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainActivityScreen( + mainViewModel: MainViewModel, + favoriteViewModel: FavoriteViewModel, + onItemClick: (MealModel) -> Unit +) { + var currentTab by rememberSaveable { mutableStateOf(MainActivity.Tab.MAIN) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = currentTab.titleRes)) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer ) ) - - itemIconTintList = iconColorStates - itemTextColor = iconColorStates - - setOnItemSelectedListener { - when (it.itemId) { - - R.id.bottom_menu_favorite -> { - supportActionBar?.title = getString(R.string.title_fav) - setupChildFragment( - frameLayout, - FavoriteFragment() - ) - return@setOnItemSelectedListener true - } - - R.id.bottom_menu_main -> { - supportActionBar?.title = getString(R.string.title_main) - setupChildFragment( - frameLayout, - MainFragment() - ) - return@setOnItemSelectedListener true - } + }, + bottomBar = { + NavigationBar { + MainActivity.Tab.values().forEach { tab -> + NavigationBarItem( + selected = currentTab == tab, + onClick = { currentTab = tab }, + label = { Text(text = stringResource(id = tab.titleRes)) }, + icon = { + Icon( + painter = painterResource(id = tab.iconRes), + contentDescription = stringResource(id = tab.titleRes) + ) + } + ) + } + } + } + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + when (currentTab) { + MainActivity.Tab.MAIN -> { + MainScreen( + viewModel = mainViewModel, + onItemClick = onItemClick + ) + } + MainActivity.Tab.FAVORITE -> { + FavoriteScreen( + viewModel = favoriteViewModel, + onItemClick = onItemClick + ) } - - false } } - } - } diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt deleted file mode 100644 index a9dca96..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainAdapter.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.amirisback.androidapp.ui.main - -import android.view.LayoutInflater -import android.view.ViewGroup -import io.github.amirisback.androidapp.common.base.BaseAdapter -import io.github.amirisback.androidapp.common.base.BaseViewHolder -import io.github.amirisback.androidapp.common.callback.OnItemClickCallback -import io.github.amirisback.androidapp.databinding.ContentArticleVerticalBinding -import io.github.amirisback.androidapp.domain.model.MealModel -import com.frogobox.sdk.ext.setImageExt - -/** - * Created by faisalamircs on 10/09/2025 - * ----------------------------------------- - * Name : Muhammad Faisal Amir - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - */ - - -class MainAdapter : BaseAdapter() { - - override fun bindVH( - holder: MainHolder, - position: Int, - ) { - holder.bindData(asyncListDiffer.currentList[position], position) - } - - override fun adapterAreItemsTheSame( - oldItem: MealModel, - newItem: MealModel, - ): Boolean { - return oldItem.idMeal == newItem.idMeal - } - - override fun adapterAreContentsTheSame( - oldItem: MealModel, - newItem: MealModel, - ): Boolean { - return oldItem == newItem - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): MainHolder { - return MainHolder( - binding = ContentArticleVerticalBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - onItemClickCallback = onItemClickCallback - ) - } - - inner class MainHolder( - private val binding: ContentArticleVerticalBinding, - private val onItemClickCallback: OnItemClickCallback? = null, - ) : BaseViewHolder(binding.root) { - - override fun bindData(model: MealModel, position: Int?) { - binding.apply { - ivUrl.setImageExt(model.strMealThumb) - tvTitle.text = model.strMeal - tvDescription.text = model.strCategory - tvPublished.text = model.strArea - - root.setOnClickListener { v -> - onItemClickCallback?.onItemClick(v, model, position) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt deleted file mode 100644 index 9beb65c..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.amirisback.androidapp.ui.main - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.LinearLayoutManager -import io.github.amirisback.androidapp.common.base.BaseFragment -import io.github.amirisback.androidapp.common.callback.OnItemClickCallback -import io.github.amirisback.androidapp.common.callback.Resource -import io.github.amirisback.androidapp.databinding.FragmentMainBinding -import io.github.amirisback.androidapp.domain.model.MealModel -import io.github.amirisback.androidapp.ui.detail.DetailActivity -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.showToast -import com.frogobox.sdk.ext.visible -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MainFragment : BaseFragment() { - - private val viewModel: MainViewModel by activityViewModels() - - private val mainAdapter: MainAdapter by lazy { - MainAdapter() - } - - override fun setupViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ): FragmentMainBinding { - return FragmentMainBinding.inflate(inflater, container, false) - } - - override fun setupViewModel() { - viewModel.mealsState.observe(this) { - when (it) { - is Resource.Error -> { - binding.progressView.gone() - requireContext().showToast(it.message.toString()) - } - - is Resource.Loading -> { - binding.progressView.visible() - } - - is Resource.Success -> { - binding.progressView.gone() - it.data?.let { items -> - mainAdapter.setItem(items) - } - } - } - } - } - - override fun onViewCreatedExt(view: View, savedInstanceState: Bundle?) { - super.onViewCreatedExt(view, savedInstanceState) - viewModel.searchMeal("Cream") - binding.apply { - rv.adapter = mainAdapter - rv.layoutManager = - LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) - - - mainAdapter.setOnItemCallBack(object : OnItemClickCallback { - override fun onItemClick( - view: View, - objects: Any, - position: Int?, - ) { - (objects as MealModel).let { - mActivity.startActivityResultExt(DetailActivity.createIntent(requireContext(), it)) - } - } - }) - - } - } - - override fun onDestroy() { - super.onDestroy() - viewModel.onClearDisposable() - } - -} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt deleted file mode 100644 index 76e9c12..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.amirisback.androidapp.ui.main - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import io.github.amirisback.androidapp.common.base.BaseViewModel -import io.github.amirisback.androidapp.common.callback.Resource -import io.github.amirisback.androidapp.domain.source.meal.usecase.MealUseCase -import io.github.amirisback.androidapp.domain.model.MealModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Created by Faisal Amir - * ----------------------------------------- - * Copyright (C) 28/04/2020. - * All rights reserved - * ----------------------------------------- - * Name : Muhammad Faisal Amir - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Frogobox Inc - * - */ - -@HiltViewModel -class MainViewModel @Inject constructor( - private val useCase: MealUseCase, -) : BaseViewModel() { - - private var _mealsState = MutableLiveData>>() - var mealsState: LiveData>> = _mealsState - - fun searchMeal(name: String = "") { - viewModelScope.launch { - useCase.searchMeal( - nameMeal = name - ).onEach { - _mealsState.postValue(it) - }.launchIn(viewModelScope) - } - } - -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fa4488d..e45f0f6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,26 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/content_article_vertical.xml b/app/src/main/res/layout/content_article_vertical.xml deleted file mode 100644 index 1c5e967..0000000 --- a/app/src/main/res/layout/content_article_vertical.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorite.xml b/app/src/main/res/layout/fragment_favorite.xml deleted file mode 100644 index f6e6591..0000000 --- a/app/src/main/res/layout/fragment_favorite.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml deleted file mode 100644 index 1daffcd..0000000 --- a/app/src/main/res/layout/fragment_main.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d747c8..af8c67f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Main Favorite Consumable + Data is empty diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e95e910..21dd2c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ chucker = "4.3.1" mixpanel = "8.8.0" frogoAndroid = "3.0.3" +coil = "2.6.0" [libraries] # Android Kit @@ -65,6 +66,7 @@ github-glide-compiler = { group = "com.github.bumptech.glide", name = "ksp", ver github-balloon = { group = "com.github.skydoves", name = "balloon", version.ref = "balloon" } frogo-android = { group = "com.github.frogobox", name = "frogo-sdk", version.ref = "frogoAndroid" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } # -------------------------------------------------------------------------------------------------- From 03cb1304ffa80f6d4f6d8074e91f602312cd6c51 Mon Sep 17 00:00:00 2001 From: amirisback Date: Fri, 10 Jul 2026 22:07:31 +0700 Subject: [PATCH 08/14] feat: implement AboutUs screen using Jetpack Compose and migrate AboutUsActivity to load it via ComposeView --- .../androidapp/ui/about/AboutUsActivity.kt | 18 +++- .../ui/features/about/AboutUsScreen.kt | 101 ++++++++++++++++++ app/src/main/res/layout/activity_about_us.xml | 49 +++------ prompt_ai/main-to-compose.md | 8 +- 4 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt index 0042a17..3fa8903 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt @@ -3,8 +3,11 @@ package io.github.amirisback.androidapp.ui.about import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.enableEdgeToEdge import io.github.amirisback.androidapp.common.base.BaseActivity import io.github.amirisback.androidapp.databinding.ActivityAboutUsBinding +import io.github.amirisback.androidapp.ui.features.about.AboutUsScreen +import io.github.amirisback.init.ui.theme.InitTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -32,7 +35,20 @@ class AboutUsActivity : BaseActivity() { override fun onCreateExt(savedInstanceState: Bundle?) { super.onCreateExt(savedInstanceState) - setupDetailActivity("") + enableEdgeToEdge() + setupToolbar() + + binding.composeView.setContent { + InitTheme { + AboutUsScreen( + onBackClick = { finish() } + ) + } + } + } + + private fun setupToolbar() { + supportActionBar?.hide() } } diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt new file mode 100644 index 0000000..51b199d --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt @@ -0,0 +1,101 @@ +package io.github.amirisback.androidapp.ui.features.about + +import androidx.compose.foundation.Image +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.amirisback.androidapp.R +import io.github.amirisback.init.ui.theme.InitTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutUsScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.title_about_us)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + }, + modifier = modifier + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_frogobox), + contentDescription = "Logo", + modifier = Modifier.size(150.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.about_frogobox), + color = Color(0xFF00C853), // Green accent color matching design + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = stringResource(id = R.string.about_copyright), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun AboutUsScreenPreview() { + InitTheme { + AboutUsScreen(onBackClick = {}) + } +} diff --git a/app/src/main/res/layout/activity_about_us.xml b/app/src/main/res/layout/activity_about_us.xml index 64df565..e45f0f6 100644 --- a/app/src/main/res/layout/activity_about_us.xml +++ b/app/src/main/res/layout/activity_about_us.xml @@ -1,41 +1,16 @@ - + android:layout_height="match_parent"> - + - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/prompt_ai/main-to-compose.md b/prompt_ai/main-to-compose.md index 1dbf346..2647f05 100644 --- a/prompt_ai/main-to-compose.md +++ b/prompt_ai/main-to-compose.md @@ -19,15 +19,11 @@ Gunakan skill pembaca file / workspace untuk mengambil source code berikut: 1. File Layout XML: -- app\src\main\res\layout\activity_main.xml -- app\src\main\res\layout\fragment_main.xml +- app\src\main\res\layout\activity_about_us.xml 1. File Activity/Fragment Terkait: -- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainActivity.kt -- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainAdapter.kt -- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainFragment.kt -- app\src\main\java\io\github\amirisback\androidapp\ui\main\MainViewModel.kt +- app\src\main\java\io\github\amirisback\androidapp\ui\about\AboutUsActivity.kt # INSTRUKSI MIGRASI From cd31d490283428d91e9337bcb41e662b7b248ed5 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 2 Aug 2026 23:17:41 +0700 Subject: [PATCH 09/14] feat: implement Compose-based UI features and architecture components for improved modularity --- .../androidapp/common/base/BaseAdapter.kt | 117 ------------------ .../androidapp/common/base/BaseViewHolder.kt | 19 --- 2 files changed, 136 deletions(-) delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt delete mode 100644 app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt diff --git a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt deleted file mode 100644 index 1b86188..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseAdapter.kt +++ /dev/null @@ -1,117 +0,0 @@ -package io.github.amirisback.androidapp.common.base - -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import io.github.amirisback.androidapp.common.callback.OnItemClickCallback - -/** Standard BaseAdapter Handling BaseModel List and BaseViewHolder**/ - -abstract class BaseAdapter> : RecyclerView.Adapter() { - - var onItemClickCallback: OnItemClickCallback? = null - - var selectedItem = mutableListOf() - - fun setSelectedItems(items: MutableList) { - selectedItem = items - notifyDataSetChanged() - } - - protected val asyncListDiffer = AsyncListDiffer(this, object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BM & Any, newItem: BM & Any): Boolean { - return adapterAreItemsTheSame(oldItem, newItem) - } - - override fun areContentsTheSame(oldItem: BM & Any, newItem: BM & Any): Boolean { - return adapterAreContentsTheSame(oldItem, newItem) - } - }) - - abstract fun bindVH(holder: T, position: Int) - - abstract fun adapterAreItemsTheSame(oldItem: BM & Any, newItem: BM & Any): Boolean - - abstract fun adapterAreContentsTheSame(oldItem: BM & Any, newItem: BM & Any): Boolean - - override fun getItemCount(): Int { - return asyncListDiffer.currentList.size - } - - fun getLastItemPosition(): Int { - return if (itemCount == 0) { - 0 - } else { - itemCount - 1 - } - } - - override fun onBindViewHolder(holder: T, position: Int) { - bindVH(holder, position) - } - - open fun setOnItemCallBack(onItemClickCallback: OnItemClickCallback) { - this.onItemClickCallback = onItemClickCallback - } - - open fun getItem(): MutableList = asyncListDiffer.currentList.toMutableList() - - open fun setItem(item: List) { - if (item.isEmpty()) { - asyncListDiffer.submitList(listOf()) - } else { - asyncListDiffer.submitList(item.map { it }) - } - } - - open fun clearItems() { - asyncListDiffer.submitList(null) - } - - open fun insert(item: BM, position: Int) { - val listItems = asyncListDiffer.currentList.toMutableList() - listItems.add(position + 1, item) - val changedCount = itemCount - position + 1 - asyncListDiffer.submitList(listItems) { - // Trigger onBindViewHolder for the rest of the items that moved to the end of the list - notifyItemRangeChanged(position, changedCount) - } - - } - - open fun delete(item: BM) { - val listItems = asyncListDiffer.currentList.toMutableList() - val position = listItems.indexOf(item) - listItems.remove(item) - val changedCount = itemCount - position - asyncListDiffer.submitList(listItems) { - // Triggered once deletion is done and notifyItemRangeRemoved has been called - notifyItemRangeChanged(position, changedCount) - } - } - - open fun delete(position: Int) { - val listItems = asyncListDiffer.currentList.toMutableList() - listItems.removeAt(position) - val changedCount = itemCount - position - asyncListDiffer.submitList(listItems) { - // Triggered once deletion is done and notifyItemRangeRemoved has been called - notifyItemRangeChanged(position, changedCount) - } - } - - open fun moveItem(from: Int, to: Int) { - val listItems = asyncListDiffer.currentList.toMutableList() - val fromLocation = listItems[from] - listItems.removeAt(from) - if (to < from) { - listItems.add(to + 1, fromLocation) - } else { - listItems.add(to - 1, fromLocation) - } - asyncListDiffer.submitList(listItems) { - notifyItemMoved(from, to) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt deleted file mode 100644 index 14e836e..0000000 --- a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseViewHolder.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.amirisback.androidapp.common.base - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -/** RV ViewHolder Base **/ -abstract class BaseViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) { - abstract fun bindData(model: BaseModel, position: Int? = -1) -} - -abstract class CheckedViewHolder(rootView: View) : - BaseViewHolder(rootView) { - abstract fun bindData(model: BaseModel, position: Int?, checked: Boolean?) -} - -abstract class EditorViewHolder(rootView: View) : - BaseViewHolder(rootView) { - abstract fun bindData(model: BaseModel, position: Int?, onChange: (String) -> Unit) -} \ No newline at end of file From b970733ceb5fe32e1bac01ecdfb8ee26bd6eaddf Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 2 Aug 2026 23:17:49 +0700 Subject: [PATCH 10/14] feat: implement core UI components, screens, and base architecture for About, Detail, and Main features using Jetpack Compose. --- .agents/skills/adaptive/SKILL.md | 301 ------- .../adaptive/flexbox/container-behavior.md | 112 --- .../layouts/adaptive/flexbox/get-started.md | 69 -- .../compose/layouts/adaptive/flexbox/index.md | 81 -- .../layouts/adaptive/flexbox/item-behavior.md | 170 ---- .../adaptive/grid/container-properties.md | 320 -------- .../layouts/adaptive/grid/get-started.md | 51 -- .../ui/compose/layouts/adaptive/grid/index.md | 73 -- .../layouts/adaptive/grid/item-properties.md | 168 ---- .../layouts/adaptive/mediaquery/index.md | 329 -------- .../develop/ui/compose/tooling/debug.md | 114 --- .../recipes/material-listdetail.md | 141 ---- .agents/skills/agp-9-upgrade/SKILL.md | 103 --- .../agp-9-upgrade/references/buildconfig.md | 54 -- .../agp-9-upgrade/references/ksp-kapt.md | 39 - .../references/paparazzi-gradle-9.md | 30 - .../agp-9-upgrade/references/recipes.md | 62 -- .agents/skills/edge-to-edge/SKILL.md | 426 ---------- .agents/skills/jetpack-compose-m3/SKILL.md | 281 ------- .../wearables/compose/migrate-to-material3.md | 677 ---------------- .../SKILL.md | 124 --- .../analysis-of-the-project-and-layout.md | 42 - .../migrate-xml-theme-to-compose.md | 171 ---- .../interoperability-apis/compose-in-views.md | 299 ------- .../interoperability-apis/views-in-compose.md | 286 ------- ...setup-compose-dependencies-and-compiler.md | 197 ----- .../identify-optimal-xml-candidate.md | 31 - .../references/xml-layout-migration.md | 86 -- .agents/skills/navigation-3/SKILL.md | 112 --- .../guide/navigation/navigation-3/index.md | 38 - .../navigation-3/migration-guide.md | 498 ------------ .../navigation-3/recipes/animations.md | 147 ---- .../navigation/navigation-3/recipes/basic.md | 89 --- .../navigation-3/recipes/basicdsl.md | 85 -- .../navigation-3/recipes/basicsaveable.md | 90 --- .../navigation-3/recipes/bottomsheet.md | 195 ----- .../navigation-3/recipes/common-ui.md | 200 ----- .../navigation-3/recipes/conditional.md | 230 ------ .../recipes/deeplinks-advanced.md | 155 ---- .../navigation-3/recipes/deeplinks-basic.md | 744 ------------------ .../navigation/navigation-3/recipes/dialog.md | 107 --- .../recipes/material-listdetail.md | 141 ---- .../recipes/material-supportingpane.md | 145 ---- .../navigation-3/recipes/modular-hilt.md | 283 ------- .../navigation-3/recipes/modular-koin.md | 287 ------- .../recipes/multiple-backstacks.md | 436 ---------- .../navigation-3/recipes/passingarguments.md | 371 --------- .../navigation-3/recipes/results-event.md | 272 ------- .../navigation-3/recipes/results-state.md | 266 ------- .../navigation-3/recipes/scenes-listdetail.md | 435 ---------- .../navigation-3/recipes/scenes-twopane.md | 244 ------ .../navigation/type-safe-destinations.md | 129 --- .agents/skills/r8-analyzer/SKILL.md | 62 -- .../references/CONFIGURATION-ANALYZER.md | 287 ------- .../r8-analyzer/references/CONFIGURATION.md | 44 -- .../references/KEEP-RULES-IMPACT-HIERARCHY.md | 83 -- .../r8-analyzer/references/REDUNDANT-RULES.md | 222 ------ .../references/REFLECTION-GUIDE.md | 139 ---- .../r8-analyzer/references/REPORT_FORMAT.md | 51 -- .../enable-app-optimization.md | 198 ----- .../testing/other-components/ui-automator.md | 312 -------- .agents/skills/styles/SKILL.md | 226 ------ .../ui/compose/designsystems/custom.md | 459 ----------- .../develop/ui/compose/styles/fundamentals.md | 421 ---------- .../ui/compose/styles/state-animations.md | 461 ----------- .../ui/compose/styles/styles-vs-modifiers.md | 48 -- .../develop/ui/compose/styles/theming.md | 257 ------ AGENTS.md | 30 + .../androidapp/common/base/BaseActivity.kt | 5 +- .../androidapp/common/base/BaseFragment.kt | 10 +- .../androidapp/ui/about/AboutUsActivity.kt | 25 +- .../androidapp/ui/components/AppTopAppBar.kt | 57 ++ .../androidapp/ui/components/CommonState.kt | 50 ++ .../androidapp/ui/components/MealCard.kt | 98 +++ .../androidapp/ui/detail/DetailActivity.kt | 85 +- .../ui/features/about/AboutUsScreen.kt | 27 +- .../ui/features/detail/DetailScreen.kt | 172 ++++ .../ui/features/favorite/FavoriteScreen.kt | 24 +- .../androidapp/ui/features/main/MainScreen.kt | 104 +-- .../androidapp/ui/main/MainActivity.kt | 46 +- app/src/main/res/layout/activity_about_us.xml | 16 - app/src/main/res/layout/activity_detail.xml | 79 -- app/src/main/res/layout/activity_main.xml | 16 - .../res/layout/content_article_horizontal.xml | 73 -- app/src/main/res/layout/content_category.xml | 41 - .../main/res/layout/fragment_consumable.xml | 60 -- gradle/libs.versions.toml | 22 +- 87 files changed, 507 insertions(+), 14339 deletions(-) delete mode 100644 .agents/skills/adaptive/SKILL.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md delete mode 100644 .agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md delete mode 100644 .agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md delete mode 100644 .agents/skills/agp-9-upgrade/SKILL.md delete mode 100644 .agents/skills/agp-9-upgrade/references/buildconfig.md delete mode 100644 .agents/skills/agp-9-upgrade/references/ksp-kapt.md delete mode 100644 .agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md delete mode 100644 .agents/skills/agp-9-upgrade/references/recipes.md delete mode 100644 .agents/skills/edge-to-edge/SKILL.md delete mode 100644 .agents/skills/jetpack-compose-m3/SKILL.md delete mode 100644 .agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md delete mode 100644 .agents/skills/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md delete mode 100644 .agents/skills/navigation-3/SKILL.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/index.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md delete mode 100644 .agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md delete mode 100644 .agents/skills/r8-analyzer/SKILL.md delete mode 100644 .agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md delete mode 100644 .agents/skills/r8-analyzer/references/CONFIGURATION.md delete mode 100644 .agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md delete mode 100644 .agents/skills/r8-analyzer/references/REDUNDANT-RULES.md delete mode 100644 .agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md delete mode 100644 .agents/skills/r8-analyzer/references/REPORT_FORMAT.md delete mode 100644 .agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md delete mode 100644 .agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md delete mode 100644 .agents/skills/styles/SKILL.md delete mode 100644 .agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md delete mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md delete mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md delete mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md delete mode 100644 .agents/skills/styles/references/android/develop/ui/compose/styles/theming.md create mode 100644 AGENTS.md create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/components/AppTopAppBar.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/components/CommonState.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/components/MealCard.kt create mode 100644 app/src/main/java/io/github/amirisback/androidapp/ui/features/detail/DetailScreen.kt delete mode 100644 app/src/main/res/layout/activity_about_us.xml delete mode 100644 app/src/main/res/layout/activity_detail.xml delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/content_article_horizontal.xml delete mode 100644 app/src/main/res/layout/content_category.xml delete mode 100644 app/src/main/res/layout/fragment_consumable.xml diff --git a/.agents/skills/adaptive/SKILL.md b/.agents/skills/adaptive/SKILL.md deleted file mode 100644 index 566374a..0000000 --- a/.agents/skills/adaptive/SKILL.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -name: adaptive -description: Instructions to make or update an app's UI so that it adapts to different - Android devices including phones, tablets, foldables, laptops, desktop, TV, Auto - and XR. It includes how to handle different window sizes, pointing devices (such - as mouse) and text entry devices (such as keyboard) using the Compose MediaQuery - API. It also covers multi-pane layouts using Navigation3 Scenes, adaptive UI components - (such as buttons) with varying target sizes, and adaptive layouts (including navigation - areas - nav rails and nav bars) using the Compose Grid and FlexBox APIs. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-07-02' - keywords: - - android - - ui - - adaptive - - Grid - - FlexBox - - MediaQuery - - navigation ---- - -## Prerequisites - -The app must: - -- Use Compose for all screens. If it's still using Fragments or Views, suggest using the XML to Compose skill to migrate those screens. -- Use Jetpack Navigation 3. If it doesn't, suggest the Navigation 3 skill to migrate the app. - -## Workflow to make an app adaptive - -To make an app adaptive, follow these steps or a subset of them adapting to the -task. - -- Step 1: Verify current UI -- Step 2: Make the navigation bar adaptive -- Step 3: Add multi-pane layouts -- Step 4: Make vertical lists adaptive by changing the number of columns -- Step 5: Hide app bars when scrolling - -## Step 1. Verify current UI - -Ensure that screenshot tests exist to verify the current UI on different form -factors. If they don't exist, add the [Compose Preview Screenshot Testing -tool](references/android/develop/ui/compose/tooling/debug.md). Use the following annotation to create previews for all the major form -factors. For example: - - -```kotlin -@Preview(name = "Phone", device = Devices.PHONE, showBackground = true) -@Preview(name = "Foldable", device = Devices.FOLDABLE, showBackground = true) -@Preview(name = "Tablet", device = Devices.TABLET, showBackground = true) -@Preview(name = "Desktop", device = Devices.DESKTOP, showBackground = true) -annotation class FormFactorPreviews - -@PreviewTest -@FormFactorPreviews -@Composable -fun FeedScreenPreview() { - SnippetsTheme { - Box { - Text("My Screen") - } - } -} -``` - -
- -## Step 2. Make the navigation bar adaptive - -Bottom navigation bars are optimized for touch input when the user is holding a -phone in portrait mode. On larger screen hand-held devices, like tablets and -unfolded foldables, the navigation area must be accessible from the edge of the -screen (navigation rail). - -If you need to provide more screen space for the content, hide the -navigation area. Examples of this include: - -- Hiding the navigation bar when the user scrolls down and showing it again when the user scrolls up. The assumption is that when the user is scrolling down, they are consuming content but when scrolling up they are trying to navigate away from that content. -- Hiding the navigation area when its content is distracting. For example, in camera previews or when displaying a full-screen photo. - -When the detail screen is displayed full-screen on mobile, full-screen mode must -be deactivated on larger screens. - -Steps to migrate: - -- Locate the existing navigation bar. -- Convert each item to a `NavigationSuiteItem`. -- Identify whether the navigation bar's visibility changes. For example, if it is wrapped with an `AnimatedContent` or `AnimatedVisibility` composable. If so, follow the guidance in the "Control navigation area visibility". -- Replace the container that held the navigation bar (often a `Scaffold`) with `NavigationSuiteScaffold` from the Material 3 adaptive layouts library. -- Supply the navigation items using the `navigationItems` parameter of `NavigationSuiteScaffold`. - -### Step 2.1. Control navigation area visibility - -If the navigation bar's visibility changes - it is hidden under certain -scenarios or on certain screens - this behavior must be maintained with the -adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state` -parameter. - -Steps to migrate: - -- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. Use `isNavBarVisible` or `shouldShowNavBar` as the variable name. -- Create an instance of `NavigationSuiteScaffoldState` using `rememberNavigationSuiteScaffoldState()` and pass it to `NavigationSuiteScaffold`. -- When the navigation area visibility changes, use a `LaunchedEffect` to call `show` or `hide` on the `NavigationSuiteScaffoldState`. - -For example: - - -```kotlin -// Pass this variable to any composable that needs to control the navigation area visibility -var isNavBarVisible by remember { mutableStateOf(true) } -val scaffoldVisibilityState = rememberNavigationSuiteScaffoldState() - -NavigationSuiteScaffold( - navigationSuiteItems = navItems, - state = scaffoldVisibilityState -) { - // Main content -} - -LaunchedEffect(isNavBarVisible){ - if (isNavBarVisible) { - scaffoldVisibilityState.show() - } else { - scaffoldVisibilityState.hide() - } -} -``` - -
- -## Step 3. Add multi-pane layouts using Navigation 3 Scenes - -Analyze the codebase looking for related screens - tapping on something in one -screen opens another screen that shows information related to the first. There -are two canonical screen relationships: list-detail and supporting pane. - -IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement -multi-pane layouts. Do not use `ListDetailPaneScaffold` or -`SupportingPaneScaffold`. - -### Step 3.1. List-detail - -#### Identify the list and detail screens - -List-detail layouts display a list of items (this is the list screen) and -clicking on an item opens a new screen that shows more details about that item -(the detail screen). - -Typical usage includes productivity apps like email, notes, and messaging. - -Unless requested explicitly, avoid this pattern when the detail content requires -substantial screen space (e.g., images or media that benefits from a full-screen -presentation). - -#### Add a Material list-detail SceneStrategy - -- Add the `androidx.compose.material3.adaptive:adaptive-navigation3` library -- Create an `androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy` using `rememberListDetailSceneStrategy` -- Pass the `ListDetailSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter - -#### Use metadata to identify the list and detail screens - -- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = { - })`. -- Use the `detailPlaceholder` parameter to add a placeholder on the detail screen when no list items are selected. -- Add metadata to the detail entry using `ListDetailSceneStrategy.detailPane()`. - -#### Important considerations - -- When a detail screen displays its content full-screen on mobile (content fills the entire screen, bars or rails are hidden), full-screen mode must be deactivated if it's part of a list-detail layout. -- Detail screens must not show a back arrow when on a list-detail layout. - -For a reference implementation, check the [Nav3 **Material** List Detail -recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md). - -### Step 3.2. Supporting pane - -Identify supporting pane screens where a main screen displays a single item, and -selecting it opens a "supporting screen" with more details. The supporting -screen complements the main screen and is shown in a supporting pane. - -#### Add a Material supporting pane `SceneStrategy` - -- If you haven't already, add the `androidx.compose.material3.adaptive:adaptive-navigation3` library -- Create an `androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy` using `rememberSupportingPaneSceneStrategy` -- Pass the `SupportingPaneSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter - -#### Use metadata to identify the main and supporting screens - -- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the main entry using `SupportingPaneSceneStrategy.mainPane()` -- Add metadata to the supporting entry using `SupportingPaneSceneStrategy.supportingPane()` - -### Step 3.3. Run screenshot tests - -If you have made changes, record new reference files. Ask the user to visually -verify that the new layouts are correct. - -## Step 4. Make vertical lists adaptive by changing the number of columns - -### Step 4.1. Make lazy lists adaptive - -Look for the following vertical list composables: `LazyColumn`, -`LazyVerticalGrid`, `LazyVerticalStaggeredGrid`. - -Steps to migrate: - -- Choose a suitable minimum width in dp for the column. The item must be clearly visible to the user at this width. -- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction later -- For `LazyVerticalGrid`: change the `columns` parameter to use `GridCells.Adaptive(.dp)` -- For `LazyVerticalStaggeredGrid`: change the `columns` parameter to use `StaggeredGridCells.Adaptive(.dp)` - -### Step 4.2. Migrate non-lazy lists to Grid - -WARNING: Grid is an experimental API available from Compose 1.11.0-beta01. -Confirm with the user that they are happy to use an experimental API in their -codebase. - -Look for any `Column` that contains multiple items of the same type and replace -it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy -layout. Do not place `Grid` inside the existing `Column`. Completely replace it. - -`Grid` is configured by supplying a lambda (an extension function on -`GridConfigurationScope`) to its `config` parameter. Inside the lambda, -`constraints` provides the minimum and maximum dimensions of the grid container -and can be used to change the number of rows and columns based on the available -size. For example, the following code configures `Grid` such that when the -available width is: - -- less than 800dp, a 2x4 grid is used -- 800dp or more, a 4x2 grid is used - - -```kotlin -Grid( - config = { - val maxWidthDp = constraints.maxWidth.toDp() - val (cols, rows) = if (maxWidthDp < 800.dp){ - 2 to 4 - } else{ - 4 to 2 - } - - val gapSizeDp = 8.dp - val cellSize = ((maxWidthDp - (gapSizeDp * (cols - 1))) / cols).coerceAtLeast(0.dp) - repeat(cols) { column(cellSize) } - repeat(rows) { row(cellSize) } - gap(gapSizeDp) - } -) { /** items **/ } -``` - -
- -`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)` -annotation to any function that uses it. - -## Step 5: Hide App Bars when scrolling - -In an app with multiple top-level destinations, each screen must manage its own -app bar state independently. There are two main scroll behaviors: - -- `exitUntilCollapsedScrollBehavior`: Hides on scroll down, stays hidden while you scroll up until you reach the very top (0 offset). -- `enterAlwaysScrollBehavior`: Hides on scroll down, shows immediately on scroll up. - -## Final step: Build and test - -Build the app and run the local tests. If the project has screenshot tests, run -them but DO NOT update the reference images. Prompt the user to do this after -they have viewed the screenshot diffs. - -## Additional documentation for experimental adaptive APIs - -The following APIs are available from Compose 1.11.0-beta01. - -### FlexBox - -Check the FlexBox documentation: - -- [Overview](references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md) -- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md) -- [Set container behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md) -- [Set item behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md) - -## MediaQuery - -Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's -screen size, pointer precision, keyboard type, whether it has cameras or -microphones, and other device capabilities. - -## Grid - -Check the Grid documentation when you need to display a fixed number of items in -a grid layout: - -- [Overview](references/android/develop/ui/compose/layouts/adaptive/grid/index.md) -- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md) -- [Set container properties](references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md) -- [Set item properties](references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md) diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md deleted file mode 100644 index 41112fc..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md +++ /dev/null @@ -1,112 +0,0 @@ -To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` -block and supply it using the `config` parameter. - - -```kotlin -FlexBox( - config = { - direction(FlexDirection.Column) - wrap(FlexWrap.Wrap) - alignItems(FlexAlignItems.Center) - alignContent(FlexAlignContent.SpaceAround) - justifyContent(FlexJustifyContent.Center) - gap(16.dp) - } -) { // child items -} -``` - -
- -Use `FlexBoxConfig` to define the layout direction, wrapping behavior, -alignment, and gaps between items. - -## Layout direction - -The `direction` function sets the main axis, which dictates the direction -items are laid out in. It accepts the following values: - -- `Row` (default): Sets the main axis to be horizontal. In left-to-right locales this will be left-to-right, with the opposite in right-to-left. -- `RowReverse`: Reverses the direction of `Row`. -- `Column`: Sets the main axis to be vertical, top-to-bottom. -- `ColumnReverse`: Reverses the direction of `Column`. - -## Align items and distribute extra space - -The following sections describe how to align items and distribute extra space -along the main and cross axes. - -### Along the main axis - -Use `justifyContent` to distribute items along the main axis. The following -table shows the behavior when the direction is `Row`. - -|---|---| -| | ![Illustration of a horizontal main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/main-axis.png) | -| `Start` | ![Items aligned to the start of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-start.png) | -| `Center` | ![Items aligned to the center of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-center.png) | -| `End` | ![Items aligned to the end of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-end.png) | -| `SpaceBetween` | ![Items distributed along the main axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacebetween.png) | -| `SpaceAround` | ![Items distributed along the main axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacearound.png) | -| `SpaceEvenly` | ![Items distributed along the main axis with space evenly around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spaceevenly.png) | - -### Along the cross axis - -Use `alignItems` to align items along the cross axis within a single line. This -behavior can be overridden by individual items using the -[`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment). - -The following images show the behavior when the direction is `Row`: - -|---|---|---|---|---|---| -| ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-start.png) | ![Items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-end.png) | ![Items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-center.png) | ![Items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-stretch.png) | ![Items aligned to their baseline along the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-baseline.png) | -| | `Start` | `End` | `Center` | `Stretch` | `Baseline` | - -Use `alignContent` to align lines to the cross axis and to distribute extra -space between lines. This property only applies when there are multiple lines -(wrapping is enabled). The following images show the behavior when the direction -is `Row`: - -|---|---|---|---|---|---|---| -| ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Multiple lines of items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-start.png) | ![Multiple lines of items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-end.png) | ![Multiple lines of items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-center.png) | ![Multiple lines of items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-stretch.png) | ![Multiple lines of items distributed along the cross axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacebetween.png) | ![Multiple lines of items distributed along the cross axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacearound.png) | -| | `Start` | `End` | `Center` | `Stretch` | `SpaceBetween` | `SpaceAround` | - -## Wrap items - -Wrapping lets a `FlexBox` container become multi-line, moving items that don't -fit onto a new row or column along the cross-axis. Configure wrapping behavior -using `wrap`. - -|---|---| -| **`FlexWrap` value** | **Example using direction `Row`** | -| `NoWrap` (default): Prevents items from wrapping. Items overflow if the main size is insufficient. | ![Items in a single line overflowing the container because wrapping is disabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-1.png) | -| `Wrap`: When there is insufficient space for an item (plus any [gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps)), a new line is created in the direction of the cross axis. For example, if the direction is `Row`, a new line is added **below**. | ![Items wrapping onto a new line below because wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-2.png) | -| `WrapReverse`: The same as `Wrap`, except the new line is added in the opposite direction to the cross axis. For example, if the direction is `Row`, a new line is added **above**. | ![Items wrapping onto a new line above because reverse wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-3.png) | - -The following example shows how the `FlexBox` wrapping algorithm works. The -`FlexBox` container has a main size of `100dp`, with `wrap` set to -`FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`, -`40dp`, and `50dp`, respectively. - -There is `100dp` available space in the line. Child 1 is `20dp`. -There is space, so Child 1 is placed into the line. -![First item placed in the FlexBox container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-1.png) **Figure 1.** First item placed in the `FlexBox` container. - -There is `80dp` available space in the line. The gap is `8dp`. Child 2 is -`40dp`. The required space is `48dp`. There is space, so the gap and Child 2 -are placed into the line. -![Second item placed in the FlexBox container after the first item.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-2.png) **Figure 2.** Second item placed in the `FlexBox` container after the first item. - -There is `32dp` available space in the line. The gap is `8dp`. Child 3 is -`50dp`. The required space is `58dp`. There is not enough space in the current -line, so Child 3 is placed in a new line. -![Third item placed on a new line because it doesn't fit on the first line.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-3.png) **Figure 3.** Third item placed on a new line because it doesn't fit on the first line. - -## Add gaps between items - -Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful -to avoid adding spacing modifiers to children. - -|---|---|---| -| ![Row gap adds vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-1.png) | ![Column gap adds horizontal space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-2.png) | ![Gap adds both horizontal and vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-3.png) | -| `rowGap` adds vertical space between items and lines. | `columnGap` adds horizontal space between items and lines. | `gap` is a convenience function that adds both `columnGap` and `rowGap`. | \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md deleted file mode 100644 index 8b00b01..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md +++ /dev/null @@ -1,69 +0,0 @@ -This page describes how to implement basic `FlexBox` layouts. - -## Set up project - -1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's - `lib.versions.toml`. - - [versions] - compose = "1.12.0-beta02" - - [libraries] - androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } - -2. Add the library dependency to your app's `build.gradle.kts`. - - dependencies { - implementation(libs.androidx.compose.foundation.layout) - } - -## Create basic FlexBox layouts - -**Example 1** : `FlexBox` lays out two `Text` elements that are centrally -aligned. - - -```kotlin -FlexBox( - config = { - direction(FlexDirection.Column) - alignItems(FlexAlignItems.Center) - } -) { - Text(text = "Hello", fontSize = 48.sp) - Text(text = "World!", fontSize = 48.sp) -} -``` - -
- -![Hello World text composables stacked on top of each other in a basic FlexBox implementation.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox.png) - -**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally -to fill the available space on each row. There is an `8.dp` -gap, both vertically and horizontally, between the items. - - -```kotlin -FlexBox( - config = { - wrap(FlexWrap.Wrap) - gap(8.dp) - } -) { - // All boxes have an intrinsic width of 100.dp - // Some grow to fill any remaining space on the row. - RedRoundedBox() - BlueRoundedBox() - GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) }) - OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) }) - PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) }) -} -``` - -
- -![Two rows of colored items, with three unequally sized items distributed across the top row and two unequally sized items across the bottom row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox-2.png) - -To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set -item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior). \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md deleted file mode 100644 index ddda0de..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md +++ /dev/null @@ -1,81 +0,0 @@ -> [!NOTE] -> **Note:** FlexBox is an experimental API and is likely to change in the future. To use it, annotate your code with `@ExperimentalFlexBoxApi`. Please file any issues or feedback on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&title=%5BFlexBox%5D). - -[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can -resize, wrap, align, and distribute space among items to optimally fill the -available space. It's a useful layout for different sized items and for resizing -items when the available space changes. - -With `FlexBox`, you can: - -- Control how items grow and shrink to fill the available space -- Wrap items onto new rows or columns when there isn't enough space for them -- Distribute extra space between items using convenient presets - -## When to use FlexBox - -`FlexBox` is usually used to display a small number of items *within* an -overall screen layout. For an overall screen layout, -`Grid` is usually a better choice. `FlexBox` does not support lazy-loading of -items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you -need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`. - -## Terminology and concepts - -> [!IMPORTANT] -> **Key Point:** `FlexBox` is heavily influenced by the [CSS Flexible Box Layout specification](https://www.w3.org/TR/css-flexbox-1/) and has almost identical concepts, terminology, and behavior. If you're familiar with `display: flex`, you'll find `FlexBox`'s properties and behavior almost identical. - -`FlexBox` lays out its items in either horizontal or vertical *lines* . This -direction of these lines establishes the *main axis* . 90 degrees to the main -axis is the *cross axis* . The length of the `FlexBox` along the main axis is -known as the *main size* . The corresponding cross axis length is known as the -*cross size* . These sizes and axes form the basis of `FlexBox`'s behavior. - - -![FlexBox with horizontal main axis and vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-row-2.png) **Figure 1.** Axes and sizes when the `FlexBox` direction is `Row`. ![FlexBox with vertical main axis and horizontal cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-column.png) **Figure 2.** Axes and sizes when the `FlexBox` direction is `Column`. - -
- -### Apply properties - -You can apply `FlexBox` properties in two ways: - -- To the `FlexBox` container using `FlexBox(config)` -- To an item inside the `FlexBox` using `Modifier.flex` - -| **Container properties (`config`**) | **Item properties (`Modifier.flex`**) | -|---|---| -| - `direction` - the item layout direction - `wrap` - whether to wrap items if the **main size** is insufficient - `justifyContent` - how to **distribute** items along the **main axis** - `alignItems` - how to **align** items along the **cross axis** - `alignContent` - how to distribute extra space from the **cross size** when there are multiple lines - `rowGap` / `columnGap` - adds space between items and lines See [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) for more information about these properties. | - `basis` - the size of the item before any extra space from the **main size** is distributed - `grow` - the share of extra space from the **main size** that this item should receive - `shrink` - the share of space deficit from the **main size** that this item should receive - `alignSelf` - how to distribute extra space from the **cross size** to this item, overrides `alignItems` - `order` - controls the layout order See [Set item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior) for more information about these properties. | - -### Understand the `FlexBox` layout algorithm - -One of `FlexBox`'s most powerful features is its ability to resize its children -to best fit the space available to it. Understanding how `FlexBox` does this can -help you set `FlexBox` properties to optimize your UI for all possible sizes. - -`FlexBox`'s layout algorithm works in the following way: - -1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size) - to calculate its initial size along the main axis before any extra space is - distributed. - -2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if - present. - -3. **Build lines** : For each child, check if its initial size plus - [`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line. - If so, place this child into the line. If not, place it onto a new line if - [wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line - where it will overflow (it will be partially obscured by the edge of the - container). - -4. **Align or resize items in the main axis** : For each line, distribute extra - space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or - [aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them. - -5. **Align or resize items in the cross axis** : For each line, distribute extra - space to or between items and lines by [stretching or aligning - them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis). - -Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to -create a basic `FlexBox`. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md deleted file mode 100644 index e4a5c0f..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md +++ /dev/null @@ -1,170 +0,0 @@ -Use `Modifier.flex` to control how an item changes size, order, and is aligned -inside a `FlexBox`. - -## Item size - -Use the `basis`, `grow`, and `shrink` functions to control an item's size. - - -```kotlin -FlexBox { - RedRoundedBox( - modifier = Modifier.flex { - basis(FlexBasis.Auto) - grow(1.0f) - shrink(0.5f) - } - ) -} -``` - -
- -### Set initial size - -Use `basis` to specify the item's initial size before any extra space is -distributed. You can think of this as the item's *preferred* size. - -|---|---|---|---| -| **Value type** | **Behavior** | **Code snippet** Note: The boxes have a maximum intrinsic size of `100dp` | **Example using container width `600dp`** | -| `Auto` (default) | Use the item's maximum intrinsic size. For example, a `Text` composable's maximum intrinsic width is the width of all its text on a single line - no wrapping. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) BlueRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) } ``` | ![Items sized based on their intrinsic size using basis Auto.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-1.png) | -| Fixed `dp` | A fixed size in Dp. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(200.dp) } ) BlueRoundedBox( Modifier.flex { basis(100.dp) } ) } ``` | ![Items sized to a fixed dp value using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-2.png) | -| Percentage | A percentage of the container size. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(0.7f) } ) BlueRoundedBox( Modifier.flex { basis(0.3f) } ) } ``` | ![Items sized as a percentage of container size using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-3.png) | - -If the basis value is less than the item's intrinsic minimum size, the intrinsic -minimum size is used instead. For example, if a `Text` item that contains a word -requires `50dp` to display, but also has `basis = 10.dp`, a -value of `50dp` is used. - -### Grow items when there's space - -Use `grow` to specify how much an item grows when there is extra space. This is -space remaining in the `FlexBox` container after all the items' `basis` values -have been added up. The `grow` value indicates *how much* of the extra space a -given child will receive, relative to its siblings. By default, items won't -grow. - -The following example shows a `FlexBox` with three child items. Each has a basis -value of `100dp`. The first child has a positive `grow` value. Since there is -only one child with a `grow` value, the actual value is irrelevant - as long as -it's positive, the child receives all the extra space. - -The images show the `FlexBox` behavior when its container size is `600dp`. - -|---|---| -| ```kotlin FlexBox { RedRoundedBox( title = "400dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox(title = "100dp") GreenRoundedBox(title = "100dp") } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-1.png) Child 1 grows by `300dp` to fill the extra space. ![First item grows to fill 300dp of extra space.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-2.png) | - -In the following example, the container size and `basis` size are the same. The -difference is that each child has a different `grow` value. - -|---|---| -| ```kotlin FlexBox { RedRoundedBox( title = "150dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox( title = "200dp", modifier = Modifier.flex { grow(2f) } ) GreenRoundedBox( title = "250dp", modifier = Modifier.flex { grow(3f) } ) } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth, with different grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-3.png) The total grow value is 6. Child 1 grows by (1 / 6) \* 300 = `50dp` Child 2 grows by (2 / 6) \* 300 = `100dp` Child 3 grows by (3 / 6) \* 300 = `150dp` ![Items grow to fill 300dp of extra space based on relative grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-4.png) | - -### Shrink items when there's insufficient space - -Use `shrink` to specify how much an item shrinks when the `FlexBox` container -has insufficient space for all the items. `shrink` works the same way as `grow` -except that, instead of distributing *extra space* to items, the *space deficit* -is distributed to items. The `shrink` value specifies how much of the space -deficit the item receives, or rather, how much the item will shrink by. By -default, items have a `shrink` value of `1f`, meaning they shrink equally. - -The following example shows two `Text` composables with the same text. The first -child has a shrink value of `1f`, meaning it shrinks to absorb all the space -deficit. - - -```kotlin -FlexBox { - Text( - "The quick brown fox", - fontSize = 36.sp, - modifier = Modifier - .background(PastelRed) - .flex { shrink(1f) } - ) - Text( - "The quick brown fox", - fontSize = 36.sp, - modifier = Modifier - .background(PastelBlue) - .flex { shrink(0f) } - ) -} -``` - -
- -As the container size shrinks, Child 1 shrinks. - -|---|---| -| **Container size** | **FlexBox UI** | -| `700dp` | ![Two items in a 700dp container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-1.png) | -| `500dp` | ![First item shrinks as container size reduces to 500dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-2.png) | -| `450dp` | ![First item shrinks further as container size reduces to 450dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-3.png) | - -## Item alignment - -Use `alignSelf` to control how an item is aligned to the cross axis. This -overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It -has all the same possible values, with the addition of `Auto` which inherits the -behavior of the `FlexBox` container. - -For example, this `FlexBox` has `alignItems` set to `Start` and five children -which override the cross axis alignment. - - -```kotlin -FlexBox( - config = { - alignItems(FlexAlignItems.Start) - } -) { - RedRoundedBox() - BlueRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Center) }) - GreenRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.End) }) - PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) }) - OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) }) -} -``` - -
- -![Five children of varying sizes overriding the alignItems property.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/item-alignment.png) - -## Item order - -By default, `FlexBox` lays out items in the order that they are declared in -code. Override this behavior using `order`. - -The default value for `order` is zero, and `FlexBox` sorts items based on this -value in ascending order. Any items that have the same `order` value are -laid out in the same order they are declared in. Use negative and positive -`order` values to move items to the start or end of a layout without changing -where they are declared. - -The following example shows two child items. The first has the default `order` -of zero, and the second has an order of `-1`. After sorting, Child 1 appears -after Child 2. - - -```kotlin -FlexBox { - // Declared first, but will be placed after visually - RedRoundedBox( - title = "World" - ) - - // Declared second, but will be placed first visually - BlueRoundedBox( - title = "Hello", - modifier = Modifier.flex { - order(-1) - } - ) -} -``` - -
- -![Two rounded boxes, with the first containing the text Hello and the second containing the text World.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/itemorder.png) \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md deleted file mode 100644 index 33b37a2..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md +++ /dev/null @@ -1,320 +0,0 @@ -You can define a Grid container configuration to create flexible layouts -that respond to different screen sizes and content types. -This page describes how to do the following: - -- [Define a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-definition): Set up the basic structure of rows and columns. -- [Place items in a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#item-placement): Understand how items are placed into grid cells and how to change flow direction. -- [Manage track sizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-track-size): Use fixed, percentage, flexible, and intrinsic sizing to set track sizes. -- [Set gaps](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-gap): Manage the "gutters" between rows and columns. - -## Define a grid - -A grid consists of columns and rows. -The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter -that accepts a lambda to define the columns and rows -within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope). -The following example defines a grid that has three rows and two columns, -each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp): - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - } -) { -} -``` - -
- -## Place items in a grid - -`Grid` takes the UI elements -in the `content` lambda and places them into grid cells. -The grid lays out items regardless of -whether you have explicitly defined the rows and columns. -By default, -`Grid` tries to place a UI element in the available grid cell in the row; -if it can't, it places it in an available grid cell in the next row. -If there are no empty cells, `Grid` creates a new row. - -In the following example, the grid has six grid cells -and places a card into each one (Figure 1). -Each grid cell is `160dp` x `90dp`, -making the total grid size `320dp` x `270dp`. - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - } -) { - Card1() - Card2() - Card3() - Card4() - Card5() - Card6() -} -``` - -
- -![Six cards are placed in a grid that has three rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/placement.png) **Figure 1**. Six cards are placed in a grid that has three rows and two columns. - -To change this default behavior to filling by column, -set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()). - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - gap(8.dp) - flow = GridFlow.Column // Grid tries to place items to fill the column - }, -) { - Card1() - Card2() - Card3() - Card4() - Card5() - Card6() -} -``` - -
- -![The flow function changes the direction to place items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-flow.png) **Figure 2** . `GridFlow.Row` (left) and `GridFlow.Column` (right). - -## Manage track sizing - -Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track). -You can specify the size of a grid track using one of the following methods: - -- **Fixed** (`Dp`): Allocates a specific size (e.g., `column(180.dp)`). -- **Percentage** (`Float`): Allocates a percentage of the total available space from `0.0f` to `1.0f` (e.g., `row(0.5f)` for 50%). -- **Flexible** ([`Fr`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Fr)): Distributes remaining space proportionally after fixed and percentage tracks are calculated. For example, if two rows are set to `1.fr` and `3.fr`, the latter receives 75% of the remaining height. -- **Intrinsic** : Sizes the track based on the content inside it. For more information, see [Determine grid track size intrinsically](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#intrinsic-grid-track-size). - -The following example uses the different track sizing options -to define the row heights: - - -```kotlin -Grid( - config = { - column(1f) - - row(100.dp) - row(0.2f) - row(1.fr) - row(GridTrackSize.Auto) - }, - modifier = Modifier.height(480.dp) -) { - PastelRedCard("Fixed(100.dp)") - PastelGreenCard("Percentage(0.2f)") - PastelBlueCard("Flex(1.fr)") - PastelYellowCard("Auto") - -} -``` - -
- -![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-sizes.png) **Figure 3** . Row heights defined using the four primary track sizing options in `Grid`. - -### Set the minimum size for flexible grid tracks - -When a grid container has no remaining space, -a standard flexible track can shrink to `0.dp`. -To prevent this and ensure content isn't crushed, -use [`GridTrackSize.MinMax`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinMax(androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.Fr)) -to enforce an explicit minimum size while keeping the track flexible. - -The following example allocates at least `100.dp` to the first row: - - -```kotlin -Grid( - config = { - column(1f) - // The first row has a minimum height of 100.dp and can expand to - // the half of the remaining space. - row(GridTrackSize.MinMax(100.dp, 1.fr)) - // The second row takes the half of the remaining space. - row(1.fr) - // The third row has a fixed height of 200.dp. - row(200.dp) - }, - modifier = Modifier.size(360.dp) // Total grid height is 360.dp -) { - PastelRedCard("MinMax(100.dp, 1.fr)") - PastelGreenCard("Flex(1.fr)") - PastelBlueCard("Fixed(200.dp)") -} -``` - -
- -![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-size-minmax.png) **Figure 4** . The first row has at least `100.dp` height. - -### Set the minimum grid track size to place lazy lists - -Standard flexible tracks automatically query the intrinsic sizes of -their children to establish a base size. -However, Jetpack Compose prohibits querying the intrinsic sizes of -[`SubcomposeLayout`](https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/SubcomposeLayout.composable#SubcomposeLayout(androidx.compose.ui.Modifier,kotlin.Function2)), which backs components, -such as [`LazyColumn`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable) and [`LazyRow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyRow.composable). - -Placing a lazy list inside a standard flexible track causes -an [`IllegalStateException`](https://developer.android.com/reference/java/lang/IllegalStateException) crash. -To safely place lazy lists inside a flexible grid track, -use `MinMax` with an explicit minimum size (such as `0.dp`) -to bypass the intrinsic measurement pass. - - -```kotlin -Grid( - config = { - column(1f) - // The first row's height is determined by the height of the Text composable. - row(GridTrackSize.Auto) - // The second row occupies the remaining space, allowing the LazyColumn to scroll. - row(GridTrackSize.MinMax(0.dp, 1.fr)) - - gap(8.dp) - }, - modifier = Modifier.size(width = 170.dp, height = 240.dp) -) { - Text("Lazy column in a Grid") - // The LazyColumn is placed in the second row, filling the remaining space. - LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) { - items(100) { number -> - PastelGreenCard("Card $number") - } - } -} -``` - -
- -![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/lazy-column-in-grid.png) **Figure 5** . `LazyColumn` in a grid cell. - -### Determine grid track size intrinsically - -You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` -when you want the layout to adapt to the content, -rather than forcing it into a fixed container. -The grid track size is determined with the following values: - -- [`GridTrackSize.MaxContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MaxContent()): Use the content's maximum intrinsic size (e.g., the width is determined by the full length of the text in a text block with no wrapping). -- [`GridTrackSize.MinContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinContent()): Use the content's minimum intrinsic size (e.g., the width is determined by the longest single word in a text block). -- [`GridTrackSize.Auto`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#Auto()): Use a flexible size for a track that adapts based on available space. It behaves like `MaxContent` by default, but shrinks and wraps its content to fit within the parent container. - -The following example places two texts side by side. -The column size for the first text is determined -by the required minimum width to display the text, -and the second column width depends on the required maximum width of the text. - - -```kotlin -Grid( - config = { - column(GridTrackSize.MinContent) - column(GridTrackSize.MaxContent) - row(1.0f) - }, - modifier = Modifier.width(480.dp) -) { - Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") - Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") -} -``` - -
- -![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 5**. Intrinsic sizes specified in the columns. - -## Set gaps between rows and columns - -Once your grid tracks are sized, -you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks. -You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function, -and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example, -there is a `16dp` gap between each row, -and an `8dp` gap between each column (Figure 5). - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - rowGap(16.dp) - columnGap(8.dp) - } -) { - Card1() - Card2() - Card3() - Card4() - Card5() - Card6() -} -``` - -
- -![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 6**. Gaps between rows and columns. - -You can also use the convenience function [`gap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#gap(androidx.compose.ui.unit.Dp)) -to define gaps of the same column and row size, -and to define column and gap sizes separately using a single function. -The following code adds `8dp` gaps to the grid: - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - gap(8.dp) // Equivalent to columnGap(8.dp) and rowGap(8.dp) - } -) { - Card1() - Card2() - Card3() - Card4() - Card5() - Card6() -} -``` - -
\ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md deleted file mode 100644 index bb78a97..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md +++ /dev/null @@ -1,51 +0,0 @@ -This page describes how to implement basic [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) layouts. - -## Set up project - -1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's - `lib.versions.toml`. - - [versions] - compose = "1.12.0-beta02" - - [libraries] - androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } - -2. Add the library dependency to your app's `build.gradle.kts`. - - dependencies { - implementation(libs.androidx.compose.foundation.layout) - } - -## Create a basic grid - -The following example creates a basic 2x3 grid, -with the columns and rows having a fixed size of `100.dp`. - - -```kotlin -Grid( - config = { - repeat(2) { - column(100.dp) - } - repeat(3) { - row(100.dp) - } - } -) { - Card1(containerColor = PastelRed) - Card2(containerColor = PastelGreen) - Card3(containerColor = PastelBlue) - Card4(containerColor = PastelPink) - Card5(containerColor = PastelOrange) - Card6(containerColor = PastelYellow) -} -``` - -
- -![A basic grid consists of rows and columns with fixed size.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/six-cards-in-grid.png) **Figure 1**. A basic grid consists of rows and columns with fixed size. - -To learn how to implement more advanced grids, -see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties). \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md deleted file mode 100644 index 316b046..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md +++ /dev/null @@ -1,73 +0,0 @@ -> [!NOTE] -> **Note:** `Grid` is an experimental API and is subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&template=1424126). - -[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API -that lets you flexibly implement a two-dimensional layout. -With this API, you can display items in multi-column -or multi-row layouts that adapt to the available container size. -![A flexible and adaptive two-dimensional layout with Grid](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/example.png) **Figure 1.** A flexible and adaptive two-dimensional layout with `Grid`. - -## How is Grid different from similar composables? - -Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)). -These components are mainly for visualization of large, homogeneous data sets--- -for example, displaying a content catalog in a video streaming app. -These components are NOT designed -for the structural layout of a screen or complex component. - -You can also implement a two-dimensional layout -by combining multiple `Row` and `Column` composables. -However, this approach has some downsides, -such as deep hierarchies and difficulties in adaptability. - -The following table provides an overview -of which layouts are suitable for each API: - -| Component | Purpose | -|---|---| -| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that require lazy loading. | -| `Row`, `Column`, `FlexBox` | One-dimensional layout | -| `Grid` | Two-dimensional layout | - -> [!NOTE] -> **Note:** `Grid` doesn't support lazy loading. - -## Terminology - -Familiarize yourself with the following terminology -to understand how `Grid` works. - -### Grid line - -A grid is made up of lines, which run horizontally and vertically. -If your grid has three rows, it has four horizontal lines, -including the one after the last row. -In the following image, each dotted line represents a grid line: -![The grid consists of four horizontal lines and three vertical lines.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-line.png) **Figure 2**. The grid consists of four horizontal lines and three vertical lines. - -### Grid track - -A grid track is the space between two grid lines. -A row track is between two horizontal lines, -and a column track is between two vertical lines. -To define the size of these tracks, -assign a size to them when you create the grid. -![A grid track for the first row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-track.png) **Figure 3**. A grid track for the first row. - -### Grid cell - -A grid cell is the intersection of a row and column track. -![A grid cell that is an intersection of the second row and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-cell.png) **Figure 4**. A grid cell that is an intersection of the second row and the second column. - -### Grid area - -A grid area consists of several grid cells. -You can define a grid area by making an item span multiple tracks. -![A grid area that consists of four grid cells.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-area.png) **Figure 5**. A grid area that consists of four grid cells. - -### Grid gap - -A grid gap is the gutter between grid tracks. -You can't place a UI element into a gap, -but you can span a UI element across it. -![A grid gap between the first column and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-gap.png) **Figure 6**. A grid gap between the first column and the second column. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md deleted file mode 100644 index 9f0500e..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md +++ /dev/null @@ -1,168 +0,0 @@ -While the `Grid` config defines the overall structure, -you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning, -and alignment of items within that structure. - -## Set the item position - -Place an item into a specific track or cell -with the `row` and `column` parameters. - -The `row` and `column` parameters specify the row and column track indexes -that the item is placed in. -Track indexes are 1-based---they start at one. -Specifying only `row` or `column` (not both) places the item -in the next available space in that track. -Specifying both places the item into that cell. - -Use a positive integer to specify the track index from the start. -For example, to place an item in the first row and column, -use `gridItem(row = 1, column = 1)`. - -Use a negative integer to specify the track relative to the end. -For example, to place an item in the second-to-last row and column, use -`gridItem(row = -2, column = -2)`. - -In the following example, Card **#2** is placed -in the second row and the second column. -Card **#3** is assigned to the last row (indexed by -1), -where it automatically occupies -the first available column in that track (Figure 1). - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - gap(8.dp) - } -) { - Card1() - Card2(modifier = Modifier.gridItem(row = 2, column = 2)) - Card3(modifier = Modifier.gridItem(row = -1, column = -2)) -} -``` - -
- -![Card #2 is placed in the grid cell -in the second row and the second column, -and Card #3 is placed in the first column in the third row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/position.png) **Figure 1** . Card **#2** is placed in the grid cell in the second row and the second column, and Card **#3** is placed in the first column in the third row. - -## Span rows and columns - -Use the `rowSpan` and `columnSpan` parameters -to span an item over multiple cells. -You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area), -which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell). -The `gridItem` modifier lets you specify the grid area -with the `rowSpan` and `columnSpan` parameters. -In the following example, -Card **#1** is placed in the area consisting of two rows and two columns -(Figure 2). - - -```kotlin -Grid( - config = { - repeat(3) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - rowGap(8.dp) - columnGap(8.dp) - } -) { - Card1(modifier = Modifier.gridItem(rowSpan = 2, columnSpan = 2)) - Card2() - Card3() - Card4(modifier = Modifier.gridItem(columnSpan = 3)) -} -``` - -
- -![Card #4 spans three columns](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/spanning.png) **Figure 2** . Card **#4** spans three columns. - -## Set the alignment in a grid area - -You can set the alignment of the UI element in a grid area -by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier. -In the following example, **#1** is placed in the center of the grid area -consisting of two columns and two rows. - - -```kotlin -Grid( - config = { - repeat(3) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - rowGap(8.dp) - columnGap(8.dp) - }, -) { - Text( - text = "#1", - modifier = Modifier - .gridItem( - rowSpan = 2, - columnSpan = 2, - alignment = Alignment.Center - ), - ) - Card2() - Card3() - Card4(modifier = Modifier.gridItem(columnSpan = 3)) -} -``` - -
- -![The Text with #1 is placed in the center of the grid area -consisting of two rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/alignment.png) **Figure 3** . The Text with **#1** is placed in the center of the grid area consisting of two rows and two columns. - -## Auto-placement mixed with placed items - -A UI element in `Grid` -that has no position specification undergoes auto-placement. -This example shows how you can mix auto-placed elements -and the UI elements with specified grid cells. -Card **#2** and Card **#4** are placed in specified grid cells, -and the other items are auto-placed. - - -```kotlin -Grid( - config = { - repeat(2) { - column(160.dp) - } - repeat(3) { - row(90.dp) - } - rowGap(16.dp) - columnGap(8.dp) - } -) { - Card1() - Card2(modifier = Modifier.gridItem(row = 2, column = 2)) - Card3() - Card4(modifier = Modifier.gridItem(row = 3, column = 1)) - Card5() - Card6() -} -``` - -
- -![Card #3 is placed next to Card #1, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to Card **#1**, as it is an auto-placement. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md b/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md deleted file mode 100644 index 4cd2564..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md +++ /dev/null @@ -1,329 +0,0 @@ -> [!NOTE] -> **Note:** The `mediaQuery` function and the related data types are experimental and subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues?q=componentid:1876021). - -You need various types of information, such as device capability -and app status, to update your app layout. -Window width and height are the most commonly used information. -In addition to that, you can refer to the following information: - -- Window posture -- Pointing devices precision -- Keyboard type -- Whether the camera and microphone are supported by the device -- The distance between a user and the device display - -Because the information is updated dynamically, -you need to monitor it and trigger recomposition when any update happens. -The [`mediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/mediaQuery.composable#mediaQuery(kotlin.Function1)) function abstracts the details of the information retrieval -and lets you focus on defining the condition to trigger the layout updates. -The following example switches the layout to `TabletopLayout` -when the foldable posture is tabletop: - - -```kotlin -@Composable -fun VideoPlayer( - // ... -) { - // ... - if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { - TabletopLayout() - } else { - FlatLayout() - } - // ... -} -``` - -
- -## Enable the `mediaQuery` function - -To enable the `mediaQuery` function, -set the `isMediaQueryIntegrationEnabled` attribute of -the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`: - - -```kotlin -class MyApplication : Application() { - override fun onCreate() { - ComposeUiFlags.isMediaQueryIntegrationEnabled = true - super.onCreate() - } -} -``` - -
- -## Define a condition with parameters - -You can define a condition as a lambda -that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope). -The `mediaQuery` function evaluates the condition according to -the current status and the device capabilities. -The function returns a boolean value, -so you can determine the layout with conditional branches -like an `if` expression. -Table 1 describes the parameters available in `UiMediaScope`. - -| Parameter | Value type | Description | -|---|---|---| -| `windowWidth` | [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp) | The current window width in dp. | -| `windowHeight` | `Dp` | The current window height in dp. | -| `windowPosture` | [`UiMediaScope.Posture`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture) | The current posture of the application window. | -| `pointerPrecision` | [`UiMediaScope.PointerPrecision`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision) | The highest precision of the available pointing devices. | -| `keyboardKind` | [`UiMediaScope.KeyboardKind`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind) | The type of keyboard available or connected. | -| `hasCamera` | `Boolean` | Whether the camera is supported on the device. | -| `hasMicrophone` | `Boolean` | Whether the microphone is supported on the device. | -| `viewingDistance` | [`UiMediaScope.ViewingDistance`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance) | The typical distance between the user and the device screen. | - -A `UiMediaScope` object resolves the values of the parameters. -The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope()) -to access the `UiMediaScope` object, -which represents the current device capabilities and context. -This object is dynamically updated when any changes are made, -such as when the user changes the device posture. -The `mediaQuery` function then evaluates the `query` lambda -with the updated `UiMediaScope` object and returns a boolean value. -For example, the following snippet chooses between `TabletopLayout` -and `FlatLayout` based on the `windowPosture` parameter value. - - -```kotlin -@Composable -fun VideoPlayer( - // ... -) { - // ... - if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { - TabletopLayout() - } else { - FlatLayout() - } - // ... -} -``` - -
- -### Make a decision based on the window size - -[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints -that help you design, develop, and test adaptive layouts. -You can compare the two parameters representing the current window size -with the threshold defined in the window size classes. -The following example changes the number of panes according to the window width. -[`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds -of window size classes (Figure 1). - -The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda -and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof). -Because `windowWidth` and `windowHeight` can update frequently, -call the `derivedMediaQuery` function instead of the `mediaQuery` function -when you refer to those parameters in the `query` lambda. - - -```kotlin -val narrowerThanMedium by derivedMediaQuery { - windowWidth < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND.dp -} -val narrowerThanExpanded by derivedMediaQuery { - windowWidth < WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND.dp -} -when { - narrowerThanMedium -> SinglePaneLayout() - narrowerThanExpanded -> TwoPaneLayout() - else -> ThreePaneLayout() -} -``` - -
- -**Figure 1**. Layout is updated according to the window width. - -### Update layout according to the window posture - -The `windowPosture` parameter describes the current window posture -as a `UiMediaScope.Posture` object. -You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter -with the values defined in the `UiMediaScope.Posture` class. -The following example switches layout according to the window posture: - - -```kotlin -when { - mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() -} -``` - -
- -### Check the precision of the available pointing device - -A high precision pointing device helps users to point a UI element precisely. -The precision of a pointing device depends on the device type. - -The `pointerPrecision` parameter describes the precision -of the available pointing devices, such as a mouse and touchscreen. -There are four values defined in the `UiMediaScope.PointerPrecision` class: -[`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()). -`None` means that no pointing device is available. -The precision ranges from highest to lowest in this order: -`Fine`, `Coarse`, and `Blunt`. - -If multiple pointing devices are available and their precisions are different, -the parameter is resolved with the highest one. -For example, if there are two pointing devices --- a `Fine` precision device and -a `Blunt` precision device --- -`Fine` is the value of the `pointerPrecision` parameter. - -The following example shows a larger button -when the user is using a pointing device with low precision: - - -```kotlin -if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) { - LargeSizeButton() -} else { - NormalSizeButton() -} -``` - -
- -### Check the available keyboard type - -The `keyboardKind` parameter represents the type of the available keyboards: -[`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()). -If an on-screen keyboard is displayed and -a hardware keyboard is available at the same time, -the parameter is resolved as `Physical`. -If neither is detected, `None` is the value of the parameter. -The following example shows a message suggesting that users connect a keyboard -when no keyboard is detected: - - -```kotlin -if (mediaQuery { keyboardKind == UiMediaScope.KeyboardKind.None }) { - SuggestKeyboardConnect() -} -``` - -
- -### Check if the device supports camera and microphone - -Some devices don't support cameras or microphones. -You can check if the device supports a camera and a microphone -with the `hasCamera` parameter and the `hasMicrophone` parameter. -The following example shows buttons to use with camera and microphone -when the device supports them: - - -```kotlin -Row { - OutlinedTextField(state = rememberTextFieldState()) - // Show the MicButton when the device supports a microphone. - if (mediaQuery { hasMicrophone }) { - MicButton() - } - // Show the CameraButton when the device supports a camera. - if (mediaQuery { hasCamera }) { - CameraButton() - } -} -``` - -
- -### Adjust UI with the estimated viewing distance - -Viewing distance is a factor that helps determine layout. -If the user is using the app from a distance, -they would expect the text and UI elements to be bigger. -The `viewingDistance` parameter provides an estimate of the viewing distance -based on the device type and its typical usage context. - -There are three values defined in the `UiMediaScope.ViewingDistance` class: -[`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()). -`Near` means that the screen is in close range, -and `Far` means that the device is viewed from a distance. -The following example increases the font size when the viewing distance is -`Far` or `Medium`: - - -```kotlin -val fontSize = when { - mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Far } -> 20.sp - mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Medium } -> 18.sp - else -> 16.sp -} -``` - -
- -## Preview a UI component - -You can call the `mediaQuery` and `derivedMediaQuery` functions in the -composable functions to preview UI components. -The following snippet chooses between `TabletopLayout` -and `FlatLayout` based on the `windowPosture` parameter value. -To preview the `TabletopLayout`, the `windowPosture` parameter should be -[`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()). - - -```kotlin -when { - mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() -} -``` - -
- -The `mediaQuery` and `derivedMediaQuery` functions evaluate -the given `query` lambda within a `UiMediaScope` object, -which is provided as `LocalUiMediaScope.current`. -You can override it with the following steps: - -1. Enable the `mediaQuery` function. -2. Define a custom object that implements the `UiMediaScope` interface. -3. Set the custom object to the `LocalUiMediaScope` with the [`CompositionLocalProvider`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocalProvider.composable#CompositionLocalProvider(androidx.compose.runtime.CompositionLocalContext,kotlin.Function0)) function. -4. Call the composable to preview in the content lambda of the `CompositionLocalProvider` function. - -You can preview the `TabletopLayout` with the following example: - - -```kotlin -@Preview -@Composable -fun PreviewLayoutForTabletop() { - // Step 1: Enable the mediaQuery function - ComposeUiFlags.isMediaQueryIntegrationEnabled = true - - val currentUiMediaScope = LocalUiMediaScope.current - // Step 2: Define a custom object implementing the UiMediaScope interface. - // The object overrides the windowPosture parameter. - // The resolution of the remaining parameters is deferred to the currentUiMediaScope object. - val uiMediaScope = remember(currentUiMediaScope) { - object : UiMediaScope by currentUiMediaScope { - override val windowPosture: UiMediaScope.Posture = UiMediaScope.Posture.Tabletop - } - } - - // Step 3: Set the object to the LocalUiMediaScope. - CompositionLocalProvider(LocalUiMediaScope provides uiMediaScope) { - // Step 4: Call the composable to preview. - when { - mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() - mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() - } - } -} -``` - -
\ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md b/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md deleted file mode 100644 index 023836f..0000000 --- a/.agents/skills/adaptive/references/android/develop/ui/compose/tooling/debug.md +++ /dev/null @@ -1,114 +0,0 @@ -Tools for debugging your Compose UI are available in Android Studio. - -## Layout Inspector - -Layout Inspector lets you inspect a Compose layout inside a running app in an -emulator or physical device. You can use the Layout Inspector to check how often -a composable is recomposed or skipped, which can help identify issues with your -app. For example, some coding errors might force your UI to recompose -excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance). -Some coding errors can prevent your UI from recomposing and, therefore, -prevent your UI changes from showing up on the screen. If you're new to -Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to -run it. - -> [!NOTE] -> **Note:** If you're not seeing Compose components in layout inspector, make sure you are not removing `META-INF/androidx.compose.*.version` files from the APK. These are required for layout inspector to work. - -### Get recomposition counts - -When debugging your Compose layouts, knowing when composables -[recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in -understanding whether your UI is implemented properly. For example, if it's -recomposing too many times, your app might be doing more work than is necessary. -On the other hand, components that don't recompose when you anticipate them to -can lead to unexpected behaviors. - -The Layout Inspector shows you when discrete composables in your layout -hierarchy have either recomposed or skipped, as you interact with your app. In -Android Studio, your recompositions are highlighted to help you determine -where in the UI your composables are recomposing. - -**Figure 1.** Recompositions are highlighted in Layout Inspector. - -The highlighted portion shows a gradient overlay of the composable in the image -section of the Layout Inspector, and gradually disappears so that you can get an -idea of where in the UI the composable with the highest recompositions can be -found. If one composable is recomposing at a higher rate than another -composable, then the first composable receives a stronger gradient overlay -color. If you double-click a composable in the layout inspector, you're taken to -the corresponding code for analysis. - -> [!NOTE] -> **Note:** To view recomposition counts, make sure your app is using an API level of 29 or higher, and `Compose 1.2.0` or higher. Then, deploy your app as you normally would. - -![](https://developer.android.com/static/develop/ui/compose/images/li-recomposition-counts.png) **Figure 2.**The composition and skip counter in Layout Inspector. - -Open the **Layout Inspector** window and connect to your app process. In the -**Component Tree** , there are two columns that appear next to the layout -hierarchy. The first column shows the number of compositions for each node and -the second column displays the number of skips for each node. Selecting a -composable node shows the dimensions and parameters of the composable, unless -it's an inline function, in which case the parameters can't be shown. You can -also see similar information in the **Attributes** pane when you select a -composable from the **Component Tree** or the **Layout Display**. - -Resetting the count can help you understand recompositions or skips during a -specific interaction with your app. If you want to reset the count, click -**Reset** near the top of the **Component Tree** pane. - -> [!NOTE] -> **Note:** If you don't see the new columns in the **Component Tree** pane, you can view them by selecting **Show Recomposition Counts** from the **View Options** menu ![Layout Inspector View Options -> icon](https://developer.android.com/static/studio/images/buttons/live-layout-inspector-view-options-icon.png) near the top of the **Component Tree** pane, as shown in the following image. - -![Enable the composition and skip counter in Layout -Inspector](https://developer.android.com/static/develop/ui/compose/images/li-show-recomposition-counts.png) - -**Figure 3**. Enable the composition and skip counter in Layout Inspector. - -### Compose semantics - -In Compose, [Semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics) describe your UI in an -alternative manner that is understandable for -[Accessibility](https://developer.android.com/develop/ui/compose/accessibility) services and for the -[Testing](https://developer.android.com/develop/ui/compose/testing) framework. You can use the Layout Inspector -to inspect semantic information in your Compose layouts. -![Semantic information displayed using the Layout Inspector.](https://developer.android.com/static/develop/ui/compose/images/layout_inspector_semantics_new.png) **Figure 4.** Semantic information displayed using the Layout Inspector. - -When selecting a Compose node, use the **Attributes** pane to check whether it -declares semantic information directly, merges semantics from its children, or -both. To quickly identify which nodes include semantics, either declared or -merged, use select the **View options** drop-down in the **Component Tree** pane -and select **Highlight Semantics Layers**. This highlights only the nodes in the -tree that include semantics, and you can use your keyboard to quickly navigate -between them. - -## Compose UI Check - -To help you build more adaptive and accessible UIs in Jetpack Compose, Android -Studio provides a UI Check mode in Compose Preview. This feature is similar -to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner) -for views. - -When you activate Compose UI check mode on a Compose Preview, Android Studio -automatically audits your Compose UI and suggests improvements to make your UI -more accessible and adaptive. Android Studio checks that your UI works across -different screen sizes. In the **Problems** panel, the tool shows the issues -that it detects, such as text stretched on large screens or low color contrast. - -To access this feature, click the UI Check icon on Compose Preview: -![](https://developer.android.com/static/studio/images/design/compose-ui-check-entry.png) **Figure 5.** Entry point to UI check mode. - -UI check automatically previews your UI in different configurations and -highlights issues found in different configurations. In the **Problems** panel, -when you click an issue, you can see the details of the issue, suggested fixes, -and the renderings that highlight the area of the issue. -![](https://developer.android.com/static/studio/images/design/compose-ui-check.png) **Figure 6.** UI check mode in action. - -### Fix with AI - -For issues detected in UI Check mode, you can use the AI agent to propose and -apply code fixes. Click the **Fix with AI** button on an issue in the -**Problems** panel. The agent analyzes the problem and your code to suggest -changes that resolve the accessibility or adaptive issue. -![](https://developer.android.com/static/studio/preview/features/images/ui-check-mode-single-fix.png) **Figure 7.** The agent fixes UI issues in UI Check mode. \ No newline at end of file diff --git a/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md deleted file mode 100644 index fbaae79..0000000 --- a/.agents/skills/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +++ /dev/null @@ -1,141 +0,0 @@ -# Material List-Detail Recipe - -This recipe demonstrates how to create an adaptive list-detail layout using the `ListDetailSceneStrategy` from the Material 3 Adaptive library. This layout automatically adjusts to show one, two, or three panes depending on the available screen width. - -## How it works - -This example has three destinations: `ConversationList`, `ConversationDetail`, and `Profile`. - -### `ListDetailSceneStrategy` - -The key to this recipe is the `rememberListDetailSceneStrategy`, which provides the logic for the adaptive layout. - -- **Pane Roles**: Each destination is assigned a role using metadata: - - - `ListDetailSceneStrategy.listPane()`: For the primary (list) content. This pane is always visible. A placeholder can be provided to be shown in the detail pane area when no detail content is selected. - - `ListDetailSceneStrategy.detailPane()`: For the secondary (detail) content. - - `ListDetailSceneStrategy.extraPane()`: For tertiary content. -- **Adaptive Layout** : The `ListDetailSceneStrategy` automatically handles the layout. On smaller screens, only one pane is shown at a time. On wider screens, it will show the list and detail panes side-by-side. On very wide screens, it can show all three panes: list, detail, and extra. - -- **Navigation** : Navigation between the panes is handled by adding and removing destinations from the back stack as usual. The `ListDetailSceneStrategy` observes the back stack and adjusts the layout accordingly. - -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/listdetail) - -``` -package com.example.nav3recipes.material.listdetail - -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy -import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentRed -import com.example.nav3recipes.content.ContentYellow -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private object ConversationList : NavKey - -@Serializable -private data class ConversationDetail(val id: String) : NavKey - -@Serializable -private data object Profile : NavKey - -class MaterialListDetailActivity : ComponentActivity() { - - @OptIn(ExperimentalMaterial3AdaptiveApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - - val backStack = rememberNavBackStack(ConversationList) - - // Override the defaults so that there isn't a horizontal space between the panes. - // See b/418201867 - val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() - val directive = remember(windowAdaptiveInfo) { - calculatePaneScaffoldDirective(windowAdaptiveInfo) - .copy(horizontalPartitionSpacerSize = 0.dp) - } - val listDetailStrategy = rememberListDetailSceneStrategy(directive = directive) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(listDetailStrategy), - entryProvider = entryProvider { - entry( - metadata = ListDetailSceneStrategy.listPane( - detailPlaceholder = { - ContentYellow("Choose a conversation from the list") - } - ) - ) { - ContentRed("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(ConversationDetail("ABC")) - }) { - Text("View conversation") - } - } - } - entry( - metadata = ListDetailSceneStrategy.detailPane() - ) { conversation -> - ContentBlue("Conversation ${conversation.id} ") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed { - backStack.add(Profile) - }) { - Text("View profile") - } - } - } - } - entry( - metadata = ListDetailSceneStrategy.extraPane() - ) { - ContentGreen("Profile") - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/SKILL.md b/.agents/skills/agp-9-upgrade/SKILL.md deleted file mode 100644 index 280347a..0000000 --- a/.agents/skills/agp-9-upgrade/SKILL.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: agp-9-upgrade -description: Upgrades, or migrates, an Android project to use Android Gradle Plugin - (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) - projects. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-06-25' - 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 to update to the latest stable version of -AGP, and confirm when done. The user may also request that this requirement be -skipped; if this is the case, you should update the version of AGP to the latest -stable version as part of the AGP 9 migration. See the -[AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this. - -Each version of AGP has its own set of compatibilities with other tools, such as -Gradle, JDK, and Kotlin. The release notes for each of these versions will -include a **Compatibility** table indicating the minimum versions for these -tools. - -Do not use this skill for KMP projects, as they are unsupported. - -## Steps - -If AGP is already at 9 or higher, then do the following: - -### Step 1: Update dependencies - -If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on -version 2.3.6 or higher. - -If Hilt is used in the project, ensure it is on version 2.59.2 or higher. - -### Step 2: Migrate to built-in Kotlin - -See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information. - -### Step 3. 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 4. Migrate kapt to KSP or legacy-kapt - -If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are -used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration -steps. - -### Step 5. BuildConfig - -If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md) -for detailed information. - -### Step 6. 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. diff --git a/.agents/skills/agp-9-upgrade/references/buildconfig.md b/.agents/skills/agp-9-upgrade/references/buildconfig.md deleted file mode 100644 index 273f876..0000000 --- a/.agents/skills/agp-9-upgrade/references/buildconfig.md +++ /dev/null @@ -1,54 +0,0 @@ -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 { - buildFeatures { - buildConfig = true - } - } - -In custom build-logic for a library module: - - extensions.configure { - buildFeatures { - buildConfig = true - } - } - -In custom build-logic using `CommonExtension`: - - extensions.configure { - 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. \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/ksp-kapt.md b/.agents/skills/agp-9-upgrade/references/ksp-kapt.md deleted file mode 100644 index e8a5d33..0000000 --- a/.agents/skills/agp-9-upgrade/references/ksp-kapt.md +++ /dev/null @@ -1,39 +0,0 @@ -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. \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md b/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md deleted file mode 100644 index a20da2a..0000000 --- a/.agents/skills/agp-9-upgrade/references/paparazzi-gradle-9.md +++ /dev/null @@ -1,30 +0,0 @@ -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().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 - } \ No newline at end of file diff --git a/.agents/skills/agp-9-upgrade/references/recipes.md b/.agents/skills/agp-9-upgrade/references/recipes.md deleted file mode 100644 index 5cf4a2d..0000000 --- a/.agents/skills/agp-9-upgrade/references/recipes.md +++ /dev/null @@ -1,62 +0,0 @@ -When migrating to AGP's new DSL, any Gradle code (plugins or logic in build -scripts) that relied on the old DSL will stop working. Such code must be -migrated. - -## Guidelines - -- **DO NOT** search the web for examples of how to do this. Use the **gradle-recipes** repository examples **only**. -- **DO NOT** use AGP internals in migrated code. -- **DO** use only public APIs in migrated code. - -In some cases, there is a one-to-one replacement for the old code. Some examples -are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes). - -In 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. \ No newline at end of file diff --git a/.agents/skills/edge-to-edge/SKILL.md b/.agents/skills/edge-to-edge/SKILL.md deleted file mode 100644 index f618ab2..0000000 --- a/.agents/skills/edge-to-edge/SKILL.md +++ /dev/null @@ -1,426 +0,0 @@ ---- -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 - last-updated: '2026-04-01' - 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 */ } - } - ``` - -
- - 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") - } - } - ``` - -
- - 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 */ } -} -``` - -
- -*** ** * ** *** - -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 */ } -} -``` - -
- -*** ** * ** *** - -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 */ } -} -``` - -
- -#### 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 */ } -} -``` - -
- -*** ** * ** *** - -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 */ } -} -``` - -
- -### 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 */ } -} -``` - -
- -*** ** * ** *** - - -```kotlin -// RIGHT -Box( - // Insets consumed - modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures -) { - Column( - modifier = Modifier.imePadding() - ) { /* Content */ } -} -``` - -
- -*** ** * ** *** - - -```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 */ } -} -``` - -
- -#### 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 */ } -} -``` - -
- -## 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) - } - ``` - -
- -- 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 - ) - ) - ) - ) -} -``` - -
- -## 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 */ } -``` - -
- -## 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. diff --git a/.agents/skills/jetpack-compose-m3/SKILL.md b/.agents/skills/jetpack-compose-m3/SKILL.md deleted file mode 100644 index 84b8be2..0000000 --- a/.agents/skills/jetpack-compose-m3/SKILL.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -name: jetpack-compose-m3 -description: Expert guidance for working with Wear OS Compose Material3. Use this - skill when creating, updating or migrating Wear OS projects. This includes the androidx.wear.compose.material3, - androidx.wear.compose.foundation and androidx.wear.compose.navigation3 libraries. - Also working with core components such as AppScaffold, ScreenScaffold and TransformingLazyColumn. - Migration from earlier versions such as Material 2.5 and Horologist. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-06-06' - keywords: - - Wear OS - - Compose - - Material3 - - Horologist - - TransformingLazyColumn - - AppScaffold - - ScreenScaffold ---- - -## Prerequisites and compatibility - -1. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. - - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`). -2. **Strict compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. -3. **Kotlin version:** For Wear Compose Material3, use Kotlin **2.0.0 or - higher**. -4. **Compose compiler:** - - If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin. - - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). -5. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). -6. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You MUST NOT propose code changes until the samples in Capability 2 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure. - -## Gotchas - -1. **Mandatory sync and validation:** After updating versions in `libs.versions.toml` or `build.gradle.kts`, you **must** perform a Gradle sync before refactoring any code. This ensures the environment has resolved the libraries correctly. -2. **Prohibition of guessing (error protocol):** If you encounter an 'Unresolved Reference' or API mismatch after a successful sync, do not attempt to 'fix' it by downgrading the library version. - -## Capabilities and tools - -### Capability 1: Migration - -Use this guidance when migrating from an older version of Wear OS Compose or -Horologist. - -1. Unless otherwise indicated by the developer, use the latest stable version of Wear Compose Material3 from `{VERSION}`. -2. Read the [migration guide](references/android/training/wearables/compose/migrate-to-material3.md). -3. Use the official component mappings from the migration guide. -4. Before refactoring any component (for example, `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. -5. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. -6. **Always** check against the component guidance in Capability 3. -7. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. - -### Capability 2: Component samples - -Wear Compose includes individual component samples for each and every component, -within the `--samples-sources.jar` file. Gradle automatically -downloads these JAR files along with the main library JAR when using any of -`compose-material3`, `compose-foundation` or `compose-navigation3`. - -Use the canonical component samples whenever adding or adjusting a Wear Compose -Material3 component. - -STRICT COMPLIANCE: Extraction is NOT optional. You are FORBIDDEN from -implementing any code until samples are extracted and read. Bypassing this step -with alternative search tools or by assuming library documentation is sufficient -is a protocol breach. You MUST verify the local cache by reading a sample file -before proceeding. - -#### Step 1: Prepare - -1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. -2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. - -#### Step 2: Check the local cache - -1. Define the cache directory path: `/samples/{VERSION}/`. Do NOT choose your own different location. -2. Check if this directory exists and contains subdirectories with `.kt` files. - - **IF YES (cache hit):** Proceed to **Step 4**. - - **IF NO (cache miss):** Proceed to **Step 3**. - -#### Step 3: Check the Gradle cache - -1. Sample sources are stored in the Gradle cache. To avoid slow, brute-force searches: - - Determine the Gradle user home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). - - The cache root is `/caches`. Call this ``. -2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include "navigation3" in the list if the `androidx.wear.compose.navigation3` library is being used. -3. For each `{ARTIFACT}` in the list: - - - Construct the expected relative path segment for the library: `androidx.wear.compose/compose-{ARTIFACT}/{VERSION}`. - - Run a targeted `find` command. Here is an example which is constructed - for efficiency: - - find /modules-2/files-2.1/androidx.wear.compose/compose-{ARTIFACT}/{VERSION}/ \ - -name "*samples-sources.jar" - -4. Use this JAR as the official sample sources. - -5. Extract the contents of each JAR to - `/samples/{VERSION}/{ARTIFACT}/` using `unzip -j` to flatten the - structure. - -6. Proceed **directly to step 4**. - -#### Step 4: Read samples and implement - -1. Read the relevant `.kt` sample files. -2. Use these official, version-matched samples as the source of truth for: - - Required parameters and slot names. - - Default styling and typography tokens. - - Interactive behaviors (for example: `onClick`, `onLongClick`). - - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). - -### Capability 3: Component guidance - -**Mandatory**: Use this capability as a checklist against any component use. It -provides more holistic guidance on how to use each component in practice, beyond -the component syntax. - -1. `AppScaffold` and `ScreenScaffold` - - \[ \] Use `AppScaffold` as the outer container, with `ScreenScaffold` children. - - \[ \] Use only **ONE** `AppScaffold` and any number of `ScreenScaffold`. -2. `ScalingLazyColumn` - Use `TransformingLazyColumn` instead. -3. `TransformingLazyColumn` - You will need the following imports: - - - ```kotlin - import androidx.wear.compose.foundation.lazy.TransformingLazyColumn - import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults - import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState - // ... - import androidx.wear.compose.material3.lazy.rememberTransformationSpec - import androidx.wear.compose.material3.lazy.transformedHeight - ``` - -
- - **Canonical example**: - - - ```kotlin - val columnState = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - ScreenScaffold( - scrollState = columnState - ) { contentPadding -> - TransformingLazyColumn( - state = columnState, - contentPadding = contentPadding - ) { - item { - ListHeader( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), - transformation = SurfaceTransformation(transformationSpec) - ) { - Text(text = "Header") - } - } - // ... other items - item { - Button( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), - transformation = SurfaceTransformation(transformationSpec), - onClick = { /* ... */ }, - icon = { - Icon( - imageVector = Icons.Default.Build, - contentDescription = "build", - ) - }, - ) { - Text( - text = "Build", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - } - ``` - -
- - - \[ \] Use `TransformingLazyColumn` instead of `ScalingLazyColumn`. - - \[ \] You must pass the `contentPadding` parameter from `ScreenScaffold` to the `TransformingLazyColumn`. - - \[ \] Use the `minimumVerticalContentPadding` modifier to achieve required padding top and bottom. - - This expects a value from defaults, such as `ButtonDefaults`, `CardDefaults`, \`ListHeaderDefaults. - - Note: This is a scoped modifier available within `TransformingLazyColumnItemScope`. - - \[ \] Ensure the list morphs and scales. - - \[ \] Use `transformedHeight` modifier. - - \[ \] Use `transform = SurfaceTransform(...)`. - - \[ \] If configuring a list for snapping, use `flingBehavior` and `rotaryScrollableBehavior` **together**: - - - ```kotlin - val columnState = rememberTransformingLazyColumnState() - ScreenScaffold(scrollState = columnState) { contentPadding -> - TransformingLazyColumn( - state = columnState, - flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), - rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) - ) { - // ... - // ... - } - } - ``` - -
- -4. `ScreenScaffold` - - - \[ \] Guard the `scrollIndicator` with `!LocalScrollCaptureInProgress.current`. -5. `EdgeButton` - - - \[ \] Do **NOT** use as the final item within a `TransformingLazyColumn`. Instead, use the slot in `ScreenScaffold`. - - \[ \] When used in a `TransformingLazyColumn`, add the required overscroll behavior: - - - ```kotlin - val columnState = rememberTransformingLazyColumnState() - ScreenScaffold( - scrollState = columnState, - edgeButton = { - EdgeButton( - onClick = { /* TODO */ }, - modifier = Modifier.scrollable( - columnState, - orientation = Orientation.Vertical, - reverseDirection = true, - // Apply overscroll to the EdgeButton for proper scrolling behavior. - overscrollEffect = rememberOverscrollEffect(), - ) - ) { - Text("More") - } - } - ) { contentPadding -> - TransformingLazyColumn( - contentPadding = contentPadding, - state = columnState, - ) { - // ... - // ... - } - } - ``` - -
- -6. `Column` - - - \[ \] USE as a direct child of `ScreenScaffold` *if* the screen is will **never** scroll, even with the largest system font. - - \[ \] Use `TransformingLazyColumn` instead for all other cases. -7. Styles - - - \[ \] Do **NOT** hard-code text sizes, use `typography` from `MaterialTheme`. - - \[ \] Do **NOT** hard-code colors, use `colorScheme` from `MaterialTheme`. -8. Use component defaults: - - - \[ \] Components such as `Button` have a corresponding `ButtonDefaults` object. - - Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. -9. Use Wear specific previews: - - - \[ \] `WearPreviewDevices` - - \[ \] `WearPreviewFontScales` -10. Ambient mode - - - \[ \] Use `LocalAmbientModeManager` instead of `AmbientLifecycleObserver`. -11. Navigation - - - \[ \] When adding navigation fresh, use Navigation3. - - \[ \] For Navigation3 in Wear OS, use `SwipeDismissableSceneStrategy()` from the Wear Compose `compose-navigation3` library. diff --git a/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md deleted file mode 100644 index 1969a7d..0000000 --- a/.agents/skills/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ /dev/null @@ -1,677 +0,0 @@ -[Material 3 Expressive](https://developer.android.com/design/ui/wear/guides/get-started) is the next evolution of Material Design. It includes -updated theming, components, and personalization features like dynamic color. - -This guide focuses on migrating from the [Wear Compose Material 2.5 -(androidx.wear.compose)](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2) Jetpack library to the [Wear Compose Material 3 -(androidx.wear.compose.material3)](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3) Jetpack library for apps. - -> [!NOTE] -> **Note:** This guide uses abbreviation "M3" to refer to the interchangeable terms of "Material 3 Expressive" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material3). The abbreviation "M2.5" is used to refer to the interchangeable terms of "Material 2.5" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material). - -## Approaches - -For migrating your app code from M2.5 to M3, follow the same approach described -in the [Compose Material migration phone guidance](https://developer.android.com/develop/ui/compose/designsystems/material2-material3), in particular: - -- You shouldn't use both [M2.5 and M3 in a single app long-term](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#approaches). -- You should no longer use the Horologist Composables, Compose Layout, or Compose Material libraries. Instead, use the components in M3. -- Adopt a [phased approach](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#phased-approach). - -## Dependencies - -M3 has a separate package and version to M2.5: - -### M2.5 - - implementation("androidx.wear.compose:compose-material:1.4.0") - -### M3 - - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha04") - -See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). - -Wear Compose Foundation library version 1.7.0-alpha04 introduced -some new components that are designed to work with Material 3 components. -Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an -updated animation when running on Wear OS 6 (API level 36) or higher. When -updating to Wear Compose Material 3 version, we suggest to also update the Wear -Compose Foundation and Navigation libraries: - - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha04") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha04") - -## Theme - -In both M2.5 and M3, the theme composable is named [`MaterialTheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)), but the -import packages and parameters differ. In M3, the `Colors` parameter has been -renamed to `ColorScheme` and `MotionScheme` has been introduced for implementing -transitions. - -### M2.5 - - import androidx.wear.compose.material.MaterialTheme - - MaterialTheme( - colors = AppColors, - typography = AppTypography, - shapes = AppShapes, - content = content - ) - -### M3 - - -```kotlin -import androidx.wear.compose.material3.MaterialTheme -// ... - MaterialTheme( - colorScheme = ColorScheme(), - typography = Typography(), - shapes = Shapes(), - motionScheme = MotionScheme.standard(), - content = { /*content here*/ } - ) -``` - -
- -### Color - -The color system in M3 is significantly different from M2.5. The number of color -parameters has increased, they have different names, and they map differently to -M3 components. In Compose, this applies to the M2.5 [`Colors`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Colors) class, the M3 -[`ColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/ColorScheme) class, and related functions: - -### M2.5 - - import androidx.wear.compose.material.Colors - - val appColorScheme: Colors = Colors( - // M2.5 Color parameters - ) - -### M3 - - -```kotlin -import androidx.wear.compose.material3.ColorScheme -// ... - val appColorScheme: ColorScheme = ColorScheme( - // M3 ColorScheme parameters - ) -``` - -
- -The following table describes the key differences between M2.5 and M3: - -| M2.5 | M3 | -|---|---| -| `Color` | Has been renamed to `ColorScheme` | -| 13 colors | 28 colors | -| N/A | New dynamic color theming | -| N/A | New tertiary colors for more expression | - -#### Dynamic color theming - -A new feature in M3 is [dynamic color theming](https://m3.material.io/styles/color/dynamic-color/overview). If users change -the watch face colors, the colors in the UI change to match. - -Use the [`dynamicColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#dynamicColorScheme(android.content.Context)) function to implement dynamic color scheme -and provide a `defaultColorScheme` as a fallback in case dynamic color scheme is -not available. - - -```kotlin -@Composable -fun myApp() { - val dynamicColorScheme = dynamicColorScheme(LocalContext.current) - MaterialTheme(colorScheme = dynamicColorScheme ?: myBrandColors) {} -} - -internal val myBrandColors: ColorScheme = ColorScheme( /* Specify colors here */) -``` - -
- -### Typography - -The [typography system](https://m3.material.io/styles/typography/overview) in M3 is different from M2.5 and it includes -the following features: - -- Nine new [text styles](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/Typography#public-properties_1) -- Flex fonts, which allow for customization of the type scales for different weights, widths, and roundness -- `AnimatedText`, which uses flex fonts - -### M2.5 - - import androidx.wear.compose.material.Typography - - val Typography = Typography( - // M2.5 TextStyle parameters - ) - -### M3 - - -```kotlin -import androidx.wear.compose.material3.Typography - -val Typography = Typography( - // M3 TextStyle parameters -) -``` - -
- -#### Flex fonts - -Flex Fonts allow designers to specify the type width and weight for specific -sizes. - -#### Text styles - -The following [TextStyles](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt;l=115?q=displayLarge&ss=androidx/platform/frameworks/support) are available in M3. These are -employed by default by various M3 components. - -| Typography | TextStyle | -|---|---| -| Display | displayLarge, displayMedium, displaySmall | -| Title | titleLarge, titleMedium, titleSmall | -| Label | labelLarge, labelMedium, labelSmall | -| Body | bodyLarge, bodyMedium, bodySmall, bodyExtraSmall | -| Numeral | numeralExtraLarge, numeralLarge, numeralMedium, numeralSmall, numeralExtraSmall | -| Arc | arcLarge, arcMedium, arcSmall | - -### Shape - -The [shape system](https://m3.material.io/styles/shape/overview) in M3 is different from M2.5. The number of shape -parameters has increased, they're named differently, and they map differently to -M3 components. The following shape sizes are available: - -- Extra-small -- Small -- Medium -- Large -- Extra-large - -In Compose, this applies to the M2 [`Shapes`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Shapes) class and the M3 -[`Shapes`](https://developer.android.com/reference/kotlin/androidx/compose/material3/Shapes) class: - -### M2.5 - - import androidx.wear.compose.material.Shapes - - val Shapes = Shapes( - // M2.5 Shapes parameters - ) - -### M3 - - -```kotlin -import androidx.wear.compose.material3.Shapes - -val Shapes = Shapes( - // M3 Shapes parameters -) -``` - -
- -> [!NOTE] -> **Note:** For shapes, we generally recommend using the default Material 3 Wear shapes which are optimized for round devices. - -Use the Shapes parameter mapping from [Migrate from Material 2 to Material 3 in -Compose](https://developer.android.com/training/wearables/compose/migrate-to-material3#shape) as a starting point. - -### Shape morphing - -M3 introduces Shape Morphing: shapes now morph in response to interactions. - -Shape Morphing behavior is available as a variation on a number of round -buttons, see the following list of buttons that support Shape Morphing: - -| Buttons | Shape morphing function | -|---|---| -| `IconButton` | [IconButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon button on press | -| `IconToggleButton` | [IconToggleButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon toggle button on press and [IconToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#variantAnimatedShapes()) animates the icon toggle button on press and check/uncheck | -| `TextButton` | [TextButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text button on press | -| `TextToggleButton` | [TextToggleButtonDefaults.animatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text toggle on press and [TextToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#variantAnimatedShapes()) animates the text toggle on press and check/uncheck | - -## Components and Layout - -Most components and layouts from M2.5 are available in M3. However, some M3 -components and layouts didn't exist in M2.5. Furthermore, some M3 components -have more variations than their equivalents in M2.5. - -While some components require special considerations, the following function -mappings are recommended as a starting point: - -| Material 2.5 | Material 3 | -|---|---| -| [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | -| [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | -| [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | -| [androidx.wear.compose.material.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function0,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0)) | -| [androidx.wear.compose.material.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.Checkbox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Checkbox(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.CheckboxColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.OutlinedButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OutlinedButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) or [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) | -| [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | -| [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | -| [androidx.wear.compose.material.LocalContentAlpha()](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#LocalContentAlpha()) | Has been removed as not used by `Text` or `Icon` in Material 3 | -| [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | -| [androidx.wear.compose.material.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#RadioButton(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.RadioButtonColors,kotlin.Boolean,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | -| [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | -| [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) | [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) and [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.Switch](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Switch(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.SwitchColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) or [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | -| [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | -| [androidx.wear.compose.material.Vignette](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Vignette(androidx.wear.compose.material.VignettePosition,androidx.compose.ui.Modifier)) | Removed as not included in Material 3 Expressive design for Wear OS | - -Here is a full list of all the Material 3 components: - -| Material 3 | Material 2.5 equivalent component (if not new in M3) | -|---|---| -| [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | -| [androidx.wear.compose.material3.AnimatedPage](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedPage(kotlin.Int,androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.graphics.Color,kotlin.Function0)) | New | -| [androidx.wear.compose.material3.AnimatedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedText(kotlin.String,androidx.wear.compose.material3.AnimatedTextFontRegistry,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment)) | New | -| [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) ) | -| [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | -| [androidx.wear.compose.material3.ButtonGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ButtonGroup(androidx.compose.ui.Modifier,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.Alignment.Vertical,kotlin.Function1)) | New | -| [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | -| [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a checkbox toggle control | -| [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) (only when no background is required) | -| [androidx.wear.compose.material3.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) | [androidx.wear.compose.material.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp)) | -| [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | -| [androidx.wear.compose.material3.ConfirmationDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ConfirmationDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ConfirmationDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Confirmation](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Confirmation(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,kotlin.Long,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | -| [androidx.wear.compose.material3.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | [androidx.wear.compose.material.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | -| [androidx.wear.compose.material3.DatePicker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#DatePicker(java.time.LocalDate,kotlin.Function1,androidx.compose.ui.Modifier,java.time.LocalDate,java.time.LocalDate,androidx.wear.compose.material3.DatePickerType,androidx.wear.compose.material3.DatePickerColors)) | New | -| [androidx.wear.compose.material3.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | -| [androidx.wear.compose.material3.EdgeButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | New | -| [androidx.wear.compose.material3.FadingExpandingLabel](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FadingExpandingLabel(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,kotlin.Boolean,kotlin.Int,kotlin.Int,androidx.compose.ui.text.TextStyle,androidx.compose.animation.core.FiniteAnimationSpec)) | New | -| [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) when a tonal button background is required | -| [androidx.wear.compose.material3.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPageIndicator(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)) | -| [androidx.wear.compose.material3.HorizontalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | -| [androidx.wear.compose.material3.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | -| [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | -| [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | -| [androidx.wear.compose.material3.LevelIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LevelIndicator(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.ranges.ClosedFloatingPointRange,kotlin.Boolean,androidx.wear.compose.material3.LevelIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Float,kotlin.Boolean)) | New | -| [androidx.wear.compose.material3.LinearProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LinearProgressIndicator(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | -| [androidx.wear.compose.material3.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | -| [androidx.wear.compose.material3.ListSubHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListSubHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | New | -| [androidx.wear.compose.material3.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)) | [androidx.wear.compose.material.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#MaterialTheme(androidx.wear.compose.material.Colors,androidx.wear.compose.material.Typography,androidx.wear.compose.material.Shapes,kotlin.Function0)) | -| [androidx.wear.compose.material3.OpenOnPhoneDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OpenOnPhoneDialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material3.OpenOnPhoneDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function1)) | New | -| [androidx.wear.compose.material3.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Picker(androidx.wear.compose.material3.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | [androidx.wear.compose.material.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Picker(androidx.wear.compose.material.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | -| [androidx.wear.compose.material3.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#PickerGroup(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Boolean,kotlin.Function1)) | [androidx.wear.compose.material.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PickerGroup(kotlin.Array,androidx.compose.ui.Modifier,androidx.wear.compose.material.PickerGroupState,kotlin.Function1,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.TouchExplorationStateProvider,kotlin.Function1)) | -| [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a radio button toggle control | -| [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1))) | -| [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | -| [androidx.wear.compose.material3.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function0)) | [androidx.wear.compose.material.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.compose.foundation.lazy.LazyListState,kotlin.Int,androidx.compose.ui.unit.Dp)) | -| [androidx.wear.compose.material3.SegmentedCircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SegmentedCircularProgressIndicator(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Float,kotlin.Float,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | -| [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | -| [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | -| [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | -| [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | -| [androidx.wear.compose.material3.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Boolean,kotlin.Function1)) | -| [androidx.wear.compose.material3.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | [androidx.wear.compose.material.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | -| [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) and [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | -| [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a switch toggle control | -| [androidx.wear.compose.material3.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | [androidx.wear.compose.material.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | -| [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | -| [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | -| [androidx.wear.compose.material3.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.wear.compose.material3.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.material.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1)) | -| [androidx.wear.compose.material3.VerticalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | - -And finally a list of some relevant components from Wear Compose Foundation -library: - -| Wear Compose Foundation 1.7.0-alpha04 | | -|---|---| -| [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | -| [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | -| [androidx.wear.compose.foundation.pager.VerticalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#VerticalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A vertically scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | -| [androidx.wear.compose.foundation.lazy.TransformingLazyColumn](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#TransformingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.TransformingLazyColumnState,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) | Can be used instead of [`ScalingLazyColumn`](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#ScalingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType,androidx.wear.compose.foundation.lazy.AutoCenteringParams,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) to add scroll transform effects to each item. | -| | | - -### Buttons - -Buttons in M3 are different from M2.5. The M2.5 Chip has been replaced by -Button. [`Button`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) implementation provides default values for `Text` -`maxLines` and `textAlign`. Those default values can be overridden in the `Text` -element. - -### M2.5 - - import androidx.wear.compose.material.Chip - - //M2.5 Buttons - Chip(...) - CompactChip(...) - Button(...) - -### M3 - - -```kotlin -//M3 Buttons -Button(onClick = { }){} -CompactButton(onClick = { }){} -IconButton(onClick = { }){} -TextButton(onClick = { }){} -``` - -
- -M3 also includes new button variations. Check them out on the [Compose Material -3 API reference overview](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2). - -M3 introduces a new button: [`EdgeButton`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)). `EdgeButton` is available in 4 -different sizes: extra small, small, medium, and large. `EdgeButton` -implementation provide a default value for `maxLines` depending on the size -which can be customized. - -If you are using `TransformingLazyColumn` or `ScalingLazyColumn`, pass the -`EdgeButton` into the `ScreenScaffold` so that it morphs, changing its shape -with scrolling, instead of adding an `EdgeButton` as the final list item. See -the following code to check how to use `EdgeButton` with `ScreenScaffold` and -`TransformingLazyColumn`. - - -```kotlin -val state = rememberTransformingLazyColumnState() -ScreenScaffold( - scrollState = state, - contentPadding = - rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader - ), - edgeButton = { - EdgeButton( - onClick = { } - ) { - Text(stringResource(R.string.show)) - } - } -){ contentPadding -> - TransformingLazyColumn(state = state, contentPadding = contentPadding,){ - // additional code here - } -} -``` - -
- -### Scaffold - -Scaffold in M3 is different from M2.5. In M3, `AppScaffold` and the new -`ScreenScaffold` composable have replaced Scaffold. `AppScaffold` and -`ScreenScaffold` lay out the structure of a screen and coordinate transitions of -the `ScrollIndicator` and `TimeText` components. - -`AppScaffold` allows static screen elements such as `TimeText` to remain visible -during in-app transitions such as swipe-to-dismiss. ​​It provides a slot for the -main application content, which will usually be supplied by a navigation -component such as `SwipeDismissableNavHost` - -You declare one `AppScaffold` for Activity and use a `ScreenScaffold` for each -Screen. -`AppScaffold` adds a default `TimeText`component to the screens. You can -override it if you want to customize it by using the `timeText` parameter. - -### M2.5 - - import androidx.wear.compose.material.Scaffold - - Scaffold {...} - -### M3 - - -```kotlin - AppScaffold { - val navController = rememberSwipeDismissableNavController() - SwipeDismissableNavHost( - navController = navController, - startDestination = "message_list" - ) { - composable("message_list") { - MessageList(onMessageClick = { id -> - navController.navigate("message_detail/$id") - }) - } - composable("message_detail/{id}") { - MessageDetail(id = it.arguments?.getString("id")!!) - } - } - } -} - -// Implementation of one of the screens in the navigation -@Composable -fun MessageDetail(id: String) { - // .. Screen level content goes here - val scrollState = rememberTransformingLazyColumnState() - - val padding = rememberResponsiveColumnPadding( - first = ColumnItemType.BodyText - ) - - ScreenScaffold( - scrollState = scrollState, - contentPadding = padding - ) { scaffoldPaddingValues -> - // Screen content goes here - // ... -``` - -
- -> [!NOTE] -> **Note:** `AppScaffold` and `ScreenScaffold` from [Horologist](https://github.com/google/horologist) haven't been migrated to M3. To maintain correct scrolling behavior and `TimeText` elements, migrate to the `AppScaffold` and `ScreenScaffold` from M3. - -If you are using a `HorizontalPager` with [HorizontalPagerIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)), you -can migrate to `HorizontalPagerScaffold`. [`HorizontalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) is -placed within an `AppScaffold`. `AppScaffold` and `HorizontalPagerScaffold` lay -out the structure of a Pager and coordinate transitions of the -`HorizontalPageIndicator` and `TimeText` components. - -`HorizontalPagerScaffold` displays the `HorizontalPageIndicator` at the -center-end of the screen by default and coordinates showing and hiding -`TimeText` and `HorizontalPageIndicator` according to whether the `Pager` is -being paged, this is determined by the `PagerState`. - -There's also a new `AnimatedPage` component, which animates a page within a -Pager with a scaling and scrim effect based on its position. - - -```kotlin -AppScaffold { - val pagerState = rememberPagerState(pageCount = { 10 }) - val columnState = rememberTransformingLazyColumnState() - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.BodyText, - ) - HorizontalPagerScaffold(pagerState = pagerState) { - HorizontalPager( - state = pagerState, - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold( - scrollState = columnState, - contentPadding = contentPadding - ) { contentPadding -> - TransformingLazyColumn( - state = columnState, - contentPadding = contentPadding - ) { - item { - ListHeader( - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Pager sample") - } - } - item { - if (page == 0) { - Text(text = "Page #$page. Swipe right") - } - else{ - Text(text = "Page #$page. Swipe left and right") - } - } - } - } - - } - } - } -} -``` - -
- -Finally, M3 introduces a [`VerticalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) which follows the same -pattern as the `HorizontalPagerScaffold`: - - -```kotlin -AppScaffold { - val pagerState = rememberPagerState(pageCount = { 10 }) - - VerticalPagerScaffold(pagerState = pagerState) { - VerticalPager( - state = pagerState - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - ///... - } - } - } - } -} -``` - -
- -### Placeholder - -There are some API changes between M2.5 and M3. -`Placeholder.PlaceholderDefaults` now provides two modifiers: - -- [`Modifier.placeholder`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholder(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)), which is drawn instead of content that is not yet loaded -- A placeholder shimmer effect [`Modifier.placeholderShimmer`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholderShimmer(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)) which provides a placeholder shimmer effect which runs in an animation loop while waiting for the data to load. - -See the following table for additional changes to the `Placeholder` component. - -| M2.5 | M3 | -|---|---| -| `PlaceholderState.startPlaceholderAnimation` | Has been removed | -| `PlaceholderState.placeholderProgression` | Has been removed | -| `PlaceholderState.isShowContent` | Has been renamed to `!PlaceholderState.isVisible` | -| `PlaceholderState.isWipeOff` | Has been removed | -| `PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush` | Has been removed | -| `PlaceholderDefaults.placeholderBackgroundBrush` | Has been removed | -| `PlaceholderDefaults.placeholderChipColors` | Has been removed | - -### SwipeDismissableNavHost - -`SwipeDismissableNavHost` is part of `wear.compose.navigation`. When this -component is used with M3, the M3 MaterialTheme updates the -`LocalSwipeToDismissBackgroundScrimColor` and -`LocalSwipeToDismissContentScrimColor`. - -### TransformingLazyColumn - -`TransformingLazyColumn` is part of `wear.compose.lazy.foundation` and adds -support for scaling and morphing animations on list items during scrolling , -enhancing the user experience. It is strongly recommended that apps migrate from -`ScalingLazyColumn` to `TransformingLazyColumn` - -Similarly to `ScalingLazyColumn`, it provides -`rememberTransformingLazyColumnState()` to create a -`TransformingLazyColumnState` that is remembered across compositions. - -For adding scaling and morphing animations, add the following to each list item: - -- `Modifier.transformedHeight`, which lets you calculate transformed height of the items using a `TransformationSpec`, you can use `rememberTransformationSpec()` unless you need further customization. -- A `SurfaceTransformation` - -To verify that the padding is correct at the top and bottom of the list, use the -`minimumVerticalContentPadding` modifier. - - -```kotlin -val columnState = rememberTransformingLazyColumnState() -val transformationSpec = rememberTransformationSpec() -ScreenScaffold( - scrollState = columnState -) { contentPadding -> - TransformingLazyColumn( - state = columnState, - contentPadding = contentPadding - ) { - item { - ListHeader( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), - transformation = SurfaceTransformation(transformationSpec) - ) { - Text(text = "Header") - } - } - // ... other items - item { - Button( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), - transformation = SurfaceTransformation(transformationSpec), - onClick = { /* ... */ }, - icon = { - Icon( - imageVector = Icons.Default.Build, - contentDescription = "build", - ) - }, - ) { - Text( - text = "Build", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } -} -``` - -
- -## Useful links - -To learn more about migrating from M2.5 to M3 in Compose, consult the following -additional resources. - -### Samples - -- [Wear OS samples on GitHub](https://github.com/android/wear-os-samples/) -- [Compose for Wear OS codelab](https://developer.android.com/codelabs/compose-for-wear-os#0) -- [Jetcaster sample](https://github.com/android/compose-samples/tree/main/Jetcaster) - -### API reference and source code - -- [Compose Material 3 API reference](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary) -- [Compose Material 3 samples in source code](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/) - -### Design - -- [Design guidance](https://developer.android.com/design/ui/wear/guides/get-started) \ No newline at end of file diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md deleted file mode 100644 index ea25f8e..0000000 --- a/.agents/skills/migrate-xml-views-to-jetpack-compose/SKILL.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: migrate-xml-views-to-jetpack-compose -description: Provides a structured workflow for migrating an Android XML View to Jetpack - Compose. This skill details the step-by-step process, from planning and dependency - setup, to theming and layout migration, validation and XML cleanup. Use this skill - when you need to migrate an XML View to Jetpack Compose in an Android project. It - solves the problem of converting the UI of a legacy XML View into modern, declarative - Compose components while maintaining interoperability. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-07-02' - keywords: - - Jetpack Compose - - migration - - XML - - Views - - interoperability - - incremental adoption - - UI development ---- - -This skill guides through the process of migrating an existing Android XML View -to Jetpack Compose. It performs a stable, safe and visually consistent -transition by following a structured, 10-step methodology. This skill migrates -UI (XML to Jetpack Compose) only. - -## Objective - -To systematically convert a single legacy XML layout into modern, declarative -Jetpack Compose UI while maintaining pixel-perfect visual parity and functional -integrity. - -## Summary of the 10-step migration process - -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** - -## Detailed steps - -### Step 1: Identify the optimal XML candidate for migration - -If the user has explicitly specified a target XML layout, proceed to Step 2. -Otherwise, analyze the codebase to identify the best candidate for migration by -following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md). - -### Step 2: Analyze the project and layout - -Analyze the identified XML View's structure, hierarchy, and implementation -details. -Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to -guide your technical audit of the layout and surrounding project context. - -### Step 3: Create a plan - -Using the outputs and analysis done in the Step 1 and 2, generate a -step-by-step plan for the migration. If you support user interaction, present -to the user and ask for approval before proceeding. If user interaction is not -supported, proceed to Step 4 following the generated plan. - -### 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. diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md deleted file mode 100644 index 3e2f0c3..0000000 --- a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md +++ /dev/null @@ -1,42 +0,0 @@ -## 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. \ No newline at end of file diff --git a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md b/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md deleted file mode 100644 index 60b7968..0000000 --- a/.agents/skills/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +++ /dev/null @@ -1,171 +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. | -| `Shapes` | 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`. | -| `ColorScheme` | 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 `Shapes` object to replicate your XML shape definitions. - 2. Provide this `Shapes` 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 or modifiers to composables, using the -[new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating -layered, reusable composable variations with the defined style. - -Provide separate @Composable functions named according to the style and the -base component, to signify the difference in styling and use cases for those -components. - -- **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:* ` - -
- -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](https://developer.android.com/guide/navigation). -- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in - Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose). -- Routes are strongly typed. If you use string-based routes, [migrate to - type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)). -- *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](https://developer.android.com/guide/navigation/design/kotlin-dsl#custom) -- Deep links - -**AI Agent**: Before changing any code, check if the project contains any of the -unsupported features. If it does, do not proceed. Inform the user of the -unsupported feature and ask for further instructions. - -## Step 1: Add Navigation 3 dependencies - -Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your -project. The core dependencies are provided for you to copy. - -**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](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey` -interface. This lets you use `rememberNavBackStack` to assist with [saving your -navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state). - -Before: - - @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](https://developer.android.com/guide/navigation/navigation-3/get-started#project-setup). - -## 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 - ): 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, - val backStacks: Map> - ) { - var topLevelRoute: NavKey by topLevelRoute - val stacksInUse: List - get() = if (topLevelRoute == startRoute) { - listOf(startRoute) - } else { - listOf(startRoute, topLevelRoute) - } - } - - /** - * Convert NavigationState into NavEntries. - */ - @Composable - fun NavigationState.toEntries( - entryProvider: (NavKey) -> NavEntry - ): SnapshotStateList> { - - val decoratedEntries = backStacks.mapValues { (_, stack) -> - val decorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - ) - 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](https://developer.android.com/topic/architecture): -> -> - 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 = , - topLevelRoutes = - ) - - 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](https://developer.android.com/guide/navigation/design#compose) -using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder), -usually inside `NavHost`'s trailing lambda. It is common to use extension -functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate). - -In Navigation 3, you define your destinations using an `entryProvider`. This -`entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the -`entryProvider` does not define parent-child relationships between entries. - -In 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](https://developer.android.com/guide/navigation/navigation-3/basics#entry-provider-DSL) at the same scope as the -`NavigationState`. - - val entryProvider = entryProvider { - - } - -## Step 5.2: Move destinations into the `entryProvider` - -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`: Move it into `entryProvider` and rename it to `entry`, retaining the type parameter. For example, `composable` becomes `entry`. -- `dialog`: Do the same as `composable`, but add metadata to the entry as follows: `entry(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{ composable{ ... } }`, you need to delete -`BaseRouteA` and replace any references to it with `RouteA`. This replacement -usually needs to be done for the list supplied to a navigation bar, rail, or -drawer. - -You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to -`EntryProviderScope` 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{ - val id = entry.toRoute().id - ScreenA(title = "Screen has ID: $id") - } - featureBSection() - dialog{ ScreenD() } - } - - fun NavGraphBuilder.featureBSection() { - navigation(startDestination = RouteB) { - composable { 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{ key -> ScreenA(title = "Screen has ID: ${key.id}") } - featureBSection() - entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } - } - - fun EntryProviderScope.featureBSection() { - entry { 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 `sceneStrategies` parameter. - -For example: - - import androidx.navigation3.ui.NavDisplay - - NavDisplay( - entries = navigationState.toEntries(entryProvider), - onBack = { navigator.goBack() }, - sceneStrategies = remember { listOf(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). \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md deleted file mode 100644 index 5e15bd4..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md +++ /dev/null @@ -1,147 +0,0 @@ -# Animations Recipe - -This recipe shows how to override the default animations at the `NavDisplay` level, and at the individual destination level. - -## How it works - -The `NavDisplay` composable takes `transitionSpec`, `popTransitionSpec`, and `predictivePopTransitionSpec` parameters to define the animations for forward, backward, and predictive back navigation respectively. These animations will be applied to all destinations by default. - -In this example, we use `slideInHorizontally` and `slideOutHorizontally` to create a sliding animation for forward and backward navigation. - -It is also possible to override these animations for a specific destination by providing a different `transitionSpec` and `popTransitionSpec` to the `entry` composable. In this recipe, `ScreenC` has a custom vertical slide animation. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/animations) - -``` -package com.example.nav3recipes.animations - - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith -import androidx.compose.material3.Button -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 -import com.example.nav3recipes.content.ContentMauve -import com.example.nav3recipes.content.ContentOrange -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - - -@Serializable -private data object ScreenA : NavKey - -@Serializable -private data object ScreenB : NavKey - -@Serializable -private data object ScreenC : NavKey - - -class AnimatedActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - - val backStack = rememberNavBackStack(ScreenA) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { - ContentOrange("This is Screen A") { - Button(onClick = dropUnlessResumed { backStack.add(ScreenB) }) { - Text("Go to Screen B") - } - } - } - entry { - ContentMauve("This is Screen B") { - Button(onClick = dropUnlessResumed { backStack.add(ScreenC) }) { - Text("Go to Screen C") - } - } - } - entry( - metadata = metadata { - // Slide new content up, keeping the old content in place underneath - put(NavDisplay.TransitionKey) { - slideInVertically( - initialOffsetY = { it }, - animationSpec = tween(1000) - ) togetherWith ExitTransition.KeepUntilTransitionsFinished - } - - // Slide old content down, revealing the new content in place underneath - put(NavDisplay.PopTransitionKey) { - EnterTransition.None togetherWith - slideOutVertically( - targetOffsetY = { it }, - animationSpec = tween(1000) - ) - } - - // Slide old content down, revealing the new content in place underneath - put(NavDisplay.PredictivePopTransitionKey) { - EnterTransition.None togetherWith - slideOutVertically( - targetOffsetY = { it }, - animationSpec = tween(1000) - ) - } - } - ) { - ContentGreen("This is Screen C") - } - }, - transitionSpec = { - // Slide in from right when navigating forward - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = tween(1000) - ) togetherWith slideOutHorizontally( - targetOffsetX = { -it }, - animationSpec = tween(1000) - ) - }, - popTransitionSpec = { - // Slide in from left when navigating back - slideInHorizontally( - initialOffsetX = { -it }, - animationSpec = tween(1000) - ) togetherWith slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = tween(1000) - ) - }, - predictivePopTransitionSpec = { - // Slide in from left when navigating back - slideInHorizontally( - initialOffsetX = { -it }, - animationSpec = tween(1000) - ) togetherWith slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = tween(1000) - ) - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md deleted file mode 100644 index 087d5cf..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md +++ /dev/null @@ -1,89 +0,0 @@ -# Basic Recipe - -This recipe shows a basic example of how to use the Navigation 3 API with two screens. - -## How it works - -This example defines two routes: `RouteA` and `RouteB`. `RouteA` is a `data object` representing a simple screen, while `RouteB` is a `data class` that takes an `id` as a parameter. - -A `mutableStateListOf` is used to manage the navigation back stack. - -The `NavDisplay` composable is used to display the current screen. Its `entryProvider` parameter is a lambda that takes a route from the back stack and returns a `NavEntry`. Inside the `entryProvider`, a `when` statement is used to determine which composable to display based on the route. - -To navigate from `RouteA` to `RouteB`, we simply add a `RouteB` instance to the back stack. The `id` is passed as an argument to the `RouteB` data class. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basic) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.basic - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -private data object RouteA - -private data class RouteB(val id: String) - -class BasicActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = remember { mutableStateListOf(RouteA) } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = { key -> - when (key) { - is RouteA -> NavEntry(key) { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to navigate") - } - } - } - - is RouteB -> NavEntry(key) { - ContentBlue("Route id: ${key.id} ") - } - - else -> { - error("Unknown route: $key") - } - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md deleted file mode 100644 index 2067c08..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md +++ /dev/null @@ -1,85 +0,0 @@ -# Basic DSL Recipe - -This recipe shows a basic example of how to use the Navigation 3 API with two screens, using the `entryProvider` DSL and a persistent back stack. - -## How it works - -This example is similar to the basic recipe, but with a few key differences: - -1. **Persistent Back Stack** : It uses `rememberNavBackStack(RouteA)` to create and remember the back stack. This makes the back stack persistent across configuration changes (e.g., screen rotation). To use `rememberNavBackStack`, the navigation keys must be serializable, which is why `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. - -2. **`entryProvider` DSL** : Instead of a `when` statement, this example uses the `entryProvider` DSL to define the content for each route. The `entry` function is used to associate a route type with its composable content. - -The navigation logic remains the same: to navigate from `RouteA` to `RouteB`, we add a `RouteB` instance to the back stack. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basicdsl) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.basicdsl - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private data object RouteA : NavKey - -@Serializable -private data class RouteB(val id: String) : NavKey - -class BasicDslActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = rememberNavBackStack(RouteA) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to navigate") - } - } - } - entry { key -> - ContentBlue("Route id: ${key.id} ") - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md deleted file mode 100644 index fd2a72d..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md +++ /dev/null @@ -1,90 +0,0 @@ -# Basic Saveable Recipe - -This recipe shows a basic example of how to create a persistent back stack that survives configuration changes. - -## How it works - -To make the back stack persistent, we use the `rememberNavBackStack` function. This function creates and remembers the back stack across configuration changes (e.g., screen rotation). - -A requirement for using `rememberNavBackStack` is that the navigation keys (routes) must be serializable. In this example, `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. - -This example uses a `when` statement within the `entryProvider` to map routes to their corresponding composables, but it could also be used with the `entryProvider` DSL. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/basicsaveable) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.basicsaveable - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private data object RouteA : NavKey - -@Serializable -private data class RouteB(val id: String) : NavKey - -class BasicSaveableActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = rememberNavBackStack(RouteA) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = { key -> - when (key) { - is RouteA -> NavEntry(key) { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to navigate") - } - } - } - - is RouteB -> NavEntry(key) { - ContentBlue("Route id: ${key.id} ") - } - - else -> { - error("Unknown route: $key") - } - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md deleted file mode 100644 index aa6b456..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md +++ /dev/null @@ -1,195 +0,0 @@ -# Bottom Sheet Recipe - -This recipe demonstrates how to display a destination as a modal bottom sheet. - -## How it works - -To show a destination as a bottom sheet, you need to do two things: - -1. **Use `BottomSheetSceneStrategy`** : Create an instance of `BottomSheetSceneStrategy` and pass it to the `sceneStrategy` parameter of the `NavDisplay` composable. - -2. **Add metadata to the destination** : For the destination that you want to display as a bottom sheet, add `BottomSheetSceneStrategy.bottomSheet()` to its metadata. This is done in the `entry` function. - -In this example, `RouteB` is configured to be a bottom sheet. When you navigate from `RouteA` to `RouteB`, `RouteB` will be displayed in a modal bottom sheet that slides up from the bottom of the screen. - -The content of the bottom sheet can be styled as needed. In this recipe, the content is clipped to have rounded corners. - -For more information, see the official documentation on [custom layouts](https://developer.android.com/guide/navigation/navigation-3/custom-layouts). -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.bottomsheet - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private data object RouteA : NavKey - -@Serializable -private data class RouteB(val id: String) : NavKey - -class BottomSheetActivity : ComponentActivity() { - - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = rememberNavBackStack(RouteA) - val bottomSheetStrategy = remember { BottomSheetSceneStrategy() } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(bottomSheetStrategy), - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to open bottom sheet") - } - } - } - entry( - metadata = BottomSheetSceneStrategy.bottomSheet() - ) { key -> - ContentBlue( - title = "Route id: ${key.id}", - modifier = Modifier.clip( - shape = RoundedCornerShape(16.dp) - ) - ) - } - } - ) - } - } -} -``` - -``` -package com.example.nav3recipes.bottomsheet - -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 data class BottomSheetScene( - override val key: T, - override val previousEntries: List>, - override val overlaidEntries: List>, - private val entry: NavEntry, - private val modalBottomSheetProperties: ModalBottomSheetProperties, - private val onBack: () -> Unit, -) : OverlayScene { - - override val entries: List> = listOf(entry) - - override val content: @Composable (() -> Unit) = { - val lifecycleOwner = rememberLifecycleOwner() - ModalBottomSheet( - onDismissRequest = onBack, - properties = modalBottomSheetProperties, - ) { - CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { - entry.Content() - } - } - } -} - -/** - * A [SceneStrategy] that displays entries that have added [bottomSheet] to their [NavEntry.metadata] - * within a [ModalBottomSheet] instance. - * - * This strategy should always be added before any non-overlay scene strategies. - */ -@OptIn(ExperimentalMaterial3Api::class) -class BottomSheetSceneStrategy : SceneStrategy { - - override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { - 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, - previousEntries = entries.dropLast(1), - overlaidEntries = entries.dropLast(1), - entry = lastEntry, - modalBottomSheetProperties = properties, - onBack = onBack - ) - } - } - - companion object { - /** - * Function to be called on the [NavEntry.metadata] to mark this entry as something that - * should be displayed within a [ModalBottomSheet]. - * - * @param modalBottomSheetProperties properties that should be passed to the containing - * [ModalBottomSheet]. - */ - fun bottomSheet(modalBottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()) = - metadata { - put(BottomSheetKey, modalBottomSheetProperties) - } - - object BottomSheetKey : NavMetadataKey - } - -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md deleted file mode 100644 index d49b399..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md +++ /dev/null @@ -1,200 +0,0 @@ -# Common UI Recipe - -This recipe 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. - -## How it works - -This example has three top-level destinations: `Home`, `ChatList`, and `Camera`. The `ChatList` destination also has a sub-route, `ChatDetail`. - -### `TopLevelBackStack` - -The core of this recipe is the `TopLevelBackStack` class, which is responsible for managing the navigation state. It works as follows: - -- It maintains a separate back stack for each top-level destination (tab). -- It keeps track of the currently selected top-level destination. -- It provides a single, flattened back stack that can be used by the `NavDisplay` composable. This flattened back stack is a combination of the individual back stacks of all the tabs. - -### UI Structure - -The UI is built using a `Scaffold` composable, with a `NavigationBar` as the `bottomBar`. - -- The `NavigationBar` displays an item for each top-level destination. When an item is clicked, it calls `topLevelBackStack.addTopLevel` to switch to the corresponding tab, preserving the navigation history of each tab. -- The `NavDisplay` composable is placed in the content area of the `Scaffold`. It is responsible for displaying the current screen based on the flattened back stack provided by `TopLevelBackStack`. - -This approach allows for a common navigation pattern where users can switch between different sections of the app, and each section maintains its own navigation history. - -### State Preservation - -It's important to note how the navigation state is managed in this recipe. When a user navigates away from a top-level destination (e.g., by pressing the back button until they return to a previous tab), the entire navigation history for that destination is cleared. The state is not saved. When the user returns to that tab later, they will start from its initial screen. - -**Note** : In this example, the `Home` route can move above the `ChatList` and `Camera` routes, meaning navigating back from `Home` doesn't necessarily leave the app. The app will exit when the user goes back from a single remaining top level route in the back stack. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/commonui) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.commonui - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Face -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentPurple -import com.example.nav3recipes.content.ContentRed -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -private sealed interface TopLevelRoute { - val icon: ImageVector -} -private data object Home : TopLevelRoute { override val icon = Icons.Default.Home } -private data object ChatList : TopLevelRoute { override val icon = Icons.Default.Face } -private data object ChatDetail -private data object Camera : TopLevelRoute { override val icon = Icons.Default.PlayArrow } - -private val TOP_LEVEL_ROUTES : List = listOf(Home, ChatList, Camera) - -class CommonUiActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val topLevelBackStack = remember { TopLevelBackStack(Home) } - - Scaffold( - bottomBar = { - NavigationBar { - TOP_LEVEL_ROUTES.forEach { topLevelRoute -> - - val isSelected = topLevelRoute == topLevelBackStack.topLevelKey - NavigationBarItem( - selected = isSelected, - onClick = { - topLevelBackStack.addTopLevel(topLevelRoute) - }, - icon = { - Icon( - imageVector = topLevelRoute.icon, - contentDescription = null - ) - } - ) - } - } - } - ) { _ -> - NavDisplay( - backStack = topLevelBackStack.backStack, - onBack = { topLevelBackStack.removeLast() }, - entryProvider = entryProvider { - entry{ - ContentRed("Home screen") - } - entry{ - ContentGreen("Chat list screen"){ - Button(onClick = dropUnlessResumed { - topLevelBackStack.add(ChatDetail) - }) { - Text("Go to conversation") - } - } - } - entry{ - ContentBlue("Chat detail screen") - } - entry{ - ContentPurple("Camera screen") - } - }, - ) - } - } - } -} - -class TopLevelBackStack(startKey: T) { - - // Maintain a stack for each top level route - private var topLevelStacks : LinkedHashMap> = linkedMapOf( - startKey to mutableStateListOf(startKey) - ) - - // Expose the current top level route for consumers - var topLevelKey by mutableStateOf(startKey) - private set - - // Expose the back stack so it can be rendered by the NavDisplay - val backStack = mutableStateListOf(startKey) - - private fun updateBackStack() = - backStack.apply { - clear() - addAll(topLevelStacks.flatMap { it.value }) - } - - fun addTopLevel(key: T){ - - // If the top level doesn't exist, add it - if (topLevelStacks[key] == null){ - topLevelStacks.put(key, mutableStateListOf(key)) - } else { - // Otherwise just move it to the end of the stacks - topLevelStacks.apply { - remove(key)?.let { - put(key, it) - } - } - } - topLevelKey = key - updateBackStack() - } - - fun add(key: T){ - topLevelStacks[topLevelKey]?.add(key) - updateBackStack() - } - - fun removeLast(){ - val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() - // If the removed key was a top level key, remove the associated top level stack - topLevelStacks.remove(removedKey) - topLevelKey = topLevelStacks.keys.last() - updateBackStack() - } -} - -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md deleted file mode 100644 index 60848b3..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md +++ /dev/null @@ -1,230 +0,0 @@ -# Conditional Navigation Recipe - -This recipe demonstrates how to implement conditional navigation, where certain destinations are only accessible if a condition is met (in this case, if the user is logged in). - -## How it works - -This example has a `Profile` destination that requires the user to be logged in. If the user is not logged in and attempts to navigate to `Profile`, they are redirected to a `Login` screen. After a successful login, they are automatically navigated to the `Profile` screen. - -### `AppBackStack` - -The core of this recipe is the custom `AppBackStack` class, which encapsulates the logic for conditional navigation. - -- **`RequiresLogin` interface** : A marker interface, `RequiresLogin`, is used to identify destinations that require the user to be logged in. The `Profile` destination implements this interface. - -- **Redirecting to Login** : When the `add` function is called with a destination that implements `RequiresLogin` and the user is not logged in, `AppBackStack` stores the intended destination and adds the `Login` route to the back stack instead. - -- **Handling Login** : When the `login` function is called, it sets the user's status to logged in. If there is a stored destination that the user was trying to access, it adds that destination to the back stack and removes the `Login` screen. - -- **Handling Logout** : When the `logout` function is called, it sets the user's status to logged out and removes any destinations from the back stack that require the user to be logged in. - -This approach provides a clean way to handle conditional navigation by centralizing the logic in a custom back stack implementation. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/conditional) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.conditional - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.saveable.rememberSerializable -import androidx.compose.runtime.setValue -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.serialization.NavBackStackSerializer -import androidx.navigation3.runtime.serialization.NavKeySerializer -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentYellow -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - - -/** - * Class for representing navigation keys in the app. - * - * Note: We use a sealed class because KotlinX Serialization handles - * polymorphic serialization of sealed classes automatically. - * - * @param requiresLogin - true if the navigation key requires that the user is logged in - * to navigate to it - */ -@Serializable -sealed class ConditionalNavKey(val requiresLogin: Boolean = false) : NavKey - -/** - * Key representing home screen - */ -@Serializable -private data object Home : ConditionalNavKey() - -/** - * Key representing profile screen that is only accessible once the user has logged in - */ -@Serializable -private data object Profile : ConditionalNavKey(requiresLogin = true) - -/** - * Key representing login screen - * - * @param redirectToKey - navigation key to redirect to after successful login - */ -@Serializable -private data class Login( - val redirectToKey: ConditionalNavKey? = null -) : ConditionalNavKey() - -class ConditionalActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - - val backStack = rememberNavBackStack(Home) - var isLoggedIn by rememberSaveable { - mutableStateOf(false) - } - val navigator = remember { - Navigator( - backStack = backStack, - onNavigateToRestrictedKey = { redirectToKey -> Login(redirectToKey) }, - isLoggedIn = { isLoggedIn } - ) - } - - NavDisplay( - backStack = backStack, - onBack = { navigator.goBack() }, - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3. Logged in? ${isLoggedIn}") { - Column { - Button(onClick = dropUnlessResumed { navigator.navigate(Profile) }) { - Text("Profile") - } - Button(onClick = dropUnlessResumed { navigator.navigate(Login()) }) { - Text("Login") - } - } - } - } - entry { - ContentBlue("Profile screen (only accessible once logged in)") { - Button(onClick = dropUnlessResumed { - isLoggedIn = false - navigator.navigate(Home) - }) { - Text("Logout") - } - } - } - entry { key -> - ContentYellow("Login screen. Logged in? $isLoggedIn") { - Button(onClick = dropUnlessResumed { - isLoggedIn = true - key.redirectToKey?.let { targetKey -> - backStack.remove(key) - navigator.navigate(targetKey) - } - }) { - Text("Login") - } - } - } - } - ) - } - } -} - - -// An overload of `rememberNavBackStack` that returns a subtype of `NavKey`. -// See https://issuetracker.google.com/issues/463382671 for a discussion of this function -@Composable -fun rememberNavBackStack(vararg elements: T): NavBackStack { - return rememberSerializable( - serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()) - ) { - NavBackStack(*elements) - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.conditional - -import androidx.navigation3.runtime.NavBackStack - -/** - * Provides navigation events with built-in support for conditional access. If the user attempts to - * navigate to a [ConditionalNavKey] that requires login ([ConditionalNavKey.requiresLogin] is true) - * but is not currently logged in, the Navigator will redirect the user to a login key. - * - * @property backStack The back stack that is modified by this class - * @property onNavigateToRestrictedKey A lambda that is called when the user attempts to navigate - * to a key that requires login. This should return the key that represents the login screen. The - * user's target key is supplied as a parameter so that after successful login the user can be - * redirected to their target destination. - * @property isLoggedIn A lambda that returns whether the user is logged in. - */ -class Navigator( - private val backStack: NavBackStack, - private val onNavigateToRestrictedKey: (targetKey: ConditionalNavKey?) -> ConditionalNavKey, - private val isLoggedIn: () -> Boolean, -) { - fun navigate(key: ConditionalNavKey) { - if (key.requiresLogin && !isLoggedIn()) { - val loginKey = onNavigateToRestrictedKey(key) - backStack.add(loginKey) - } else { - backStack.add(key) - } - } - - fun goBack() = backStack.removeLastOrNull() -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md deleted file mode 100644 index f241291..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md +++ /dev/null @@ -1,155 +0,0 @@ -# Deep Link Advanced Recipe - -This recipe demonstrates how to apply the principles of navigation in the context of deep links by -managing a synthetic backStack and Task stacks. - -# Recipe Structure - -This recipe simulates a real-world scenario where "App A" deeplinks -into "App B". - -"App A" is simulated by the module [com.example.nav3recipes.deeplink.advanced](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/advanced), which -contains the `CreateAdvancedDeepLinkActivity` that allows you to create a deeplink intent and -trigger that in either the existing Task, or in a new Task. - -"App B" is simulated by the module [advanceddeeplinkapp](https://developer.android.com/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced), which contains -the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack -and how to manage the Task stack properly in order to support both Back and Up buttons. - -# Core implementation - -The core helper functions for navigateUp and building synthetic backStack can be -found [here](https://developer.android.com/static/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt) - -# Further Read - -Check out the [deep link guide](https://developer.android.com/docs/deeplink-guide) for a -comprehensive guide on Deep linking principles and how to apply them in Navigation 3. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/advanced) - -``` -package com.example.nav3recipes.deeplink.advanced - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.core.net.toUri -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.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 -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com" - -/** - * The recipe entry point that allows users to create a deep link and make a request with it. - * - * **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deeplinks - * into "App B". - * - * "App A" is simulated by this current module [com.example.nav3recipes.deeplink.advanced], which - * contains the [AdvancedCreateDeepLinkActivity] that allows you to create a deeplink intent and - * trigger that in either the existing Task, or in a new Task. - * - * "App B" is simulated by the module [com.example.nav3recipes.deeplink.advanced], which contains - * the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack - * and how to manage the Task stack properly in order to support both Back and Up buttons. - * - * See the [README](README.md) file of current module for more info on advanced deep linking. - */ -class AdvancedCreateDeepLinkActivity: ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - EntryScreen("Sandbox - Build Your Deeplink Intent") { - val initFirstName = MENU_OPTIONS_FIRST_NAME.values.first().first() - val initLocation = MENU_OPTIONS_LOCATION.values.last().first() - val initTaskStack = MENU_OPTIONS_TASK_STACK.values.first().first() - var firstName by remember { mutableStateOf(initFirstName) } - var location by remember { mutableStateOf(initLocation) } - var taskStack by remember { mutableStateOf(initTaskStack) } - - // select first name - MenuDropDown( - menuOptions = MENU_OPTIONS_FIRST_NAME, - ) { _, selected -> - firstName = selected - } - - // select first name - MenuDropDown( - menuOptions = MENU_OPTIONS_LOCATION, - ) { _, selected -> - location = selected - } - - // select current task stack or build new task stack - MenuDropDown( - menuOptions = MENU_OPTIONS_TASK_STACK, - ) { _, selected -> - taskStack = selected - } - - // build final deeplink URL and Intent - val finalUrl = "${ADVANCED_PATH_BASE}/user/$firstName/$location" - - // display Intent info - val flagString = if (taskStack == TAG_NEW_TASK) { - "Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK" - } else "" - val intentString = """ - | Final Intent: - | data = "$finalUrl" - | action = Intent.ACTION_VIEW - | flags = $flagString - """.trimMargin() - - TextContent(intentString) - - // deeplink to target - PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { - val intent = Intent().apply { - data = finalUrl.toUri() - action = Intent.ACTION_VIEW - if (taskStack == TAG_NEW_TASK) { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } - - startActivity(intent) - }) - } - } - } -} - -private const val TAG_FIRST_NAME = "firstName" -private const val TAG_LOCATION = "location" -private const val TAG_TASK_STACK = "Task stack" -private const val TAG_CURRENT_TASK = "Use Current Task Stack" -private const val TAG_NEW_TASK = "Start New Task Stack" - -private val MENU_OPTIONS_FIRST_NAME = mapOf( - TAG_FIRST_NAME to LIST_FIRST_NAMES -) - -private val MENU_OPTIONS_LOCATION = mapOf( - TAG_LOCATION to LIST_LOCATIONS -) - -private val MENU_OPTIONS_TASK_STACK = mapOf( - TAG_TASK_STACK to listOf(TAG_CURRENT_TASK, TAG_NEW_TASK), -) -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md deleted file mode 100644 index 66312de..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md +++ /dev/null @@ -1,744 +0,0 @@ -# Deep Link Basic Recipe - -This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. - -## How it works - -It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. - -## Demonstrated forms of deeplink - -The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: - -1. `HomeKey` - deeplink with an exact url (no deeplink arguments) -2. `UsersKey` - deeplink with path arguments -3. `SearchKey` - deeplink with query arguments - -See `MainActivity.deepLinkPatterns` for the actual url pattern of each. - -## Recipe structure - -This recipe consists of three main packages: - -1. `basic.deeplink` - Contains the two activities -2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc -3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks - -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/basic) - -``` -package com.example.nav3recipes.deeplink.basic - -import androidx.navigation3.runtime.NavKey -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS -import kotlinx.serialization.Serializable - -internal interface NavRecipeKey: NavKey { - val name: String -} - -@Serializable -internal object HomeKey: NavRecipeKey { - override val name: String = STRING_LITERAL_HOME -} - -@Serializable -internal data class UsersKey( - val filter: String, -): NavRecipeKey { - override val name: String = STRING_LITERAL_USERS - companion object { - const val FILTER_KEY = STRING_LITERAL_FILTER - const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" - const val FILTER_OPTION_ALL = "all" - } -} - -@Serializable -internal data class SearchKey( - val firstName: String? = null, - val ageMin: Int? = null, - val ageMax: Int? = null, - val location: String? = null, -): NavRecipeKey { - override val name: String = STRING_LITERAL_SEARCH -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.core.net.toUri -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FriendsList -import com.example.nav3recipes.common.deeplink.LIST_USERS -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT -import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher -import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern -import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest -import com.example.nav3recipes.deeplink.basic.util.KeyDecoder -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * Parses a target deeplink into a NavKey. There are several crucial steps involved: - * - * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable - * format (see [DeepLinkPattern]) - * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) - * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each - * other to facilitate comparison and finding a match - * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match - * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result - * to backstack key, regardless of what the conversion method may be. - * STEP 4. Associate the match results with the correct backstack key - * - * This recipes provides an example for each of the above steps by way of kotlinx.serialization. - * - * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional - * deeplink considerations are not included in this scope** - * - Create synthetic backStack - * - Multi-modular setup - * - DI - * - Managing TaskStack - * - Up button ves Back Button - * - */ -class MainActivity : ComponentActivity() { - /** STEP 1. Parse supported deeplinks */ - // internal so that landing activity can link to this in the kdocs - internal val deepLinkPatterns: List> = listOf( - // "https://www.nav3recipes.com/home" - DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), - // "https://www.nav3recipes.com/users/with/{filter}" - DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), - // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" - DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), - ) - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - // retrieve the target Uri - val uri: Uri? = intent.data - // associate the target with the correct backstack key - val key: NavKey = uri?.let { - /** STEP 2. Parse requested deeplink */ - val request = DeepLinkRequest(uri) - /** STEP 3. Compared requested with supported deeplink to find match*/ - val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> - DeepLinkMatcher(request, pattern).match() - } - /** STEP 4. If match is found, associate match to the correct key*/ - match?.let { - //leverage kotlinx.serialization's Decoder to decode - // match result into a backstack key - KeyDecoder(match.args) - .decodeSerializableValue(match.serializer) - } - } ?: HomeKey // fallback if intent.uri is null or match is not found - - /** - * Then pass starting key to backstack - */ - setContent { - val backStack: NavBackStack = rememberNavBackStack(key) - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { key -> - EntryScreen(key.name) { - TextContent("") - } - } - entry { key -> - EntryScreen("${key.name} : ${key.filter}") { - TextContent("") - val list = when { - key.filter.isEmpty() -> LIST_USERS - key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS - else -> LIST_USERS.take(5) - } - FriendsList(list) - } - } - entry { search -> - EntryScreen(search.name) { - TextContent("") - val matchingUsers = LIST_USERS.filter { user -> - (search.firstName == null || user.firstName == search.firstName) && - (search.location == null || user.location == search.location) && - (search.ageMin == null || user.age >= search.ageMin) && - (search.ageMax == null || user.age <= search.ageMax) - } - FriendsList(matchingUsers) - } - } - } - ) - } - } -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.core.net.toUri -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.EMPTY -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE -import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY -import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM -import com.example.nav3recipes.common.deeplink.LOCATION_BC -import com.example.nav3recipes.common.deeplink.LOCATION_BR -import com.example.nav3recipes.common.deeplink.LOCATION_CA -import com.example.nav3recipes.common.deeplink.LOCATION_US -import com.example.nav3recipes.common.deeplink.MenuDropDown -import com.example.nav3recipes.common.deeplink.MenuTextInput -import com.example.nav3recipes.common.deeplink.PaddedButton -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE -import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE -import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * This activity allows the user to create a deep link and make a request with it. - * - * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct - * and trigger the deeplink request, and the [MainActivity] to show how an app can handle - * that request. - * - * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to - * demonstrate different types of supported deeplinks: - * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) - * 2. [UsersKey] - deeplink with path arguments - * 3. [SearchKey] - deeplink with query arguments - * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. - * - * **RECIPE STRUCTURE** This recipe consists of three main packages: - * 1. basic.deeplink - Contains the two activities - * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc - * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match - * the deeplinks - * - * See [MainActivity] for how the requested deeplink is handled. - */ -class CreateDeepLinkActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - /** - * UI for deeplink sandbox - */ - EntryScreen("Sandbox - Build Your Deeplink") { - TextContent("Base url:\n${PATH_BASE}/") - var showFilterOptions by remember { mutableStateOf(false) } - val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } - - var showQueryOptions by remember { mutableStateOf(false) } - var selectedFilter by remember { mutableStateOf("") } - val selectedSearchQuery = remember { mutableStateMapOf() } - - // manage path options - MenuDropDown( - menuOptions = MENU_OPTIONS_PATH, - ) { _, selection -> - selectedPath.value = selection - when (selection) { - PATH_SEARCH -> { - showQueryOptions = true - showFilterOptions = false - } - - PATH_INCLUDE -> { - showQueryOptions = false - showFilterOptions = true - } - - else -> { - showQueryOptions = false - showFilterOptions = false - } - } - } - - // manage path filter options, reset state if menu is closed - LaunchedEffect(showFilterOptions) { - selectedFilter = if (showFilterOptions) { - MENU_OPTIONS_FILTER.values.first().first() - } else { - "" - } - } - if (showFilterOptions) { - MenuDropDown( - menuOptions = MENU_OPTIONS_FILTER, - ) { _, selected -> - selectedFilter = selected - } - } - - // manage query options, reset state if menu is closed - LaunchedEffect(showQueryOptions) { - if (showQueryOptions) { - val initEntry = MENU_OPTIONS_SEARCH.entries.first() - selectedSearchQuery[initEntry.key] = initEntry.value.first() - } else { - selectedSearchQuery.clear() - } - } - if (showQueryOptions) { - MenuTextInput( - menuLabels = MENU_LABELS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - MenuDropDown( - menuOptions = MENU_OPTIONS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - } - - // form final deeplink url - val arguments = when (selectedPath.value) { - PATH_INCLUDE -> "/${selectedFilter}" - PATH_SEARCH -> { - buildString { - selectedSearchQuery.forEach { entry -> - if (entry.value.isNotEmpty()) { - val prefix = if (isEmpty()) "?" else "&" - append("$prefix${entry.key}=${entry.value}") - } - } - } - } - - else -> "" - } - val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" - TextContent("Final url:\n$finalUrl") - // deeplink to target - PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { - val intent = Intent( - this@CreateDeepLinkActivity, - MainActivity::class.java - ) - // start activity with the url - intent.data = finalUrl.toUri() - startActivity(intent) - }) - } - } - } -} - -private const val KEY_PATH = "path" -private val MENU_OPTIONS_PATH = mapOf( - KEY_PATH to listOf( - STRING_LITERAL_HOME, - PATH_INCLUDE, - PATH_SEARCH, - ), -) - -private val MENU_OPTIONS_FILTER = mapOf( - UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), -) - -private val MENU_OPTIONS_SEARCH = mapOf( - SearchKey::firstName.name to listOf( - EMPTY, - FIRST_NAME_JOHN, - FIRST_NAME_TOM, - FIRST_NAME_MARY, - FIRST_NAME_JULIE - ), - SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) -) - -private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) - -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -/** - * Decodes the list of arguments into a a back stack key - * - * **IMPORTANT** This decoder assumes that all argument types are Primitives. - */ -@OptIn(ExperimentalSerializationApi::class) -internal class KeyDecoder( - private val arguments: Map, -) : AbstractDecoder() { - - override val serializersModule: SerializersModule = EmptySerializersModule() - private var elementIndex: Int = -1 - private var elementName: String = "" - - /** - * Decodes the index of the next element to be decoded. Index represents a position of the - * current element in the [descriptor] that can be found with [descriptor].getElementIndex. - * - * The returned index will trigger deserializer to call [decodeValue] on the argument at that - * index. - * - * The decoder continually calls this method to process the next available argument until this - * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more - * arguments to decode. - * - * This method should sequentially return the element index for every element that has its value - * available within [arguments]. - */ - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - var currentIndex = elementIndex - while (true) { - // proceed to next element - currentIndex++ - // if we have reached the end, let decoder know there are not more arguments to decode - if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE - val currentName = descriptor.getElementName(currentIndex) - // Check if bundle has argument value. If so, we tell decoder to process - // currentIndex. Otherwise, we skip this index and proceed to next index. - if (arguments.contains(currentName)) { - elementIndex = currentIndex - elementName = currentName - return elementIndex - } - } - } - - /** - * Returns argument value from the [arguments] for the argument at the index returned by - * [decodeElementIndex] - */ - override fun decodeValue(): Any { - val arg = arguments[elementName] - checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } - return arg - } - - override fun decodeNull(): Nothing? = null - - // we want to know if it is not null, so its !isNull - override fun decodeNotNullMark(): Boolean = arguments[elementName] != null -} -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri - -/** - * Parse the requested Uri and store it in a easily readable format - * - * @param uri the target deeplink uri to link to - */ -internal class DeepLinkRequest( - val uri: Uri -) { - /** - * A list of path segments - */ - val pathSegments: List = uri.pathSegments - - /** - * A map of query name to query value - */ - val queries = buildMap { - uri.queryParameterNames.forEach { argName -> - this[argName] = uri.getQueryParameter(argName)!! - } - } - - // TODO add parsing for other Uri components, i.e. fragments, mimeType, action -} -``` - -```` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.encoding.CompositeDecoder -import java.io.Serializable - -/** - * Parse a supported deeplink and stores its metadata as a easily readable format - * - * The following notes applies specifically to this particular sample implementation: - * - * The supported deeplink is expected to be built from a serializable backstack key [T] that - * supports deeplink. This means that if this deeplink contains any arguments (path or query), - * the argument name must match any of [T] member field name. - * - * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] - * supports two deeplink patterns: - * ``` - * val deeplink1 = www.nav3recipes.com/home - * val deeplink2 = www.nav3recipes.com/profile/{userId} - * ``` - * Then two [DeepLinkPattern] should be created - * ``` - * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) - * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) - * ``` - * - * This implementation assumes a few things: - * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match - * 2. all query arguments are optional by way of nullable/has default value - * - * @param T the backstack key type that supports the deeplinking of [uriPattern] - * @param serializer the serializer of [T] - * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" - */ -internal class DeepLinkPattern( - val serializer: KSerializer, - val uriPattern: Uri -) { - /** - * Help differentiate if a path segment is an argument or a static value - */ - private val regexPatternFillIn = Regex("\\{(.+?)\\}") - - // TODO make these lazy - /** - * parse the path into a list of [PathSegment] - * - * order matters here - path segments need to match in value and order when matching - * requested deeplink to supported deeplink - */ - val pathSegments: List = buildList { - uriPattern.pathSegments.forEach { segment -> - // first, check if it is a path arg - var result = regexPatternFillIn.find(segment) - if (result != null) { - // if so, extract the path arg name (the string value within the curly braces) - val argName = result.groups[1]!!.value - // from [T], read the primitive type of this argument to get the correct type parser - val elementIndex = serializer.descriptor.getElementIndex(argName) - if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { - throw IllegalArgumentException( - "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." - ) - } - - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - // finally, add the arg name and its respective type parser to the map - add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) - } else { - // if its not a path arg, then its just a static string path segment - add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) - } - } - } - - /** - * Parse supported queries into a map of queryParameterNames to [TypeParser] - * - * This will be used later on to parse a provided query value into the correct KType - */ - val queryValueParsers: Map = buildMap { - uriPattern.queryParameterNames.forEach { paramName -> - val elementIndex = serializer.descriptor.getElementIndex(paramName) - // Ignore static query parameters that are not in the Serializable class - if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - this[paramName] = getTypeParser(elementDescriptor.kind) - } - } - } - - /** - * Metadata about a supported path segment - */ - class PathSegment( - val stringValue: String, - val isParamArg: Boolean, - val typeParser: TypeParser - ) -} - -/** - * Parses a String into a Serializable Primitive - */ -private typealias TypeParser = (String) -> Serializable - -private fun getTypeParser(kind: SerialKind): TypeParser { - return when (kind) { - PrimitiveKind.STRING -> Any::toString - PrimitiveKind.INT -> String::toInt - PrimitiveKind.BOOLEAN -> String::toBoolean - PrimitiveKind.BYTE -> String::toByte - PrimitiveKind.CHAR -> String::toCharArray - PrimitiveKind.DOUBLE -> String::toDouble - PrimitiveKind.FLOAT -> String::toFloat - PrimitiveKind.LONG -> String::toLong - PrimitiveKind.SHORT -> String::toShort - else -> throw IllegalArgumentException( - "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." - ) - } -} -```` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.util.Log -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer - -internal class DeepLinkMatcher( - val request: DeepLinkRequest, - val deepLinkPattern: DeepLinkPattern -) { - /** - * Match a [DeepLinkRequest] to a [DeepLinkPattern]. - * - * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise - */ - fun match(): DeepLinkMatchResult? { - if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null - if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null - if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null - // exact match (url does not contain any arguments) - if (request.uri == deepLinkPattern.uriPattern) - return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) - - val args = mutableMapOf() - // match the path - request.pathSegments - .asSequence() - // zip to compare the two objects side by side, order matters here so we - // need to make sure the compared segments are at the same position within the url - .zip(deepLinkPattern.pathSegments.asSequence()) - .forEach { it -> - // retrieve the two path segments to compare - val requestedSegment = it.first - val candidateSegment = it.second - // if the potential match expects a path arg for this segment, try to parse the - // requested segment into the expected type - if (candidateSegment.isParamArg) { - val parsedValue = try { - candidateSegment.typeParser.invoke(requestedSegment) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) - return null - } - args[candidateSegment.stringValue] = parsedValue - } else if(requestedSegment != candidateSegment.stringValue){ - // if it's path arg is not the expected type, its not a match - return null - } - } - // match queries (if any) - request.queries.forEach { query -> - val name = query.key - // If the pattern does not define this query parameter, ignore it. - // This prevents a NullPointerException. - val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach - - val queryParsedValue = try { - queryStringParser.invoke(query.value) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) - return null - } - args[name] = queryParsedValue - } - // provide the serializer of the matching key and map of arg names to parsed arg values - return DeepLinkMatchResult(deepLinkPattern.serializer, args) - } -} - - -/** - * Created when a requested deeplink matches with a supported deeplink - * - * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink - * @param serializer serializer for [T] - * @param args The map of argument name to argument value. The value is expected to have already - * been parsed from the raw url string back into its proper KType as declared in [T]. - * Includes arguments for all parts of the uri - path, query, etc. - * */ -internal data class DeepLinkMatchResult( - val serializer: KSerializer, - val args: Map -) - -const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" -``` - -``` -package com.example.nav3recipes.deeplink.basic.ui - -import com.example.nav3recipes.deeplink.basic.SearchKey - -/** - * String resources - */ -internal const val STRING_LITERAL_FILTER = "filter" -internal const val STRING_LITERAL_HOME = "home" -internal const val STRING_LITERAL_USERS = "users" -internal const val STRING_LITERAL_SEARCH = "search" -internal const val STRING_LITERAL_INCLUDE = "include" -internal const val PATH_BASE = "https://www.nav3recipes.com" -internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" -internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" -internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" - -internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" -internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + - "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + - "&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" + - "&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" + - "&${SearchKey::location.name}={${SearchKey::location.name}}" -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md deleted file mode 100644 index 6ef8efe..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md +++ /dev/null @@ -1,107 +0,0 @@ -# Dialog Recipe - -This recipe demonstrates how to display a destination as a dialog. - -## How it works - -To show a destination as a dialog, you need to do two things: - -1. **Use `DialogSceneStrategy`** : Create an instance of `DialogSceneStrategy` and pass it to the `sceneStrategy` parameter of the `NavDisplay` composable. - -2. **Add metadata to the destination** : For the destination that you want to display as a dialog, add `DialogSceneStrategy.dialog()` to its metadata. This is done in the `entry` function. You can also pass a `DialogProperties` object to customize the dialog's behavior and appearance. - -In this example, `RouteB` is configured to be a dialog. When you navigate from `RouteA` to `RouteB`, `RouteB` will be displayed in a dialog window. - -The content of the dialog can be styled as needed. In this recipe, the content is clipped to have rounded corners. - -For more information, see the official documentation on [custom layouts](https://developer.android.com/guide/navigation/navigation-3/custom-layouts). -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/dialog) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.dialog - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.scene.DialogSceneStrategy -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private data object RouteA : NavKey - -@Serializable -private data class RouteB(val id: String) : NavKey - -class DialogActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = rememberNavBackStack(RouteA) - val dialogStrategy = remember { DialogSceneStrategy() } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(dialogStrategy), - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to open dialog") - } - } - } - entry( - metadata = DialogSceneStrategy.dialog( - DialogProperties(windowTitle = "Route B dialog") - ) - ) { key -> - ContentBlue( - title = "Route id: ${key.id}", - modifier = Modifier.clip( - shape = RoundedCornerShape(16.dp) - ) - ) - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md deleted file mode 100644 index fbaae79..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +++ /dev/null @@ -1,141 +0,0 @@ -# Material List-Detail Recipe - -This recipe demonstrates how to create an adaptive list-detail layout using the `ListDetailSceneStrategy` from the Material 3 Adaptive library. This layout automatically adjusts to show one, two, or three panes depending on the available screen width. - -## How it works - -This example has three destinations: `ConversationList`, `ConversationDetail`, and `Profile`. - -### `ListDetailSceneStrategy` - -The key to this recipe is the `rememberListDetailSceneStrategy`, which provides the logic for the adaptive layout. - -- **Pane Roles**: Each destination is assigned a role using metadata: - - - `ListDetailSceneStrategy.listPane()`: For the primary (list) content. This pane is always visible. A placeholder can be provided to be shown in the detail pane area when no detail content is selected. - - `ListDetailSceneStrategy.detailPane()`: For the secondary (detail) content. - - `ListDetailSceneStrategy.extraPane()`: For tertiary content. -- **Adaptive Layout** : The `ListDetailSceneStrategy` automatically handles the layout. On smaller screens, only one pane is shown at a time. On wider screens, it will show the list and detail panes side-by-side. On very wide screens, it can show all three panes: list, detail, and extra. - -- **Navigation** : Navigation between the panes is handled by adding and removing destinations from the back stack as usual. The `ListDetailSceneStrategy` observes the back stack and adjusts the layout accordingly. - -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/listdetail) - -``` -package com.example.nav3recipes.material.listdetail - -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy -import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentRed -import com.example.nav3recipes.content.ContentYellow -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private object ConversationList : NavKey - -@Serializable -private data class ConversationDetail(val id: String) : NavKey - -@Serializable -private data object Profile : NavKey - -class MaterialListDetailActivity : ComponentActivity() { - - @OptIn(ExperimentalMaterial3AdaptiveApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - - val backStack = rememberNavBackStack(ConversationList) - - // Override the defaults so that there isn't a horizontal space between the panes. - // See b/418201867 - val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() - val directive = remember(windowAdaptiveInfo) { - calculatePaneScaffoldDirective(windowAdaptiveInfo) - .copy(horizontalPartitionSpacerSize = 0.dp) - } - val listDetailStrategy = rememberListDetailSceneStrategy(directive = directive) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(listDetailStrategy), - entryProvider = entryProvider { - entry( - metadata = ListDetailSceneStrategy.listPane( - detailPlaceholder = { - ContentYellow("Choose a conversation from the list") - } - ) - ) { - ContentRed("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(ConversationDetail("ABC")) - }) { - Text("View conversation") - } - } - } - entry( - metadata = ListDetailSceneStrategy.detailPane() - ) { conversation -> - ContentBlue("Conversation ${conversation.id} ") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed { - backStack.add(Profile) - }) { - Text("View profile") - } - } - } - } - entry( - metadata = ListDetailSceneStrategy.extraPane() - ) { - ContentGreen("Profile") - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md deleted file mode 100644 index 58a6a7c..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md +++ /dev/null @@ -1,145 +0,0 @@ -# Material Supporting Pane Recipe - -This recipe demonstrates how to create an adaptive layout with a main pane and a supporting pane using the `SupportingPaneSceneStrategy` from the Material 3 Adaptive library. This layout is useful for displaying supplementary content alongside the main content on larger screens. - -## How it works - -This example has three destinations: `MainVideo`, `RelatedVideos`, and `Profile`. - -### `SupportingPaneSceneStrategy` - -The `rememberSupportingPaneSceneStrategy` provides the logic for this adaptive layout. - -- **Pane Roles**: Each destination is assigned a role using metadata: - - - `SupportingPaneSceneStrategy.mainPane()`: For the primary content. This pane is always visible. - - `SupportingPaneSceneStrategy.supportingPane()`: For the supplementary content. This pane is shown alongside the main pane on larger screens. - - `SupportingPaneSceneStrategy.extraPane()`: For tertiary content that can be displayed alongside the supporting pane on even larger screens. -- **Adaptive Layout** : The `SupportingPaneSceneStrategy` automatically handles the layout. On smaller screens, only the main pane is shown. On larger screens, the supporting pane is shown next to the main pane. - -- **Back Navigation** : The `BackNavigationBehavior` is customized in this example to `PopUntilCurrentDestinationChange`. This means that when the user presses the back button, the supporting pane will be dismissed, revealing the main pane underneath. - -- **Navigation** : Navigation is handled by adding and removing destinations from the back stack. The `SupportingPaneSceneStrategy` observes these changes and adjusts the layout accordingly. - -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/material/supportingpane) - -``` -package com.example.nav3recipes.material.supportingpane - -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy -import androidx.compose.material3.adaptive.navigation3.rememberSupportingPaneSceneStrategy -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentRed -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private object MainVideo : NavKey - -@Serializable -private data object RelatedVideos : NavKey - -@Serializable -private data object Profile : NavKey - -class MaterialSupportingPaneActivity : ComponentActivity() { - - @OptIn(ExperimentalMaterial3AdaptiveApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - - val backStack = rememberNavBackStack(MainVideo) - - // Override the defaults so that there isn't a horizontal or vertical space between the panes. - // See b/444438086 - val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() - val directive = remember(windowAdaptiveInfo) { - calculatePaneScaffoldDirective(windowAdaptiveInfo) - .copy(horizontalPartitionSpacerSize = 0.dp, verticalPartitionSpacerSize = 0.dp) - } - - // Override the defaults so that the supporting pane can be dismissed by pressing back. - // See b/445826749 - val supportingPaneStrategy = rememberSupportingPaneSceneStrategy( - backNavigationBehavior = BackNavigationBehavior.PopUntilCurrentDestinationChange, - directive = directive - ) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(supportingPaneStrategy), - entryProvider = entryProvider { - entry( - metadata = SupportingPaneSceneStrategy.mainPane() - ) { - ContentRed("Video content") { - Button(onClick = dropUnlessResumed { - backStack.add(RelatedVideos) - }) { - Text("View related videos") - } - } - } - entry( - metadata = SupportingPaneSceneStrategy.supportingPane() - ) { - ContentBlue("Related videos") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed { - backStack.add(Profile) - }) { - Text("View profile") - } - } - } - } - entry( - metadata = SupportingPaneSceneStrategy.extraPane() - ) { - ContentGreen("Profile") - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md deleted file mode 100644 index fcc94a2..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md +++ /dev/null @@ -1,283 +0,0 @@ -# Modular Navigation Recipe (Hilt) - -This recipe demonstrates how to structure a multi-module application using Navigation 3 and Dagger/Hilt for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. - -## How it works - -The application is divided into several modules: - -- **`app` module** : This is the main application module. It initializes a common `Navigator` and injects a set of `EntryProviderInstaller`s from the feature modules. It then uses these installers to build the final `entryProvider` for the `NavDisplay`. - -- **`common` module**: This module contains the core navigation logic, including: - - - A `Navigator` class that manages the back stack. - - An `EntryProviderInstaller` type, which is a function that feature modules use to contribute their navigation entries to the application's `entryProvider`. -- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: - - - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. - - **`impl` module** : Provides the implementation of the feature, including its composables and an `EntryProviderInstaller` that maps the feature's routes to its composables. This installer is then provided to the `app` module using Dagger/Hilt. - -This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/hilt) - -``` -package com.example.nav3recipes.modular.hilt - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.multibindings.IntoSet - -// API -object Profile - -// IMPLEMENTATION -@Module -@InstallIn(ActivityRetainedComponent::class) -object ProfileModule { - - @IntoSet - @Provides - fun provideEntryProviderInstaller() : EntryProviderInstaller = { - entry{ - ProfileScreen() - } - } -} - -@Composable -private fun ProfileScreen() { - val profileColor = MaterialTheme.colorScheme.surfaceVariant - Column( - modifier = Modifier - .fillMaxSize() - .background(profileColor) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Profile Screen", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } -} -``` - -``` -package com.example.nav3recipes.modular.hilt - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class HiltModularActivity : ComponentActivity() { - - @Inject - lateinit var navigator: Navigator - - @Inject - lateinit var entryProviderScopes: Set<@JvmSuppressWildcards EntryProviderInstaller> - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setEdgeToEdgeConfig() - setContent { - Scaffold { paddingValues -> - NavDisplay( - backStack = navigator.backStack, - modifier = Modifier.padding(paddingValues), - onBack = { navigator.goBack() }, - entryProvider = entryProvider { - entryProviderScopes.forEach { builder -> this.builder() } - } - ) - } - } - } -} -``` - -``` -package com.example.nav3recipes.modular.hilt - -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.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.material3.Button -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.ui.theme.colors -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.multibindings.IntoSet - -// API -object ConversationList -data class ConversationDetail(val id: Int) { - val color: Color - get() = colors[id % colors.size] -} - -// IMPL -@Module -@InstallIn(ActivityRetainedComponent::class) -object ConversationModule { - - @IntoSet - @Provides - fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = - { - entry { - ConversationListScreen( - onConversationClicked = { conversationDetail -> - navigator.goTo(conversationDetail) - } - ) - } - entry { key -> - ConversationDetailScreen(key) { navigator.goTo(Profile) } - } - } -} - -@Composable -private fun ConversationListScreen( - onConversationClicked: (ConversationDetail) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(10) { index -> - val conversationId = index + 1 - val conversationDetail = ConversationDetail(conversationId) - val backgroundColor = conversationDetail.color - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = dropUnlessResumed { - onConversationClicked(conversationDetail) - }), - headlineContent = { - Text( - text = "Conversation $conversationId", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - colors = ListItemDefaults.colors( - containerColor = backgroundColor // Set container color directly - ) - ) - } - } -} - -@Composable -private fun ConversationDetailScreen( - conversationDetail: ConversationDetail, - onProfileClicked: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(conversationDetail.color) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Conversation Detail Screen: ${conversationDetail.id}", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onProfileClicked)) { - Text("View Profile") - } - } -} -``` - -``` -package com.example.nav3recipes.modular.hilt - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.navigation3.runtime.EntryProviderScope -import dagger.hilt.android.scopes.ActivityRetainedScoped - - -typealias EntryProviderInstaller = EntryProviderScope.() -> Unit - -@ActivityRetainedScoped -class Navigator(startDestination: Any) { - val backStack : SnapshotStateList = mutableStateListOf(startDestination) - - fun goTo(destination: Any){ - backStack.add(destination) - } - - fun goBack(){ - backStack.removeLastOrNull() - } -} -``` - -``` -package com.example.nav3recipes.modular.hilt - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.hilt.android.scopes.ActivityRetainedScoped - -@Module -@InstallIn(ActivityRetainedComponent::class) -object AppModule { - - @Provides - @ActivityRetainedScoped - fun provideNavigator() : Navigator = Navigator(startDestination = ConversationList) -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md deleted file mode 100644 index d1e7ad6..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md +++ /dev/null @@ -1,287 +0,0 @@ -# Modular Navigation Recipe (Koin) - -This recipe demonstrates how to structure a multi-module application using Navigation 3 and Koin for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. It relies on the [`koin-compose-navigation3`](https://insert-koin.io/docs/reference/koin-compose/navigation3) artifact. - -## How it works - -The application is divided into several Android modules: - -- **`app` module** : This is the main application module. It `includes()` the feature modules and initializes a common `Navigator`. - -- **`common` module** : This module contains the core navigation logic used by both the application module and the feature modules. Namely, it defines a `Navigator` class that manages the back stack. - -- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: - - - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. - - **`impl` module** : Provides the implementation of the feature, including its composables and Koin `Module`. The Koin module uses the [`navigation`](https://insert-koin.io/docs/reference/koin-compose/navigation3/#declaring-navigation-entries) DSL to define the entry provider installers for the feature module. - -This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/koin) - -``` -package com.example.nav3recipes.modular.koin - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.dsl.module -import org.koin.dsl.navigation3.navigation - -// API -object Profile - -// IMPL -@OptIn(KoinExperimentalAPI::class) -val profileModule = module { - activityRetainedScope { - navigation { ProfileScreen() } - } -} - -@Composable -private fun ProfileScreen() { - val profileColor = MaterialTheme.colorScheme.surfaceVariant - Column( - modifier = Modifier - .fillMaxSize() - .background(profileColor) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Profile Screen", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -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.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.material3.Button -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.ui.theme.colors -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.dsl.module -import org.koin.dsl.navigation3.navigation - -// API -object ConversationList -data class ConversationDetail(val id: Int) { - val color: Color - get() = colors[id % colors.size] -} - -// IMPL -@OptIn(KoinExperimentalAPI::class) -val conversationModule = module { - activityRetainedScope { - navigation { - ConversationListScreen( - onConversationClicked = { conversationDetail -> - get().goTo(conversationDetail) - } - ) - } - - navigation { key -> - ConversationDetailScreen(key) { - get().goTo(Profile) - } - } - } -} - -@Composable -private fun ConversationListScreen( - onConversationClicked: (ConversationDetail) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(10) { index -> - val conversationId = index + 1 - val conversationDetail = ConversationDetail(conversationId) - val backgroundColor = conversationDetail.color - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = dropUnlessResumed { - onConversationClicked(conversationDetail) - }), - headlineContent = { - Text( - text = "Conversation $conversationId", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - colors = ListItemDefaults.colors( - containerColor = backgroundColor // Set container color directly - ) - ) - } - } -} - -@Composable -private fun ConversationDetailScreen( - conversationDetail: ConversationDetail, - onProfileClicked: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(conversationDetail.color) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Conversation Detail Screen: ${conversationDetail.id}", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onProfileClicked)) { - Text("View Profile") - } - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList - -class Navigator(startDestination: Any) { - val backStack : SnapshotStateList = mutableStateListOf(startDestination) - - fun goTo(destination: Any){ - backStack.add(destination) - } - - fun goBack(){ - backStack.removeLastOrNull() - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.dsl.module - -val appModule = module { - includes(profileModule,conversationModule) - - activityRetainedScope { - scoped { - Navigator(startDestination = ConversationList) - } - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import org.koin.android.ext.android.inject -import org.koin.android.scope.AndroidScopeComponent -import org.koin.androidx.compose.navigation3.getEntryProvider -import org.koin.androidx.scope.activityRetainedScope -import org.koin.core.Koin -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.core.component.KoinComponent -import org.koin.core.scope.Scope -import org.koin.dsl.koinApplication - -/** - * This recipe demonstrates how to use a modular approach with Navigation 3, - * where different parts of the application are defined in separate modules and injected - * into the main app using Koin. - * - * Features (Conversation and Profile) are split into two modules: - * - api: defines the public facing routes for this feature - * - impl: defines the entryProviders for this feature, these are injected into the app's main activity - * The common module defines: - * - a common navigator class that exposes a back stack and methods to modify that back stack - * - a type that should be used by feature modules to inject entryProviders into the app's main activity - * The app module creates the navigator by supplying a start destination and provides this navigator - * to the rest of the app module (i.e. MainActivity) and the feature modules. - */ -@OptIn(KoinExperimentalAPI::class) -class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComponent { - // Local Koin Context Instance - companion object { - private val localKoin = koinApplication { - modules(appModule) - }.koin - } - // Override default Koin context to use the local one - override fun getKoin(): Koin = localKoin - override val scope : Scope by activityRetainedScope() - val navigator: Navigator by inject() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setEdgeToEdgeConfig() - setContent { - Scaffold { paddingValues -> - NavDisplay( - backStack = navigator.backStack, - modifier = Modifier.padding(paddingValues), - onBack = { navigator.goBack() }, - entryProvider = getEntryProvider() - ) - } - } - } - -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md deleted file mode 100644 index 81cd794..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md +++ /dev/null @@ -1,436 +0,0 @@ -# Multiple back stacks recipe - -This recipe demonstrates how to create multiple back stacks. - -The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. The content for the sub routes is a counter that can be used to verify state retention through configuration changes and process death. - -The app's navigation state is held in the `NavigationState` class. The state itself is created using `rememberNavigationState`. - -Navigation events are handled by the `Navigator`. It updates the navigation state. - -The navigation state is converted into `NavEntry`s with `NavigationState.toDecoratedEntries`. These entries are then displayed by `NavDisplay`. - -Key behaviors: - -- This app follows the "exit through home" pattern where the user always exits through the starting back stack. This means that `RouteA`'s entries are *always* in the list of entries. -- Navigating to a top level route that is not the starting route *replaces* the other entries. For example, navigating A-\>B-\>C would result in entries for A+C, B's entries are removed. - -Important implementation details: - -- Each top level route has its own `SaveableStateHolderNavEntryDecorator`. This is the object responsible for managing the state for the entries in its back stack. - -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/multiplestacks) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.multiplestacks - -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() - } - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.multiplestacks - -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.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. - * - * @param startRoute - The top level route to start on. This should also be in `topLevelRoutes`. - * @param topLevelRoutes - The top level routes in the app. - */ -@Composable -fun rememberNavigationState( - startRoute: NavKey, - topLevelRoutes: Set -): NavigationState { - - val topLevelRoute = rememberSerializable( - startRoute, topLevelRoutes, - serializer = MutableStateSerializer(NavKeySerializer()) - ) { - mutableStateOf(startRoute) - } - - // Create a back stack for each top level route. - val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } - - return remember(startRoute, topLevelRoutes) { - NavigationState( - startRoute = startRoute, - topLevelRoute = topLevelRoute, - backStacks = backStacks - ) - } -} - -/** - * State holder for navigation state. This class does not modify its own state. It is designed - * to be modified using the `Navigator` class. - * - * @param startRoute - the start route. The user will exit the app through this route. - * @param topLevelRoute - the state object that backs the top level route. - * @param backStacks - the back stacks for each top level route. - */ -class NavigationState( - val startRoute: NavKey, - topLevelRoute: MutableState, - val backStacks: Map> -) { - - /** - * The top level route. - */ - var topLevelRoute: NavKey by topLevelRoute - - /** - * Convert the navigation state into `NavEntry`s that have been decorated with a - * `SaveableStateHolder`. - * - * @param entryProvider - the entry provider used to convert the keys in the - * back stacks to `NavEntry`s. - */ - @Composable - fun toDecoratedEntries( - entryProvider: (NavKey) -> NavEntry - ): List> { - - // For each back stack, create a `SaveableStateHolder` decorator and use it to decorate - // the entries from that stack. When backStacks changes, `rememberDecoratedNavEntries` will - // be recomposed and a new list of decorated entries is returned. - val decoratedEntries = backStacks.mapValues { (_, stack) -> - val decorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - ) - rememberDecoratedNavEntries( - backStack = stack, - entryDecorators = decorators, - entryProvider = entryProvider - ) - } - - // Only return the entries for the stacks that are currently in use. - return getTopLevelRoutesInUse() - .flatMap { decoratedEntries[it] ?: emptyList() } - } - - /** - * Get the top level routes that are currently in use. The start route is always the first route - * in the list. This means the user will always exit the app through the starting route - * ("exit through home" pattern). The list will contain a maximum of one other route. This is a - * design decision. In your app, you may wish to allow more than two top level routes to be - * active. - * - * Note that even if a top level route is not in use its state is still retained. - * - * @return the current top level routes that are in use. - */ - private fun getTopLevelRoutesInUse() : List = - if (topLevelRoute == startRoute) { - listOf(startRoute) - } else { - listOf(startRoute, topLevelRoute) - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.multiplestacks - -import android.annotation.SuppressLint -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Camera -import androidx.compose.material.icons.filled.Face -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - - -@Serializable -data object RouteA : NavKey - -@Serializable -data object RouteA1 : NavKey - -@Serializable -data object RouteB : NavKey - -@Serializable -data object RouteB1 : NavKey - -@Serializable -data object RouteC : NavKey - -@Serializable -data object RouteC1 : NavKey - -private val TOP_LEVEL_ROUTES = mapOf( - RouteA to NavBarItem(icon = Icons.Default.Home, description = "Route A"), - RouteB to NavBarItem(icon = Icons.Default.Face, description = "Route B"), - RouteC to NavBarItem(icon = Icons.Default.Camera, description = "Route C"), -) - -data class NavBarItem( - val icon: ImageVector, - val description: String -) - -class MultipleStacksActivity : ComponentActivity() { - @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val navigationState = rememberNavigationState( - startRoute = RouteA, - topLevelRoutes = TOP_LEVEL_ROUTES.keys - ) - - val navigator = remember { Navigator(navigationState) } - - val entryProvider = entryProvider { - featureASection(onSubRouteClick = { navigator.navigate(RouteA1) }) - featureBSection(onSubRouteClick = { navigator.navigate(RouteB1) }) - featureCSection(onSubRouteClick = { navigator.navigate(RouteC1) }) - } - - Scaffold(bottomBar = { - NavigationBar { - TOP_LEVEL_ROUTES.forEach { (key, value) -> - val isSelected = key == navigationState.topLevelRoute - NavigationBarItem( - selected = isSelected, - onClick = { navigator.navigate(key) }, - icon = { - Icon( - imageVector = value.icon, - contentDescription = value.description - ) - }, - label = { Text(value.description) } - ) - } - } - }) { - NavDisplay( - entries = navigationState.toDecoratedEntries(entryProvider), - onBack = { navigator.goBack() } - ) - } - } - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.multiplestacks - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.EntryProviderScope -import androidx.navigation3.runtime.NavKey -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentMauve -import com.example.nav3recipes.content.ContentOrange -import com.example.nav3recipes.content.ContentPink -import com.example.nav3recipes.content.ContentPurple -import com.example.nav3recipes.content.ContentRed - -fun EntryProviderScope.featureASection( - onSubRouteClick: () -> Unit, -) { - entry { - ContentRed("Route A") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { - Text("Go to A1") - } - } - } - } - entry { - ContentPink("Route A1") { - var count by rememberSaveable { - mutableIntStateOf(0) - } - - Button(onClick = { count++ }) { - Text("Value: $count") - } - } - } -} - -fun EntryProviderScope.featureBSection( - onSubRouteClick: () -> Unit, -) { - entry { - ContentGreen("Route B") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { - Text("Go to B1") - } - } - } - } - entry { - ContentPurple("Route B1") { - var count by rememberSaveable { - mutableIntStateOf(0) - } - Button(onClick = { count++ }) { - Text("Value: $count") - } - } - } -} - -fun EntryProviderScope.featureCSection( - onSubRouteClick: () -> Unit, -) { - entry { - ContentMauve("Route C") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { - Text("Go to C1") - } - } - } - } - entry { - ContentOrange("Route C1") { - var count by rememberSaveable { - mutableIntStateOf(0) - } - - Button(onClick = { count++ }) { - Text("Value: $count") - } - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md deleted file mode 100644 index 2640a22..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md +++ /dev/null @@ -1,371 +0,0 @@ -# Passing Arguments to ViewModels (Hilt) - -This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using Hilt for dependency injection. - -## How it works - -This example uses Dagger/Hilt's assisted injection feature: - -1. The `ViewModel` is annotated with `@HiltViewModel` and its constructor uses `@AssistedInject` to receive the navigation key (which is annotated with `@Assisted`). -2. An `@AssistedFactory` interface is defined to create the `ViewModel`. -3. The `hiltViewModel` composable function is used to obtain the `ViewModel` instance. A `creationCallback` is provided to pass the navigation key to the factory, making it available to the `ViewModel`. - -**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/hilt) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.passingarguments.viewmodels.hilt - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.passingarguments.viewmodels.basic.RouteB -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.lifecycle.HiltViewModel - -data object RouteA -data class RouteB(val id: String) - -@AndroidEntryPoint -class HiltViewModelsActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = remember { mutableStateListOf(RouteA) } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - - // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) - // we also need to add the default `NavEntryDecorator`s as well. These provide - // extra information to the entry's content to enable it to display correctly - // and save its state. - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator() - ), - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - LazyColumn { - items(10) { i -> - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("$i")) - }) { - Text("$i") - } - } - } - } - } - entry { key -> - val viewModel = hiltViewModel( - // Note: We need a new ViewModel for every new RouteB instance. Usually - // we would need to supply a `key` String that is unique to the - // instance, however, the ViewModelStoreNavEntryDecorator (supplied - // above) does this for us, using `NavEntry.contentKey` to uniquely - // identify the viewModel. - // - // tl;dr: Make sure you use rememberViewModelStoreNavEntryDecorator() - // if you want a new ViewModel for each new navigation key instance. - creationCallback = { factory -> - factory.create(key) - } - ) - ScreenB(viewModel = viewModel) - } - } - ) - } - } -} - -@Composable -fun ScreenB(viewModel: RouteBViewModel) { - ContentBlue("Route id: ${viewModel.navKey.id} ") -} - -@HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) -class RouteBViewModel @AssistedInject constructor( - @Assisted val navKey: RouteB -) : ViewModel() { - - @AssistedFactory - interface Factory { - fun create(navKey: RouteB): RouteBViewModel - } -} -``` - -# Passing Arguments to ViewModels (Basic) - -This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using a custom `ViewModelProvider.Factory`. - -## How it works - -1. A custom `ViewModelProvider.Factory` is created that takes the navigation key as a constructor parameter. -2. Inside the `entry` composable, `viewModel(factory = ...)` is used to create the `ViewModel` instance, passing the current navigation key to the factory. This makes the navigation key available to the `ViewModel`. - -**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/basic) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.passingarguments.viewmodels.basic - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -data object RouteA - -data class RouteB(val id: String) - -class BasicViewModelsActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = remember { mutableStateListOf(RouteA) } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) - // we also need to add the default `NavEntryDecorator`s as well. These provide - // extra information to the entry's content to enable it to display correctly - // and save its state. - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator() - ), - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - LazyColumn { - items(10) { i -> - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("$i")) - }) { - Text("$i") - } - } - } - } - } - entry { key -> - // Note: We need a new ViewModel for every new RouteB instance. Usually - // we would need to supply a `key` String that is unique to the - // instance, however, the ViewModelStoreNavEntryDecorator (supplied - // above) does this for us, using `NavEntry.contentKey` to uniquely - // identify the viewModel. - // - // tl;dr: Make sure you use rememberViewModelStoreNavEntryDecorator() - // if you want a new ViewModel for each new navigation key instance. - ScreenB(viewModel = viewModel(factory = RouteBViewModel.Factory(key))) - } - } - ) - } - } -} - -@Composable -fun ScreenB(viewModel: RouteBViewModel = viewModel()) { - ContentBlue("Route id: ${viewModel.key.id} ") -} - -class RouteBViewModel( - val key: RouteB -) : ViewModel() { - class Factory( - private val key: RouteB, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return RouteBViewModel(key) as T - } - } -} -``` - -# Passing Arguments to ViewModels (Koin) - -This recipe demonstrates how to pass navigation arguments (keys) to a `ViewModel` using Koin for dependency injection. - -## How it works - -1. A Koin module is defined that provides the `ViewModel`. -2. The `koinViewModel` composable function is used to get the `ViewModel` instance. -3. The navigation key is passed to the `ViewModel`'s constructor using `parametersOf(key)`. This makes the navigation key available to the `ViewModel`. - -**Note** : The `rememberViewModelStoreNavEntryDecorator` is added to the `NavDisplay`'s `entryDecorators`. This ensures that `ViewModel`s are correctly scoped to their corresponding `NavEntry`, so that a new `ViewModel` instance is created for each unique navigation key. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/koin) - -``` -package com.example.nav3recipes.passingarguments.viewmodels.koin - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import org.koin.compose.KoinApplication -import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.module.dsl.viewModelOf -import org.koin.core.parameter.parametersOf -import org.koin.dsl.koinConfiguration -import org.koin.dsl.module - -data object RouteA -data class RouteB(val id: String) - -class KoinViewModelsActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = remember { mutableStateListOf(RouteA) } - - // Koin Compose Entry point - KoinApplication( - configuration = koinConfiguration { - modules(appModule) - } - ) { - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - - // In order to add the `ViewModelStoreNavEntryDecorator` (see comment below for why) - // we also need to add the default `NavEntryDecorator`s as well. These provide - // extra information to the entry's content to enable it to display correctly - // and save its state. - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator() - ), - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - LazyColumn { - items(10) { i -> - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("$i")) - }) { - Text("$i") - } - } - } - } - } - entry { key -> - val viewModel = koinViewModel { - parametersOf(key) - } - ScreenB(viewModel = viewModel) - } - } - ) - } - } - } -} - -// Local Koin Module -private val appModule = module { - viewModelOf(::RouteBViewModel) -} - -@Composable -fun ScreenB(viewModel: RouteBViewModel) { - ContentBlue("Route id: ${viewModel.navKey.id} ") -} - -class RouteBViewModel(val navKey: RouteB) : ViewModel() -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md deleted file mode 100644 index a61736f..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md +++ /dev/null @@ -1,272 +0,0 @@ -# Returning a Result (Event-Based) - -This recipe demonstrates how to return a result from one screen to a previous screen using an event-based approach. - -## How it works - -This example uses a `ResultEventBus` to facilitate communication between the screens. - -1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. -2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. -3. **Sending the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back as a one-time event. -4. **Receiving the result** : The screen that needs the result uses a `ResultEffect` composable to listen for results of a specific type. When a result is received, the effect's lambda is triggered. - -This approach is useful for results that are transient and should be handled as one-time events. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/results/event) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel - -class HomeViewModel : ViewModel() { - var person by mutableStateOf(null) -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.Serializable - -@Serializable -data object Home : NavKey - -@Serializable -class PersonDetailsForm : NavKey -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelable -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.Button -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen - -@Composable -fun HomeScreen( - person: Person?, - onNext: () -> Unit -) { - ContentBlue("Hello ${person?.name ?: "unknown person"}") { - - if (person != null) { - Text("Your favorite color is ${person.favoriteColor}") - } - - Spacer(Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onNext)) { - Text("Tell us about yourself") - } - } -} - -@Composable -fun PersonDetailsScreen( - onSubmit: (Person) -> Unit -) { - ContentGreen("About you") { - - val nameTextState = rememberTextFieldState() - OutlinedTextField( - state = nameTextState, - label = { Text("Please enter your name") } - ) - - val favoriteColorTextState = rememberTextFieldState() - OutlinedTextField( - state = favoriteColorTextState, - label = { Text("Please enter your favorite color") } - ) - - Button( - onClick = dropUnlessResumed { - val person = Person( - name = nameTextState.text.toString(), - favoriteColor = favoriteColorTextState.text.toString() - ) - onSubmit(person) - }, - enabled = nameTextState.text.isNotBlank() && - favoriteColorTextState.text.isNotBlank() - ) { - Text("Submit") - } - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.event - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.runtime.result.LocalResultEventBus -import androidx.navigation3.runtime.result.ResultEffect -import androidx.navigation3.runtime.result.ResultEventBus -import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.results.common.Home -import com.example.nav3recipes.results.common.HomeScreen -import com.example.nav3recipes.results.common.HomeViewModel -import com.example.nav3recipes.results.common.Person -import com.example.nav3recipes.results.common.PersonDetailsForm -import com.example.nav3recipes.results.common.PersonDetailsScreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -class ResultEventActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - Scaffold { paddingValues -> - - val backStack = rememberNavBackStack(Home) - - NavDisplay( - backStack = backStack, - modifier = Modifier.padding(paddingValues), - onBack = { backStack.removeLastOrNull() }, - entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), - entryProvider = entryProvider { - entry { - val viewModel = viewModel(key = Home.toString()) - ResultEffect { person -> - viewModel.person = person - } - - val person = viewModel.person - HomeScreen( - person = person, - onNext = { backStack.add(PersonDetailsForm()) } - ) - } - entry { - val resultBus = LocalResultEventBus.current - PersonDetailsScreen( - onSubmit = { person -> - resultBus.sendResult(result = person) - backStack.removeLastOrNull() - } - ) - } - } - ) - } - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md deleted file mode 100644 index 71c2ccf..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md +++ /dev/null @@ -1,266 +0,0 @@ -# Returning a Result (State-Based) - -This recipe demonstrates how to return a result from one screen to a previous screen using a state-based approach. - -## How it works - -This example uses a `ResultEventBus` to manage the result as state. - -1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. -2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. -3. **Setting the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back. -4. **Observing the result** : The screen that needs the result calls `resultBus.conflateAsState()` to get a `State` object representing the result. The UI then observes this state and recomposes whenever the result changes. - -This approach is suitable when only the latest result is required. The result state does not survive configuration change or process death. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/results/state) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel - -class HomeViewModel : ViewModel() { - var person by mutableStateOf(null) -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.Serializable - -@Serializable -data object Home : NavKey - -@Serializable -class PersonDetailsForm : NavKey -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelable -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.common - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.Button -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen - -@Composable -fun HomeScreen( - person: Person?, - onNext: () -> Unit -) { - ContentBlue("Hello ${person?.name ?: "unknown person"}") { - - if (person != null) { - Text("Your favorite color is ${person.favoriteColor}") - } - - Spacer(Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onNext)) { - Text("Tell us about yourself") - } - } -} - -@Composable -fun PersonDetailsScreen( - onSubmit: (Person) -> Unit -) { - ContentGreen("About you") { - - val nameTextState = rememberTextFieldState() - OutlinedTextField( - state = nameTextState, - label = { Text("Please enter your name") } - ) - - val favoriteColorTextState = rememberTextFieldState() - OutlinedTextField( - state = favoriteColorTextState, - label = { Text("Please enter your favorite color") } - ) - - Button( - onClick = dropUnlessResumed { - val person = Person( - name = nameTextState.text.toString(), - favoriteColor = favoriteColorTextState.text.toString() - ) - onSubmit(person) - }, - enabled = nameTextState.text.isNotBlank() && - favoriteColorTextState.text.isNotBlank() - ) { - Text("Submit") - } - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.state - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.runtime.result.LocalResultEventBus -import androidx.navigation3.runtime.result.ResultEffect -import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.results.common.Home -import com.example.nav3recipes.results.common.HomeScreen -import com.example.nav3recipes.results.common.HomeViewModel -import com.example.nav3recipes.results.common.Person -import com.example.nav3recipes.results.common.PersonDetailsForm -import com.example.nav3recipes.results.common.PersonDetailsScreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -class ResultStateActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - Scaffold { paddingValues -> - val backStack = rememberNavBackStack(Home) - NavDisplay( - backStack = backStack, - modifier = Modifier.padding(paddingValues), - onBack = { backStack.removeLastOrNull() }, - entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), - entryProvider = entryProvider { - entry { - val resultState = LocalResultEventBus - .current - .conflateAsState(null) - val person = resultState.value - HomeScreen( - person = person, - onNext = { backStack.add(PersonDetailsForm()) } - ) - } - entry { - val resultBus = LocalResultEventBus.current - PersonDetailsScreen( - onSubmit = { person -> - resultBus.sendResult(result = person) - backStack.removeLastOrNull() - } - ) - } - } - ) - } - } - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md deleted file mode 100644 index 32063b3..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md +++ /dev/null @@ -1,435 +0,0 @@ -# List-Detail Scene Recipe - -This example shows how to create a list-detail layout using the Scenes API. - -A `ListDetailSceneStrategy` will return a `ListDetailScene` if: - -- the window width is over 600dp -- A `Detail` entry is the last item in the back stack -- A `List` entry is in the back stack - -The `ListDetailScene` provides a `CompositionLocal` named `LocalBackButtonVisibility` that can be used by the detail `NavEntry` to control whether it displays a back button. This is useful when the detail entry usually displays a back button but should not display it when being displayed in a `ListDetailScene`. See for more details on this use case. - -See `ListDetailScene.kt` for more implementation details. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/scenes/listdetail) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.scenes.listdetail - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -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 - -/** - * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. - * - */ -data class ListDetailScene( - override val key: Any, - override val previousEntries: List>, - val listEntry: NavEntry, - val detailEntry: NavEntry, -) : Scene { - override val entries: List> = listOf(listEntry, detailEntry) - override val content: @Composable (() -> Unit) = { - Row(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.weight(0.4f)) { - listEntry.Content() - } - - // Let the detail entry know not to display a back button. - CompositionLocalProvider(LocalBackButtonVisibility provides false) { - Column(modifier = Modifier.weight(0.6f)) { - AnimatedContent( - targetState = detailEntry, - contentKey = { entry -> entry.contentKey }, - transitionSpec = { - slideInHorizontally( - initialOffsetX = { it } - ) togetherWith - slideOutHorizontally(targetOffsetX = { -it }) - } - ) { entry -> - entry.Content() - } - } - } - } - } - - companion object { - /** - * Helper function to add metadata to a [NavEntry] indicating it can be displayed - * in the list pane of a [ListDetailScene]. - */ - 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() = metadata { - put(DetailKey, true) - } - } - - object ListKey : NavMetadataKey - object DetailKey : NavMetadataKey -} - -/** - * This `CompositionLocal` can be used by a detail `NavEntry` to decide whether to display - * 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 } - -@Composable -fun rememberListDetailSceneStrategy(): ListDetailSceneStrategy { - val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass - - return remember(windowSizeClass) { - ListDetailSceneStrategy(windowSizeClass) - } -} - - -/** - * A [SceneStrategy] that returns a [ListDetailScene] if: - * - * - the window width is over 600dp - * - A `Detail` entry is the last item in the back stack - * - A `List` entry is in the back stack - * - * Notably, when the detail entry changes the scene's key does not change. This allows the scene, - * rather than the NavDisplay, to handle animations when the detail entry changes. - */ -class ListDetailSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { - - override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { - - if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { - return null - } - - val detailEntry = - 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 - // having NavDisplay animate the whole scene out when the selected detail item changes. - val sceneKey = listEntry.contentKey - - return ListDetailScene( - key = sceneKey, - previousEntries = entries.dropLast(1), - listEntry = listEntry, - detailEntry = detailEntry - ) - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.scenes.listdetail - -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 -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -/** - * This example shows how to create a list-detail layout using the Scenes API. - * - * A `ListDetailScene` will render content in two panes if: - * - * - the window width is over 600dp - * - A `Detail` entry is the last item in the back stack - * - A `List` entry is in the back stack - * - * @see `ListDetailScene` - */ -@Serializable -data object ConversationList : NavKey - -@Serializable -data class ConversationDetail( - val id: Int, - val colorId: Int -) : NavKey - -@Serializable -data object Profile : NavKey - -class ListDetailActivity : ComponentActivity() { - - @OptIn(ExperimentalSharedTransitionApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - - Scaffold { paddingValues -> - - val backStack = rememberNavBackStack(ConversationList) - val listDetailStrategy = rememberListDetailSceneStrategy() - - SharedTransitionLayout { - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(listDetailStrategy), - sharedTransitionScope = this, - modifier = Modifier.padding(paddingValues), - entryProvider = entryProvider { - entry( - metadata = ListDetailScene.listPane() - ) { - ConversationListScreen( - onConversationClicked = { detailRoute -> - backStack.addDetail(detailRoute) - } - ) - } - entry( - metadata = ListDetailScene.detailPane() - ) { conversationDetail -> - ConversationDetailScreen( - conversationDetail = conversationDetail, - onBack = { backStack.removeLastOrNull() }, - onProfileClicked = { backStack.add(Profile) } - ) - } - entry { - ProfileScreen() - } - } - ) - } - } - } - } -} - -private fun NavBackStack.addDetail(detailRoute: ConversationDetail) { - - // Remove any existing detail routes before adding this detail route. - // In certain scenarios, such as when multiple detail panes can be shown at once, it may - // be desirable to keep existing detail routes on the back stack. - removeIf { it is ConversationDetail } - add(detailRoute) -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.scenes.listdetail - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -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.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.ui.theme.colors - -@Composable -fun ConversationListScreen( - onConversationClicked: (ConversationDetail) -> Unit -) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface), - ) { - items(10) { index -> - val conversationId = index + 1 - val conversationDetail = ConversationDetail( - id = conversationId, - colorId = conversationId % colors.size - ) - val backgroundColor = colors[conversationDetail.colorId] - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = dropUnlessResumed { - onConversationClicked(conversationDetail) - }), - headlineContent = { - Text( - text = "Conversation $conversationId", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - colors = ListItemDefaults.colors( - containerColor = backgroundColor // Set container color directly - ) - ) - } - } -} - -@Composable -fun ConversationDetailScreen( - conversationDetail: ConversationDetail, - onBack: () -> Unit, - onProfileClicked: () -> Unit -) { - Box( - modifier = Modifier - .fillMaxSize() - .background(colors[conversationDetail.colorId]) - .padding(16.dp) - ) { - if (LocalBackButtonVisibility.current) { - IconButton( - onClick = onBack, - modifier = Modifier.align(Alignment.TopStart) - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - } - Column( - modifier = Modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Conversation Detail Screen: ${conversationDetail.id}", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onProfileClicked)) { - Text("View Profile") - } - } - } -} - -@Composable -fun ProfileScreen() { - val profileColor = MaterialTheme.colorScheme.surfaceVariant - Column( - modifier = Modifier - .fillMaxSize() - .background(profileColor) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Profile Screen", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md b/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md deleted file mode 100644 index fed0bf9..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md +++ /dev/null @@ -1,244 +0,0 @@ -# Two-Pane Scene Recipe - -This example shows how to create a two pane layout using the Scenes API. - -A `TwoPaneSceneStrategy` will return a `TwoPaneScene` if: - -- the window width is over 600dp -- the last two nav entries on the back stack have indicated that they support being displayed in a `TwoPaneScene` in their metadata. - -See `TwoPaneScene.kt` for more implementation details. -[![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/scenes/twopane) - -``` -package com.example.nav3recipes.scenes.twopane - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -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. - */ -data class TwoPaneScene( - override val key: Any, - override val previousEntries: List>, - val firstEntry: NavEntry, - val secondEntry: NavEntry -) : Scene { - override val entries: List> = listOf(firstEntry, secondEntry) - override val content: @Composable (() -> Unit) = { - Row(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.weight(0.5f)) { - firstEntry.Content() - } - Column(modifier = Modifier.weight(0.5f)) { - secondEntry.Content() - } - } - } - - companion object { - /** - * Helper function to add metadata to a [NavEntry] indicating it can be displayed - * in a two-pane layout. - */ - fun twoPane() = metadata { - put(TwoPaneKey, true) - } - } - - object TwoPaneKey : NavMetadataKey -} - -@Composable -fun rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy { - val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass - - return remember(windowSizeClass) { - TwoPaneSceneStrategy(windowSizeClass) - } -} - - -// --- TwoPaneSceneStrategy --- -/** - * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough - * and the top two back stack entries declare support for two-pane display. - */ -class TwoPaneSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { - - override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { - - // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. - // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). - if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { - return null - } - - val lastTwoEntries = entries.takeLast(2) - - // 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.contains(TwoPaneScene.TwoPaneKey) } - ) { - val firstEntry = lastTwoEntries.first() - val secondEntry = lastTwoEntries.last() - - // The scene key must uniquely represent the state of the scene. - // A Pair of the first and second entry keys ensures uniqueness. - val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) - - TwoPaneScene( - key = sceneKey, - // Where we go back to is a UX decision. In this case, we only remove the top - // entry from the back stack, despite displaying two entries in this scene. - // This is because in this app we only ever add one entry to the - // back stack at a time. It would therefore be confusing to the user to add one - // when navigating forward, but remove two when navigating back. - previousEntries = entries.dropLast(1), - firstEntry = firstEntry, - secondEntry = secondEntry - ) - - } else { - null - } - } - - -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.scenes.twopane - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBase -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.content.ContentRed -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import com.example.nav3recipes.ui.theme.colors -import kotlinx.serialization.Serializable - -@Serializable -private object Home : NavKey - -@Serializable -private data class Product(val id: Int) : NavKey - -@Serializable -private data object Profile : NavKey - -class TwoPaneActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - val backStack = rememberNavBackStack(Home) - val twoPaneStrategy = rememberTwoPaneSceneStrategy() - - SharedTransitionLayout { - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - sceneStrategies = listOf(twoPaneStrategy), - sharedTransitionScope = this, - entryProvider = entryProvider { - entry( - metadata = TwoPaneScene.twoPane() - ) { - ContentRed("Welcome to Nav3") { - Button(onClick = { backStack.addProductRoute(1) }) { - Text("View the first product") - } - } - } - entry( - 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 { - ContentGreen("Profile (single pane only)") - } - } - ) - } - } - } -} - -private fun NavBackStack.addProductRoute(productId: Int) { - val productRoute = - Product(productId) - // Avoid adding the same product route to the back stack twice. - if (!contains(productRoute)) { - add(productRoute) - } -} -``` \ No newline at end of file diff --git a/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md b/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md deleted file mode 100644 index c570000..0000000 --- a/.agents/skills/navigation-3/references/android/guide/navigation/type-safe-destinations.md +++ /dev/null @@ -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 { - HomeScreen(...) - } - composable { 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() - 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( - typeMap = mapOf(typeOf() 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()` to check the current destination in a type-safe manner during UI tests \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/SKILL.md b/.agents/skills/r8-analyzer/SKILL.md deleted file mode 100644 index 7ad9e5e..0000000 --- a/.agents/skills/r8-analyzer/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -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 - last-updated: '2026-06-09' - keywords: - - R8 - - proguard - - keep rules - - app size - - optimization ---- - -## Step 1. Setup and configuration check - -- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. -- Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. -- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement - performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) -- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. - -## Step 2. Analysis path selection - -- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and - `libs.versions.toml` to get the R8 version - -- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. - -- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. - -### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) - -- **Check requirements** : Python and `protobuf` package are mandatory. -- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. -- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. - -### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) - -*(Use ONLY if quantitative data generation is not possible)* - -- **Manual evaluation** : Inspect `proguard-rules.pro`. -- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. -- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). -- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. - -## Step 3. Report generation - -- **Format** : Follow [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) strictly. -- **Input**: Extract metrics (Scores, Impacts, Example Classes) directly from generated file analysis.txt if using Path A, or from manual findings if using Path B. -- **Output** : Output ONLY the raw Markdown report in the chat. Do NOT output conversational filler (for example, "Here is your report..."). Do NOT provide recommendations, next steps, or any other text outside of the sections defined in [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) Do NOT mention the path used for analysis of the configuration - -## Constraints - -- **Strict output limit**: The final output MUST strictly be the Markdown report and nothing else. -- **No code changes**: Research and suggest only; Do not modify files. -- **No redundancy**: Do not explain R8 benefits or reference skill internal files in the report. -- **Focus**: Omit sections (for example, Subsumed Rules, Configuration) if no issues or items are found. diff --git a/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md deleted file mode 100644 index 4039dcf..0000000 --- a/.agents/skills/r8-analyzer/references/CONFIGURATION-ANALYZER.md +++ /dev/null @@ -1,287 +0,0 @@ -On each step, keep the user informed of the progress by displaying the output. - -### 1. Requirements - -- R8 Version: 9.3.7-dev or later - -### 2. Generate proto - -The report and files must be generated at `{project_root}/tmp/r8analysis`. If -the folder is not present, create it. For example: - - mkdir -p "$PWD/tmp/r8analysis" - -### 3. Remove existing files - -To make sure that this invocation doesn't source data from previous runs, remove -the intermediate files `keepruleradius.json` and `analysis_result.txt` and -remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example -bash commands: - - # Remove the intermediate JSON and the directory containing protobuf files - rm tmp/r8analysis/keepruleradius.json - rm tmp/r8analysis/*.pb - - # Copy the previous result to history before deleting the analysis - if [ -f tmp/r8analysis/analysis_result.txt ]; - then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && - rm tmp/r8analysis/analysis_result.txt; fi - -### 4. Generate the Configuration Analyzer report - -Run the R8 enabled build with the system property -"-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to -generate Configuration Analyzer report - - ./gradlew assembleRelease \ - -Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis - -### 5. Convert to JSON - -To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into -json, run the following script. The json must be generated in -`{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is -in the same directory. - - import sys - import os - import glob - from google.protobuf import json_format - import keep_radius_pb2 - - def convert_pb_to_json(input_pb_path, output_json_path): - bundle = keep_radius_pb2.BlastRadiusContainer() - - try: - with open(input_pb_path, "rb") as pb_file: - binary_data = pb_file.read() - except Exception as e: - print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) - return False - - try: - bundle.ParseFromString(binary_data) - except Exception as e: - print(f"Error parsing protobuf: {e}", file=sys.stderr) - return False - - try: - json_string = json_format.MessageToJson( - bundle, - always_print_fields_with_no_presence=True, - preserving_proto_field_name=True, - indent=4 - ) - with open(output_json_path, "w", encoding="utf-8") as json_file: - json_file.write(json_string) - return True - except Exception as e: - print(f"Error writing JSON: {e}", file=sys.stderr) - return False - - if __name__ == "__main__": - input_pb = sys.argv[1] if len(sys.argv) > 1 else None - if not input_pb: - pb_files = glob.glob("tmp/r8analysis/*.pb") - if not pb_files: - print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) - sys.exit(1) - input_pb = sorted(pb_files)[-1] # Use the most recent one - output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" - if not convert_pb_to_json(input_pb, output_json): - sys.exit(1) - -### 6. Analyze - -Run the following analysis script on the generated JSON to get the impact of the -keep rules and sort it. - - import json, sys - - def analyze(path): - try: - with open(path, 'r') as f: - d = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return - - # Build reference map - c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} - r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} - - tot_opt = tot_obf = tot_shr = tot_items = 0 - - # Tally constraints across all kept items - for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): - for i in d.get(tbl, []): - tot_items += 1 - kb = i.get('kept_by', []) - if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 - if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 - if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 - - # Find denominator - bi = d.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else tot_items - - # Check for globals - globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] - def score(cnt, flag): - if any(flag in src for src in globals_src): return 0.0 - return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) - - result = [ - f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", - f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", - f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" - ] - for line in result: - print(line) - with open("tmp/r8analysis/analysis_result.txt", "w") as f: - f.write("\n".join(result)) - - if __name__ == "__main__": - path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" - analyze(path) - -Outputs `analysis_result.txt` containing scores and rule impacts. - -### 7. Report impactful rules - -Identify the keep rules with the highest impact and the subsumed rules using the -following script. - - import json, sys - - def report(path): - try: - with open(path, 'r') as f: - data = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return - - # Calculate denominator for percentage - bi = data.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) - - processed = [] - for r in data.get('keep_rule_blast_radius_table', []): - br = r.get('blast_radius', {}) - c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) - impact = c + f + m - if impact == 0: - continue - impact_pct = (impact / denom * 100) if denom > 0 else 0.0 - processed.append({ - 'id': r.get('id'), - 'source': r.get('source'), - 'impact': impact, - 'impact_pct': f"{impact_pct:.2f}%", - 'classes': c, - 'fields': f, - 'methods': m, - 'subsumed_by': br.get('subsumed_by', []) - }) - - processed.sort(key=lambda x: x['impact'], reverse=True) - - # Output JSON for the agent to fetch and process - print(json.dumps({ - "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], - "subsumed": [r for r in processed if r['subsumed_by']] - }, indent=2)) - - if __name__ == "__main__": - report("tmp/r8analysis/keepruleradius.json") - -Add this data to the `analysis_result.txt` with the top impactful rules and -subsumed rules. - -### 8. Compare with previous report - -If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script -to compare the previous run. Use this to compare with the current values - -### 9. Remove generated files - -After the final report and analysis results are generated, remove the -intermediate files `keepruleradius.json` and `analysis_result.txt` and remove -the proto files in "{project_root}/tmp/r8analysis" folder - - rm tmp/r8analysis/keepruleradius.json - rm tmp/r8analysis/*.pb - -### 10. Protobuf Python bindings - -The following script `keep_radius_pb2.py` is required by the conversion script -in Step 5. - - from google.protobuf import descriptor as _descriptor - from google.protobuf import descriptor_pool as _descriptor_pool - from google.protobuf import runtime_version as _runtime_version - from google.protobuf import symbol_database as _symbol_database - from google.protobuf.internal import builder as _builder - _runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 4, - '', - 'keep_radius.proto' - ) - # @@protoc_insertion_point(imports) - - _sym_db = _symbol_database.Default() - DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') - - _globals = globals() - _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) - _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) - if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' - _globals['_KEEPRULETAG']._serialized_start = 3332 - _globals['_KEEPRULETAG']._serialized_end = 3363 - _globals['_KEEPCONSTRAINT']._serialized_start = 3365 - _globals['_KEEPCONSTRAINT']._serialized_end = 3437 - _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 - _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 - _globals['_BLASTRADIUS']._serialized_start = 351 - _globals['_BLASTRADIUS']._serialized_end = 470 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 - _globals['_KEEPCONSTRAINTS']._serialized_start = 601 - _globals['_KEEPCONSTRAINTS']._serialized_end = 707 - _globals['_KEPTCLASSINFO']._serialized_start = 709 - _globals['_KEPTCLASSINFO']._serialized_end = 805 - _globals['_KEPTFIELDINFO']._serialized_start = 807 - _globals['_KEPTFIELDINFO']._serialized_end = 903 - _globals['_KEPTMETHODINFO']._serialized_start = 905 - _globals['_KEPTMETHODINFO']._serialized_end = 1003 - _globals['_FIELDREFERENCE']._serialized_start = 1005 - _globals['_FIELDREFERENCE']._serialized_end = 1102 - _globals['_METHODREFERENCE']._serialized_start = 1104 - _globals['_METHODREFERENCE']._serialized_end = 1203 - _globals['_PROTOREFERENCE']._serialized_start = 1205 - _globals['_PROTOREFERENCE']._serialized_end = 1280 - _globals['_TYPEREFERENCE']._serialized_start = 1282 - _globals['_TYPEREFERENCE']._serialized_end = 1334 - _globals['_TYPEREFERENCELIST']._serialized_start = 1336 - _globals['_TYPEREFERENCELIST']._serialized_end = 1395 - _globals['_FILEORIGIN']._serialized_start = 1398 - _globals['_FILEORIGIN']._serialized_end = 1557 - _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 - _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 - _globals['_TEXTFILEORIGIN']._serialized_start = 1634 - _globals['_TEXTFILEORIGIN']._serialized_end = 1718 - _globals['_MAVENCOORDINATE']._serialized_start = 1720 - _globals['_MAVENCOORDINATE']._serialized_end = 1805 - _globals['_BUILDINFO']._serialized_start = 1808 - _globals['_BUILDINFO']._serialized_end = 1962 - _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 - _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 - # @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/CONFIGURATION.md b/.agents/skills/r8-analyzer/references/CONFIGURATION.md deleted file mode 100644 index ae04947..0000000 --- a/.agents/skills/r8-analyzer/references/CONFIGURATION.md +++ /dev/null @@ -1,44 +0,0 @@ -To achieve maximum utilization of R8, the codebase must be configured correctly -depending on the build script language (Kotlin DSL versus Groovy DSL). - -## 1. App Modules (`com.android.application`) - -The app's `build.gradle` or `build.gradle.kts` file must 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** : Explicitly enable 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 \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md b/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md deleted file mode 100644 index 2e07317..0000000 --- a/.agents/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +++ /dev/null @@ -1,83 +0,0 @@ -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 { ; } - -keepclasseswithmembers class * { native ; } \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md b/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md deleted file mode 100644 index 0021890..0000000 --- a/.agents/skills/r8-analyzer/references/REDUNDANT-RULES.md +++ /dev/null @@ -1,222 +0,0 @@ -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.* ; }` - -### 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. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md b/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md deleted file mode 100644 index 8f23679..0000000 --- a/.agents/skills/r8-analyzer/references/REFLECTION-GUIDE.md +++ /dev/null @@ -1,139 +0,0 @@ -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 register(clazz: Class) { } - // 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 { - (); - }` - -### 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. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md b/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md deleted file mode 100644 index 629c777..0000000 --- a/.agents/skills/r8-analyzer/references/REPORT_FORMAT.md +++ /dev/null @@ -1,51 +0,0 @@ -## 1. Configuration - -*(Optional section for the report, omit if no relevant findings are present.)* - -- **AGP Version**: \[Current\] -\> Upgrade to 9.0. -- **Full Mode** : Not enabled. Remove `android.enableR8.fullMode=false` from `gradle.properties`. - -## 2. Global disable rules - -*(Optional section for the report, omit if no relevant findings are present.)* - -- \[Rule\]: Disables R8 globally. **Action**: Remove. - -If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, -mention in this section - -## 3. Optimization summary - -- **Optimization score**: \[X\]% code is available for R8 optimizations (e.g., inlining, merging). \[100-X\]% of codebase can't be optimized by R8. -- **Shrinking score**: \[X\]% of code will be optimized by R8 by removing unused classes, fields and methods. \[100-X\]% of codebase contains redundant classes, fields and methods that can't be removed by R8. -- **Obfuscation score**: \[X\]% of the codebase is available for R8 to obfuscate. - -Increasing these scores increases the codebase available to R8 for -optimizations. - -## 4. Keep rules evaluation - -### \[Rule text\] - -- **Keeps**: \[X\] items or \[X\] % of the codebase from optimization. Classes: \[X\], Fields: \[X\], Methods: \[X\] are prevented from optimization due to this keep rule -- **Kept items**: \[Class1\], \[Class2\] -- **Action** : **Remove** (Library bundles rules) OR **Refine** (Too broad, use \[Surgical Rule\]). - -## 5. Subsumed keep rules - -*(Optional section for the report, omit if no relevant findings are present.)* - -### \[Redundant rules\] - -- **Subsumed By**: \[Broader Rule\] -- **Action** : **Remove**. - -## 6. Historical analysis summary - -*(Only include this section if a previous report existed. Summarize the changes -in optimization scores here to track progress. For example:)* The previous app -had scores: Optimization (XX%), Obfuscation (XX%), and Shrinking (XX%). The -current app has scores: Optimization (YY%), Obfuscation (YY%), and Shrinking -(YY%). -**Change**: Optimization improved by ZZ%, Obfuscation improved by ZZ%, and -Shrinking improved by ZZ%. \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md b/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md deleted file mode 100644 index 69c4295..0000000 --- a/.agents/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +++ /dev/null @@ -1,198 +0,0 @@ -For the best user experience, you should optimize your app to make it as small -and fast as possible. Our app optimizer, called R8, streamlines your app by -removing unused code and resources, rewriting code to optimize runtime -performance, and more. To your users, this means: - -- Faster startup time -- Reduced memory usage -- Improved rendering and runtime performance -- Fewer [ANRs](https://developer.android.com/topic/performance/anrs/keep-your-app-responsive) - -> [!IMPORTANT] -> **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the -> optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization). - -> [!IMPORTANT] -> **Important:** We released an agent skill that you can use to improve your app performance with R8. Try out the skill from the [Android skills repository](https://github.com/android/skills). - -## R8 optimization overview - -R8 uses a multi-phase process to optimize your app for size and speed. Key -operations include the following: - -- **Code shrinking (also known as tree shaking)** : R8 identifies and removes - unreachable code from your application and its library dependencies. By - analyzing the entry points of your app (such as `Activities` or `Services` - defined in the manifest), R8 builds a graph of referenced code and removes - anything that remains unreferenced. - -- **Logical optimizations**: R8 rewrites your code to improve execution - efficiency and reduce overhead. Key techniques include: - - - **Method inlining**: R8 replaces a method call site with the actual body - of the called method. This eliminates the overhead of a function call - and lets R8 conduct further optimizations. - - - **Class merging**: R8 combines sets of classes and interfaces into a - single class. This reduces the number of classes in the app, lowering - memory pressure and improving startup speed. - -- **Obfuscation (also known as minification)** : To reduce the size of the DEX - file, R8 shortens the names of classes, fields, and methods (for example, - `com.example.MyActivity` could become `a.b.a`). - -Since 8.12.0 version of Android Gradle Plugin (AGP), R8 also optimizes resources -as part of its optimization phases. For more information, see [Optimized -resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking). - -## Enable optimization - -To enable app optimization, set `isMinifyEnabled = true` (for code optimization) -and `isShrinkResources = true` (for resource optimization) in your [release -build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script as shown in the following code. We recommend -that you always enable both settings. We also recommend enabling app -optimization only in the final version of your app that you test before -publishing---usually your release build---because the optimizations increase the -build time of your project and can make debugging harder due to the way it -modifies code. - -### Kotlin - -```kotlin -android { - buildTypes { - release { - - // Enables code-related app optimization. - isMinifyEnabled = true - - // Enables resource shrinking. - isShrinkResources = true - - proguardFiles( - // Default file with automatically generated optimization rules. - getDefaultProguardFile("proguard-android-optimize.txt"), - - ... - ) - ... - } - } - ... -} -``` - -### Groovy - -```groovy -android { - buildTypes { - release { - - // Enables code-related app optimization. - minifyEnabled = true - - // Enables resource shrinking. - shrinkResources = true - - // Default file with automatically generated optimization rules. - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') - - ... - } - } -} -``` - -## Improve R8 optimization - -The performance benefits of R8 are directly correlated to how much of your -codebase R8 is able to optimize. To get the maximum benefits out of R8, follow -best practices: - -- Enable R8 in [full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) -- Enable [obfuscation, optimization, and shrinking](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally) -- Enable resource shrinking and [optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) -- [Refine keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-best-practices) to allow maximum optimization of classes, fields and methods. - -To help you refine keep rules, use the [R8 Configuration Analyzer](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). - -The R8 Configuration Analyzer lets you do the following: - -- Track and improve the overall R8 configuration quality by monitoring the metrics provided by the R8 Configuration Analyzer report. -- Find the broadest keep rules - those which prevent the most optimization -- and understand what optimization they prevent to refine them. - -The R8 Configuration Analyzer is available in AGP version 9.3.0-alpha05 or from -R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). - -## Optimize resource shrinking for even smaller apps - -The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource -shrinking, which aims to integrate resource and code optimization to create even -smaller and faster apps. - -Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2) -generated keep rules that effectively treating resource shrinking separately -from code, often retaining inaccessible code or resources that referenced each -other. - -With optimized resource shrinking, resources are considered like a part of -program code, forming the reference graph. When a collection of code or -resources is not referenced, it is not protected by a keep rule, and can be -removed. - -### Enable optimized resource shrinking - -To enable the new optimized resource shrinking pipeline for AGP 8.12 or 8.13, -add the following to your project's `gradle.properties` file: - - android.r8.optimizedResourceShrinking=true - -If you are using AGP 9.0.0 or a newer version, you don't need to set -`android.r8.optimizedResourceShrinking=true`. Optimized resource shrinking is -automatically applied when `isShrinkResources = true` is enabled in your build -configuration. - -## Verify and configure R8 optimization settings - -To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the -following line from your project's `gradle.properties` file, if it exists: - - android.enableR8.fullMode=false # Remove this line from your codebase. - -Note that enabling app optimization makes stack traces difficult to understand, -especially if R8 renames class or method names. To get stack traces that -correctly correspond to your source code, see [Recover the original stack -trace](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization#recover-original-stack-trace). - -If R8 is enabled, you should also [create Startup Profiles](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations) for even better -startup performance. - -If you enable app optimization and it causes errors, here are some strategies to -fix them: - -- [Add keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to keep some code untouched. -- [Adopt optimizations incrementally](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally). -- Update your code to [use libraries that are better suited for - optimization](https://developer.android.com/topic/performance/app-optimization/choose-libraries-wisely). - -> [!CAUTION] -> **Caution:** Tools that replace or modify R8's output can negatively impact runtime performance. R8 is careful about including and testing many optimizations at the code level, in [DEX layout](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations), and in correctly producing Baseline Profiles - other tools producing or modifying DEX files can break these optimizations, or otherwise regress performance. - -If you are interested in optimizing your build speed, see [Configure how R8 -runs](https://developer.android.com/build/r8-execution-profiles) for information on how to configure R8 based on your environment. - -## AGP and R8 version behavior changes - -The following table outlines the key features introduced in various versions of -the Android Gradle Plugin (AGP) and the R8 compiler. - -| AGP version | Features introduced | -|---|---| -| 9.1 | **Classes repackaged by default:** R8 repackages classes (moving them to the unnamed package, at the top level) to compact DEX further, eliminating the need to specify `-repackageclasses` option. For information about how this works and how to opt out, see [global options](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). | -| 9.0 | **Optimized resource shrinking:** Enabled by default (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline, leading to smaller, faster apps. By optimizing both code and resource references simultaneously, it identifies and removes resources referenced exclusively from unused code. This is a significant improvement over the previous separate optimization processes. This is especially useful for apps that share substantial resources and code across different form factor verticals, with measured improvements of over 50% in app size. The resulting size reduction leads to smaller downloads, faster installations, and a better user experience with faster startup, improved rendering, and fewer ANRs. **Library rule filtering:** Support for global options (for example, `-dontobfuscate`) in library consumer rules has been dropped, and apps will filter them out. For more information, see [Add global options](https://developer.android.com/topic/performance/app-optimization/global-options). **Kotlin null checks:** Optimized by default (controlled using `-processkotlinnullchecks`). This version also introduced significant improvements in build speed. For more information, see [Global options for additional optimization](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). **Optimize specific packages:** You can use `packageScope` to optimize specific packages. This is in experimental support. For more information, see [Optimize specified packages with `packageScope`](https://developer.android.com/topic/performance/app-optimization/optimize-specified-packages). **Optimized by default:** Support for `getDefaultProguardFile("proguard-android.txt")` has been dropped, because it includes `-dontoptimize`, which should be avoided. Instead, use `"proguard-android-optimize.txt"`. If you need to globally disable optimization in your app, [add the flag manually to a proguard file](https://developer.android.com/topic/performance/app-optimization/global-options#global-options-2). | -| 8.12 | **Optimized resource shrinking:** Initial support added (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline. You must manually enable it in this version of AGP. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). | -| 8.6 | **Improved retracing:** Includes filename and line number retracing by default for all `minSdk` levels (previously required `minSdk` 26+ in version 8.2). Updating R8 helps ensure that stack traces from obfuscated builds are readily and clearly readable. This version improves how line numbers and source files are mapped, making it easier for tools like the Android Studio Logcat to automatically retrace crashes to the original source code. | -| 8.0 | **Full mode by default:** [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) provides significantly more powerful optimization. It is enabled by default. You can opt out using `android.enableR8.fullMode=false`. | -| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. | \ No newline at end of file diff --git a/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md b/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md deleted file mode 100644 index 2b5b253..0000000 --- a/.agents/skills/r8-analyzer/references/android/training/testing/other-components/ui-automator.md +++ /dev/null @@ -1,312 +0,0 @@ -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`, 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)` | \ No newline at end of file diff --git a/.agents/skills/styles/SKILL.md b/.agents/skills/styles/SKILL.md deleted file mode 100644 index daf7256..0000000 --- a/.agents/skills/styles/SKILL.md +++ /dev/null @@ -1,226 +0,0 @@ ---- -name: styles -description: Use this skill to integrate the Jetpack Compose Styles API into an Android - project. This skill guides you through upgrading dependencies, setting up component - themes, making custom components styleable, and migrating existing layout properties - to use unified styles. Migrate custom design system components, replace hard coded - parameters with Style attributes, and use Modifier.styleable for interaction states. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-07-02' - keywords: - - Jetpack Compose - - Styles - - Theming with Styles - - Migrate to Styles - - Modifier.styleable ---- - -## Limitations - -- Warn the user that this skill is EXPERIMENTAL and requires updating to alpha version of Compose and opting in to the Experimental APIs. -- This skill only supports custom UI components and custom themes. -- This skill does not support Material Design component Styles. - -## Prerequisites - -### 1. Upgrade dependencies - -- The project must use `compileSdk` version 37 or higher. -- The project must use `androidx.compose.foundation:foundation` version `1.12.0-alpha01` or higher. -- Alternatively, the project must use Compose BOM version `2026.04.01` or higher. -- The API requires this exact package: `import - androidx.compose.foundation.style.Style` - -### 2. Configure compiler options to enable experimental API - -You must opt-in to the experimental API at the project level. Add the following -block to your module's `build.gradle.kts`: - - kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget("17") - freeCompilerArgs.add("-opt-in=androidx.compose.foundation.style.ExperimentalFoundationStyleApi") - } - } - -## Core workflows and guides - -Refer to the official documentation to complete specific development tasks: - -- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals - Guide](references/android/develop/ui/compose/styles/fundamentals.md). -- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling - Guide](references/android/develop/ui/compose/styles/state-animations.md). -- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers - Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md). -- Theme Level Integration: To connect style definitions with custom themes, follow [Theming with Styles](references/android/develop/ui/compose/styles/theming.md) and [Custom Themes in Compose](references/android/develop/ui/compose/designsystems/custom.md). - -## Step-by-Step Migration Workflow - -### Step 1: Analyze theme structure - -1. Locate your central theme file (such as `Theme.kt`). -2. Identify design tokens. Note references for colors, typography, and shapes (for example, `LocalColorScheme`, `LocalTypography`, or `LocalShapes`). -3. If the project lacks Jetpack Compose dependencies, stop. Instruct the user to migrate to Jetpack Compose first. -4. If the project imports `androidx.compose.material.MaterialTheme`, recommend migrating to Material 3 before proceeding. - -### Step 2: Establish `ComponentStyles` - -1. Create a new file named `ComponentStyles.kt` in your theme directory. -2. Define a top-level data class to hold your component styles, for example, the Jetsnack one is called `JetsnackStyles`: - - - ```kotlin - object ExampleComponentStyles { - val customButtonStyle: Style = { - - } - val customTextFieldStyle: Style = { - - } - } - ``` - -
- -3. Expose this class through your custom theme with a static reference, don't - use `CompositionLocals` here as it's not required. - - - ```kotlin - @Immutable - class JetsnackTheme( - // other Design system properties - ) { - companion object { - val colors: CustomThemingWithStyles.JetsnackColors - @Composable @ReadOnlyComposable - get() = LocalJetsnackTheme.current.colors - // ... - - // add helper static reference - val styles: ComponentStyles = ComponentStyles - } - } - ``` - -
- -4. Provide extensions on `StyleScope` to reference theme tokens directly if - they are exposed using `CompositionLocals`. For example: - - - ```kotlin - val StyleScope.colors: JetsnackColors - get() = LocalJetsnackTheme.currentValue.colors - - val StyleScope.typography: androidx.compose.material3.Typography - get() = LocalJetsnackTheme.currentValue.typography - - val StyleScope.shapes: Shapes - get() = LocalJetsnackTheme.currentValue.shapes - ``` - -
- -### Step 3: Migrate a component to Styles API - -For each custom component (for example, `CustomButton`), complete the following -sequence: - -1. **Establish a visual baseline (If an emulator is available):** - - **If you CANNOT run an Android emulator:** Skip this step entirely and proceed to Step 2. - - **If you CAN run an Android emulator:** Perform the following to capture a baseline screenshot: - - **Option A:** Locate and run an existing screenshot test for the component. - - **Option B (If no test exists):** Create a test using the project's existing testing framework, then run it. - - **Option C (If no framework exists):** Create a minimal screenshot test using UI Automator or Espresso, then run it. -2. **Remove individual styling parameters** : Remove styling parameters such as `backgroundColor`, `shape`, `textStyle`, and `contentPadding` from the signature - anything that `StyleScope` supports. -3. **Add the style parameter** : Add `style: Style = Style` to the function signature. Always ensure the default value is exactly `Style` (e.g., `style: - Style = Style`) and not a specific style default like `ChipStyleDefault` or any other value. -4. **Declare state tracking** : If the component is interactable, create a `MutableStyleState` using the interaction source. Update state fields (such as `isEnabled`) inside the Composable to track the state correctly. -5. **Apply styleable modifier** : Replace specific layout modifiers on the root element with `Modifier.styleable()`. -6. **Move defaults to ComponentStyles** : Move hardcoded values from the component definition to a dedicated `Style` instance in `ComponentStyles.kt`. -7. **Validate component:** Compare the baseline screenshot image taken at the start 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. - -#### Migration example - -Before Migration: - - -```kotlin -@Composable -fun CustomButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - backgroundColor: Color = JetsnackTheme.colors.brandLight, - disabledBackgroundColor: Color = JetsnackTheme.colors.brandSecondary, - shape: Shape = JetsnackTheme.shapes.extraLarge, - textStyle: TextStyle = JetsnackTheme.typography.labelLarge, - enabled: Boolean = true, - content: @Composable RowScope.() -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - Row( - modifier - .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) - .background(if (enabled) backgroundColor else disabledBackgroundColor, shape) - .defaultMinSize(58.dp, 40.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = content, - ) -} -``` - -
- -After Migration: - - -```kotlin -// Exposed via ComponentStyles.kt -object ComponentStyles { - val buttonStyle = Style { - background(colors.brandLight) - shape(shapes.extraLarge) - minWidth(58.dp) - minHeight(40.dp) - textStyle(typography.labelLarge) - disabled { - background(colors.brandSecondary) - } - } -} - -@Composable -fun CustomButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - style: Style = Style, - enabled: Boolean = true, - content: @Composable RowScope.() -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - val styleState = rememberUpdatedStyleState(interactionSource) { - it.isEnabled = enabled - } - Row( - modifier - .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) - .styleable(styleState, JetsnackTheme.styles.buttonStyle, style), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = content, - ) -} -``` - -
- -### Step 4: Validate Changes - -1. Build the project. Verify that there are no compilation errors. -2. Run your module's screenshot tests. -3. Compare visual outputs of the whole app between the previous and updated components. Verify that no visual layout regressions occur. diff --git a/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md b/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md deleted file mode 100644 index 59dbb25..0000000 --- a/.agents/skills/styles/references/android/develop/ui/compose/designsystems/custom.md +++ /dev/null @@ -1,459 +0,0 @@ -While Material is our recommended design system and Jetpack Compose ships an -implementation of Material, you are not forced to use it. Material is built -entirely on public APIs, so it's possible to create your own design system in -the same manner. - -There are several approaches you might take: - -- [Extend `MaterialTheme`](https://developer.android.com/develop/ui/compose/designsystems/custom#extending-material) with additional theming values. -- [Replace one or more Material systems](https://developer.android.com/develop/ui/compose/designsystems/custom#replacing-systems) --- `Colors`, `Typography`, or `Shapes` --- with custom implementations while keeping the others. -- [Implement a fully custom design system](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom) to replace `MaterialTheme`. - -You may also want to continue using Material components with a custom design -system. It's possible to do this but there are things to keep in mind to suit -the approach you've taken. - -To learn more about the lower-level constructs and APIs used by `MaterialTheme` -and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide. - -## Extend Material Theming - -Compose Material closely models -[Material Theming](https://m3.material.io/) -to make it straightforward and type-safe to follow the Material guidelines. -However, it's possible to extend the color, typography, and shape sets with -additional values. The simplest approach is to add extension properties: - - -```kotlin -// Use with MaterialTheme.colorScheme.snackbarAction -val ColorScheme.snackbarAction: Color - @Composable - get() = if (isSystemInDarkTheme()) Red300 else Red700 - -// Use with MaterialTheme.typography.textFieldInput -val Typography.textFieldInput: TextStyle - get() = TextStyle(/* ... */) - -// Use with MaterialTheme.shapes.card -val Shapes.card: Shape - get() = RoundedCornerShape(size = 20.dp) -``` - -
- -This provides consistency with `MaterialTheme` usage APIs. An example of this -defined by Compose itself is -[`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)), -which determines the surface color that should be used depending on the -elevation. - -> [!NOTE] -> **Note:** This approach is only recommended for straightforward theming value additions, or for values that are the same in different themes. If you have multiple themes, it's better to define a class with new properties instead. - -Another approach is to define an extended theme that "wraps" `MaterialTheme` and -its values. - -Suppose you want to add two additional colors --- `caution` and `onCaution`, a -yellow color used for actions that are semi-dangerous --- whilst keeping the -existing Material colors: - - -```kotlin -@Immutable -data class ExtendedColors( - val caution: Color, - val onCaution: Color -) - -val LocalExtendedColors = staticCompositionLocalOf { - ExtendedColors( - caution = Color.Unspecified, - onCaution = Color.Unspecified - ) -} - -@Composable -fun ExtendedTheme( - /* ... */ - content: @Composable () -> Unit -) { - val extendedColors = ExtendedColors( - caution = Color(0xFFFFCC02), - onCaution = Color(0xFF2C2D30) - ) - CompositionLocalProvider(LocalExtendedColors provides extendedColors) { - MaterialTheme( - /* colors = ..., typography = ..., shapes = ... */ - content = content - ) - } -} - -// Use with eg. ExtendedTheme.colors.caution -object ExtendedTheme { - val colors: ExtendedColors - @Composable - get() = LocalExtendedColors.current -} -``` - -
- -This is similar to `MaterialTheme` usage APIs. It also supports multiple themes -as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`. - -### Use Material components - -When extending Material Theming, existing `MaterialTheme` values are maintained -and Material components still have reasonable defaults. - -If you want to use extended values in components, wrap them in your own -composable functions, directly setting the values you want to alter, and -exposing others as parameters to the containing composable: - - -```kotlin -@Composable -fun ExtendedButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit -) { - Button( - colors = ButtonDefaults.buttonColors( - containerColor = ExtendedTheme.colors.caution, - contentColor = ExtendedTheme.colors.onCaution - /* Other colors use values from MaterialTheme */ - ), - onClick = onClick, - modifier = modifier, - content = content - ) -} -``` - -
- -You would then replace usages of `Button` with `ExtendedButton` where -appropriate. - - -```kotlin -@Composable -fun ExtendedApp() { - ExtendedTheme { - /*...*/ - ExtendedButton(onClick = { /* ... */ }) { - /* ... */ - } - } -} -``` - -
- -## Replace Material subsystems - -Instead of extending Material Theming, you may want to replace one or more -systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation, -while maintaining the others. - -Suppose you want to replace the type and shape systems while keeping the color -system: - - -```kotlin -@Immutable -data class ReplacementTypography( - val body: TextStyle, - val title: TextStyle -) - -@Immutable -data class ReplacementShapes( - val component: Shape, - val surface: Shape -) - -val LocalReplacementTypography = staticCompositionLocalOf { - ReplacementTypography( - body = TextStyle.Default, - title = TextStyle.Default - ) -} -val LocalReplacementShapes = staticCompositionLocalOf { - ReplacementShapes( - component = RoundedCornerShape(ZeroCornerSize), - surface = RoundedCornerShape(ZeroCornerSize) - ) -} - -@Composable -fun ReplacementTheme( - /* ... */ - content: @Composable () -> Unit -) { - val replacementTypography = ReplacementTypography( - body = TextStyle(fontSize = 16.sp), - title = TextStyle(fontSize = 32.sp) - ) - val replacementShapes = ReplacementShapes( - component = RoundedCornerShape(percent = 50), - surface = RoundedCornerShape(size = 40.dp) - ) - CompositionLocalProvider( - LocalReplacementTypography provides replacementTypography, - LocalReplacementShapes provides replacementShapes - ) { - MaterialTheme( - /* colors = ... */ - content = content - ) - } -} - -// Use with eg. ReplacementTheme.typography.body -object ReplacementTheme { - val typography: ReplacementTypography - @Composable - get() = LocalReplacementTypography.current - val shapes: ReplacementShapes - @Composable - get() = LocalReplacementShapes.current -} -``` - -
- -### Use Material components - -When one or more systems of `MaterialTheme` have been replaced, using Material -components as-is may result in unwanted Material color, type, or shape values. - -If you want to use replacement values in components, wrap them in your own -composable functions, directly setting the values for the relevant system, and -exposing others as parameters to the containing composable. - -> [!NOTE] -> **Note:** Not all values may be exposed as parameters in Material composables, in particular with `CompositionLocal` composables (such as `LocalTextStyle`). In such cases you may need to wrap `content` lambdas in provider functions (like `ProvideTextStyle`). - - -```kotlin -@Composable -fun ReplacementButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit -) { - Button( - shape = ReplacementTheme.shapes.component, - onClick = onClick, - modifier = modifier, - content = { - ProvideTextStyle( - value = ReplacementTheme.typography.body - ) { - content() - } - } - ) -} -``` - -
- -You would then replace usages of `Button` with `ReplacementButton` where -appropriate. - - -```kotlin -@Composable -fun ReplacementApp() { - ReplacementTheme { - /*...*/ - ReplacementButton(onClick = { /* ... */ }) { - /* ... */ - } - } -} -``` - -
- -## Implement a fully custom design system - -You may want to replace Material Theming with a fully custom design system. -Consider that `MaterialTheme` provides the following systems: - -- `Colors`, `Typography`, and `Shapes`: Material Theming systems -- `TextSelectionColors`: Colors used for text selection by `Text` and `TextField` -- `Ripple` and `RippleTheme`: Material implementation of `Indication` - -If you want to continue using Material components, you must replace some of -these systems in your custom themes or handle the systems in your -components to avoid unwanted behavior. - -However, design systems are not limited to the concepts Material relies on. You -can modify existing systems and introduce entirely new ones --- with new classes -and types --- to make other concepts compatible with themes. - -In the following code, we model a custom color system that includes gradients -(`List`), include a type system, introduce a new elevation system, -and exclude other systems provided by `MaterialTheme`: - -![Screenshot of a mobile app UI demonstrating a custom design system with elements using gradients for colors, custom typography, and elevation.](https://developer.android.com/static/develop/ui/compose/images/themes/custom-color-gradients.png) - - -```kotlin -@Immutable -data class CustomColors( - val content: Color, - val component: Color, - val background: List -) - -@Immutable -data class CustomTypography( - val body: TextStyle, - val title: TextStyle -) - -@Immutable -data class CustomElevation( - val default: Dp, - val pressed: Dp -) - -val LocalCustomColors = staticCompositionLocalOf { - CustomColors( - content = Color.Unspecified, - component = Color.Unspecified, - background = emptyList() - ) -} -val LocalCustomTypography = staticCompositionLocalOf { - CustomTypography( - body = TextStyle.Default, - title = TextStyle.Default - ) -} -val LocalCustomElevation = staticCompositionLocalOf { - CustomElevation( - default = Dp.Unspecified, - pressed = Dp.Unspecified - ) -} - -@Composable -fun CustomTheme( - /* ... */ - content: @Composable () -> Unit -) { - val customColors = CustomColors( - content = Color(0xFFDD0D3C), - component = Color(0xFFC20029), - background = listOf(Color.White, Color(0xFFF8BBD0)) - ) - val customTypography = CustomTypography( - body = TextStyle(fontSize = 16.sp), - title = TextStyle(fontSize = 32.sp) - ) - val customElevation = CustomElevation( - default = 4.dp, - pressed = 8.dp - ) - CompositionLocalProvider( - LocalCustomColors provides customColors, - LocalCustomTypography provides customTypography, - LocalCustomElevation provides customElevation, - content = content - ) -} - -// Use with eg. CustomTheme.elevation.small -object CustomTheme { - val colors: CustomColors - @Composable - get() = LocalCustomColors.current - val typography: CustomTypography - @Composable - get() = LocalCustomTypography.current - val elevation: CustomElevation - @Composable - get() = LocalCustomElevation.current -} -``` - -
- -### Use Material components - -When no `MaterialTheme` is present, using Material components as-is will result -in unwanted Material color, type, and shape values and indication behavior. - -If you want to use custom values in components, wrap them in your own composable -functions, directly setting the values for the relevant system, and exposing -others as parameters to the containing composable. - -We recommend that you access values you set from your custom theme. -Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or -other systems, you can hardcode them. - - -```kotlin -@Composable -fun CustomButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit -) { - Button( - colors = ButtonDefaults.buttonColors( - containerColor = CustomTheme.colors.component, - contentColor = CustomTheme.colors.content, - disabledContainerColor = CustomTheme.colors.content - .copy(alpha = 0.12f) - .compositeOver(CustomTheme.colors.component), - disabledContentColor = CustomTheme.colors.content - .copy(alpha = 0.38f) - - ), - shape = ButtonShape, - elevation = ButtonDefaults.elevatedButtonElevation( - defaultElevation = CustomTheme.elevation.default, - pressedElevation = CustomTheme.elevation.pressed - /* disabledElevation = 0.dp */ - ), - onClick = onClick, - modifier = modifier, - content = { - ProvideTextStyle( - value = CustomTheme.typography.body - ) { - content() - } - } - ) -} - -val ButtonShape = RoundedCornerShape(percent = 50) -``` - -
- -> [!NOTE] -> **Note:** `Button` uses `rememberRipple()` internally to provide a `Ripple` `Indication`. It's a good idea to check the source code when implementing other custom components that wrap existing components. - -If you've introduced new class types --- such as `List` to represent -gradients --- then it may be better to implement components from scratch instead -of wrapping them. For an example, take a look at -[`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt) -from the Jetsnack sample. - -## Recommended for you - -- Note: link text is displayed when JavaScript is off -- [Material Design 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material3) -- [Migrate from Material 2 to Material 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material2-material3) -- [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) \ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md deleted file mode 100644 index ab0d26d..0000000 --- a/.agents/skills/styles/references/android/develop/ui/compose/styles/fundamentals.md +++ /dev/null @@ -1,421 +0,0 @@ -There are three ways you can adopt Styles throughout your app: - -1. Use directly on existing components that expose a [`Style`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/Style) parameter. -2. Apply a style with [`Modifier.styleable`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/styleable.modifier#(androidx.compose.ui.Modifier).styleable(androidx.compose.foundation.style.StyleState,androidx.compose.foundation.style.Style)) on layout composables that don't accept a `Style` parameter. -3. In your own custom design system, use `Modifier.styleable{}` and expose a style parameter on your own components. - -## Available properties on Styles - -Styles support many of the same properties that modifiers support; however, not -everything that is a modifier can be replicated with a Style. You still need -modifiers for certain behaviors, like interactions, custom drawing, or stacking -of properties. - -| Grouping | Properties | Inherited by children | -|---|---|---| -| **Layout and sizing** | | | -| Content Padding (inner) | - `contentPadding(all: Dp)` - `contentPadding(horizontal: Dp, vertical: Dp)` - `contentPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `contentPaddingHorizontal(value: Dp)` / `contentPaddingVertical(value: Dp)` - `contentPaddingStart(value: Dp)` / `contentPaddingTop(value: Dp)` / `contentPaddingEnd(value: Dp)` / `contentPaddingBottom(value: Dp)` | No | -| External Padding (outer) | - `externalPadding(all: Dp)` - `externalPadding(horizontal: Dp, vertical: Dp)` - `externalPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `externalPaddingHorizontal(value: Dp)` / `externalPaddingVertical(value: Dp)` - `externalPaddingStart(value: Dp)` / `externalPaddingTop(value: Dp)` / `externalPaddingEnd(value: Dp)` / `externalPaddingBottom(value: Dp)` | No | -| Dimensions | `fillWidth()/fillHeight()/fillSize()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No | -| Positioning | `left/top/right/bottom` offsets. | No | -| **Visual Appearance** | | | -| Fills | `background` and `foreground` (supports `Color` or `Brush`). | No | -| Borders | `borderWidth`, `borderColor`, and `borderBrush`. | No | -| Shape | `shape` | No - but used in conjunction with other properties. `clip` and `border` use this defined shape. | -| Shadows | `dropShadow`, `innerShadow` | No | -| **Transformations** | | | -| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/scaleY`, `rotationX/rotationY/rotationZ` | No | -| Control | `alpha`, `zIndex` (stacking order), and `transformOrigin` (pivot point) | No | -| **Typography** | | | -| Styling | `textStyle`, `fontSize`, `fontWeight`, `fontStyle`, and `fontFamily` | Yes | -| Coloration | `contentColor` and `contentBrush`. This is also used for Icons styling. | Yes | -| Paragraph | `lineHeight`, `letterSpacing`, `textAlign`, `textDirection`, `lineBreak`, and `hyphens`. | Yes | -| Decoration | `textDecoration`, `textIndent`, and `baselineShift`. | Yes | - -## Use Styles directly on components with Style parameters - -Components that expose a `Style` parameter allow you to set their styling: - - -```kotlin -BaseButton( - onClick = { }, - style = { } -) { - BaseText("Click me") -} -``` - -
- -Within the style lambda, you can set various properties, such as `externalPadding` -or `background`: - - -```kotlin -BaseButton( - onClick = { }, - style = { background(Color.Blue) } -) { - BaseText("Click me") -} -``` - -
- -For the full list of supported properties, see [Available properties on -Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles). - -## Apply Styles using modifiers for components with no existing parameter - -For components that lack a built-in style parameter, you can still apply styles -with the `styleable` modifier. This approach is also useful when developing your -own custom components. - - -```kotlin -Row( - modifier = Modifier.styleable { } -) { - BaseText("Content") -} -``` - -
- -Similar to the `style` parameter, you can include properties like `background`, -`contentPadding`, or `externalPadding` inside the lambda. - - -```kotlin -Row( - modifier = Modifier.styleable { - background(Color.Blue) - } -) { - BaseText("Content") -} -``` - -
- -> [!NOTE] -> **Note:** When using `Modifier.styleable`, the child composables won't have those properties applied to them, unless they are inherited properties. Only the container with the `styleable` modifier has those properties applied. - -Multiple chained `Modifier.styleable` modifiers are additive with non-inherited -properties on the applied composable, behaving similarly to multiple modifiers -defining the same properties. For inherited properties, these are overridden, -and the last `styleable` modifier in the chain sets the values. - -When using `Modifier.styleable`, you may also want to create and supply a -`StyleState` to be used with the modifier to apply state-based styling. For more -details, see [State and animations with -Styles](https://developer.android.com/develop/ui/compose/styles/state-animations). - -## Define a standalone Style - -You can define a standalone Style for reusability purposes: - - -```kotlin -val style = Style { background(Color.Blue) } -``` - -
- -You can then pass that defined style into a composable's style parameter or with -`Modifier.styleable`. When using `Modifier.styleable`, you also need to create a -`StyleState` object. `StyleState` is covered in detail in the [State and -animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation. - -The following example shows how you can apply a Style either directly through a -component's built-in parameters, or through a `Modifier.styleable`: - - -```kotlin -val style = Style { background(Color.Blue) } - -// built in parameter -BaseButton(onClick = { }, style = style) { - BaseText("Button") -} - -// modifier styleable -val styleState = remember { MutableStyleState(null) } -Column( - Modifier.styleable(styleState, style) -) { - BaseText("Column content") -} -``` - -
- -You can also pass that Style into multiple components: - - -```kotlin -val style = Style { background(Color.Blue) } - -// built in parameter -BaseButton(onClick = { }, style = style) { - BaseText("Button") -} -BaseText("Different text that uses the same style parameter", style = style) - -// modifier styleable -val columnStyleState = remember { MutableStyleState(null) } -Column( - Modifier.styleable(columnStyleState, style) -) { - BaseText("Column") -} -val rowStyleState = remember { MutableStyleState(null) } -Row( - Modifier.styleable(rowStyleState, style) -) { - BaseText("Row") -} -``` - -
- -## Add multiple Style properties - -You can add multiple Style properties by setting different properties on each -line: - - -```kotlin -BaseButton( - onClick = { }, - style = { - background(Color.Blue) - contentPaddingStart(16.dp) - } -) { - BaseText("Button") -} -``` - -
- -> [!IMPORTANT] -> **Important:** Unlike modifier-based styling, properties in Styles override one another; the last property defined takes precedence. - -Properties in Styles are not additive, unlike modifier-based styling. Styles -take the last set value in the list of properties within one style block. In the -following example, with the background set twice, the `TealColor` is the applied -background. For padding, `contentPaddingTop` overrides the top -padding set by `contentPadding` and does not combine the values. - - -```kotlin -BaseButton( - style = { - background(Color.Red) - // Background of Red is now overridden with TealColor instead - background(TealColor) - // All directions of padding are set to 64.dp (top, start, end, bottom) - contentPadding(64.dp) - // Top padding is now set to 16.dp, all other paddings remain at 64.dp - contentPaddingTop(16.dp) - }, - onClick = { - // - } -) { - BaseText("Click me!") -} -``` - -
- -![Button with two background colors set, and two contentPadding -overrides](https://developer.android.com/static/develop/ui/compose/styles/images/basic_style_button.png) **Figure 1.** Button with two background colors set and two `contentPadding` overrides. - -## Merge multiple style objects - -You can create multiple Style objects and pass them into the style parameter of -your composable. - - -```kotlin -val style1 = Style { background(TealColor) } -val style2 = Style { contentPaddingTop(16.dp) } - -BaseButton( - style = style1 then style2, - onClick = { - - }, -) { - BaseText("Click me!") -} -``` - -
- -![Button with background color and contentPaddingTop -set](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_top.png) **Figure 2.** Button with background color and `contentPaddingTop` set. - -When multiple Styles specify the same property, the last set -property is chosen. Because properties are not additive in Styles, the last -padding passed in overrides the `contentPaddingHorizontal` set by the initial -`contentPadding`. Additionally, the last background color overrides the -background color set by the initial style passed in. - - -```kotlin -val style1 = Style { - background(Color.Red) - contentPadding(32.dp) -} - -val style2 = Style { - contentPaddingHorizontal(8.dp) - background(Color.LightGray) -} - -BaseButton( - style = style1 then style2, - onClick = { - - }, -) { - BaseText("Click me!") -} -``` - -
- -In this case, the styling applied has a light gray background and `32.dp` padding, -except for the left and right padding, which has a value of `8.dp`. -![Button with contentPadding that's overridden by different -Styles](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_overrides.png) **Figure 3.** Button with `contentPadding` that's overridden by different Styles. - -## Style inheritance - -> [!NOTE] -> **Note:** While the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`. - -Certain style properties, such as `contentColor` and text style-related -properties, propagate to the child composables. A style set on a child -composable overrides the inherited parent styling for that specific child. -![Style propagation with Style, styleable, and direct -parameters](https://developer.android.com/static/develop/ui/compose/styles/images/styles_modifiers_precedence_ordering.png) **Figure 4.** Style propagation with `Style`, `styleable`, and direct parameters. - -| Priority | Method | Effect | -|---|---|---| -| 1 (Highest) | Direct arguments on a composable | Overrides everything; for example, `Text(color = Color.Red)` | -| 2 | Style parameter | Local style overrides `Text(style = Style { contentColor(Color.Red)}` | -| 3 | Modifier chain | `Modifier.styleable{ contentColor(Color.Red)` on the component itself. | -| 4 (Lowest) | Parent styles | For properties that can be inherited (Typography/Color) passed down from the parent. | - -> [!NOTE] -> **Note:** Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, similar to having multiple modifiers defining the same properties. For inherited properties, these are overridden; the last `styleable` modifier in the chain sets the values. - -### Parent styling - -You can set text properties (such as `contentColor`) from the parent composable, -and they propagate to all child `Text` composables. - - -```kotlin -val styleState = remember { MutableStyleState(null) } -Column( - modifier = Modifier.styleable(styleState) { - background(Color.LightGray) - val blue = Color(0xFF4285F4) - val purple = Color(0xFFA250EA) - val colors = listOf(blue, purple) - contentBrush(Brush.linearGradient(colors)) - }, -) { - BaseText("Children inherit", style = { width(60.dp) }) - BaseText("certain properties") - BaseText("from their parents") -} -``` - -
- -![Child composables' property -inheritance](https://developer.android.com/static/develop/ui/compose/styles/images/children_inherit_styles_parents.png) **Figure 5.** Child composables' property inheritance. - -### Child override of properties - -You can also set styling on a specific `Text` composable. If the parent composable -has styling set, the styling set on the child composable overrides the -parent composable's styling. - - -```kotlin -val styleState = remember { MutableStyleState(null) } -Column( - modifier = Modifier.styleable(styleState) { - background(Color.LightGray) - val blue = Color(0xFF4285F4) - val purple = Color(0xFFA250EA) - val colors = listOf(blue, purple) - contentBrush(Brush.linearGradient(colors)) - }, -) { - BaseText("Children can ", style = { - contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue))) - }) - BaseText("override properties") - BaseText("set by their parents") -} -``` - -
- -![Child composables override parent -properties](https://developer.android.com/static/develop/ui/compose/styles/images/children_override_styles.png) **Figure 6.** Child composables override parent properties. - -## Implement custom Style properties - -You can create custom properties that map to existing Style definitions by using -extension functions on the `StyleScope`, as shown in the following example: - - -```kotlin -fun StyleScope.outlinedBackground(color: Color) { - border(1.dp, color) - background(color) -} -``` - -
- -Apply this new property within a Style definition: - - -```kotlin -val customExtensionStyle = Style { - outlinedBackground(Color.Blue) -} -``` - -
- -Creating new styleable properties is unsupported. If your use case -requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128). - -## Read `CompositionLocal` values - -It's a common pattern to store design system tokens within a `CompositionLocal`, -to access the variables without needing to pass them as parameters. Styles -can access `CompositionLocal`s to retrieve system-wide values within a style: - - -```kotlin -val buttonStyle = Style { - contentPadding(12.dp) - shape(RoundedCornerShape(50)) - background(Brush.verticalGradient(LocalCustomColors.currentValue.background)) -} -``` - -
\ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md deleted file mode 100644 index 01683bd..0000000 --- a/.agents/skills/styles/references/android/develop/ui/compose/styles/state-animations.md +++ /dev/null @@ -1,461 +0,0 @@ -
- -The Styles API offers a declarative and streamlined approach to managing UI -changes during interaction states like `hovered`, `focused`, and `pressed`. With -this API, you can significantly decrease the boilerplate code typically required -when using modifiers. - -To facilitate reactive styling, `StyleState` acts as a stable, read-only -interface that tracks the active state of an element (such as its enabled, -pressed, or focused status). Within a `StyleScope`, you can access this through -the `state` property to implement conditional logic directly in your Style -definitions. - -## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled - -Styles come with built-in support for common interactions: - -- Pressed -- Hovered -- Selected -- Enabled -- Toggled - -It's also possible to support custom states. See the [Custom State Styling with -StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information. - -### Handle interaction states with Style parameters - -The following example demonstrates modifying the `background` and `borderColor` -in response to interaction states, specifically switching to purple when hovered -and blue when focused: - - -```kotlin -@Preview -@Composable -private fun OpenButton() { - BaseButton( - style = outlinedButtonStyle then { - background(Color.White) - hovered { - background(lightPurple) - border(2.dp, lightPurple) - } - focused { - background(lightBlue) - } - }, - onClick = { }, - content = { - BaseText("Open in Studio", style = { - contentColor(Color.Black) - fontSize(26.sp) - textAlign(TextAlign.Center) - }) - } - ) -} -``` - -
- -**Figure 1.** Changing background color based on hovered and focused states. - -You can also create nested state definitions. For example, you can define a -specific style for when a button is being both pressed and hovered -simultaneously: - - -```kotlin -@Composable -private fun OpenButton_CombinedStates() { - BaseButton( - style = outlinedButtonStyle then { - background(Color.White) - hovered { - // light purple - background(lightPurple) - pressed { - // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked - background(lightOrange) - } - } - pressed { - // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only - background(lightRed) - } - focused { - background(lightBlue) - } - }, - onClick = { }, - content = { - BaseText("Open in Studio", style = { - contentColor(Color.Black) - fontSize(26.sp) - textAlign(TextAlign.Center) - }) - } - ) -} -``` - -
- -**Figure 2.** Hovered and pressed state together on a button. - -### Custom composables with Modifier.styleable - -When creating your own `styleable` components, you must connect an -`interactionSource` to a `styleState`. Then, pass this state into -`Modifier.styleable` to utilize it. - -Consider a scenario where your design system includes a `GradientButton`. You -may want to create a `LoginButton` that inherits from `GradientButton`, but -alters its colors during interactions, like being pressed. - -- To enable `interactionSource` style updates, include an `interactionSource` as a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a new `MutableInteractionSource`. -- Initialize the `styleState` by providing the `interactionSource`. Make sure the `styleState`'s enabled status reflects the value of the provided enabled parameter. -- Assign the `interactionSource` to the `focusable` and `clickable` modifiers. Finally, apply the `styleState` to the modifier's `styleable` parameter. - - -```kotlin -@Composable -private fun GradientButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - style: Style = Style, - enabled: Boolean = true, - interactionSource: MutableInteractionSource? = null, - content: @Composable RowScope.() -> Unit, -) { - val interactionSource = interactionSource ?: remember { MutableInteractionSource() } - val styleState = rememberUpdatedStyleState(interactionSource) { - it.isEnabled = enabled - } - Row( - modifier = - modifier - .clickable( - onClick = onClick, - enabled = enabled, - interactionSource = interactionSource, - indication = null, - ) - .styleable(styleState, baseGradientButtonStyle then style), - content = content, - ) -} -``` - -
- -You can now use the `interactionSource` state to drive style modifications with -the pressed, focused, and hovered options inside the style block: - - -```kotlin -@Preview -@Composable -fun LoginButton() { - val loginButtonStyle = Style { - pressed { - background( - Brush.linearGradient( - listOf(Color.Magenta, Color.Red) - ) - ) - } - } - GradientButton(onClick = { - // Login logic - }, style = loginButtonStyle) { - BaseText("Login") - } -} -``` - -
- -**Figure 3.** Changing a custom composable state based on `interactionSource`. - -## Animate style changes - -Styles state changes come with built-in animation support. You can wrap the new -property within any state change block with `animate` to automatically add -animations between different states. This is similar to the `animate*AsState` -APIs. The following example animates the `borderColor` from black to blue when -the state changes to focused: - - -```kotlin -val animatingStyle = Style { - externalPadding(48.dp) - border(3.dp, Color.Black) - background(Color.White) - size(100.dp) - - pressed { - animate { - borderColor(Color.Magenta) - background(Color(0xFFB39DDB)) - } - } -} - -@Preview -@Composable -private fun AnimatingStyleChanges() { - val interactionSource = remember { MutableInteractionSource() } - val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } - Box(modifier = Modifier - .clickable( - interactionSource, - enabled = true, - indication = null, - onClick = { - - } - ) - .styleable(styleState, animatingStyle)) { - - } -} -``` - -
- -**Figure 4.** Animating color changes on press. - -The `animate` API accepts an `animationSpec` to change the duration or shape of -the animation curve. The following example animates the size of the box with a -`spring` spec: - - -```kotlin -val animatingStyleSpec = Style { - externalPadding(48.dp) - border(3.dp, Color.Black) - background(Color.White) - size(100.dp) - transformOrigin(TransformOrigin.Center) - pressed { - animate { - borderColor(Color.Magenta) - background(Color(0xFFB39DDB)) - } - animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { - scale(1.2f) - } - } -} - -@Preview(showBackground = true) -@Composable -fun AnimatingStyleChangesSpec() { - val interactionSource = remember { MutableInteractionSource() } - val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } - Box(modifier = Modifier - .clickable( - interactionSource, - enabled = true, - indication = null, - onClick = { - - } - ) - .styleable(styleState, animatingStyleSpec)) -} -``` - -
- -**Figure 5.** Animating size and color changes on press. - -## Custom state styling with StyleState - -Depending on your composable use case, you may have different styles that are -backed by custom states. For example, if you have a media app, you may want to -have different styling for the buttons in your `MediaPlayer` composable -depending on the playback state of the player. Follow these steps to create and -use your own custom state: - -1. Define custom key -2. Create `StyleState` extension -3. Link to custom state - -### Define custom key - -To create a custom state-based style, first create a -[`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the -app launches, the media player is in the `Stopped` state, so it's initialized in -this way: - - -```kotlin -enum class PlayerState { - Stopped, - Playing, - Paused -} - -val playerStateKey = StyleStateKey(PlayerState.Stopped) -``` - -
- -### Create StyleState extension functions - -Define an extension function on `StyleState` to query the current `playState`. -Then, create extension functions on `StyleScope` with your custom states passing -in the `playStateKey`, a lambda with the specific state, and the style. - - -```kotlin -// Extension Function on MutableStyleState to query and set the current playState -var MutableStyleState.playerState - get() = this[playerStateKey] - set(value) { this[playerStateKey] = value } - -fun StyleScope.playerPlaying(block: () -> Unit) { - state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) -} -fun StyleScope.playerPaused(block: () -> Unit) { - state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) -} -``` - -
- -### Link to custom state - -Define the `styleState` in your composable and set the `styleState.playState` -equal to incoming state. Pass `styleState` into the `styleable` function on the -modifier. - - -```kotlin -@Composable -fun MediaPlayer( - url: String, - modifier: Modifier = Modifier, - style: Style = Style, - state: PlayerState = remember { PlayerState.Paused } -) { - // Hoist style state, set playstate as a parameter, - val styleState = remember { MutableStyleState(null) } - // Set equal to incoming state to link the two together - styleState.playerState = state - Box( - modifier = modifier.styleable(styleState, style)) { - ///.. - } -} -``` - -
- -Within the `style` lambda, you can apply state-based styling for custom states, -using the previously defined extension functions. - - -```kotlin -@Composable -fun StyleStateKeySample() { - // Using the extension function to change the border color to green while playing - val style = Style { - borderColor(Color.Gray) - playerPlaying { - animate { - borderColor(Color.Green) - } - } - playerPaused { - animate { - borderColor(Color.Blue) - } - } - } - val styleState = remember { MutableStyleState(null) } - styleState[playerStateKey] = PlayerState.Playing - - // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. - MediaPlayer(url = "https://example.com/media/video", - style = style, - state = PlayerState.Stopped) -} -``` - -
- -The following code is the full snippet for this example: - - -```kotlin -enum class PlayerState { - Stopped, - Playing, - Paused -} -val playerStateKey = StyleStateKey(PlayerState.Stopped) -var MutableStyleState.playerState - get() = this[playerStateKey] - set(value) { this[playerStateKey] = value } - -fun StyleScope.playerPlaying(block: () -> Unit) { - state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) -} -fun StyleScope.playerPaused(block: () -> Unit) { - state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) - -} - -@Composable -fun MediaPlayer( - url: String, - modifier: Modifier = Modifier, - style: Style = Style, - state: PlayerState = remember { PlayerState.Paused } -) { - // Hoist style state, set playstate as a parameter, - val styleState = remember { MutableStyleState(null) } - // Set equal to incoming state to link the two together - styleState.playerState = state - Box( - modifier = modifier.styleable(styleState, Style { - size(100.dp) - border(2.dp, Color.Red) - - }, style, )) { - - ///.. - } -} -@Composable -fun StyleStateKeySample() { - // Using the extension function to change the border color to green while playing - val style = Style { - borderColor(Color.Gray) - playerPlaying { - animate { - borderColor(Color.Green) - } - } - playerPaused { - animate { - borderColor(Color.Blue) - } - } - } - val styleState = remember { MutableStyleState(null) } - styleState[playerStateKey] = PlayerState.Playing - - // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. - MediaPlayer(url = "https://example.com/media/video", - style = style, - state = PlayerState.Stopped) -} -``` - -
\ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md deleted file mode 100644 index cd9bc50..0000000 --- a/.agents/skills/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md +++ /dev/null @@ -1,48 +0,0 @@ -Styles differ from modifiers by design. Styles don't replace modifiers; instead, -the two systems coexist with different goals. Internally, a Style is a modifier. -You can do everything Styles can do with modifiers, but not all functionality in -modifiers is available in Styles. -**Important:** - -- **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define a theme-wide set of properties for a component. -- **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties. - -The following is a comparison between Styles versus modifiers: - -| Feature | Modifiers | Styles | -|---|---|---| -| **Primary Goal** | Define behaviors, semantics, and complex layouts. Modifiers manipulate individual elements on the fly for a particular composable and don't trickle down from the theme. | Define visual appearance, individual item sizing and themeable properties. Styles operate at a theme level and are over-writeable at a component level. They trickle down and apply styling across different composables. | -| **Logic** | Additive - the modifiers combine together to form a new result. | Over-writable - the last property set in the Style wins. Styles act as a single layer of properties that override each other based on a defined precedence hierarchy. | -| **Theming** | Challenging to lift into a theme, normally used individually. | By design, Styles are themeable (they can access `CompositionLocal`s) and can be defined once and used across components. | -| **Performance** | Updates often require all three phases of Compose: composition, layout and draw. Achieving good animation performance of modifiers often requires writing lambda-based versions. | Skips composition phase, only active in layout and draw phase, reducing recompositions. Requires less object allocation. | -| **Animations** | Requires using separate animation primitives like `animate*AsState` | Features built-in `animate { }` API that handles some animations for you. | - -## Limitations of modifiers - -Modifiers have many benefits in the current Compose landscape. However, Styles -address some limitations of modifiers, which the following list describes: - -- Modifiers are typically created in the Composition phase. Updates can force a full rerun of Composition, Layout, and Draw, even for small visual changes like color, unless you create lambda-based modifiers. -- Conditional modifiers require disruptive if-else logic within fluent chains. Animating them requires manual state boilerplate and lacks a high-performance "auto-animate" mechanism. -- Modifiers stack rather than replace. You can't override a component's default border; you can only draw a second one on top. -- Modifiers are difficult to abstract into global themes. Consequently, themes usually store raw values instead of reusable modifier configurations. - -## Limitations of Styles - -While Styles can fill in some of the gaps that modifiers have, they also have -some limitations, which show how they cannot entirely replace modifiers: - -- Styles are specialized Modifiers. While a modifier can do anything a Style does, the reverse is not true. Consequently, Styles can supplement, but cannot replace, modifiers. -- Styles are limited to visual configuration (backgrounds, padding, borders). They cannot handle behaviors like click logic, gesture detection, or accessibility semantics. -- Resolving a Style into its final state is *more expensive than applying a - single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this. - -## When to use Styles over modifiers - -While the choice to use Styles is largely dependent on your app and use cases, -the following guidance helps determine when to prefer a style over a modifier: - -- **To achieve theme-wide consistency:** Styles are designed to be "lifted" into a global theme. Instead of passing repetitive Modifiers to every component, you can define a single Style in your theme to create a unified look across the entire app. -- **When performing frequent animations:** Styles evaluate during the Layout and Draw phases, allowing properties like color or scale to animate while bypassing the Composition phase entirely. This significantly reduces performance overhead. Use a Style instead of a modifier when doing visual property animations. -- **Overriding vs. stacking:** Use Styles when you need to replace a default property. Modifiers are additive (adding a border stacks a second one), whereas Styles use "last-write-wins" logic, making it easier to swap out backgrounds or padding without visual clutter. -- **Customizing Material components:** If a Material component provides a Style parameter, it is the suggested approach for customization. These styles allow you to access and modify specific properties within the composable's internal structure that might otherwise be inaccessible. \ No newline at end of file diff --git a/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md b/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md deleted file mode 100644 index c8a373b..0000000 --- a/.agents/skills/styles/references/android/develop/ui/compose/styles/theming.md +++ /dev/null @@ -1,257 +0,0 @@ -> [!NOTE] -> **Note:** Styles are `@Experimental` and likely to change in upcoming releases, with Material support for Styles added in future releases. If you have any feedback, [file Styles issues](https://issuetracker.google.com/issues/new?component=612128). - -There are several ways you can build out your apps using Styles. What you choose -depends on where your app sits in relation to its adoption of Material Design: - -1. Fully custom design system, not using Material Design - - **Recommendation**: Define component styles that consume values from the theme, and expose style parameters on design system components. -2. Using Material Design - - **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible. - -## The Style layer - -In the traditional Compose model, customization often relies heavily on -overriding global tokens (colors and typography) provided by `MaterialTheme`, or -wrapping and overriding properties of a design system composable where possible. -Sometimes, there are properties within the Material layer that are not exposed -through the subsystems or parameters, but are hardcoded defaults on the -component itself. - -With the Styles API, there's a new layer of abstraction that's a bridge between -subsystems and components: **Styles**. - -| Layer | Responsibility | Example | -|---|---|---| -| **Subsystem values** | Named values | `val Primary = Color(0xFF34A85E)` | -| **Atomic Styles** | Style that does exactly one property change | `val largeSizeAtomic = Style { size(100.dp, 40.dp) }` | -| **Component Styles** | Component-specific configurations | A Button with Primary background and 16dp padding. `val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }` | -| **Components** | The functional UI element that consumes a Style. | `Button(style = buttonStyle) { ... }` | - -![Diagram showing Theming with Styles with the new layer introduction](https://developer.android.com/static/develop/ui/compose/styles/images/theming_styles_layer.png) **Figure 1.** An example of a component and how it accesses styles from a theme. - -### Atomic versus monolithic Styles - -With the Styles API, you can break down a Style into separate atomic styles. -Instead of defining complex, component-specific styles like `baseButtonStyle`, -you can also create small, single-purpose utility styles. These act as your -"atoms". - - -```kotlin -// Define single-purpose "atomic" styles -val paddingAtomic = Style { - contentPadding(16.dp) -} -val roundedCornerShapeAtomic = Style { - shape(RoundedCornerShape(8.dp)) -} -val primaryBackgroundAtomic = Style { - background(Color.Blue) -} -val largeSizeAtomic = Style { - size(100.dp, 40.dp) -} -val interactiveShadowAtomic = Style { - hovered { - animate { - dropShadow( - Shadow( - offset = DpOffset( - 0.dp, - 0.dp - ), - radius = 2.dp, - spread = 0.dp, - color = Color.Blue, - ) - ) - } - } -} -``` - -
- -#### Composition using "then" - -One of the powerful features of the new Styles API is the `then` operator, which -lets you merge multiple `Style` objects. This lets you build a component using -atomic utility classes. - -**Traditional (non-atomic)**: - - -```kotlin -// One large monolithic style -val buttonStyle = Style { - contentPadding(16.dp) - shape(RoundedCornerShape(8.dp)) - background(Color.Blue) -} -``` - -
- -**Atomic refactor**: - - -```kotlin -// Combine atoms to create the final appearance -val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic -``` - -
- -## Adopt Styles in your design system - -Consider the following options when adopting Styles within your design system, -depending on where in the spectrum your design system lies. - -### Custom design system with Styles - -***Consider when**: You've been handed an extensive brand guide that is not -based on Material Design, and you are not planning to use Material Design*. - -***Strategy**: Implement a fully custom design system, and expose styles as part -of the theme*. - -This option is the custom path if you don't use Material as your main design -system language. You bypass `MaterialTheme` entirely for visual definitions and -have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that -acts as a container for your Styles. - -- **How it works** : Create a `CompanyTheme` object that holds `Style` objects for every component in your system. Your components (either wrappers around Material logic or custom `Box` or `Layout` implementations) consume these styles directly, and expose a `Style` parameter for consumers of your design system. -- **The Style layer**: Styles are the primary definition of your design system. Tokens are named variables fed into these styles. This allows for deep customization, such as defining unique animations for state changes (for example, animating scale and color on press). - -If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and -want to adopt styles, add your list of styles to your Theme. This lets you -access your base styles from anywhere in your project. - -1. Create a `Styles` class that stores the various styles in your application - and create the defaults. For example, in the Jetsnack app - the class is - named `JetsnackStyles`: - - - ```kotlin - object JetsnackStyles{ - val buttonStyle: Style = Style { - shape(shapes.medium) - background(colors.brand) - contentColor(colors.textPrimary) - contentPaddingVertical(8.dp) - contentPaddingHorizontal(24.dp) - textStyle(typography.labelLarge) - disabled { - animate { - background(colors.brandSecondary) - } - } - } - val cardStyle: Style = Style { - shape(shapes.medium) - background(colors.uiBackground) - contentColor(colors.textPrimary) - } - } - ``` - -
- -2. Provide `Styles` as part of your overall theme, and expose helper extension - functions on `StyleScope` to access the subsystems: - - - ```kotlin - @Immutable - class JetsnackTheme( - val colors: JetsnackColors = LightJetsnackColors, - val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), - val shapes: Shapes = Shapes() - ) { - companion object { - val colors: JetsnackColors - @Composable @ReadOnlyComposable - get() = LocalJetsnackTheme.current.colors - - val typography: androidx.compose.material3.Typography - @Composable @ReadOnlyComposable - get() = LocalJetsnackTheme.current.typography - - val shapes: Shapes - @Composable @ReadOnlyComposable - get() = LocalJetsnackTheme.current.shapes - - val styles: JetsnackStyles = JetsnackStyles - - val LocalJetsnackTheme: ProvidableCompositionLocal - get() = LocalJetsnackThemeInstance - } - } - - val StyleScope.colors: JetsnackColors - get() = LocalJetsnackTheme.currentValue.colors - - val StyleScope.typography: androidx.compose.material3.Typography - get() = LocalJetsnackTheme.currentValue.typography - - val StyleScope.shapes: Shapes - get() = LocalJetsnackTheme.currentValue.shapes - - internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } - - @Composable - fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors - val theme = JetsnackTheme(colors = colors) - - CompositionLocalProvider( - LocalJetsnackTheme provides theme, - ) { - MaterialTheme( - typography = LocalJetsnackTheme.current.typography, - shapes = LocalJetsnackTheme.current.shapes, - content = content, - ) - } - } - ``` - -
- -3. Access `JetsnackStyles` within your composable: - - - ```kotlin - @Composable - fun CustomButton(modifier: Modifier, - style: Style = Style, - text: String) { - val interactionSource = remember { MutableInteractionSource() } - val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } - - // Apply style to top level container in combination with incoming style from parameter. - Box(modifier = modifier - .clickable( - interactionSource = interactionSource, - indication = null, - enabled = true, - role = Role.Button, - onClick = { - - }, - ) - .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { - Text(text) - } - } - ``` - -
- -Beyond global theme adoption, there are alternative strategies for incorporating -`Styles` into your apps. You can leverage `Styles` inline for specific call -sites or use static definitions when full theming capabilities are unnecessary. -`Styles` shouldn't be swapped conditionally unless the whole style is -fundamentally different. You should prefer accessing dynamic tokens inside a -visual definition rather than switching between distinct style objects. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3b94d95 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# Agent Project Guidelines & Android Skills Enforcement + +## Instruksi Penggunaan Skill Android +Setiap kali mengembangkan, memodifikasi, merombak (*refactoring*), mengoptimalkan, atau menguji proyek Android **piano-tiles** ini, Agent WAJIB secara aktif membaca dan menerapkan petunjuk dari **Skills Agent Android** yang relevan sebelum melakukan perubahan kode: + +### 1. Pengembangan & Arsitektur UI +- **`frogo-sdk`**: Gunakan panduan dan komponen Frogo SDK untuk pengembangan UI (Compose/XML), integrasi iklan (AdMob), helper RecyclerView, dan utilitas Android core. +- **`edge-to-edge`**: Terapkan panduan adaptif edge-to-edge untuk memastikan komponen UI tidak tertutup status bar, navigation bar, atau IME/keyboard inset. +- **`migrate-xml-views-to-jetpack-compose`**: Ikuti alur kerja terstruktur saat melakukan migrasi layout dari legacy XML View ke Jetpack Compose. +- **`jetpack-compose-m3`** & **`android-jetpack-compose-expert`**: Gunakan praktik terbaik Material Design 3 dan pengoptimalan performa Jetpack Compose. + +### 2. Navigasi & Monetisasi +- **`navigation-3`**: Gunakan panduan Jetpack Navigation 3 untuk manajemen navigasi halaman, deep link, multiple backstack, dan scene transitions. +- **`play-billing-library-version-upgrade`**: Ikuti panduan pembaruan Google Play Billing Library (PBL) saat menangani in-app purchase atau langganan. + +### 3. Build Optimization & CLI Operations +- **`android-cli`**: Gunakan skill `android-cli` untuk otomasi perintah Android CLI, pengujian, deployment, emulator, dan diagnosa lingkungan pengembangan. +- **`agp-9-upgrade`**: Ikuti aturan & panduan migrasi saat memperbarui Android Gradle Plugin (AGP) ke versi 9. +- **`r8-analyzer`**: Analisis file build dan Proguard/R8 keep rules untuk mengeliminasi aturan redundan dan meminimalkan ukuran APK. + +### 4. Performa & Asinkron +- **`kotlin-coroutines-expert`**: Gunakan praktik terbaik penanganan Coroutines, Flow, dan operasi asinkron tanpa memblokir Main UI Thread. +- **`diagnose-android-overheating`**: Periksa dan hindari kebocoran memori (*memory leak*) atau pemakaian CPU tinggi yang dapat menyebabkan perangkat cepat panas. + +--- + +## Prosedur Eksekusi Agent +1. **Periksa Skill**: Sebelum mulai menulis atau merombak kode, identifikasi skill Android di atas yang relevan dengan tugas. +2. **Baca `SKILL.md`**: Gunakan `view_file` pada `SKILL.md` dari skill yang relevan untuk membaca dokumentasi & petunjuk teknisnya. +3. **Patuhi Panduan**: Ikuti standar coding, arsitektur, dan keamanan yang tertera pada skill tersebut secara konsisten. diff --git a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt index 3d9a586..615bcfe 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseActivity.kt @@ -1,7 +1,6 @@ package io.github.amirisback.androidapp.common.base -import androidx.viewbinding.ViewBinding -import com.frogobox.ads.ui.FrogoAdBindActivity +import com.frogobox.ads.ui.compose.FrogoAdComposeActivity /** * Created by Faisal Amir @@ -19,6 +18,6 @@ import com.frogobox.ads.ui.FrogoAdBindActivity * */ -abstract class BaseActivity : FrogoAdBindActivity(), IBaseActivity { +abstract class BaseActivity : FrogoAdComposeActivity(), IBaseActivity { } \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt index 4dfe19a..4899aa7 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/common/base/BaseFragment.kt @@ -1,7 +1,6 @@ package io.github.amirisback.androidapp.common.base -import androidx.viewbinding.ViewBinding -import com.frogobox.sdk.view.FrogoBindFragment +import com.frogobox.compose.view.FrogoComposeFragment /** * Created by Faisal Amir @@ -21,11 +20,6 @@ import com.frogobox.sdk.view.FrogoBindFragment * */ -abstract class BaseFragment : FrogoBindFragment(), IBaseFragment { - - - protected val mActivity: BaseActivity<*> by lazy { - (activity as BaseActivity<*>) - } +abstract class BaseFragment : FrogoComposeFragment(), IBaseFragment { } \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt index 3fa8903..96951a6 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/about/AboutUsActivity.kt @@ -4,21 +4,19 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable import io.github.amirisback.androidapp.common.base.BaseActivity -import io.github.amirisback.androidapp.databinding.ActivityAboutUsBinding import io.github.amirisback.androidapp.ui.features.about.AboutUsScreen import io.github.amirisback.init.ui.theme.InitTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class AboutUsActivity : BaseActivity() { +class AboutUsActivity : BaseActivity() { companion object { fun createIntent(context: Context): Intent { - return Intent(context, AboutUsActivity::class.java).apply { - - } + return Intent(context, AboutUsActivity::class.java) } fun launch(context: Context) { @@ -27,23 +25,20 @@ class AboutUsActivity : BaseActivity() { } - override fun setupViewBinding(): ActivityAboutUsBinding { - return ActivityAboutUsBinding.inflate(layoutInflater) - } - override fun setupViewModel() {} override fun onCreateExt(savedInstanceState: Bundle?) { super.onCreateExt(savedInstanceState) enableEdgeToEdge() setupToolbar() + } - binding.composeView.setContent { - InitTheme { - AboutUsScreen( - onBackClick = { finish() } - ) - } + @Composable + override fun SetupCompose() { + InitTheme { + AboutUsScreen( + onBackClick = { finish() } + ) } } diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/components/AppTopAppBar.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/components/AppTopAppBar.kt new file mode 100644 index 0000000..4370138 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/components/AppTopAppBar.kt @@ -0,0 +1,57 @@ +package io.github.amirisback.androidapp.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.github.amirisback.init.ui.theme.InitTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppTopAppBar( + title: String, + modifier: Modifier = Modifier, + onBackClick: (() -> Unit)? = null, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) +) { + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + if (onBackClick != null) { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + }, + colors = colors, + modifier = modifier + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +fun AppTopAppBarPreview() { + InitTheme { + AppTopAppBar( + title = "App Title", + onBackClick = {} + ) + } +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/components/CommonState.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/components/CommonState.kt new file mode 100644 index 0000000..4bc06b6 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/components/CommonState.kt @@ -0,0 +1,50 @@ +package io.github.amirisback.androidapp.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.github.amirisback.androidapp.R + +@Composable +fun LoadingIndicator( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ErrorMessage( + message: String, + modifier: Modifier = Modifier +) { + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + modifier = modifier.padding(16.dp) + ) +} + +@Composable +fun EmptyState( + message: String = stringResource(id = R.string.frogo_is_empty_data), + modifier: Modifier = Modifier +) { + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + modifier = modifier.padding(16.dp) + ) +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/components/MealCard.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/components/MealCard.kt new file mode 100644 index 0000000..2b87d03 --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/components/MealCard.kt @@ -0,0 +1,98 @@ +package io.github.amirisback.androidapp.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.init.ui.theme.InitTheme + +@Composable +fun MealCard( + meal: MealModel, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { onClick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(meal.strMealThumb) + .crossfade(true) + .build(), + contentDescription = meal.strMeal, + modifier = Modifier + .fillMaxWidth() + .height(128.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strMeal ?: "", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = meal.strArea ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strCategory ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MealCardPreview() { + InitTheme { + MealCard( + meal = MealModel( + idMeal = "1", + strMeal = "Creamy Chicken Pasta", + strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", + strCategory = "Pasta description here. This is a very delicious and easy meal to make.", + strArea = "Italian" + ), + onClick = {} + ) + } +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt index 0d0c441..9b992c3 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/detail/DetailActivity.kt @@ -3,21 +3,24 @@ package io.github.amirisback.androidapp.ui.detail import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels -import io.github.amirisback.androidapp.common.base.BaseActivity -import io.github.amirisback.androidapp.common.callback.Resource -import io.github.amirisback.androidapp.databinding.ActivityDetailBinding -import io.github.amirisback.androidapp.domain.model.MealModel +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.frogobox.sdk.ext.getExtraExt -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.setImageExt import com.frogobox.sdk.ext.showToast import com.frogobox.sdk.ext.toJson -import com.frogobox.sdk.ext.visible import dagger.hilt.android.AndroidEntryPoint +import io.github.amirisback.androidapp.common.base.BaseActivity +import io.github.amirisback.androidapp.common.callback.Resource +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.features.detail.DetailScreen +import io.github.amirisback.init.ui.theme.InitTheme @AndroidEntryPoint -class DetailActivity : BaseActivity() { +class DetailActivity : BaseActivity() { companion object { const val EXTRA_DATA = "EXTRA_DATA" @@ -31,33 +34,30 @@ class DetailActivity : BaseActivity() { fun launch(context: Context, data: MealModel) { context.startActivity(createIntent(context, data)) } - } private val viewModel: DetailViewModel by viewModels() - override fun setupViewBinding(): ActivityDetailBinding { - return ActivityDetailBinding.inflate(layoutInflater) - } + private var isLoadingState by mutableStateOf(false) + private var isFavoriteState by mutableStateOf(false) override fun setupViewModel() { viewModel.mealsState.observe(this) { when (it) { is Resource.Error -> { - binding.progressView.gone() + isLoadingState = false showToast(it.message.toString()) } is Resource.Loading -> { - binding.progressView.visible() + isLoadingState = true } is Resource.Success -> { - binding.progressView.gone() + isLoadingState = false it.data?.let { items -> - if (!items.isEmpty()) { - binding.btnInsert.gone() - binding.btnDelete.visible() + if (items.isNotEmpty()) { + isFavoriteState = true } } } @@ -67,18 +67,17 @@ class DetailActivity : BaseActivity() { viewModel.insertState.observe(this) { when (it) { is Resource.Error -> { - binding.progressView.gone() + isLoadingState = false showToast(it.message.toString()) } is Resource.Loading -> { - binding.progressView.visible() + isLoadingState = true } is Resource.Success -> { - binding.progressView.gone() - binding.btnInsert.gone() - binding.btnDelete.visible() + isLoadingState = false + isFavoriteState = true showToast("Berhasil Menambahkan Ke Favorite ${it.data?.strMeal}") } } @@ -87,16 +86,16 @@ class DetailActivity : BaseActivity() { viewModel.deleteState.observe(this) { when (it) { is Resource.Error -> { - binding.progressView.gone() + isLoadingState = false showToast(it.message.toString()) } is Resource.Loading -> { - binding.progressView.visible() + isLoadingState = true } is Resource.Success -> { - binding.progressView.gone() + isLoadingState = false finish() } } @@ -105,29 +104,29 @@ class DetailActivity : BaseActivity() { override fun onCreateExt(savedInstanceState: Bundle?) { super.onCreateExt(savedInstanceState) - setupDetailActivity("Detail Meals") + enableEdgeToEdge() + setupToolbar() val extra = getExtraExt(EXTRA_DATA) viewModel.mealModel = extra viewModel.getData() + } - extra?.let { - binding.apply { - ivUrl.setImageExt(it.strMealThumb) - tvSource.text = it.strArea - tvTitle.text = it.strMeal - tvContent.text = it.strCategory - - btnInsert.setOnClickListener { - viewModel.insertToDB() - } - - btnDelete.setOnClickListener { - viewModel.removeFromDb() - } - } + @Composable + override fun SetupCompose() { + InitTheme { + DetailScreen( + meal = viewModel.mealModel, + isLoading = isLoadingState, + isFavorite = isFavoriteState, + onBackClick = { finish() }, + onInsertClick = { viewModel.insertToDB() }, + onDeleteClick = { viewModel.removeFromDb() } + ) } - } + private fun setupToolbar() { + supportActionBar?.hide() + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt index 51b199d..d1325e8 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/about/AboutUsScreen.kt @@ -8,16 +8,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -29,9 +22,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.github.amirisback.androidapp.R +import io.github.amirisback.androidapp.ui.components.AppTopAppBar import io.github.amirisback.init.ui.theme.InitTheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AboutUsScreen( onBackClick: () -> Unit, @@ -39,21 +32,9 @@ fun AboutUsScreen( ) { Scaffold( topBar = { - TopAppBar( - title = { Text(text = stringResource(id = R.string.title_about_us)) }, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) + AppTopAppBar( + title = stringResource(id = R.string.title_about_us), + onBackClick = onBackClick ) }, modifier = modifier diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/detail/DetailScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/detail/DetailScreen.kt new file mode 100644 index 0000000..db5d30f --- /dev/null +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/detail/DetailScreen.kt @@ -0,0 +1,172 @@ +package io.github.amirisback.androidapp.ui.features.detail + +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.components.AppTopAppBar +import io.github.amirisback.androidapp.ui.components.LoadingIndicator +import io.github.amirisback.init.ui.theme.InitTheme + +@Composable +fun DetailScreen( + meal: MealModel?, + isLoading: Boolean, + isFavorite: Boolean, + onBackClick: () -> Unit, + onInsertClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + topBar = { + AppTopAppBar( + title = "Detail Meals", + onBackClick = onBackClick + ) + }, + modifier = modifier + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + if (meal != null) { + DetailContent( + meal = meal, + isFavorite = isFavorite, + onInsertClick = onInsertClick, + onDeleteClick = onDeleteClick, + modifier = Modifier.fillMaxSize() + ) + } + + if (isLoading) { + LoadingIndicator( + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +fun DetailContent( + meal: MealModel, + isFavorite: Boolean, + onInsertClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollState = rememberScrollState() + + Column( + modifier = modifier + .padding(16.dp) + ) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(meal.strMealThumb) + .crossfade(true) + .build(), + contentDescription = meal.strMeal, + modifier = Modifier + .fillMaxWidth() + .height(350.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strMeal ?: "", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strArea ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = meal.strCategory ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (isFavorite) { + OutlinedButton( + onClick = onDeleteClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text(text = "Remove From Database") + } + } else { + Button( + onClick = onInsertClick, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Insert to Database") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun DetailScreenPreview() { + InitTheme { + DetailScreen( + meal = MealModel( + idMeal = "1", + strMeal = "Creamy Chicken Pasta", + strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", + strCategory = "Delicious pasta description", + strArea = "Italian" + ), + isLoading = false, + isFavorite = false, + onBackClick = {}, + onInsertClick = {}, + onDeleteClick = {} + ) + } +} diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt index ca3bab3..02481a4 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/favorite/FavoriteScreen.kt @@ -2,12 +2,8 @@ package io.github.amirisback.androidapp.ui.features.favorite import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -17,14 +13,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.github.amirisback.androidapp.R import io.github.amirisback.androidapp.common.callback.Resource import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.components.EmptyState +import io.github.amirisback.androidapp.ui.components.ErrorMessage +import io.github.amirisback.androidapp.ui.components.LoadingIndicator +import io.github.amirisback.androidapp.ui.components.MealCard import io.github.amirisback.androidapp.ui.favorite.FavoriteViewModel -import io.github.amirisback.androidapp.ui.features.main.MealCard @Composable fun FavoriteScreen( @@ -45,23 +43,15 @@ fun FavoriteScreen( ) { when (val state = resourceState.value) { is Resource.Loading -> { - CircularProgressIndicator() + LoadingIndicator() } is Resource.Error -> { - Text( - text = state.message ?: stringResource(id = R.string.frogo_is_empty_data), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(16.dp) - ) + ErrorMessage(message = state.message ?: stringResource(id = R.string.frogo_is_empty_data)) } is Resource.Success -> { val meals = state.data ?: emptyList() if (meals.isEmpty()) { - Text( - text = stringResource(id = R.string.frogo_is_empty_data), - style = MaterialTheme.typography.bodyLarge - ) + EmptyState() } else { LazyColumn( modifier = Modifier.fillMaxSize() diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt index 1df6e9a..e759bd1 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/features/main/MainScreen.kt @@ -1,35 +1,19 @@ package io.github.amirisback.androidapp.ui.features.main -import androidx.compose.foundation.clickable 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.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import coil.request.ImageRequest import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.components.ErrorMessage +import io.github.amirisback.androidapp.ui.components.LoadingIndicator +import io.github.amirisback.androidapp.ui.components.MealCard import io.github.amirisback.init.ui.theme.InitTheme @Composable @@ -59,15 +43,10 @@ fun MainContent( ) { when (uiState) { is MainUiState.Loading -> { - CircularProgressIndicator() + LoadingIndicator() } is MainUiState.Error -> { - Text( - text = uiState.message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(16.dp) - ) + ErrorMessage(message = uiState.message) } is MainUiState.Success -> { LazyColumn( @@ -88,79 +67,6 @@ fun MainContent( } } -@Composable -fun MealCard( - meal: MealModel, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .clickable { onClick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(meal.strMealThumb) - .crossfade(true) - .build(), - contentDescription = meal.strMeal, - modifier = Modifier - .fillMaxWidth() - .height(128.dp), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = meal.strMeal ?: "", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = meal.strArea ?: "", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = meal.strCategory ?: "", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Preview(showBackground = true) -@Composable -fun MealCardPreview() { - InitTheme { - MealCard( - meal = MealModel( - idMeal = "1", - strMeal = "Creamy Chicken Pasta", - strMealThumb = "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", - strCategory = "Pasta description here. This is a very delicious and easy meal to make.", - strArea = "Italian" - ), - onClick = {} - ) - } -} - @Preview(showBackground = true) @Composable fun MainContentSuccessPreview() { diff --git a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt index f6fc243..ea8d32d 100644 --- a/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt +++ b/app/src/main/java/io/github/amirisback/androidapp/ui/main/MainActivity.kt @@ -7,15 +7,11 @@ import androidx.activity.viewModels import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,20 +20,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import dagger.hilt.android.AndroidEntryPoint import io.github.amirisback.androidapp.R import io.github.amirisback.androidapp.common.base.BaseActivity -import io.github.amirisback.androidapp.databinding.ActivityMainBinding import io.github.amirisback.androidapp.domain.model.MealModel +import io.github.amirisback.androidapp.ui.components.AppTopAppBar import io.github.amirisback.androidapp.ui.detail.DetailActivity import io.github.amirisback.androidapp.ui.favorite.FavoriteViewModel import io.github.amirisback.androidapp.ui.features.favorite.FavoriteScreen import io.github.amirisback.androidapp.ui.features.main.MainScreen import io.github.amirisback.androidapp.ui.features.main.MainViewModel import io.github.amirisback.init.ui.theme.InitTheme -import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainActivity : BaseActivity() { +class MainActivity : BaseActivity() { private val favoriteViewModel: FavoriteViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels() @@ -47,10 +43,6 @@ class MainActivity : BaseActivity() { FAVORITE(R.string.title_fav, R.drawable.ic_favorite) } - override fun setupViewBinding(): ActivityMainBinding { - return ActivityMainBinding.inflate(layoutInflater) - } - override fun setupActivityResultExt(result: ActivityResult) { super.setupActivityResultExt(result) favoriteViewModel.getData() @@ -62,18 +54,19 @@ class MainActivity : BaseActivity() { super.onCreateExt(savedInstanceState) enableEdgeToEdge() setupToolbar() - mainViewModel.searchMeal("Cream") // Trigger search for meals inside MainViewModel + mainViewModel.searchMeal("Cream") + } - binding.composeView.setContent { - InitTheme { - MainActivityScreen( - mainViewModel = mainViewModel, - favoriteViewModel = favoriteViewModel, - onItemClick = { meal -> - startActivityResultExt(DetailActivity.createIntent(this, meal)) - } - ) - } + @Composable + override fun SetupCompose() { + InitTheme { + MainActivityScreen( + mainViewModel = mainViewModel, + favoriteViewModel = favoriteViewModel, + onItemClick = { meal -> + startActivityResultExt(DetailActivity.createIntent(this, meal)) + } + ) } } @@ -82,7 +75,6 @@ class MainActivity : BaseActivity() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainActivityScreen( mainViewModel: MainViewModel, @@ -93,12 +85,8 @@ fun MainActivityScreen( Scaffold( topBar = { - TopAppBar( - title = { Text(text = stringResource(id = currentTab.titleRes)) }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) + AppTopAppBar( + title = stringResource(id = currentTab.titleRes) ) }, bottomBar = { diff --git a/app/src/main/res/layout/activity_about_us.xml b/app/src/main/res/layout/activity_about_us.xml deleted file mode 100644 index e45f0f6..0000000 --- a/app/src/main/res/layout/activity_about_us.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml deleted file mode 100644 index c4f31da..0000000 --- a/app/src/main/res/layout/activity_detail.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - -