Skip to content

Commit

Permalink
implement result passing
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Dec 21, 2022
1 parent 30f28ae commit 7d4e6d5
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 7 deletions.
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 the 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 the 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

0 comments on commit 7d4e6d5

Please sign in to comment.