Navigation Results
The ability for a NavigationKey to define a result type is an important feature of Enro. It allows you to define a rich contract for a screen, where the contract represents not just an input type, but also an output type. This allows screens within an application to be more independent of one another and helps screens be as re-usable as possible.
Making use of this feature is very simple, and is done by defining a NavigationKey that implements NavigationKey.SupportsPush.WithResult
or NavigationKey.SupportsPresent.WithResult
. For more information on defining NavigationKeys, please see the NavigationKeys documentation.
Defining a NavigationKey.WithResult
Any NavigationKey can define a result type by implementing the ...WithResult<T>
interface, where T
is the type of the result. For example, a NavigationKey that returns a LocalDate
might look like this:
@Parcelize
data class SelectDate(
val minDate: LocalDate? = null,
val maxDate: LocalDate? = null,
) : NavigationKey.SupportsPresent.WithResult<LocalDate>
For more information on defining NavigationKeys, please see the NavigationKeys documentation.
Receiving results
Receiving a result from a NavigationKey works in a similar way to Android’s registerForActivityResult
functionality. To receive a result, you’ll need to create a NavigationResultChannel
using the registerForNavigationResult
function. This function takes a lambda that will be invoked when a result is received. The lambda will be invoked with the result value.
Defining a NavigationResultChannel
A NavigationResultChannel
can be defined in any Activity, Fragment, Composable or Enro-supported ViewModel. The syntax for defining a NavigationResultChannel
is slightly different depending on the type of screen you’re in, but always uses a function called registerForNavigationResult
.
registerForNavigationResult
takes the following arguments:
- (Required) A generic type argument, which represents the type of the result that will be received.
- (Required) An
onResult
lambda, which will be invoked when a result is received, which receives a single argument of the generic type provided toregisterForNavigationResult
. - (Optional) An
onClosed
lambda, which will be invoked when if a screen opened using the result channel is closed without a result being sent.
When you have created a result channel using registerForNavigationResult<T>
, the result channel can be used to push
or present
any NavigationKey that implements NavigationKey.SupportsPush.WithResult<T>
or NavigationKey.SupportsPresent.WithResult<T>
. The result channel will then receive the result from the destination, and invoke the onResult
lambda that was provided when creating the result channel. The T
type of the ...WithResult<T>
NavigationKey must match the T
used when creating the result channel.
If there are multiple result channels in the same screen, registered for the same T
, they can all be used to receive results, and the result channel that is used to push
or present
will be the one that receives the result (this works safely across configuration change and process death).
Activities/Fragments/ViewModels
In an Activity, Fragment or ViewModel, the registerForNavigationResult
function should be used as follows:
Composables
In a Composable, the registerForNavigationResult
function should be used as follows:
@Composable
fun ExampleComposable() {
val exampleResultChannel = registerForNavigationResult<ExampleResult> { result: ExampleResult ->
// handle result
}
Button(
onClick = { exampleResultChannel.present(ExampleResultNavigationKey()) }
) {
Text("Launch Example Result")
}
}
class MyActivityFragmentOrViewModel : ... {
private val exampleResultChannel by registerForNavigationResult<ExampleResult> { result: ExampleResult ->
// handle result
}
fun launchExampleResult() {
exampleResultChannel.present(ExampleResultNavigationKey())
}
}
Sending results
From a screen that is bound to a ...WithResult<T>
NavigationKey, you can send a result by calling the closeWithResult
function on the NavigationHandle. This function takes a single argument, which is the result value. The type of the result value must match the type of the ...WithResult<T>
NavigationKey. The closeWithResult<T>
function is only available for TypedNavigationHandle<K>
, where K
is a NavigationKey ...WithResult<T>
, which have been created through the navigationHandle<K>()
function. For more information on NavigationHandles, please see the NavigationHandles documentation.
Essentially, when you want to send a result, make sure you use the typed version of the navigationHandle
functions, and pass in the type of the NavigationKey ...WithResult<T>
that you want to send a result for, and then the closeWithResult<T>
will be available.
It is also possible to delegate a result to another screen, rather than return the result yourself. For more information on delegating results, please see the Delegating results section.
Composables
In a Composable, the closeWithResult
function should be used as follows:
@Parcelize
class ExampleResultKey: NavigationKey.SupportsPush.WithResult<ExampleResult>
@Composable
fun ExampleComposable() {
val navigationHandle = navigationHandle<ExampleResultKey>()
// private val navigationHandle = navigationHandle() <- this won't work, as it returns an untyped NavigationHandle
Button(
onClick = { navigationHandle.closeWithResult(ExampleResult(...)) }
) {
Text("Close with result")
}
}
Activities/Fragments/ViewModels
In an Activity, Fragment or ViewModel, the closeWithResult
function should be used as follows:
@Parcelize
class ExampleResultKey: NavigationKey.SupportsPush.WithResult<ExampleResult>
class ExampleFragmentActivityOrViewModel : ... {
private val navigationHandle by navigationHandle<ExampleResultKey>()
// private val navigationHandle by navigationHandle() <- this won't work, as it returns an untyped NavigationHandle
fun sendResult() {
navigationHandle.closeWithResult(ExampleResult(...))
}
}
Advanced
Delegating results
Enro provides support for delegating results to another screen. This functionality can be used to build small, multi-step flows that are made up of multiple screens. For example, a flow that requires the user to select a date, and then select a time, could be built using two screens, where the first screen delegates to the second screen. These kinds of flows are often referred to as “embedded navigation flows”, as the logic for the flow is embedded within each screen within the flow. When the screen which is delegated to returns a result, the screen that delegated to it will be closed at the same time, and the result will be delivered to the original screen that requested the result.
For example:
- Starting on “Screen A” where the backstack is
... -> A
- Screen A requests a result from Screen B, the backstack becomes
... -> A -> B
- Screen B delegates its result to Screen C, the backstack becomes
... -> A -> B -> C
- There are several possible outcomes: a. Screen C returns a result, which would cause the result to be delivered to Screen A, and the backstack would become
... -> A
b. Screen C closes without a result, which would cause no result to be delivered, but Screen C to close, and the backstack would become... A -> B
c. Screen C continues the by delegating to Screen D, the backstack would become... -> A -> B -> C -> D
, and the result behaviour would be the same when Screen D is closed; the result would be delivered to Screen A, and the backstack would become... -> A
To delegate a result, you’ll need to use the deliverResultFromPush
or deliverResultFromPresent
functions on the NavigationHandle
. This function takes a single argument, which is the NavigationKey
that you want to delegate to. The NavigationKey
that you delegate to must be a ...WithResult<T>
NavigationKey, where T
is the same type as the ...WithResult<T>
NavigationKey that you are delegating from.