├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── kotlinc.xml ├── migrations.xml ├── .gitignore └── deploymentTargetSelector.xml ├── demo ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ └── themes.xml │ │ ├── mipmap-anydpi │ │ │ └── ic_launcher.xml │ │ └── drawable │ │ │ ├── ic_launcher_monochrome.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_background.xml │ │ ├── kotlin │ │ └── dev │ │ │ └── hrach │ │ │ └── navigation │ │ │ └── demo │ │ │ ├── screens │ │ │ ├── List.kt │ │ │ ├── Profile.kt │ │ │ ├── Modal2.kt │ │ │ ├── BottomSheet.kt │ │ │ ├── Home.kt │ │ │ └── Modal1.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Destinations.kt │ │ │ ├── App.kt │ │ │ └── NavHost.kt │ │ └── AndroidManifest.xml └── build.gradle.kts ├── bottomsheet ├── .gitignore ├── gradle.properties ├── build.gradle.kts ├── src │ └── main │ │ └── kotlin │ │ └── dev │ │ └── hrach │ │ └── navigation │ │ └── bottomsheet │ │ ├── BottomSheetNavigator.kt │ │ ├── NavGraphBuilder.kt │ │ ├── BottomSheetNavigatorDestinationBuilder.kt │ │ └── BottomSheetHost.kt └── api │ └── bottomsheet.api ├── modalsheet ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── id.xml │ │ │ └── styles.xml │ │ └── values-v30 │ │ │ └── styles.xml │ │ └── kotlin │ │ └── dev │ │ └── hrach │ │ └── navigation │ │ └── modalsheet │ │ ├── ModalSheetNavigator.kt │ │ ├── NavGraphBuilder.kt │ │ ├── ModalSheetNavigatorDestinationBuilder.kt │ │ ├── ModalSheetHost.kt │ │ └── ModalSheetDialog.kt ├── gradle.properties ├── build.gradle.kts └── api │ └── modalsheet.api ├── results ├── .gitignore ├── gradle.properties ├── api │ └── results.api ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── dev │ └── hrach │ └── navigation │ └── results │ └── Results.kt ├── .github ├── funding.yml ├── release-drafter.yml ├── workflows │ ├── release.yml │ ├── release-drafter.yml │ └── build.yml └── renovate.json5 ├── release ├── .gitignore ├── signing-cleanup.sh ├── secring.gpg.aes ├── signing.properties.aes ├── signing-pack.sh └── signing-unpack.sh ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── gradle.properties ├── settings.gradle.kts ├── .editorconfig ├── license.md ├── readme.md ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | NavigationCompose -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /bottomsheet/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /modalsheet/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /results/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: hrach 2 | -------------------------------------------------------------------------------- /release/.gitignore: -------------------------------------------------------------------------------- 1 | /secring.gpg 2 | /signing.properties 3 | -------------------------------------------------------------------------------- /release/signing-cleanup.sh: -------------------------------------------------------------------------------- 1 | rm -f release/*.gpg 2 | rm -f release/*.properties 3 | -------------------------------------------------------------------------------- /release/secring.gpg.aes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrach/navigation-compose-ext/HEAD/release/secring.gpg.aes -------------------------------------------------------------------------------- /release/signing.properties.aes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrach/navigation-compose-ext/HEAD/release/signing.properties.aes -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrach/navigation-compose-ext/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /modalsheet/src/main/res/values/id.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | .cxx 9 | local.properties 10 | .kotlin 11 | .idea 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /modalsheet/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=modalsheet 2 | POM_NAME=Navigation Modal Sheet 3 | POM_DESCRIPTION=Modal sheet implementation for Jetpack Navigation Compose 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /bottomsheet/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=bottomsheet 2 | POM_NAME=Navigation Bottom Sheet 3 | POM_DESCRIPTION=Bottom sheet implementation for Jetpack Navigation Compose 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /results/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=results 2 | POM_NAME=Navigation Results sharing 3 | POM_DESCRIPTION=Results sharing between screens API for Jetpack Navigation Compose 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 16 | 17 | 1 18 | 19 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | env: 12 | TERM: dumb 13 | steps: 14 | - uses: actions/checkout@v6 15 | - name: set up JDK 18 16 | uses: actions/setup-java@v5 17 | with: 18 | distribution: 'zulu' 19 | java-version: 18 20 | - name: Decrypt secrets 21 | run: | 22 | echo ${{ secrets.ENCRYPT_KEY }} | release/signing-unpack.sh 23 | - name: Deploy to Sonatype 24 | run: | 25 | ./gradlew apiCheck 26 | ./gradlew :publishAggregatedPublicationToCentralPortal --no-configuration-cache 27 | - name: Build APK 28 | run: | 29 | ./gradlew :demo:assembleRelease 30 | mv demo/build/outputs/apk/release/demo-release-unsigned.apk demo/build/outputs/apk/release/demo-release.apk 31 | - name: Upload APK to release 32 | uses: softprops/action-gh-release@v2 33 | id: release 34 | with: 35 | files: | 36 | demo/build/outputs/apk/release/demo-release.apk 37 | - name: Clean secrets 38 | if: always() 39 | run: release/signing-cleanup.sh 40 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "schedule:weekly", 6 | ], 7 | "labels": [ 8 | "chore", 9 | "skip-changelog", 10 | ], 11 | "rebaseWhen": "conflicted", 12 | // deps that shouldn't be forced on users to be the latest ones 13 | "ignoreDeps": [ 14 | "org.jetbrains.kotlinx:kotlinx-serialization-core", 15 | "org.jetbrains.kotlinx:kotlinx-serialization-json", 16 | "androidx.appcompat:appcompat", 17 | "androidx.compose.foundation:foundation", 18 | "androidx.compose.material3:material3", 19 | "androidx.lifecycle:lifecycle-runtime", 20 | ], 21 | "packageRules": [ 22 | { 23 | "groupName": "GitHub Actions", 24 | "matchPaths": [ 25 | ".github/**", 26 | ], 27 | }, 28 | { 29 | "groupName": "Compose & Accompanist", 30 | "matchPackageNames": [ 31 | "androidx.compose", 32 | "com.google.accompanist", 33 | ], 34 | }, 35 | { 36 | "groupName": "Kotlin & Dokka & Compose Compiler", 37 | "matchPackagePrefixes": [ 38 | "org.jetbrains.kotlin", 39 | "org.jetbrains.dokka", 40 | "androidx.compose.compiler", 41 | ], 42 | }, 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal2.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.WindowInsets 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.systemBars 10 | import androidx.compose.foundation.layout.windowInsetsPadding 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import dev.hrach.navigation.demo.Destinations 19 | 20 | @Composable 21 | internal fun Modal2(navController: NavController) { 22 | Column( 23 | Modifier 24 | .fillMaxSize() 25 | .background(MaterialTheme.colorScheme.surface) 26 | .windowInsetsPadding(WindowInsets.systemBars), 27 | ) { 28 | Text("Modal 2") 29 | 30 | Spacer(Modifier.height(32.dp)) 31 | OutlinedButton( 32 | onClick = { 33 | navController.popBackStack(inclusive = true) 34 | }, 35 | ) { 36 | Text("Close modals") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/screens/BottomSheet.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo.screens 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.OutlinedButton 7 | import androidx.compose.material3.OutlinedTextField 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.saveable.rememberSaveable 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import androidx.navigation.NavController 17 | import dev.hrach.navigation.demo.Destinations 18 | import dev.hrach.navigation.results.setResult 19 | import kotlin.random.Random 20 | 21 | @Composable 22 | internal fun BottomSheet(navController: NavController) { 23 | Column(Modifier.padding(horizontal = 16.dp)) { 24 | Text("This is a bottomsheet") 25 | var value by rememberSaveable { mutableStateOf("") } 26 | OutlinedTextField(value = value, onValueChange = { value = it }) 27 | OutlinedButton( 28 | onClick = { 29 | navController.setResult(Destinations.BottomSheet.Result(Random.nextInt())) 30 | navController.popBackStack() 31 | }, 32 | modifier = Modifier.fillMaxWidth(), 33 | ) { 34 | Text("Close") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bottomsheet/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 8 | id("com.vanniktech.maven.publish") 9 | id("com.gradleup.nmcp") 10 | id("org.jmailen.kotlinter") 11 | } 12 | 13 | version = property("VERSION_NAME") as String 14 | 15 | android { 16 | namespace = "dev.hrach.navigation.bottomsheet" 17 | 18 | compileSdk = libs.versions.compileSdk.get().toInt() 19 | 20 | defaultConfig { 21 | minSdk = libs.versions.minSdk.get().toInt() 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildFeatures { 26 | compose = true 27 | buildConfig = false 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | freeCompilerArgs = freeCompilerArgs.toMutableList().apply { 37 | add("-Xexplicit-api=strict") 38 | }.toList() 39 | } 40 | 41 | lint { 42 | disable.add("GradleDependency") 43 | abortOnError = true 44 | warningsAsErrors = true 45 | } 46 | } 47 | 48 | nmcp { 49 | publishAllPublications {} 50 | } 51 | 52 | kotlinter { 53 | reporters = arrayOf("json") 54 | } 55 | 56 | dependencies { 57 | implementation(libs.navigation.compose) 58 | implementation(libs.compose.material3) 59 | 60 | testImplementation(libs.junit) 61 | } 62 | -------------------------------------------------------------------------------- /modalsheet/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 8 | id("com.vanniktech.maven.publish") 9 | id("com.gradleup.nmcp") 10 | id("org.jmailen.kotlinter") 11 | } 12 | 13 | version = property("VERSION_NAME") as String 14 | 15 | android { 16 | namespace = "dev.hrach.navigation.modalsheet" 17 | 18 | compileSdk = libs.versions.compileSdk.get().toInt() 19 | 20 | defaultConfig { 21 | minSdk = libs.versions.minSdk.get().toInt() 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildFeatures { 26 | compose = true 27 | buildConfig = false 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | freeCompilerArgs = freeCompilerArgs.toMutableList().apply { 37 | add("-Xexplicit-api=strict") 38 | }.toList() 39 | } 40 | 41 | lint { 42 | disable.add("GradleDependency") 43 | abortOnError = true 44 | warningsAsErrors = true 45 | } 46 | } 47 | 48 | nmcp { 49 | publishAllPublications {} 50 | } 51 | 52 | kotlinter { 53 | reporters = arrayOf("json") 54 | } 55 | 56 | dependencies { 57 | implementation(libs.appcompat) 58 | implementation(libs.compose.foundation) 59 | implementation(libs.navigation.compose) 60 | 61 | testImplementation(libs.junit) 62 | } 63 | -------------------------------------------------------------------------------- /results/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 8 | id("com.vanniktech.maven.publish") 9 | id("com.gradleup.nmcp") 10 | id("org.jmailen.kotlinter") 11 | } 12 | 13 | version = property("VERSION_NAME") as String 14 | 15 | android { 16 | namespace = "dev.hrach.navigation.results" 17 | 18 | compileSdk = libs.versions.compileSdk.get().toInt() 19 | 20 | defaultConfig { 21 | minSdk = libs.versions.minSdk.get().toInt() 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildFeatures { 26 | compose = true 27 | buildConfig = false 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | freeCompilerArgs = freeCompilerArgs.toMutableList().apply { 37 | add("-Xexplicit-api=strict") 38 | }.toList() 39 | } 40 | 41 | lint { 42 | disable.add("GradleDependency") 43 | abortOnError = true 44 | warningsAsErrors = true 45 | } 46 | } 47 | 48 | nmcp { 49 | publishAllPublications {} 50 | } 51 | 52 | kotlinter { 53 | reporters = arrayOf("json") 54 | } 55 | 56 | dependencies { 57 | implementation(libs.navigation.compose) 58 | implementation(libs.kotlin.serialization.json) 59 | implementation(libs.androidx.lifecycle.runtime) 60 | testImplementation(libs.junit) 61 | } 62 | -------------------------------------------------------------------------------- /demo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("org.jetbrains.kotlin.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | id("org.jetbrains.kotlin.plugin.serialization") 8 | id("org.jmailen.kotlinter") 9 | } 10 | 11 | android { 12 | namespace = "dev.hrach.navigation.demo" 13 | 14 | compileSdk = libs.versions.compileSdk.get().toInt() 15 | 16 | defaultConfig { 17 | applicationId = "dev.hrach.navigation.demo" 18 | minSdk = 26 19 | targetSdk = 35 20 | versionName = "1.0.0" 21 | versionCode = 1 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildFeatures { 26 | compose = true 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | } 33 | 34 | buildFeatures { 35 | compose = true 36 | buildConfig = false 37 | aidl = false 38 | renderScript = false 39 | resValues = false 40 | shaders = false 41 | } 42 | 43 | kotlinOptions { 44 | freeCompilerArgs = freeCompilerArgs.toMutableList().apply { 45 | add("-Xexplicit-api=strict") 46 | }.toList() 47 | } 48 | 49 | lint { 50 | disable.add("GradleDependency") 51 | disable.add("OldTargetApi") 52 | abortOnError = true 53 | warningsAsErrors = true 54 | } 55 | } 56 | 57 | kotlinter { 58 | reporters = arrayOf("json") 59 | } 60 | 61 | dependencies { 62 | implementation(projects.bottomsheet) 63 | implementation(projects.modalsheet) 64 | implementation(projects.results) 65 | 66 | implementation(libs.kotlin.serialization.core) 67 | implementation(libs.compose.material3) 68 | implementation(libs.navigation.compose) 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | # write permission is required to create a github release 13 | contents: write 14 | steps: 15 | - uses: release-drafter/release-drafter@v6 16 | id: create_release 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | - uses: actions/checkout@v6 20 | with: 21 | persist-credentials: false 22 | - run: | 23 | TAG_NAME="${{ steps.create_release.outputs.tag_name }}" 24 | VERSION_NAME="${TAG_NAME:1}" 25 | sed -i "s/VERSION_NAME=[0-9]\+.[0-9]\+.[0-9]\+/VERSION_NAME=$VERSION_NAME/g" gradle.properties 26 | if git diff --exit-code; then 27 | echo "::set-output name=changes_exist::false" 28 | else 29 | echo "::set-output name=changes_exist::true" 30 | echo "::set-output name=VERSION_NAME::$VERSION_NAME" 31 | fi 32 | id: version_job 33 | - name: Commit and push files 34 | if: ${{ steps.version_job.outputs.changes_exist == 'true' }} 35 | env: 36 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 37 | run: | 38 | ssh-agent -a $SSH_AUTH_SOCK > /dev/null 39 | ssh-add - <<< "${{ secrets.GH_ACTION_DEPLOY_PRIVATE_KEY }}" 40 | git config --local user.email "action@github.com" 41 | git config --local user.name "GitHub Action" 42 | git commit -m "Bump version to ${{ steps.version_job.outputs.VERSION_NAME }}" -a 43 | git push git@github.com:$GITHUB_REPOSITORY.git 44 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Home.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo.screens 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.OutlinedButton 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableIntStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.runtime.setValue 13 | import androidx.navigation.NavController 14 | import dev.hrach.navigation.demo.Destinations 15 | import dev.hrach.navigation.results.NavigationResultEffect 16 | 17 | @SuppressLint("UnrememberedGetBackStackEntry") 18 | @Composable 19 | internal fun Home( 20 | navController: NavController, 21 | ) { 22 | var bottomSheetResult by rememberSaveable { mutableIntStateOf(-1) } 23 | NavigationResultEffect( 24 | backStackEntry = remember(navController) { navController.getBackStackEntry() }, 25 | navController = navController, 26 | ) { result -> 27 | bottomSheetResult = result.id 28 | } 29 | Home( 30 | navigate = navController::navigate, 31 | bottomSheetResult = bottomSheetResult, 32 | ) 33 | } 34 | 35 | @Composable 36 | private fun Home( 37 | navigate: (Any) -> Unit, 38 | bottomSheetResult: Int, 39 | ) { 40 | Column { 41 | Text("Home") 42 | 43 | OutlinedButton(onClick = { navigate(Destinations.Modal1) }) { 44 | Text("Modal 1") 45 | } 46 | 47 | OutlinedButton(onClick = { navigate(Destinations.BottomSheet) }) { 48 | Text("BottomSheet") 49 | } 50 | 51 | Text("BottomSheetResult: $bottomSheetResult") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v6 15 | - name: set up JDK 18 16 | uses: actions/setup-java@v5 17 | with: 18 | distribution: 'zulu' 19 | java-version: 18 20 | - name: Assemble & Linters 21 | run: | 22 | ./gradlew :bottomsheet:testDebugUnitTest :modalsheet:testDebugUnitTest 23 | ./gradlew lintDebug lintKotlin :demo:assembleDebug apiCheck 24 | - uses: yutailang0119/action-android-lint@v5.0.0 25 | name: App Lint errors to annotations 26 | if: ${{ failure() }} 27 | continue-on-error: true # lint may be ok 28 | with: 29 | xml_path: app/build/reports/lint-results-debug.xml 30 | - uses: yutailang0119/action-android-lint@v5.0.0 31 | name: Lib Lint errors to annotations 32 | if: ${{ failure() }} 33 | continue-on-error: true # lint may be ok 34 | with: 35 | xml_path: lib/build/reports/lint-results-debug.xml 36 | - name: KTLint errors to annotations 37 | if: ${{ failure() }} 38 | run: | 39 | jq --raw-output '[.[] | ({ f: .file } + ( .errors[] | { l: .line, c: .column, m: .message, r: .rule } )) | "::error file=core/\(.f),line=\(.l),col=\(.c)::\(.m) [\(.r)]" ] | join("\n")' core/build/reports/ktlint/main-lint.json || true 40 | jq --raw-output '[.[] | ({ f: .file } + ( .errors[] | { l: .line, c: .column, m: .message, r: .rule } )) | "::error file=demo/\(.f),line=\(.l),col=\(.c)::\(.m) [\(.r)]" ] | join("\n")' demo/build/reports/ktlint/main-lint.json || true 41 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /bottomsheet/src/main/kotlin/dev/hrach/navigation/bottomsheet/BottomSheetNavigator.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.bottomsheet 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.window.SecureFlagPolicy 5 | import androidx.navigation.FloatingWindow 6 | import androidx.navigation.NavBackStackEntry 7 | import androidx.navigation.NavDestination 8 | import androidx.navigation.NavOptions 9 | import androidx.navigation.Navigator 10 | 11 | @Navigator.Name("M3BottomSheetNavigator") 12 | public class BottomSheetNavigator : Navigator() { 13 | 14 | /** 15 | * Get the back stack from the [state]. 16 | */ 17 | internal val backStack get() = state.backStack 18 | 19 | /** 20 | * Dismiss the sheet destination associated with the given [backStackEntry]. 21 | */ 22 | internal fun dismiss(backStackEntry: NavBackStackEntry) { 23 | state.popWithTransition(backStackEntry, false) 24 | } 25 | 26 | override fun navigate( 27 | entries: List, 28 | navOptions: NavOptions?, 29 | navigatorExtras: Extras?, 30 | ) { 31 | entries.forEach { entry -> 32 | state.pushWithTransition(entry) 33 | } 34 | } 35 | 36 | override fun createDestination(): Destination { 37 | return Destination(this) { } 38 | } 39 | 40 | override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { 41 | state.popWithTransition(popUpTo, savedState) 42 | } 43 | 44 | internal fun onTransitionComplete(entry: NavBackStackEntry) { 45 | state.markTransitionComplete(entry) 46 | } 47 | 48 | /** 49 | * NavDestination specific to [BottomSheetNavigator] 50 | */ 51 | @NavDestination.ClassType(Composable::class) 52 | public class Destination( 53 | navigator: BottomSheetNavigator, 54 | internal val content: @Composable (NavBackStackEntry) -> Unit, 55 | ) : NavDestination(navigator), FloatingWindow { 56 | internal var securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit 57 | internal var skipPartiallyExpanded: Boolean = true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bottomsheet/src/main/kotlin/dev/hrach/navigation/bottomsheet/NavGraphBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.bottomsheet 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.window.SecureFlagPolicy 5 | import androidx.navigation.NamedNavArgument 6 | import androidx.navigation.NavBackStackEntry 7 | import androidx.navigation.NavDeepLink 8 | import androidx.navigation.NavGraphBuilder 9 | import androidx.navigation.NavType 10 | import androidx.navigation.get 11 | import kotlin.reflect.KType 12 | 13 | public inline fun NavGraphBuilder.bottomSheet( 14 | typeMap: Map> = emptyMap(), 15 | deepLinks: List = emptyList(), 16 | securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, 17 | skipPartiallyExpanded: Boolean = true, 18 | noinline content: @Composable (NavBackStackEntry) -> Unit, 19 | ) { 20 | destination( 21 | BottomSheetNavigatorDestinationBuilder( 22 | navigator = provider[BottomSheetNavigator::class], 23 | route = T::class, 24 | typeMap = typeMap, 25 | content = content, 26 | ).apply { 27 | deepLinks.forEach { deepLink -> 28 | deepLink(deepLink) 29 | } 30 | this.securePolicy = securePolicy 31 | this.skipPartiallyExpanded = skipPartiallyExpanded 32 | }, 33 | ) 34 | } 35 | 36 | public fun NavGraphBuilder.bottomSheet( 37 | route: String, 38 | arguments: List = emptyList(), 39 | deepLinks: List = emptyList(), 40 | securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, 41 | skipPartiallyExpanded: Boolean = true, 42 | content: @Composable (backstackEntry: NavBackStackEntry) -> Unit, 43 | ) { 44 | destination( 45 | BottomSheetNavigatorDestinationBuilder( 46 | navigator = provider[BottomSheetNavigator::class], 47 | route = route, 48 | content = content, 49 | ).apply { 50 | arguments.forEach { (argumentName, argument) -> 51 | argument(argumentName, argument) 52 | } 53 | deepLinks.forEach { deepLink -> 54 | deepLink(deepLink) 55 | } 56 | this.securePolicy = securePolicy 57 | this.skipPartiallyExpanded = skipPartiallyExpanded 58 | }, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Extensions for Navigation Compose 2 | ================================= 3 | 4 | [![CI Build](https://img.shields.io/github/actions/workflow/status/hrach/navigation-compose-ext/build.yml?branch=main)](https://github.com/hrach/navigation-compose-ext/actions/workflows/build.yml) 5 | [![GitHub release](https://img.shields.io/github/v/release/hrach/navigation-compose-ext)](https://github.com/hrach/navigation-compose-ext/releases) 6 | 7 | See `demo` module. 8 | 9 | Use Maven Central and these dependencies: 10 | 11 | ```kotlin 12 | dependencies { 13 | implementation("dev.hrach.navigation:bottomsheet:") 14 | implementation("dev.hrach.navigation:modalsheet:") 15 | implementation("dev.hrach.navigation:results:") 16 | } 17 | ``` 18 | 19 | Components: 20 | 21 | - **BottomSheet** - Connects the official Material 3 BottomSheet with Jetpack Navigation. 22 | - **ModalSheet** - A custom destination type for Jetpack Navigation that brings fullscreen content with modal animation. 23 | - **Results** - Passing a result simply between destinations. 24 | 25 | Quick setup: 26 | 27 | ```kotlin 28 | val modalSheetNavigator = remember { ModalSheetNavigator() } 29 | val bottomSheetNavigator = remember { BottomSheetNavigator() } 30 | val navController = rememberNavController(modalSheetNavigator, bottomSheetNavigator) 31 | 32 | NavHost( 33 | navController = navController, 34 | startDestination = Destinations.Home, 35 | ) { 36 | composable { Home(navController) } 37 | modalSheet { Modal(navController) } 38 | bottomSheet { BottomSheet(navController) } 39 | } 40 | ModalSheetHost(modalSheetNavigator, containerColor = MaterialTheme.colorScheme.background) 41 | BottomSheetHost(bottomSheetNavigator) 42 | ``` 43 | 44 | Results sharing: 45 | 46 | ```kotlin 47 | object Destinations { 48 | @Serializable 49 | data object BottomSheet { 50 | @Serializable 51 | data class Result( 52 | val id: Int, 53 | ) 54 | } 55 | } 56 | 57 | @Composable 58 | fun Home(navController: NavController) { 59 | NavigationResultEffect( 60 | backStackEntry = remember(navController) { navController.getBackStackEntry() }, 61 | navController = navController, 62 | ) { result -> 63 | // process result - 64 | } 65 | } 66 | 67 | @Composable 68 | fun BottomSheet(navController: NavController) { 69 | OutlineButton(onClick = { navController.setResult(Destinations.BottomSheet.Result(42)) }) { 70 | Text("Close") 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /bottomsheet/src/main/kotlin/dev/hrach/navigation/bottomsheet/BottomSheetNavigatorDestinationBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.bottomsheet 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.window.SecureFlagPolicy 5 | import androidx.navigation.NavBackStackEntry 6 | import androidx.navigation.NavDestinationBuilder 7 | import androidx.navigation.NavDestinationDsl 8 | import androidx.navigation.NavType 9 | import kotlin.reflect.KClass 10 | import kotlin.reflect.KType 11 | 12 | /** 13 | * DSL for constructing a new [BottomSheetNavigator.Destination] 14 | */ 15 | @Suppress("UnnecessaryOptInAnnotation") 16 | @NavDestinationDsl 17 | public class BottomSheetNavigatorDestinationBuilder : 18 | NavDestinationBuilder { 19 | 20 | private val composeNavigator: BottomSheetNavigator 21 | private val content: @Composable (NavBackStackEntry) -> Unit 22 | 23 | public var securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit 24 | public var skipPartiallyExpanded: Boolean = true 25 | 26 | /** 27 | * DSL for constructing a new [BottomSheetNavigator.Destination] 28 | * 29 | * @param navigator navigator used to create the destination 30 | * @param route the destination's unique route 31 | * @param content composable for the destination 32 | */ 33 | public constructor( 34 | navigator: BottomSheetNavigator, 35 | route: String, 36 | content: @Composable (NavBackStackEntry) -> Unit, 37 | ) : super(navigator, route) { 38 | this.composeNavigator = navigator 39 | this.content = content 40 | } 41 | 42 | /** 43 | * DSL for constructing a new [BottomSheetNavigator.Destination] 44 | * 45 | * @param navigator navigator used to create the destination 46 | * @param route the destination's unique route from a [KClass] 47 | * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom 48 | * [NavType]. May be empty if [route] does not use custom NavTypes. 49 | * @param content composable for the destination 50 | */ 51 | public constructor( 52 | navigator: BottomSheetNavigator, 53 | route: KClass<*>, 54 | typeMap: Map>, 55 | content: @Composable (NavBackStackEntry) -> Unit, 56 | ) : super(navigator, route, typeMap) { 57 | this.composeNavigator = navigator 58 | this.content = content 59 | } 60 | 61 | override fun instantiateDestination(): BottomSheetNavigator.Destination { 62 | return BottomSheetNavigator.Destination(composeNavigator, content) 63 | } 64 | 65 | override fun build(): BottomSheetNavigator.Destination { 66 | return super.build().also { destination -> 67 | destination.securePolicy = securePolicy 68 | destination.skipPartiallyExpanded = skipPartiallyExpanded 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.modalsheet 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.animation.AnimatedContentTransitionScope 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.SizeTransform 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.ui.window.SecureFlagPolicy 11 | import androidx.navigation.FloatingWindow 12 | import androidx.navigation.NavBackStackEntry 13 | import androidx.navigation.NavDestination 14 | import androidx.navigation.NavOptions 15 | import androidx.navigation.Navigator 16 | import dev.hrach.navigation.modalsheet.ModalSheetNavigator.Destination 17 | 18 | @Navigator.Name("ModalSheetNavigator") 19 | public class ModalSheetNavigator : Navigator() { 20 | internal val backStack get() = state.backStack 21 | internal val transitionsInProgress get() = state.transitionsInProgress 22 | 23 | internal val isPop = mutableStateOf(false) 24 | 25 | override fun navigate( 26 | entries: List, 27 | navOptions: NavOptions?, 28 | navigatorExtras: Extras?, 29 | ) { 30 | entries.forEach { entry -> 31 | state.pushWithTransition(entry) 32 | } 33 | isPop.value = false 34 | } 35 | 36 | override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { 37 | state.popWithTransition(popUpTo, savedState) 38 | isPop.value = true 39 | } 40 | 41 | public fun prepareForTransition(entry: NavBackStackEntry) { 42 | state.prepareForTransition(entry) 43 | } 44 | 45 | internal fun onTransitionComplete(entry: NavBackStackEntry) { 46 | state.markTransitionComplete(entry) 47 | } 48 | 49 | override fun createDestination(): Destination = 50 | Destination(this) {} 51 | 52 | @NavDestination.ClassType(Composable::class) 53 | public class Destination( 54 | navigator: ModalSheetNavigator, 55 | internal val content: @Composable AnimatedContentScope.(@JvmSuppressWildcards NavBackStackEntry) -> Unit, 56 | ) : NavDestination(navigator), FloatingWindow { 57 | internal var securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit 58 | 59 | internal var enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = 60 | null 61 | 62 | internal var exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = 63 | null 64 | 65 | internal var popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = 66 | null 67 | 68 | internal var popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = 69 | null 70 | 71 | internal var sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = 72 | null 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo.screens 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.activity.compose.BackHandler 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.WindowInsets 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.systemBars 12 | import androidx.compose.foundation.layout.windowInsetsPadding 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.OutlinedButton 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Switch 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableIntStateOf 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.unit.dp 28 | import androidx.navigation.NavController 29 | import dev.hrach.navigation.demo.Destinations 30 | import dev.hrach.navigation.results.NavigationResultEffect 31 | 32 | @SuppressLint("UnrememberedGetBackStackEntry") 33 | @Composable 34 | internal fun Modal1(navController: NavController) { 35 | var bottomSheetResult by rememberSaveable { mutableIntStateOf(-1) } 36 | NavigationResultEffect( 37 | backStackEntry = remember(navController) { navController.getBackStackEntry() }, 38 | navController = navController, 39 | ) { result -> 40 | bottomSheetResult = result.id 41 | } 42 | Modal1( 43 | navigate = navController::navigate, 44 | close = navController::popBackStack, 45 | bottomSheetResult = bottomSheetResult, 46 | ) 47 | } 48 | 49 | @Composable 50 | private fun Modal1( 51 | navigate: (Any) -> Unit, 52 | close: () -> Unit, 53 | bottomSheetResult: Int, 54 | ) { 55 | var disableBackHandling by rememberSaveable { mutableStateOf(false) } 56 | BackHandler(disableBackHandling) { 57 | // no-op 58 | } 59 | 60 | Surface( 61 | color = MaterialTheme.colorScheme.inverseSurface, 62 | ) { 63 | Column( 64 | modifier = Modifier 65 | .fillMaxSize() 66 | .windowInsetsPadding(WindowInsets.systemBars), 67 | ) { 68 | Text("Modal 1") 69 | OutlinedButton(onClick = { navigate(Destinations.Modal2) }) { 70 | Text("Modal 2") 71 | } 72 | OutlinedButton(onClick = { navigate(Destinations.BottomSheet) }) { 73 | Text("BottomSheet") 74 | } 75 | Text("BottomSheetResult: $bottomSheetResult") 76 | 77 | Spacer(Modifier.height(32.dp)) 78 | 79 | Row(verticalAlignment = Alignment.CenterVertically) { 80 | Text("Disable back handling") 81 | Switch(disableBackHandling, onCheckedChange = { disableBackHandling = it }) 82 | } 83 | 84 | Spacer(Modifier.height(32.dp)) 85 | OutlinedButton(onClick = close) { 86 | Text("Close") 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/App.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.automirrored.filled.List 7 | import androidx.compose.material.icons.filled.Home 8 | import androidx.compose.material.icons.filled.Person 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.NavigationBar 12 | import androidx.compose.material3.NavigationBarItem 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.vector.ImageVector 20 | import androidx.navigation.NavGraph.Companion.findStartDestination 21 | import androidx.navigation.NavHostController 22 | import androidx.navigation.compose.currentBackStackEntryAsState 23 | import androidx.navigation.compose.rememberNavController 24 | import dev.hrach.navigation.bottomsheet.BottomSheetNavigator 25 | import dev.hrach.navigation.modalsheet.ModalSheetNavigator 26 | 27 | @Composable 28 | public fun App() { 29 | MaterialTheme { 30 | val modalSheetNavigator = remember { ModalSheetNavigator() } 31 | val bottomSheetNavigator = remember { BottomSheetNavigator() } 32 | val navController = rememberNavController(modalSheetNavigator, bottomSheetNavigator) 33 | Scaffold( 34 | bottomBar = { 35 | BottomBar(navController) 36 | }, 37 | ) { 38 | Box(Modifier.padding(it)) { 39 | NavHost(navController, modalSheetNavigator, bottomSheetNavigator) 40 | } 41 | } 42 | } 43 | } 44 | 45 | private data class Item( 46 | val id: Int, 47 | val title: String, 48 | val icon: ImageVector, 49 | val destination: Any, 50 | ) 51 | 52 | private val Items = listOf( 53 | Item( 54 | 0, 55 | "Home", 56 | Icons.Default.Home, 57 | Destinations.Home, 58 | ), 59 | Item( 60 | 1, 61 | "List", 62 | Icons.AutoMirrored.Default.List, 63 | Destinations.List, 64 | ), 65 | Item( 66 | 2, 67 | "Profile", 68 | Icons.Default.Person, 69 | Destinations.Profile, 70 | ), 71 | ) 72 | 73 | @Composable 74 | private fun BottomBar(navController: NavHostController) { 75 | val navBackStackEntry by navController.currentBackStackEntryAsState() 76 | 77 | @Suppress("UNUSED_VARIABLE") 78 | val currentDestination = navBackStackEntry?.destination 79 | 80 | NavigationBar { 81 | Items.forEach { item -> 82 | NavigationBarItem( 83 | // selected = currentDestination?.hierarchy?.any { it.hasRoute(item.destination) } == true, 84 | selected = false, 85 | onClick = { 86 | navController.navigate(item.destination) { 87 | popUpTo(navController.graph.findStartDestination().id) { 88 | saveState = true 89 | } 90 | launchSingleTop = true 91 | restoreState = true 92 | } 93 | }, 94 | label = { 95 | Text(item.title) 96 | }, 97 | icon = { 98 | Icon(item.icon, contentDescription = null) 99 | }, 100 | ) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /bottomsheet/api/bottomsheet.api: -------------------------------------------------------------------------------- 1 | public final class dev/hrach/navigation/bottomsheet/BottomSheetHostKt { 2 | public static final fun BottomSheetHost-u5qS3lI (Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator;Landroidx/compose/ui/Modifier;FLandroidx/compose/ui/graphics/Shape;JJFJLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V 3 | } 4 | 5 | public final class dev/hrach/navigation/bottomsheet/BottomSheetNavigator : androidx/navigation/Navigator { 6 | public static final field $stable I 7 | public fun ()V 8 | public synthetic fun createDestination ()Landroidx/navigation/NavDestination; 9 | public fun createDestination ()Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator$Destination; 10 | public fun navigate (Ljava/util/List;Landroidx/navigation/NavOptions;Landroidx/navigation/Navigator$Extras;)V 11 | public fun popBackStack (Landroidx/navigation/NavBackStackEntry;Z)V 12 | } 13 | 14 | public final class dev/hrach/navigation/bottomsheet/BottomSheetNavigator$Destination : androidx/navigation/NavDestination, androidx/navigation/FloatingWindow { 15 | public static final field $stable I 16 | public fun (Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator;Lkotlin/jvm/functions/Function3;)V 17 | } 18 | 19 | public final class dev/hrach/navigation/bottomsheet/BottomSheetNavigatorDestinationBuilder : androidx/navigation/NavDestinationBuilder { 20 | public static final field $stable I 21 | public fun (Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)V 22 | public fun (Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator;Lkotlin/reflect/KClass;Ljava/util/Map;Lkotlin/jvm/functions/Function3;)V 23 | public synthetic fun build ()Landroidx/navigation/NavDestination; 24 | public fun build ()Ldev/hrach/navigation/bottomsheet/BottomSheetNavigator$Destination; 25 | public final fun getSecurePolicy ()Landroidx/compose/ui/window/SecureFlagPolicy; 26 | public final fun getSkipPartiallyExpanded ()Z 27 | public synthetic fun instantiateDestination ()Landroidx/navigation/NavDestination; 28 | public final fun setSecurePolicy (Landroidx/compose/ui/window/SecureFlagPolicy;)V 29 | public final fun setSkipPartiallyExpanded (Z)V 30 | } 31 | 32 | public final class dev/hrach/navigation/bottomsheet/ComposableSingletons$BottomSheetHostKt { 33 | public static final field INSTANCE Ldev/hrach/navigation/bottomsheet/ComposableSingletons$BottomSheetHostKt; 34 | public static field lambda-1 Lkotlin/jvm/functions/Function2; 35 | public fun ()V 36 | public final fun getLambda-1$bottomsheet_release ()Lkotlin/jvm/functions/Function2; 37 | } 38 | 39 | public final class dev/hrach/navigation/bottomsheet/ComposableSingletons$BottomSheetNavigatorKt { 40 | public static final field INSTANCE Ldev/hrach/navigation/bottomsheet/ComposableSingletons$BottomSheetNavigatorKt; 41 | public static field lambda-1 Lkotlin/jvm/functions/Function3; 42 | public fun ()V 43 | public final fun getLambda-1$bottomsheet_release ()Lkotlin/jvm/functions/Function3; 44 | } 45 | 46 | public final class dev/hrach/navigation/bottomsheet/NavGraphBuilderKt { 47 | public static final fun bottomSheet (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Landroidx/compose/ui/window/SecureFlagPolicy;ZLkotlin/jvm/functions/Function3;)V 48 | public static synthetic fun bottomSheet$default (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Landroidx/compose/ui/window/SecureFlagPolicy;ZLkotlin/jvm/functions/Function3;ILjava/lang/Object;)V 49 | } 50 | 51 | -------------------------------------------------------------------------------- /modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/NavGraphBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.modalsheet 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.animation.EnterTransition 5 | import androidx.compose.animation.ExitTransition 6 | import androidx.compose.animation.SizeTransform 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.window.SecureFlagPolicy 9 | import androidx.navigation.NamedNavArgument 10 | import androidx.navigation.NavBackStackEntry 11 | import androidx.navigation.NavDeepLink 12 | import androidx.navigation.NavGraphBuilder 13 | import androidx.navigation.NavType 14 | import androidx.navigation.get 15 | import kotlin.reflect.KType 16 | import androidx.compose.animation.AnimatedContentTransitionScope as ACTS 17 | 18 | public inline fun NavGraphBuilder.modalSheet( 19 | typeMap: Map> = emptyMap(), 20 | deepLinks: List = emptyList(), 21 | securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, 22 | noinline enterTransition: (ACTS.() -> @JvmSuppressWildcards EnterTransition?)? = null, 23 | noinline exitTransition: (ACTS.() -> @JvmSuppressWildcards ExitTransition?)? = null, 24 | noinline popEnterTransition: (ACTS.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, 25 | noinline popExitTransition: (ACTS.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, 26 | noinline sizeTransform: (ACTS.() -> @JvmSuppressWildcards SizeTransform?)? = null, 27 | noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 28 | ) { 29 | destination( 30 | ModalSheetNavigatorDestinationBuilder( 31 | provider[ModalSheetNavigator::class], 32 | T::class, 33 | typeMap, 34 | content, 35 | ).apply { 36 | deepLinks.forEach { deepLink -> 37 | deepLink(deepLink) 38 | } 39 | this.securePolicy = securePolicy 40 | this.enterTransition = enterTransition 41 | this.exitTransition = exitTransition 42 | this.popEnterTransition = popEnterTransition 43 | this.popExitTransition = popExitTransition 44 | this.sizeTransform = sizeTransform 45 | }, 46 | ) 47 | } 48 | 49 | public fun NavGraphBuilder.modalSheet( 50 | route: String, 51 | arguments: List = emptyList(), 52 | deepLinks: List = emptyList(), 53 | securePolicy: SecureFlagPolicy, 54 | enterTransition: (ACTS.() -> @JvmSuppressWildcards EnterTransition?)? = null, 55 | exitTransition: (ACTS.() -> @JvmSuppressWildcards ExitTransition?)? = null, 56 | popEnterTransition: (ACTS.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, 57 | popExitTransition: (ACTS.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, 58 | sizeTransform: (ACTS.() -> @JvmSuppressWildcards SizeTransform?)? = null, 59 | content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 60 | ) { 61 | destination( 62 | ModalSheetNavigatorDestinationBuilder( 63 | provider[ModalSheetNavigator::class], 64 | route, 65 | content, 66 | ).apply { 67 | arguments.forEach { (argumentName, argument) -> 68 | argument(argumentName, argument) 69 | } 70 | deepLinks.forEach { deepLink -> 71 | deepLink(deepLink) 72 | } 73 | this.securePolicy = securePolicy 74 | this.enterTransition = enterTransition 75 | this.exitTransition = exitTransition 76 | this.popEnterTransition = popEnterTransition 77 | this.popExitTransition = popExitTransition 78 | this.sizeTransform = sizeTransform 79 | }, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /results/src/main/kotlin/dev/hrach/navigation/results/Results.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.results 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleEventObserver 7 | import androidx.navigation.NavBackStackEntry 8 | import androidx.navigation.NavController 9 | import kotlinx.serialization.ExperimentalSerializationApi 10 | import kotlinx.serialization.KSerializer 11 | import kotlinx.serialization.json.Json 12 | import kotlinx.serialization.serializer 13 | 14 | /** 15 | * Registers an effect for processing a dialog destination's result. 16 | * 17 | * To work properly, obtain the "current" destinations backstack using [NavController.getBackStackEntry]. 18 | * 19 | * !!! DO NOT USE !!! [NavController.currentBackStackEntry] !!! DO NOT USE !!! 20 | * as it may provide a dialog's entry instead of the source's entry that should receive 21 | * e.g. the dialog's result. See the [official documentation](https://developer.android.com/guide/navigation/use-graph/programmatic#additional_considerations). 22 | * 23 | * ```kotlin 24 | * NavigationResultEffect( 25 | * backStackEntry = remember(navController) { navController.getBackStackEntry() }, 26 | * navController = navController, 27 | * ) { result: Destinations.Dialog.Result -> 28 | * // process result 29 | * } 30 | * ``` 31 | */ 32 | @Composable 33 | public inline fun NavigationResultEffect( 34 | backStackEntry: NavBackStackEntry, 35 | navController: NavController, 36 | noinline block: (R) -> Unit, 37 | ) { 38 | NavigationResultEffectImpl( 39 | backStackEntry = backStackEntry, 40 | navController = navController, 41 | resultSerializer = serializer(), 42 | block = block, 43 | ) 44 | } 45 | 46 | /** 47 | * Implementation of ResultEffect. Use [NavigationResultEffect]. 48 | */ 49 | @OptIn(ExperimentalSerializationApi::class) 50 | @PublishedApi 51 | @Composable 52 | internal fun NavigationResultEffectImpl( 53 | backStackEntry: NavBackStackEntry, 54 | navController: NavController, 55 | resultSerializer: KSerializer, 56 | block: (R) -> Unit, 57 | ) { 58 | DisposableEffect(navController) { 59 | // The implementation is based on the official documentation of the Result sharing. 60 | // It takes into consideration the possibility of a dialog usage (see the docs). 61 | // https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations 62 | val resultKey = resultSerializer.descriptor.serialName + "_result" 63 | val observer = LifecycleEventObserver { _, event -> 64 | if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(resultKey)) { 65 | val result = backStackEntry.savedStateHandle.remove(resultKey)!! 66 | val decoded = Json.decodeFromString(resultSerializer, result) 67 | block(decoded) 68 | } 69 | } 70 | backStackEntry.lifecycle.addObserver(observer) 71 | onDispose { 72 | backStackEntry.lifecycle.removeObserver(observer) 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Sets a result for the previous backstack entry. 79 | * 80 | * The result type has to be KotlinX Serializable. 81 | */ 82 | public inline fun NavController.setResult( 83 | data: R, 84 | ) { 85 | setResultImpl(data, serializer()) 86 | } 87 | 88 | @OptIn(ExperimentalSerializationApi::class) 89 | @PublishedApi 90 | internal fun NavController.setResultImpl( 91 | data: R, 92 | resultSerializer: KSerializer, 93 | ) { 94 | val result = Json.encodeToString(resultSerializer, data) 95 | val resultKey = resultSerializer.descriptor.serialName + "_result" 96 | previousBackStackEntry?.savedStateHandle?.set(resultKey, result) 97 | } 98 | -------------------------------------------------------------------------------- /modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigatorDestinationBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.modalsheet 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.animation.AnimatedContentTransitionScope 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.SizeTransform 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.window.SecureFlagPolicy 10 | import androidx.navigation.NavBackStackEntry 11 | import androidx.navigation.NavDestinationBuilder 12 | import androidx.navigation.NavDestinationDsl 13 | import androidx.navigation.NavType 14 | import kotlin.reflect.KClass 15 | import kotlin.reflect.KType 16 | 17 | /** 18 | * DSL for constructing a new [ModalSheetNavigator.Destination] 19 | */ 20 | @Suppress("UnnecessaryOptInAnnotation") 21 | @NavDestinationDsl 22 | public class ModalSheetNavigatorDestinationBuilder : 23 | NavDestinationBuilder { 24 | 25 | private val composeNavigator: ModalSheetNavigator 26 | private val content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit) 27 | 28 | public var securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit 29 | 30 | public var enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = 31 | null 32 | 33 | public var exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = 34 | null 35 | 36 | public var popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = 37 | null 38 | 39 | public var popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = 40 | null 41 | 42 | public var sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = 43 | null 44 | 45 | /** 46 | * DSL for constructing a new [ModalSheetNavigator.Destination] 47 | * 48 | * @param navigator navigator used to create the destination 49 | * @param route the destination's unique route 50 | * @param content composable for the destination 51 | */ 52 | public constructor( 53 | navigator: ModalSheetNavigator, 54 | route: String, 55 | content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 56 | ) : super(navigator, route) { 57 | this.composeNavigator = navigator 58 | this.content = content 59 | } 60 | 61 | /** 62 | * DSL for constructing a new [ModalSheetNavigator.Destination] 63 | * 64 | * @param navigator navigator used to create the destination 65 | * @param route the destination's unique route from a [KClass] 66 | * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom 67 | * [NavType]. May be empty if [route] does not use custom NavTypes. 68 | * @param content composable for the destination 69 | */ 70 | public constructor( 71 | navigator: ModalSheetNavigator, 72 | route: KClass<*>, 73 | typeMap: Map>, 74 | content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 75 | ) : super(navigator, route, typeMap) { 76 | this.composeNavigator = navigator 77 | this.content = content 78 | } 79 | 80 | override fun instantiateDestination(): ModalSheetNavigator.Destination { 81 | return ModalSheetNavigator.Destination(composeNavigator, content) 82 | } 83 | 84 | override fun build(): ModalSheetNavigator.Destination { 85 | return super.build().also { destination -> 86 | destination.enterTransition = enterTransition 87 | destination.exitTransition = exitTransition 88 | destination.popEnterTransition = popEnterTransition 89 | destination.popExitTransition = popExitTransition 90 | destination.sizeTransform = sizeTransform 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modalsheet/api/modalsheet.api: -------------------------------------------------------------------------------- 1 | public final class dev/hrach/navigation/modalsheet/ComposableSingletons$ModalSheetDialogKt { 2 | public static final field INSTANCE Ldev/hrach/navigation/modalsheet/ComposableSingletons$ModalSheetDialogKt; 3 | public static field lambda-1 Lkotlin/jvm/functions/Function2; 4 | public fun ()V 5 | public final fun getLambda-1$modalsheet_release ()Lkotlin/jvm/functions/Function2; 6 | } 7 | 8 | public final class dev/hrach/navigation/modalsheet/ComposableSingletons$ModalSheetNavigatorKt { 9 | public static final field INSTANCE Ldev/hrach/navigation/modalsheet/ComposableSingletons$ModalSheetNavigatorKt; 10 | public static field lambda-1 Lkotlin/jvm/functions/Function4; 11 | public fun ()V 12 | public final fun getLambda-1$modalsheet_release ()Lkotlin/jvm/functions/Function4; 13 | } 14 | 15 | public final class dev/hrach/navigation/modalsheet/ModalSheetHostKt { 16 | public static final fun ModalSheetHost-Y2L_72g (Ldev/hrach/navigation/modalsheet/ModalSheetNavigator;JLandroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V 17 | } 18 | 19 | public final class dev/hrach/navigation/modalsheet/ModalSheetNavigator : androidx/navigation/Navigator { 20 | public static final field $stable I 21 | public fun ()V 22 | public synthetic fun createDestination ()Landroidx/navigation/NavDestination; 23 | public fun createDestination ()Ldev/hrach/navigation/modalsheet/ModalSheetNavigator$Destination; 24 | public fun navigate (Ljava/util/List;Landroidx/navigation/NavOptions;Landroidx/navigation/Navigator$Extras;)V 25 | public fun popBackStack (Landroidx/navigation/NavBackStackEntry;Z)V 26 | public final fun prepareForTransition (Landroidx/navigation/NavBackStackEntry;)V 27 | } 28 | 29 | public final class dev/hrach/navigation/modalsheet/ModalSheetNavigator$Destination : androidx/navigation/NavDestination, androidx/navigation/FloatingWindow { 30 | public static final field $stable I 31 | public fun (Ldev/hrach/navigation/modalsheet/ModalSheetNavigator;Lkotlin/jvm/functions/Function4;)V 32 | } 33 | 34 | public final class dev/hrach/navigation/modalsheet/ModalSheetNavigatorDestinationBuilder : androidx/navigation/NavDestinationBuilder { 35 | public static final field $stable I 36 | public fun (Ldev/hrach/navigation/modalsheet/ModalSheetNavigator;Ljava/lang/String;Lkotlin/jvm/functions/Function4;)V 37 | public fun (Ldev/hrach/navigation/modalsheet/ModalSheetNavigator;Lkotlin/reflect/KClass;Ljava/util/Map;Lkotlin/jvm/functions/Function4;)V 38 | public synthetic fun build ()Landroidx/navigation/NavDestination; 39 | public fun build ()Ldev/hrach/navigation/modalsheet/ModalSheetNavigator$Destination; 40 | public final fun getEnterTransition ()Lkotlin/jvm/functions/Function1; 41 | public final fun getExitTransition ()Lkotlin/jvm/functions/Function1; 42 | public final fun getPopEnterTransition ()Lkotlin/jvm/functions/Function1; 43 | public final fun getPopExitTransition ()Lkotlin/jvm/functions/Function1; 44 | public final fun getSecurePolicy ()Landroidx/compose/ui/window/SecureFlagPolicy; 45 | public final fun getSizeTransform ()Lkotlin/jvm/functions/Function1; 46 | public synthetic fun instantiateDestination ()Landroidx/navigation/NavDestination; 47 | public final fun setEnterTransition (Lkotlin/jvm/functions/Function1;)V 48 | public final fun setExitTransition (Lkotlin/jvm/functions/Function1;)V 49 | public final fun setPopEnterTransition (Lkotlin/jvm/functions/Function1;)V 50 | public final fun setPopExitTransition (Lkotlin/jvm/functions/Function1;)V 51 | public final fun setSecurePolicy (Landroidx/compose/ui/window/SecureFlagPolicy;)V 52 | public final fun setSizeTransform (Lkotlin/jvm/functions/Function1;)V 53 | } 54 | 55 | public final class dev/hrach/navigation/modalsheet/NavGraphBuilderKt { 56 | public static final fun modalSheet (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Landroidx/compose/ui/window/SecureFlagPolicy;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V 57 | public static synthetic fun modalSheet$default (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Landroidx/compose/ui/window/SecureFlagPolicy;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V 58 | } 59 | 60 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 137 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/dev/hrach/navigation/demo/NavHost.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.demo 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.FastOutLinearInEasing 6 | import androidx.compose.animation.core.LinearOutSlowInEasing 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.animation.fadeIn 9 | import androidx.compose.animation.fadeOut 10 | import androidx.compose.animation.slideInHorizontally 11 | import androidx.compose.animation.slideInVertically 12 | import androidx.compose.animation.slideOutHorizontally 13 | import androidx.compose.animation.slideOutVertically 14 | import androidx.compose.foundation.shape.CornerSize 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.ExperimentalMaterial3Api 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.platform.LocalDensity 20 | import androidx.compose.ui.unit.Density 21 | import androidx.compose.ui.unit.dp 22 | import androidx.navigation.NavHostController 23 | import androidx.navigation.compose.NavHost 24 | import androidx.navigation.compose.composable 25 | import dev.hrach.navigation.bottomsheet.BottomSheetHost 26 | import dev.hrach.navigation.bottomsheet.BottomSheetNavigator 27 | import dev.hrach.navigation.bottomsheet.bottomSheet 28 | import dev.hrach.navigation.demo.screens.BottomSheet 29 | import dev.hrach.navigation.demo.screens.Home 30 | import dev.hrach.navigation.demo.screens.List 31 | import dev.hrach.navigation.demo.screens.Modal1 32 | import dev.hrach.navigation.demo.screens.Modal2 33 | import dev.hrach.navigation.demo.screens.Profile 34 | import dev.hrach.navigation.modalsheet.ModalSheetHost 35 | import dev.hrach.navigation.modalsheet.ModalSheetNavigator 36 | import dev.hrach.navigation.modalsheet.modalSheet 37 | 38 | @OptIn(ExperimentalMaterial3Api::class) 39 | @Composable 40 | internal fun NavHost( 41 | navController: NavHostController, 42 | modalSheetNavigator: ModalSheetNavigator, 43 | bottomSheetNavigator: BottomSheetNavigator, 44 | ) { 45 | val density = LocalDensity.current 46 | NavHost( 47 | navController = navController, 48 | startDestination = Destinations.Home, 49 | enterTransition = { SharedXAxisEnterTransition(density) }, 50 | exitTransition = { SharedXAxisExitTransition(density) }, 51 | popEnterTransition = { SharedXAxisPopEnterTransition(density) }, 52 | popExitTransition = { SharedXAxisPopExitTransition(density) }, 53 | ) { 54 | composable { Home(navController) } 55 | composable { List() } 56 | composable { Profile() } 57 | modalSheet { Modal1(navController) } 58 | modalSheet { Modal2(navController) } 59 | bottomSheet { BottomSheet(navController) } 60 | } 61 | ModalSheetHost( 62 | modalSheetNavigator = modalSheetNavigator, 63 | containerColor = MaterialTheme.colorScheme.background, 64 | enterTransition = { SharedYAxisEnterTransition(density) }, 65 | exitTransition = { SharedYAxisExitTransition(density) }, 66 | ) 67 | BottomSheetHost( 68 | navigator = bottomSheetNavigator, 69 | shape = RoundedCornerShape( 70 | // optional, just an example of bottom sheet custom property 71 | topStart = CornerSize(12.dp), 72 | topEnd = CornerSize(12.dp), 73 | bottomStart = CornerSize(0.dp), 74 | bottomEnd = CornerSize(0.dp), 75 | ), 76 | ) 77 | } 78 | 79 | private val SharedXAxisEnterTransition: (Density) -> EnterTransition = { density -> 80 | fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + 81 | slideInHorizontally(animationSpec = tween(durationMillis = 300)) { 82 | with(density) { 30.dp.roundToPx() } 83 | } 84 | } 85 | 86 | private val SharedXAxisPopEnterTransition: (Density) -> EnterTransition = { density -> 87 | fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + 88 | slideInHorizontally(animationSpec = tween(durationMillis = 300)) { 89 | with(density) { (-30).dp.roundToPx() } 90 | } 91 | } 92 | 93 | private val SharedXAxisExitTransition: (Density) -> ExitTransition = { density -> 94 | fadeOut(animationSpec = tween(durationMillis = 90, easing = FastOutLinearInEasing)) + 95 | slideOutHorizontally(animationSpec = tween(durationMillis = 300)) { 96 | with(density) { (-30).dp.roundToPx() } 97 | } 98 | } 99 | 100 | private val SharedXAxisPopExitTransition: (Density) -> ExitTransition = { density -> 101 | fadeOut(animationSpec = tween(durationMillis = 90, easing = FastOutLinearInEasing)) + 102 | slideOutHorizontally(animationSpec = tween(durationMillis = 300)) { 103 | with(density) { 30.dp.roundToPx() } 104 | } 105 | } 106 | 107 | private val SharedYAxisEnterTransition: (Density) -> EnterTransition = { density -> 108 | fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + 109 | slideInVertically(animationSpec = tween(durationMillis = 300)) { 110 | with(density) { 30.dp.roundToPx() } 111 | } 112 | } 113 | 114 | private val SharedYAxisExitTransition: (Density) -> ExitTransition = { density -> 115 | fadeOut(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + 116 | slideOutVertically(animationSpec = tween(durationMillis = 300)) { 117 | with(density) { 30.dp.roundToPx() } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /bottomsheet/src/main/kotlin/dev/hrach/navigation/bottomsheet/BottomSheetHost.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.bottomsheet 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.material3.BottomSheetDefaults 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ModalBottomSheet 7 | import androidx.compose.material3.ModalBottomSheetProperties 8 | import androidx.compose.material3.SheetState 9 | import androidx.compose.material3.contentColorFor 10 | import androidx.compose.material3.rememberModalBottomSheetState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.DisposableEffect 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.derivedStateOf 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.produceState 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.saveable.SaveableStateHolder 19 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.Shape 23 | import androidx.compose.ui.unit.Dp 24 | import androidx.compose.ui.unit.dp 25 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 26 | import androidx.navigation.NavBackStackEntry 27 | import androidx.navigation.compose.LocalOwnersProvider 28 | import kotlinx.coroutines.CancellationException 29 | import kotlinx.coroutines.flow.Flow 30 | import kotlinx.coroutines.flow.flow 31 | 32 | @ExperimentalMaterial3Api 33 | @Composable 34 | public fun BottomSheetHost( 35 | navigator: BottomSheetNavigator, 36 | modifier: Modifier = Modifier, 37 | sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, 38 | shape: Shape = BottomSheetDefaults.ExpandedShape, 39 | containerColor: Color = BottomSheetDefaults.ContainerColor, 40 | contentColor: Color = contentColorFor(containerColor), 41 | tonalElevation: Dp = 0.dp, 42 | scrimColor: Color = BottomSheetDefaults.ScrimColor, 43 | dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, 44 | contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, 45 | ) { 46 | val saveableStateHolder = rememberSaveableStateHolder() 47 | 48 | val sheetBackStackState by remember { 49 | navigator.backStack.zipWithPreviousCount() 50 | }.collectAsStateWithLifecycle(Pair(0, emptyList())) 51 | 52 | val count by remember { 53 | derivedStateOf { 54 | maxOf( 55 | sheetBackStackState.first, 56 | sheetBackStackState.second.count(), 57 | ) 58 | } 59 | } 60 | 61 | repeat(count) { i -> 62 | val backStackEntry = sheetBackStackState.second.getOrNull(i) 63 | BottomSheetHost( 64 | navigator = navigator, 65 | modifier = modifier, 66 | sheetMaxWidth = sheetMaxWidth, 67 | shape = shape, 68 | containerColor = containerColor, 69 | contentColor = contentColor, 70 | tonalElevation = tonalElevation, 71 | scrimColor = scrimColor, 72 | dragHandle = dragHandle, 73 | contentWindowInsets = contentWindowInsets, 74 | saveableStateHolder = saveableStateHolder, 75 | targetBackStackEntry = backStackEntry, 76 | ) 77 | } 78 | } 79 | 80 | private fun Flow>.zipWithPreviousCount(): Flow>> = 81 | flow { 82 | var previous = 0 83 | collect { value -> 84 | emit(Pair(previous, value)) 85 | previous = value.count() 86 | } 87 | } 88 | 89 | @OptIn(ExperimentalMaterial3Api::class) 90 | @Composable 91 | private fun BottomSheetHost( 92 | navigator: BottomSheetNavigator, 93 | modifier: Modifier = Modifier, 94 | sheetMaxWidth: Dp, 95 | shape: Shape, 96 | containerColor: Color, 97 | contentColor: Color, 98 | tonalElevation: Dp, 99 | scrimColor: Color, 100 | dragHandle: @Composable (() -> Unit)?, 101 | contentWindowInsets: @Composable () -> WindowInsets, 102 | saveableStateHolder: SaveableStateHolder, 103 | targetBackStackEntry: NavBackStackEntry?, 104 | ) { 105 | val destination = targetBackStackEntry?.destination as? BottomSheetNavigator.Destination 106 | val sheetState = rememberModalBottomSheetState( 107 | skipPartiallyExpanded = destination?.skipPartiallyExpanded ?: true, 108 | ) 109 | 110 | @Suppress("ProduceStateDoesNotAssignValue") // false positive 111 | val backStackEntry by produceState( 112 | initialValue = null, 113 | key1 = targetBackStackEntry, 114 | ) { 115 | try { 116 | sheetState.hide() 117 | } catch (_: CancellationException) { 118 | // We catch but ignore possible cancellation exceptions as we don't want 119 | // them to bubble up and cancel the whole produceState coroutine 120 | } finally { 121 | value = targetBackStackEntry 122 | } 123 | } 124 | 125 | BottomSheetHost( 126 | navigator = navigator, 127 | modifier = modifier, 128 | sheetMaxWidth = sheetMaxWidth, 129 | shape = shape, 130 | containerColor = containerColor, 131 | contentColor = contentColor, 132 | tonalElevation = tonalElevation, 133 | scrimColor = scrimColor, 134 | dragHandle = dragHandle, 135 | sheetState = sheetState, 136 | contentWindowInsets = contentWindowInsets, 137 | saveableStateHolder = saveableStateHolder, 138 | backStackEntry = backStackEntry ?: return, 139 | ) 140 | } 141 | 142 | @OptIn(ExperimentalMaterial3Api::class) 143 | @Composable 144 | private fun BottomSheetHost( 145 | navigator: BottomSheetNavigator, 146 | modifier: Modifier = Modifier, 147 | sheetMaxWidth: Dp, 148 | shape: Shape, 149 | containerColor: Color, 150 | contentColor: Color, 151 | tonalElevation: Dp, 152 | scrimColor: Color, 153 | dragHandle: @Composable (() -> Unit)?, 154 | contentWindowInsets: @Composable () -> WindowInsets, 155 | sheetState: SheetState, 156 | saveableStateHolder: SaveableStateHolder, 157 | backStackEntry: NavBackStackEntry, 158 | ) { 159 | LaunchedEffect(backStackEntry) { 160 | sheetState.show() 161 | } 162 | 163 | backStackEntry.LocalOwnersProvider(saveableStateHolder) { 164 | val destination = backStackEntry.destination as BottomSheetNavigator.Destination 165 | 166 | ModalBottomSheet( 167 | onDismissRequest = { navigator.dismiss(backStackEntry) }, 168 | modifier = modifier, 169 | sheetState = sheetState, 170 | sheetMaxWidth = sheetMaxWidth, 171 | shape = shape, 172 | containerColor = containerColor, 173 | contentColor = contentColor, 174 | tonalElevation = tonalElevation, 175 | scrimColor = scrimColor, 176 | dragHandle = dragHandle, 177 | contentWindowInsets = contentWindowInsets, 178 | properties = ModalBottomSheetProperties(securePolicy = destination.securePolicy), 179 | ) { 180 | LaunchedEffect(backStackEntry) { 181 | navigator.onTransitionComplete(backStackEntry) 182 | } 183 | DisposableEffect(backStackEntry) { 184 | onDispose { 185 | navigator.onTransitionComplete(backStackEntry) 186 | } 187 | } 188 | destination.content(backStackEntry) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.modalsheet 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.AnimatedContentTransitionScope 5 | import androidx.compose.animation.ContentTransform 6 | import androidx.compose.animation.EnterTransition 7 | import androidx.compose.animation.ExitTransition 8 | import androidx.compose.animation.SizeTransform 9 | import androidx.compose.animation.core.SeekableTransitionState 10 | import androidx.compose.animation.core.animate 11 | import androidx.compose.animation.core.rememberTransition 12 | import androidx.compose.animation.core.tween 13 | import androidx.compose.animation.fadeIn 14 | import androidx.compose.animation.fadeOut 15 | import androidx.compose.foundation.background 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.foundation.layout.fillMaxSize 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.DisposableEffect 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableFloatStateOf 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.window.SecureFlagPolicy 32 | import androidx.navigation.NavBackStackEntry 33 | import androidx.navigation.NavDestination.Companion.hierarchy 34 | import androidx.navigation.compose.LocalOwnersProvider 35 | import kotlinx.coroutines.CancellationException 36 | import kotlinx.coroutines.launch 37 | 38 | @Suppress("UNUSED_ANONYMOUS_PARAMETER") 39 | @Composable 40 | public fun ModalSheetHost( 41 | modalSheetNavigator: ModalSheetNavigator, 42 | containerColor: Color, 43 | modifier: Modifier = Modifier, 44 | enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition) = 45 | { fadeIn(animationSpec = tween(700)) }, 46 | exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition) = 47 | { fadeOut(animationSpec = tween(700)) }, 48 | popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition) = 49 | enterTransition, 50 | popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition) = 51 | exitTransition, 52 | sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = 53 | null, 54 | ) { 55 | var progress by remember { mutableFloatStateOf(0f) } 56 | var inPredictiveBack by remember { mutableStateOf(false) } 57 | val zIndices = remember { mutableMapOf() } 58 | 59 | val saveableStateHolder = rememberSaveableStateHolder() 60 | 61 | val modalBackStack by modalSheetNavigator.backStack.collectAsState(listOf()) 62 | val backStackEntry: NavBackStackEntry? = modalBackStack.lastOrNull() 63 | 64 | val finalEnter: AnimatedContentTransitionScope.() -> EnterTransition = { 65 | val targetDestination = targetState.destination as ModalSheetNavigator.Destination 66 | if (modalSheetNavigator.isPop.value) { 67 | targetDestination.hierarchy.firstNotNullOfOrNull { destination -> 68 | null // destination.createPopEnterTransition(this) 69 | } ?: popEnterTransition.invoke(this) 70 | } else { 71 | targetDestination.hierarchy.firstNotNullOfOrNull { destination -> 72 | null // destination.createEnterTransition(this) 73 | } ?: enterTransition.invoke(this) 74 | } 75 | } 76 | val finalExit: AnimatedContentTransitionScope.() -> ExitTransition = { 77 | val initialDestination = initialState.destination as ModalSheetNavigator.Destination 78 | if (modalSheetNavigator.isPop.value) { 79 | initialDestination.hierarchy.firstNotNullOfOrNull { destination -> 80 | null // destination.createPopExitTransition(this) 81 | } ?: popExitTransition.invoke(this) 82 | } else { 83 | initialDestination.hierarchy.firstNotNullOfOrNull { destination -> 84 | null // destination.createExitTransition(this) 85 | } ?: exitTransition.invoke(this) 86 | } 87 | } 88 | val finalSizeTransform: AnimatedContentTransitionScope.() -> SizeTransform? = { 89 | val targetDestination = targetState.destination as ModalSheetNavigator.Destination 90 | targetDestination.hierarchy.firstNotNullOfOrNull { destination -> 91 | null // destination.createSizeTransform(this) 92 | } ?: sizeTransform?.invoke(this) 93 | } 94 | 95 | val transitionState = remember { 96 | // The state returned here cannot be nullable cause it produces the input of the 97 | // transitionSpec passed into the AnimatedContent and that must match the non-nullable 98 | // scope exposed by the transitions on the NavHost and composable APIs. 99 | SeekableTransitionState(backStackEntry) 100 | } 101 | val transitionsInProgress = modalSheetNavigator.transitionsInProgress.collectAsState().value 102 | val transition = rememberTransition(transitionState, label = "entry") 103 | val nothingToShow = transition.currentState == transition.targetState && 104 | transition.currentState == null && 105 | backStackEntry == null && 106 | transitionsInProgress.isEmpty() 107 | 108 | if (inPredictiveBack) { 109 | LaunchedEffect(progress) { 110 | val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2) 111 | transitionState.seekTo(progress, previousEntry) 112 | } 113 | } else { 114 | LaunchedEffect(backStackEntry) { 115 | // This ensures we don't animate after the back gesture is cancelled and we 116 | // are already on the current state 117 | if (transitionState.currentState != backStackEntry) { 118 | transitionState.animateTo(backStackEntry) 119 | } else { 120 | // convert from nanoseconds to milliseconds 121 | val totalDuration = transition.totalDurationNanos / 1000000 122 | // When the predictive back gesture is cancel, we need to manually animate 123 | // the SeekableTransitionState from where it left off, to zero and then 124 | // snapTo the final position. 125 | animate( 126 | transitionState.fraction, 127 | 0f, 128 | animationSpec = tween((transitionState.fraction * totalDuration).toInt()), 129 | ) { value, _ -> 130 | this@LaunchedEffect.launch { 131 | if (value > 0) { 132 | // Seek the original transition back to the currentState 133 | transitionState.seekTo(value) 134 | } 135 | if (value == 0f) { 136 | // Once we animate to the start, we need to snap to the right state. 137 | transitionState.snapTo(backStackEntry) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | if (!nothingToShow) { 146 | val securePolicy = (backStackEntry?.destination as? ModalSheetNavigator.Destination) 147 | ?.securePolicy 148 | ?: SecureFlagPolicy.Inherit 149 | 150 | ModalSheetDialog( 151 | onPredictiveBack = onPredictBack@{ backEvent -> 152 | progress = 0f 153 | // early return: already animating backstack out, repeated back handling 154 | // probably reproducible only with slowed animations 155 | val currentBackStackEntry = modalBackStack.lastOrNull() ?: return@onPredictBack 156 | modalSheetNavigator.prepareForTransition(currentBackStackEntry) 157 | val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2) 158 | if (previousEntry != null) { 159 | modalSheetNavigator.prepareForTransition(previousEntry) 160 | } 161 | try { 162 | backEvent.collect { 163 | inPredictiveBack = true 164 | progress = it.progress 165 | } 166 | inPredictiveBack = false 167 | modalSheetNavigator.popBackStack(currentBackStackEntry, false) 168 | } catch (e: CancellationException) { 169 | inPredictiveBack = false 170 | } 171 | }, 172 | securePolicy = securePolicy, 173 | ) { 174 | transition.AnimatedContent( 175 | modifier = modifier 176 | .background(if (transition.targetState == null || transition.currentState == null) Color.Transparent else containerColor), 177 | contentAlignment = Alignment.TopStart, 178 | transitionSpec = block@{ 179 | @Suppress("UNCHECKED_CAST") 180 | val initialState = initialState ?: return@block ContentTransform( 181 | enterTransition(this as AnimatedContentTransitionScope), 182 | fadeOut(), // irrelevant 183 | 0f, 184 | ) 185 | 186 | @Suppress("UNCHECKED_CAST") 187 | val targetState = targetState ?: return@block ContentTransform( 188 | fadeIn(), // irrelevant 189 | exitTransition(this as AnimatedContentTransitionScope), 190 | 0f, 191 | ) 192 | 193 | val initialZIndex = 194 | zIndices[initialState.id] ?: 0f.also { zIndices[initialState.id] = 0f } 195 | val targetZIndex = when { 196 | targetState.id == initialState.id -> initialZIndex 197 | modalSheetNavigator.isPop.value -> initialZIndex - 1f 198 | else -> initialZIndex + 1f 199 | }.also { zIndices[targetState.id] = it } 200 | 201 | // cast to proper type as null is already handled 202 | @Suppress("UNCHECKED_CAST") 203 | this as AnimatedContentTransitionScope 204 | ContentTransform( 205 | targetContentEnter = finalEnter(this), 206 | initialContentExit = finalExit(this), 207 | targetContentZIndex = targetZIndex, 208 | sizeTransform = finalSizeTransform(this), 209 | ) 210 | }, 211 | ) { currentEntry -> 212 | if (currentEntry == null) { 213 | Box(Modifier.fillMaxSize()) {} 214 | return@AnimatedContent 215 | } 216 | 217 | currentEntry.LocalOwnersProvider(saveableStateHolder) { 218 | (currentEntry.destination as ModalSheetNavigator.Destination) 219 | .content(this, currentEntry) 220 | } 221 | DisposableEffect(currentEntry) { 222 | onDispose { 223 | modalSheetNavigator.onTransitionComplete(currentEntry) 224 | } 225 | } 226 | } 227 | } 228 | } 229 | LaunchedEffect(transition.currentState, transition.targetState) { 230 | if (transition.currentState == transition.targetState) { 231 | transitionsInProgress.forEach { entry -> modalSheetNavigator.onTransitionComplete(entry) } 232 | zIndices 233 | .filter { it.key != transition.targetState?.id } 234 | .forEach { zIndices.remove(it.key) } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package dev.hrach.navigation.modalsheet 2 | 3 | import android.content.Context 4 | import android.graphics.Outline 5 | import android.os.Build 6 | import android.view.View 7 | import android.view.ViewOutlineProvider 8 | import android.view.Window 9 | import android.view.WindowManager 10 | import androidx.activity.BackEventCompat 11 | import androidx.activity.ComponentDialog 12 | import androidx.activity.compose.PredictiveBackHandler 13 | import androidx.activity.setViewTreeOnBackPressedDispatcherOwner 14 | import androidx.appcompat.view.ContextThemeWrapper 15 | import androidx.compose.foundation.isSystemInDarkTheme 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.CompositionContext 19 | import androidx.compose.runtime.DisposableEffect 20 | import androidx.compose.runtime.SideEffect 21 | import androidx.compose.runtime.State 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.rememberCompositionContext 26 | import androidx.compose.runtime.rememberUpdatedState 27 | import androidx.compose.runtime.saveable.rememberSaveable 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.AbstractComposeView 31 | import androidx.compose.ui.platform.LocalDensity 32 | import androidx.compose.ui.platform.LocalLayoutDirection 33 | import androidx.compose.ui.platform.LocalView 34 | import androidx.compose.ui.platform.ViewRootForInspector 35 | import androidx.compose.ui.semantics.dialog 36 | import androidx.compose.ui.semantics.semantics 37 | import androidx.compose.ui.unit.Density 38 | import androidx.compose.ui.unit.LayoutDirection 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.window.DialogWindowProvider 41 | import androidx.compose.ui.window.SecureFlagPolicy 42 | import androidx.core.view.WindowCompat 43 | import androidx.lifecycle.findViewTreeLifecycleOwner 44 | import androidx.lifecycle.findViewTreeViewModelStoreOwner 45 | import androidx.lifecycle.setViewTreeLifecycleOwner 46 | import androidx.lifecycle.setViewTreeViewModelStoreOwner 47 | import androidx.savedstate.findViewTreeSavedStateRegistryOwner 48 | import androidx.savedstate.setViewTreeSavedStateRegistryOwner 49 | import java.util.UUID 50 | import kotlinx.coroutines.flow.Flow 51 | 52 | @Composable 53 | internal fun ModalSheetDialog( 54 | onPredictiveBack: suspend (Flow) -> Unit, 55 | securePolicy: SecureFlagPolicy, 56 | content: @Composable () -> Unit, 57 | ) { 58 | val view = LocalView.current 59 | val density = LocalDensity.current 60 | val layoutDirection = LocalLayoutDirection.current 61 | val composition = rememberCompositionContext() 62 | val currentContent by rememberUpdatedState(content) 63 | val dialogId = rememberSaveable { UUID.randomUUID() } 64 | val darkThemeEnabled = isSystemInDarkTheme() 65 | val currentOnPredictiveBack = rememberUpdatedState(onPredictiveBack) 66 | 67 | val dialog = remember(view, density) { 68 | ModalSheetDialogWrapper( 69 | onPredictiveBack = currentOnPredictiveBack, 70 | composeView = view, 71 | securePolicy = securePolicy, 72 | layoutDirection = layoutDirection, 73 | density = density, 74 | dialogId = dialogId, 75 | darkThemeEnabled = darkThemeEnabled, 76 | ).apply { 77 | setContent(composition) { 78 | Box( 79 | Modifier.semantics { dialog() }, 80 | ) { 81 | currentContent() 82 | } 83 | } 84 | } 85 | } 86 | DisposableEffect(dialog) { 87 | dialog.show() 88 | onDispose { 89 | dialog.dismiss() 90 | dialog.disposeComposition() 91 | } 92 | } 93 | SideEffect { 94 | dialog.updateParameters( 95 | securePolicy = securePolicy, 96 | layoutDirection = layoutDirection, 97 | darkThemeEnabled = darkThemeEnabled, 98 | ) 99 | } 100 | } 101 | 102 | // Fork of androidx.compose.ui.window.DialogLayout 103 | // Additional parameters required for current predictive back implementation. 104 | @Suppress("ViewConstructor") 105 | private class ModalSheetDialogLayout( 106 | context: Context, 107 | override val window: Window, 108 | private val onPredictiveBack: State) -> Unit>, 109 | ) : AbstractComposeView(context), DialogWindowProvider { 110 | private var content: @Composable () -> Unit by mutableStateOf({}) 111 | override var shouldCreateCompositionOnAttachedToWindow: Boolean = false 112 | private set 113 | 114 | fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { 115 | setParentCompositionContext(parent) 116 | this.content = content 117 | shouldCreateCompositionOnAttachedToWindow = true 118 | createComposition() 119 | } 120 | 121 | @Composable 122 | override fun Content() { 123 | PredictiveBackHandler(onBack = onPredictiveBack.value) 124 | content() 125 | } 126 | } 127 | 128 | // Fork of androidx.compose.ui.window.DialogWrapper. 129 | // predictiveBackProgress and scope params added for predictive back implementation. 130 | // EdgeToEdgeFloatingDialogWindowTheme provided to allow theme to extend into status bar. 131 | internal class ModalSheetDialogWrapper( 132 | onPredictiveBack: State) -> Unit>, 133 | private val composeView: View, 134 | securePolicy: SecureFlagPolicy, 135 | layoutDirection: LayoutDirection, 136 | density: Density, 137 | dialogId: UUID, 138 | darkThemeEnabled: Boolean, 139 | ) : ComponentDialog(ContextThemeWrapper(composeView.context, R.style.EdgeToEdgeFloatingDialogWindowTheme)), 140 | ViewRootForInspector { 141 | private val dialogLayout: ModalSheetDialogLayout 142 | 143 | // On systems older than Android S, there is a bug in the surface insets matrix math used by 144 | // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477. 145 | private val maxSupportedElevation = 8.dp 146 | override val subCompositionView: AbstractComposeView get() = dialogLayout 147 | 148 | init { 149 | val window = window ?: error("Dialog has no window") 150 | window.requestFeature(Window.FEATURE_NO_TITLE) 151 | window.setBackgroundDrawableResource(android.R.color.transparent) 152 | WindowCompat.setDecorFitsSystemWindows(window, false) 153 | dialogLayout = ModalSheetDialogLayout( 154 | context = context, 155 | window = window, 156 | onPredictiveBack = onPredictiveBack, 157 | ).apply { 158 | // Set unique id for AbstractComposeView. This allows state restoration for the state 159 | // defined inside the Dialog via rememberSaveable() 160 | setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId") 161 | // Enable children to draw their shadow by not clipping them 162 | clipChildren = false 163 | // Allocate space for elevation 164 | with(density) { elevation = maxSupportedElevation.toPx() } 165 | // Simple outline to force window manager to allocate space for shadow. 166 | // Note that the outline affects clickable area for the dismiss listener. In case of 167 | // shapes like circle the area for dismiss might be to small (rectangular outline 168 | // consuming clicks outside of the circle). 169 | outlineProvider = object : ViewOutlineProvider() { 170 | override fun getOutline(view: View, result: Outline) { 171 | result.setRect(0, 0, view.width, view.height) 172 | // We set alpha to 0 to hide the view's shadow and let the composable to draw 173 | // its own shadow. This still enables us to get the extra space needed in the 174 | // surface. 175 | result.alpha = 0f 176 | } 177 | } 178 | } 179 | // Clipping logic removed because we are spanning edge to edge. 180 | setContentView(dialogLayout) 181 | dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner()) 182 | dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner()) 183 | dialogLayout.setViewTreeSavedStateRegistryOwner( 184 | composeView.findViewTreeSavedStateRegistryOwner(), 185 | ) 186 | dialogLayout.setViewTreeOnBackPressedDispatcherOwner(this) 187 | // Initial setup 188 | updateParameters(securePolicy, layoutDirection, darkThemeEnabled) 189 | } 190 | 191 | private fun setLayoutDirection(layoutDirection: LayoutDirection) { 192 | dialogLayout.layoutDirection = when (layoutDirection) { 193 | LayoutDirection.Ltr -> android.util.LayoutDirection.LTR 194 | LayoutDirection.Rtl -> android.util.LayoutDirection.RTL 195 | } 196 | } 197 | 198 | fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) { 199 | dialogLayout.setContent(parentComposition, children) 200 | } 201 | 202 | private fun setSecurePolicy(securePolicy: SecureFlagPolicy) { 203 | val secureFlagEnabled = 204 | securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled()) 205 | window!!.setFlags( 206 | if (secureFlagEnabled) { 207 | WindowManager.LayoutParams.FLAG_SECURE 208 | } else { 209 | WindowManager.LayoutParams.FLAG_SECURE.inv() 210 | }, 211 | WindowManager.LayoutParams.FLAG_SECURE, 212 | ) 213 | } 214 | 215 | fun updateParameters( 216 | securePolicy: SecureFlagPolicy, 217 | layoutDirection: LayoutDirection, 218 | darkThemeEnabled: Boolean, 219 | ) { 220 | setSecurePolicy(securePolicy) 221 | setLayoutDirection(layoutDirection) 222 | // Window flags to span parent window. 223 | window?.setLayout( 224 | WindowManager.LayoutParams.MATCH_PARENT, 225 | WindowManager.LayoutParams.MATCH_PARENT, 226 | ) 227 | window?.setSoftInputMode( 228 | if (Build.VERSION.SDK_INT >= 30) { 229 | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING 230 | } else { 231 | @Suppress("DEPRECATION") 232 | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE 233 | }, 234 | ) 235 | WindowCompat.getInsetsController(window!!, window!!.decorView).apply { 236 | isAppearanceLightStatusBars = !darkThemeEnabled 237 | isAppearanceLightNavigationBars = !darkThemeEnabled 238 | } 239 | } 240 | 241 | fun disposeComposition() { 242 | dialogLayout.disposeComposition() 243 | } 244 | 245 | override fun cancel() { 246 | // Prevents the dialog from dismissing itself 247 | return 248 | } 249 | } 250 | 251 | internal fun View.isFlagSecureEnabled(): Boolean { 252 | val windowParams = rootView.layoutParams as? WindowManager.LayoutParams 253 | if (windowParams != null) { 254 | return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0 255 | } 256 | return false 257 | } 258 | 259 | // Taken from AndroidPopup.android.kt 260 | private fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean { 261 | return when (this) { 262 | SecureFlagPolicy.SecureOff -> false 263 | SecureFlagPolicy.SecureOn -> true 264 | SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent 265 | } 266 | } 267 | --------------------------------------------------------------------------------