Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement result passing #23

Merged
merged 1 commit into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kiwi.navigationcompose.typed

/**
* Marks the destinations as destination being able to return a result.
*
* The result type is defined as the generic argument if this interface and has to be KotlinX Serializable.
*
* Use [ComposableResultEffect] or [DialogResultEffect] to observe the result.
* Use [setResult] to send the result from a destination.
*
* ```
* sealed interface Destinations : Destination {
* // ...
* @Serializable
* object Dialog : Destinations, ResultDestination<Dialog.Result> {
* @Serializable
* data class Result(val name: String)
* }
* }
* ```
*
* Opening the same ResultDestination multiple times per screen has to be distinguished by manually sending an
* identifier to the result destination and this identifier has to be propagated back by the result type.
*/
public interface ResultDestination<T : Any> : Destination
126 changes: 126 additions & 0 deletions core/src/main/kotlin/com/kiwi/navigationcompose/typed/ResultSharing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.kiwi.navigationcompose.typed

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
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 composable destination's result. See [ResultDestination].
*
* Use only to process a result from "full" destination created by `composable` NavGraph builder function.
* If you are expecting a result from dialog or full-screen dialog, [DialogResultEffect] has to be used
* to properly handle configuration changes.
*
* To avoid specifying generic argument types, use type-inference and type the result argument of the block:
* ```
* ComposableResultEffect(navController) { result: Destinations.Dialog.Result ->
* // handle result
* }
* ```
*/
@Suppress("unused") // T generic parameter is a typecheck for R being the type from ResultDestination
@ExperimentalSerializationApi
@Composable
public inline fun <T : ResultDestination<R>, reified R : Any> ComposableResultEffect(
navController: NavController,
noinline block: (R) -> Unit,
) {
@Suppress("RemoveExplicitTypeArguments")
ResultEffectImpl(
navController = navController,
currentRoute = navController.currentDestination!!.route!!,
resultSerializer = serializer<R>(),
block = block,
)
}

/**
* Registers an effect for processing a dialog destination's result. See [ResultDestination].
*
* Use only to process a result from dialog (and full-screen dialog) destination created by `dialog` NavGraph
* builder function.
*
* To avoid specifying generic argument types, use type-inference and type the result argument of the block:
* ```
* DialogResultEffect(
* currentRoutePattern = createRoutePattern<Destinations.CurrentDestination>(),
* navController = navController,
* ) { result: Destinations.Dialog.Result ->
* ```
*/
@Suppress("unused") // T generic parameter is a typecheck for R being the type from ResultDestination
@ExperimentalSerializationApi
@Composable
public inline fun <T : ResultDestination<R>, reified R : Any> DialogResultEffect(
currentRoutePattern: String,
navController: NavController,
noinline block: (R) -> Unit,
) {
@Suppress("RemoveExplicitTypeArguments")
ResultEffectImpl(
navController = navController,
currentRoute = currentRoutePattern,
resultSerializer = serializer<R>(),
block = block,
)
}

/**
* Implementation of ResultEffect. Use [ComposableResultEffect] or [DialogResultEffect] directly.
*/
@ExperimentalSerializationApi
@Composable
public fun <R : Any> ResultEffectImpl(
navController: NavController,
currentRoute: String,
resultSerializer: KSerializer<R>,
block: (R) -> Unit,
) {
DisposableEffect(navController, block) {
// 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 backStackEntry = navController.getBackStackEntry(currentRoute)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(resultKey)) {
val result = backStackEntry.savedStateHandle.remove<String>(resultKey)!!
val decoded = Json.decodeFromString(resultSerializer, result)
block(decoded)
}
}
backStackEntry.lifecycle.addObserver(observer)
onDispose {
backStackEntry.lifecycle.removeObserver(observer)
}
}
}

/**
* Sets a destination result for the previous backstack entry's destination.
*
* The result type has to be KotlinX Serializable.
*/
@ExperimentalSerializationApi
@Suppress("unused") // T generic parameter is a typecheck for R being the type from ResultDestination
public inline fun <T : ResultDestination<R>, reified R : Any> NavController.setResult(
data: R,
) {
setResultImpl(serializer(), data)
}

