├── .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 |
4 |
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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/List.kt:
--------------------------------------------------------------------------------
1 | package dev.hrach.navigation.demo.screens
2 |
3 | import androidx.compose.material3.Text
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | internal fun List() {
8 | Text("List")
9 | }
10 |
--------------------------------------------------------------------------------
/modalsheet/src/main/res/values-v30/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 3
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Profile.kt:
--------------------------------------------------------------------------------
1 | package dev.hrach.navigation.demo.screens
2 |
3 | import androidx.compose.material3.Text
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | internal fun Profile() {
8 | Text("Profile")
9 | }
10 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /caches
4 | /libraries
5 | /compiler.xml
6 | /deploymentTargetDropDown.xml
7 | /jarRepositories.xml
8 | /gradle.xml
9 | /misc.xml
10 | /modules.xml
11 | /workspace.xml
12 | /navEditor.xml
13 | /assetWizardSettings.xml
14 | /markdown.xml
15 | /uiDesigner.xml
16 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/results/api/results.api:
--------------------------------------------------------------------------------
1 | public final class dev/hrach/navigation/results/ResultsKt {
2 | public static final fun NavigationResultEffectImpl (Landroidx/navigation/NavBackStackEntry;Landroidx/navigation/NavController;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V
3 | public static final fun setResultImpl (Landroidx/navigation/NavController;Ljava/lang/Object;Lkotlinx/serialization/KSerializer;)V
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/release/signing-pack.sh:
--------------------------------------------------------------------------------
1 | read -sp "Enter the encrypt key: " ENCRYPT_KEY
2 | echo
3 |
4 | if [[ -n "$ENCRYPT_KEY" ]]; then
5 | openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in release/secring.gpg -out release/secring.gpg.aes -k ${ENCRYPT_KEY}
6 | openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in release/signing.properties -out release/signing.properties.aes -k ${ENCRYPT_KEY}
7 |
8 | else
9 | echo "Encrypt key is empty"
10 | fi
11 |
--------------------------------------------------------------------------------
/release/signing-unpack.sh:
--------------------------------------------------------------------------------
1 | read -sp "Enter the encrypt key: " ENCRYPT_KEY
2 | echo
3 |
4 | if [[ -n "$ENCRYPT_KEY" ]]; then
5 | openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in release/secring.gpg.aes -out release/secring.gpg -k ${ENCRYPT_KEY}
6 | openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in release/signing.properties.aes -out release/signing.properties -k ${ENCRYPT_KEY}
7 |
8 | else
9 | echo "Encrypt key is empty"
10 | fi
11 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/dev/hrach/navigation/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.hrach.navigation.demo
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 |
8 | public class MainActivity : ComponentActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | enableEdgeToEdge()
12 | setContent { App() }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/dev/hrach/navigation/demo/Destinations.kt:
--------------------------------------------------------------------------------
1 | package dev.hrach.navigation.demo
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | internal object Destinations {
6 | @Serializable
7 | data object Home
8 |
9 | @Serializable
10 | data object List
11 |
12 | @Serializable
13 | data object Profile
14 |
15 | @Serializable
16 | data object Modal1
17 |
18 | @Serializable
19 | data object Modal2
20 |
21 | @Serializable
22 | data object BottomSheet {
23 | @Serializable
24 | data class Result(
25 | val id: Int,
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 |
3 | compileSdk = "34"
4 | minSdk = "21"
5 |
6 | [libraries]
7 |
8 | kotlin-serialization-core = "org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3"
9 | kotlin-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
10 | appcompat = "androidx.appcompat:appcompat:1.6.1"
11 | androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime:2.8.5"
12 | compose-foundation = "androidx.compose.foundation:foundation:1.6.8"
13 | compose-material3 = "androidx.compose.material3:material3:1.3.0"
14 | navigation-compose = "androidx.navigation:navigation-compose:2.8.9"
15 | junit = { module = "junit:junit", version = "4.13.2" }
16 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | category-template: '### $TITLE'
4 | categories:
5 | - title: '🚀 Features'
6 | label: 'feature'
7 | - title: '🐛 Bug Fixes'
8 | label: 'bug'
9 | - title: '🧰 Maintenance'
10 | label: 'chore'
11 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
12 | change-title-escapes: '\<*_&'
13 | exclude-labels:
14 | - 'skip-changelog'
15 | version-resolver:
16 | major:
17 | labels:
18 | - 'semver:major'
19 | minor:
20 | labels:
21 | - 'semver:minor'
22 | patch:
23 | labels:
24 | - 'semver:patch'
25 | default: minor
26 | template: |
27 | Changes in this release:
28 |
29 | $CHANGES
30 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | org.gradle.configuration-cache=true
3 | org.gradle.configuration-cache.problems=warn
4 | android.useAndroidX=true
5 | kotlin.code.style=official
6 |
7 | GROUP=dev.hrach.navigation
8 | VERSION_NAME=0.8.0
9 |
10 | POM_URL=https://github.com/hrach/navigation-compose-ext/
11 | POM_SCM_URL=https://github.com/hrach/navigation-compose-ext/
12 | POM_SCM_CONNECTION=scm:git:git://github.com/hrach/navigation-compose-ext.git
13 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/hrach/navigation-compose-ext.git
14 |
15 | POM_LICENCE_NAME=MIT License
16 | POM_LICENCE_URL=https://github.com/hrach/navigation-compose-ext/blob/main/license.md
17 | POM_LICENCE_DIST=repo
18 |
19 | POM_DEVELOPER_ID=hrach
20 | POM_DEVELOPER_NAME=Jan Skrasek
21 |
22 | RELEASE_SIGNING_ENABLED=true
23 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | rootProject.name = "NavigationCompose"
4 |
5 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
6 |
7 | pluginManagement {
8 | resolutionStrategy {
9 | eachPlugin {
10 | when (requested.id.name) {
11 | "com.android.application" -> useModule("com.android.tools.build:gradle")
12 | }
13 | }
14 | }
15 | repositories {
16 | gradlePluginPortal()
17 | mavenCentral()
18 | google()
19 | }
20 | buildscript {
21 | repositories {
22 | google()
23 | }
24 | }
25 | }
26 |
27 | dependencyResolutionManagement {
28 | repositories {
29 | google()
30 | mavenCentral()
31 | }
32 | }
33 |
34 | include(":bottomsheet")
35 | include(":modalsheet")
36 | include(":results")
37 | include(":demo")
38 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | indent_style=tab
3 | indent_size=4
4 | ij_kotlin_imports_layout=*,^
5 | ij_kotlin_allow_trailing_comma=true
6 | ij_kotlin_allow_trailing_comma_on_call_site=true
7 | ktlint_experimental = enabled
8 | ktlint_standard_function-signature = disabled
9 | ktlint_standard_multiline-expression-wrapping = disabled
10 | ktlint_standard_chain-method-continuation = disabled # weird rule
11 | ktlint_standard_discouraged-comment-location = disabled # it disallows comments to multi-line fun args
12 | ktlint_standard_property-naming = disabled
13 | ktlint_standard_function-naming = disabled
14 | ktlint_standard_string-template-indent = disabled # due to multiline-expression-wrapping dependency
15 | ktlint_standard_class-signature = disabled # due to discouraged-comment-location
16 | ktlint_standard_if-else-wrapping = disabled # due to discouraged-comment-location
17 | ktlint_standard_no-empty-first-line-in-class-body = disabled # I don't like it
18 | ktlint_standard_function-expression-body = disabled # I don't like it
19 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 | ===========
3 |
4 | Copyright (c) 2024 Jan Škrášek
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/modalsheet/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | [](https://github.com/hrach/navigation-compose-ext/actions/workflows/build.yml)
5 | [](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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | xmlns:android
27 |
28 | ^$
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | xmlns:.*
38 |
39 | ^$
40 |
41 |
42 | BY_NAME
43 |
44 |
45 |
46 |
47 |
48 |
49 | .*:id
50 |
51 | http://schemas.android.com/apk/res/android
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | .*:name
61 |
62 | http://schemas.android.com/apk/res/android
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | name
72 |
73 | ^$
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | style
83 |
84 | ^$
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | .*
94 |
95 | ^$
96 |
97 |
98 | BY_NAME
99 |
100 |
101 |
102 |
103 |
104 |
105 | .*
106 |
107 | http://schemas.android.com/apk/res/android
108 |
109 |
110 | ANDROID_ATTRIBUTE_ORDER
111 |
112 |
113 |
114 |
115 |
116 |
117 | .*
118 |
119 | .*
120 |
121 |
122 | BY_NAME
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
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 |
--------------------------------------------------------------------------------