From 24eed586785a66c1f06cf32caca4fa88693f0f14 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Wed, 22 May 2024 00:13:56 +0200 Subject: [PATCH] add results module --- demo/build.gradle.kts | 3 +- .../dev/hrach/navigation/demo/Destinations.kt | 7 +- .../navigation/demo/screens/BottomSheet.kt | 11 ++- .../dev/hrach/navigation/demo/screens/Home.kt | 32 +++++- .../hrach/navigation/demo/screens/Modal1.kt | 31 +++++- gradle/libs.versions.toml | 4 +- readme.md | 42 +++++++- results/.gitignore | 1 + results/build.gradle.kts | 61 ++++++++++++ results/gradle.properties | 4 + .../dev/hrach/navigation/results/Results.kt | 97 +++++++++++++++++++ settings.gradle.kts | 1 + 12 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 results/.gitignore create mode 100644 results/build.gradle.kts create mode 100644 results/gradle.properties create mode 100644 results/src/main/kotlin/dev/hrach/navigation/results/Results.kt diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 56f846f..77783e6 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -60,8 +60,9 @@ kotlinter { dependencies { implementation(projects.bottomsheet) implementation(projects.modalsheet) + implementation(projects.results) - implementation(libs.kotlin.serialization) + implementation(libs.kotlin.serialization.core) implementation(libs.compose.material3) implementation(libs.navigation.compose) } diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/Destinations.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/Destinations.kt index 9294bf7..79a67dc 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/Destinations.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/Destinations.kt @@ -19,5 +19,10 @@ internal object Destinations { data object Modal2 @Serializable - data object BottomSheet + data object BottomSheet { + @Serializable + data class Result( + val id: Int, + ) + } } diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/BottomSheet.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/BottomSheet.kt index ca136a1..cc6d073 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/BottomSheet.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/BottomSheet.kt @@ -14,6 +14,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import dev.hrach.navigation.demo.Destinations +import dev.hrach.navigation.results.setResult +import kotlin.random.Random @Composable internal fun BottomSheet(navController: NavController) { @@ -21,7 +24,13 @@ internal fun BottomSheet(navController: NavController) { Text("This is a bottomsheet") var value by rememberSaveable { mutableStateOf("") } OutlinedTextField(value = value, onValueChange = { value = it }) - OutlinedButton(onClick = { navController.popBackStack() }, Modifier.fillMaxWidth()) { + OutlinedButton( + onClick = { + navController.setResult(Destinations.BottomSheet.Result(Random.nextInt())) + navController.popBackStack() + }, + modifier = Modifier.fillMaxWidth(), + ) { Text("Close") } } diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Home.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Home.kt index c4db0a6..e8cb359 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Home.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Home.kt @@ -1,25 +1,53 @@ package dev.hrach.navigation.demo.screens +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.navigation.NavController import dev.hrach.navigation.demo.Destinations +import dev.hrach.navigation.results.NavigationResultEffect +@SuppressLint("UnrememberedGetBackStackEntry") @Composable internal fun Home( navController: NavController, +) { + var bottomSheetResult by rememberSaveable { mutableIntStateOf(-1) } + NavigationResultEffect( + backStackEntry = remember(navController) { navController.getBackStackEntry() }, + navController = navController, + ) { result -> + bottomSheetResult = result.id + } + Home( + navigate = navController::navigate, + bottomSheetResult = bottomSheetResult, + ) +} + +@Composable +private fun Home( + navigate: (Any) -> Unit, + bottomSheetResult: Int, ) { Column { Text("Home") - OutlinedButton(onClick = { navController.navigate(Destinations.Modal1) }) { + OutlinedButton(onClick = { navigate(Destinations.Modal1) }) { Text("Modal 1") } - OutlinedButton(onClick = { navController.navigate(Destinations.BottomSheet) }) { + OutlinedButton(onClick = { navigate(Destinations.BottomSheet) }) { Text("BottomSheet") } + + Text("BottomSheetResult: $bottomSheetResult") } } diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt index 17bc2ed..a5725c2 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt @@ -1,5 +1,6 @@ package dev.hrach.navigation.demo.screens +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets @@ -10,12 +11,37 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.navigation.NavController import dev.hrach.navigation.demo.Destinations +import dev.hrach.navigation.results.NavigationResultEffect +@SuppressLint("UnrememberedGetBackStackEntry") @Composable internal fun Modal1(navController: NavController) { + var bottomSheetResult by rememberSaveable { mutableIntStateOf(-1) } + NavigationResultEffect( + backStackEntry = remember(navController) { navController.getBackStackEntry() }, + navController = navController, + ) { result -> + bottomSheetResult = result.id + } + Modal1( + navigate = navController::navigate, + bottomSheetResult = bottomSheetResult, + ) +} + +@Composable +private fun Modal1( + navigate: (Any) -> Unit, + bottomSheetResult: Int, +) { Column( Modifier .fillMaxSize() @@ -23,11 +49,12 @@ internal fun Modal1(navController: NavController) { .windowInsetsPadding(WindowInsets.systemBars), ) { Text("Modal 1") - OutlinedButton(onClick = { navController.navigate(Destinations.Modal2) }) { + OutlinedButton(onClick = { navigate(Destinations.Modal2) }) { Text("Modal 2") } - OutlinedButton(onClick = { navController.navigate(Destinations.BottomSheet) }) { + OutlinedButton(onClick = { navigate(Destinations.BottomSheet) }) { Text("BottomSheet") } + Text("BottomSheetResult: $bottomSheetResult") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6a6d1f..80e6d8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,10 @@ minSdk = "21" [libraries] -kotlin-serialization = "org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3" +kotlin-serialization-core = "org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3" +kotlin-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" appcompat = "androidx.appcompat:appcompat:1.6.1" +androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime:2.8.0" compose-material3 = "androidx.compose.material3:material3:1.3.0-beta01" navigation-compose = "androidx.navigation:navigation-compose:2.8.0-beta01" junit = { module = "junit:junit", version = "4.13.2" } diff --git a/readme.md b/readme.md index 7513d72..41ab21c 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,7 @@ Use Maven Central and these dependencies: dependencies { implementation("dev.hrach.navigation:bottomsheet:") implementation("dev.hrach.navigation:modalsheet:") + implementation("dev.hrach.navigation:results:") } ``` @@ -19,6 +20,7 @@ Components: - **BottomSheet** - Connects the official Material 3 BottomSheet with Jetpack Navigation. - **ModalSheet** - A custom destination type for Jetpack Navigation that brings fullscreen content with modal animation. +- **Results** - Passing a result simply between destinations. Quick setup: @@ -28,12 +30,44 @@ val bottomSheetNavigator = remember { BottomSheetNavigator() } val navController = rememberNavController(modalSheetNavigator, bottomSheetNavigator) NavHost( - navController = navController, - startDestination = Destinations.Home, + navController = navController, + startDestination = Destinations.Home, ) { - modalSheet { Modal1(navController) } - bottomSheet { BottomSheet(navController) } + composable { Home(navController) } + modalSheet { Modal(navController) } + bottomSheet { BottomSheet(navController) } } ModalSheetHost(modalSheetNavigator) BottomSheetHost(bottomSheetNavigator) ``` + +Results sharing: + +```kotlin +object Destinations { + @Serializable + data object BottomSheet { + @Serializable + data class Result( + val id: Int, + ) + } +} + +@Composable +fun Home(navController: NavController) { + NavigationResultEffect( + backStackEntry = remember(navController) { navController.getBackStackEntry() }, + navController = navController, + ) { result -> + // process result - + } +} + +@Composable +fun BottomSheet(navController: NavController) { + OutlineButton(onClick = { navController.setResult(Destinations.BottomSheet.Result(42)) }) { + Text("Close") + } +} +``` diff --git a/results/.gitignore b/results/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/results/.gitignore @@ -0,0 +1 @@ +/build diff --git a/results/build.gradle.kts b/results/build.gradle.kts new file mode 100644 index 0000000..2cd4875 --- /dev/null +++ b/results/build.gradle.kts @@ -0,0 +1,61 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.kotlinx.binary-compatibility-validator") + id("com.vanniktech.maven.publish") + id("com.gradleup.nmcp") + id("org.jmailen.kotlinter") +} + +version = property("VERSION_NAME") as String + +android { + namespace = "dev.hrach.navigation.results" + + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + buildConfig = false + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + freeCompilerArgs = freeCompilerArgs.toMutableList().apply { + add("-Xexplicit-api=strict") + }.toList() + } + + lint { + disable.add("GradleDependency") + abortOnError = true + warningsAsErrors = true + } +} + +nmcp { + publishAllPublications {} +} + +kotlinter { + reporters = arrayOf("json") +} + +dependencies { + implementation(libs.navigation.compose) + implementation(libs.kotlin.serialization.json) + implementation(libs.androidx.lifecycle.runtime) + testImplementation(libs.junit) +} diff --git a/results/gradle.properties b/results/gradle.properties new file mode 100644 index 0000000..d2e8a95 --- /dev/null +++ b/results/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=results +POM_NAME=Navigation Results sharing +POM_DESCRIPTION=Results sharing between screens API for Jetpack Navigation Compose +POM_PACKAGING=aar diff --git a/results/src/main/kotlin/dev/hrach/navigation/results/Results.kt b/results/src/main/kotlin/dev/hrach/navigation/results/Results.kt new file mode 100644 index 0000000..5efa789 --- /dev/null +++ b/results/src/main/kotlin/dev/hrach/navigation/results/Results.kt @@ -0,0 +1,97 @@ +package dev.hrach.navigation.results + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer + +/** + * Registers an effect for processing a dialog destination's result. + * + * To work properly, obtain the "current" destinations backstack using [NavController.getBackStackEntry]. + * + * !!! DO NOT USE !!! [NavController.currentBackStackEntry] !!! DO NOT USE !!! + * as it may provide a dialog's entry instead of the source's entry that should receive + * e.g. the dialog's result. See the [official documentation](https://developer.android.com/guide/navigation/use-graph/programmatic#additional_considerations). + * + * ```kotlin + * NavigationResultEffect( + * backStackEntry = remember(navController) { navController.getBackStackEntry() }, + * navController = navController, + * ) { result: Destinations.Dialog.Result -> + * // process result + * } + * ``` + */ +@Composable +public inline fun NavigationResultEffect( + backStackEntry: NavBackStackEntry, + navController: NavController, + noinline block: (R) -> Unit, +) { + NavigationResultEffectImpl( + backStackEntry = backStackEntry, + navController = navController, + resultSerializer = serializer(), + block = block, + ) +} + +/** + * Implementation of ResultEffect. Use [NavigationResultEffect]. + */ +@OptIn(ExperimentalSerializationApi::class) +@PublishedApi +@Composable +internal fun NavigationResultEffectImpl( + backStackEntry: NavBackStackEntry, + navController: NavController, + resultSerializer: KSerializer, + block: (R) -> Unit, +) { + DisposableEffect(navController) { + // The implementation is based on the official documentation of the Result sharing. + // It takes into consideration the possibility of a dialog usage (see the docs). + // https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations + val resultKey = resultSerializer.descriptor.serialName + "_result" + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(resultKey)) { + val result = backStackEntry.savedStateHandle.remove(resultKey)!! + val decoded = Json.decodeFromString(resultSerializer, result) + block(decoded) + } + } + backStackEntry.lifecycle.addObserver(observer) + onDispose { + backStackEntry.lifecycle.removeObserver(observer) + } + } +} + +/** + * Sets a result for the previous backstack entry. + * + * The result type has to be KotlinX Serializable. + */ +public inline fun NavController.setResult( + data: R, +) { + setResultImpl(data, serializer()) +} + +@OptIn(ExperimentalSerializationApi::class) +@PublishedApi +internal fun NavController.setResultImpl( + data: R, + resultSerializer: KSerializer, +) { + val result = Json.encodeToString(resultSerializer, data) + val resultKey = resultSerializer.descriptor.serialName + "_result" + previousBackStackEntry?.savedStateHandle?.set(resultKey, result) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4fa7a07..36556c9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,4 +33,5 @@ dependencyResolutionManagement { include(":bottomsheet") include(":modalsheet") +include(":results") include(":demo")