@ExperimentalSerializationApi
public fun <R : Any> NavController.setResultImpl(
serializer: KSerializer<R>,
data: R,
) {
val result = Json.encodeToString(serializer, data)
val resultKey = serializer.descriptor.serialName + "_result"
previousBackStackEntry?.savedStateHandle?.set(resultKey, result)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kiwi.navigationcompose.typed.demo

import com.kiwi.navigationcompose.typed.Destination
import com.kiwi.navigationcompose.typed.ResultDestination
import kotlinx.serialization.Serializable

internal sealed interface Destinations : Destination {
Expand Down Expand Up @@ -30,3 +31,20 @@ internal sealed interface HomeDestinations : Destination {
val stringNullableOptional: String? = null,
) : HomeDestinations
}

internal sealed interface ProfileDestinations : Destination {
@Serializable
object Home : ProfileDestinations

@Serializable
object NameEditDialog : ProfileDestinations, ResultDestination<NameEditDialog.Result> {
@Serializable
data class Result(val name: String)
}

@Serializable
object NameEditScreen : ProfileDestinations, ResultDestination<NameEditScreen.Result> {
@Serializable
data class Result(val name: String)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import com.kiwi.navigationcompose.typed.createRoutePattern
import com.kiwi.navigationcompose.typed.demo.screens.Demo
import com.kiwi.navigationcompose.typed.demo.screens.Home
import com.kiwi.navigationcompose.typed.demo.screens.List
import com.kiwi.navigationcompose.typed.demo.screens.NameEditDialog
import com.kiwi.navigationcompose.typed.demo.screens.NameEditScreen
import com.kiwi.navigationcompose.typed.demo.screens.Profile
import com.kiwi.navigationcompose.typed.dialog
import com.kiwi.navigationcompose.typed.navigate
import com.kiwi.navigationcompose.typed.navigation
import kotlinx.serialization.ExperimentalSerializationApi
Expand All @@ -27,6 +30,12 @@ internal fun NavHost(navController: NavHostController) {
composable<HomeDestinations.Demo> { Demo(this, navController::navigateUp) }
}
composable<Destinations.List> { List() }
composable<Destinations.Profile> { Profile() }
navigation<Destinations.Profile>(
startDestination = createRoutePattern<ProfileDestinations.Home>(),
) {
composable<ProfileDestinations.Home> { Profile(navController) }
dialog<ProfileDestinations.NameEditDialog> { NameEditDialog(navController) }
composable<ProfileDestinations.NameEditScreen> { NameEditScreen(navController) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.kiwi.navigationcompose.typed.demo.screens

import androidx.compose.material.AlertDialog
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.navigation.NavController
import com.kiwi.navigationcompose.typed.demo.ProfileDestinations
import com.kiwi.navigationcompose.typed.setResult
import kotlinx.serialization.ExperimentalSerializationApi

@ExperimentalSerializationApi
@Composable
internal fun NameEditDialog(navController: NavController) {
NameEdit(
onNameSave = { name ->
navController.setResult(ProfileDestinations.NameEditDialog.Result(name))
navController.navigateUp()
},
onDismiss = navController::navigateUp,
)
}

@Composable
private fun NameEdit(
onNameSave: (String) -> Unit,
onDismiss: () -> Unit,
) {
var name by rememberSaveable { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Set your name") },
text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
)
},
confirmButton = {
TextButton(onClick = { onNameSave(name) }) {
Text("Save")
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.kiwi.navigationcompose.typed.demo.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.navigation.NavController
import com.kiwi.navigationcompose.typed.demo.ProfileDestinations
import com.kiwi.navigationcompose.typed.setResult
import kotlinx.serialization.ExperimentalSerializationApi

@ExperimentalSerializationApi
@Composable
internal fun NameEditScreen(navController: NavController) {
NameEdit(
onNameSave = { name ->
navController.setResult(ProfileDestinations.NameEditScreen.Result(name))
navController.navigateUp()
},
)
}

@Composable
private fun NameEdit(
onNameSave: (String) -> Unit,
) {
var name by rememberSaveable { mutableStateOf("") }
Column {
Text("Set your name")
OutlinedTextField(
value = name,
onValueChange = { name = it },
)
TextButton(onClick = { onNameSave(name) }) {
Text("Save")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
package com.kiwi.navigationcompose.typed.demo.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.kiwi.navigationcompose.typed.ComposableResultEffect
import com.kiwi.navigationcompose.typed.Destination
import com.kiwi.navigationcompose.typed.DialogResultEffect
import com.kiwi.navigationcompose.typed.createRoutePattern
import com.kiwi.navigationcompose.typed.demo.ProfileDestinations
import com.kiwi.navigationcompose.typed.navigate
import kotlinx.serialization.ExperimentalSerializationApi

@ExperimentalSerializationApi
@Composable
internal fun Profile() {
Text("Profile")
internal fun Profile(navController: NavController) {
var name by rememberSaveable { mutableStateOf("") }
var times by rememberSaveable { mutableStateOf(0) }

DialogResultEffect(
currentRoutePattern = createRoutePattern<ProfileDestinations.Home>(),
navController = navController,
) { result: ProfileDestinations.NameEditDialog.Result ->
name = "Dialog: ${result.name}"
times += 1
}
ComposableResultEffect(navController) { result: ProfileDestinations.NameEditScreen.Result ->
name = "Screen: ${result.name}"
times += 1
}

Profile(name, times, navController::navigate)
}

@Composable
private fun Profile(
name: String,
times: Int,
onNavigate: (Destination) -> Unit,
) {
Column(
Modifier
.fillMaxSize()
.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Profile")

Text("Name: $name, changed: $times")

OutlinedButton(
onClick = { onNavigate(ProfileDestinations.NameEditDialog) },
) {
Text("Change Name Dialog")
}
OutlinedButton(
onClick = { onNavigate(ProfileDestinations.NameEditScreen) },
) {
Text("Change Name Screen")
}
}
}
Loading