Skip to content

Commit

Permalink
Add updates to NavigationFlow for transient and NavigationFlowReference
Browse files Browse the repository at this point in the history
  • Loading branch information
isaac-udy committed May 1, 2024
1 parent 3d92497 commit 3c925bb
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* Added `instruction` property to `NavigationContext`, and marked `NavigationContext` as `@AdvancedEnroApi`
* Updated `NavigationContext` and `NavigationHandle` to bind each other to allow for easier access to the other from either reference, and to ensure the lazy references are still available while the context is being referenced
* Updated result handling for forwarding results to fix several bugs and improve behaviour (including correctly handling forwarded results through Activities)
* Added `transient` configuration to NavigationFlow steps, which allows a step to only be re-executed if it's dependencies have changed
* Added `navigationFlowReference` as a parcealble object which can be passed to NavigationKeys, and then later used to retrieve the parent navigation flow
* Prevent more than one registerForNavigationResult from occurring within the context of a single NavigationHandle
* Remove `next` from the public API of NavigationFlow, in favour of doing this automatically on creation of the flow

## 2.3.0
* Updated NavigationFlow to return from `next` after `onCompleted` is called, rather than continuing to set the backstack from the flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dev.enro.core.NavigationHandle
import dev.enro.core.NavigationInstruction
import dev.enro.core.NavigationKey
import dev.enro.core.container.toBackstack
import dev.enro.core.controller.usecase.extras
import dev.enro.core.onActiveContainer
import dev.enro.core.result.NavigationResultChannel
import dev.enro.core.result.internal.ResultChannelImpl
Expand All @@ -27,6 +28,7 @@ internal fun interface CreateResultChannel {

@dev.enro.annotations.ExperimentalEnroApi
public class NavigationFlow<T> internal constructor(
internal val reference: NavigationFlowReference,
private val savedStateHandle: SavedStateHandle,
private val navigation: NavigationHandle,
private val resultManager: FlowResultManager,
Expand All @@ -42,15 +44,16 @@ public class NavigationFlow<T> internal constructor(
private val resultChannel = registerForNavigationResult(
onClosed = { key ->
val step = key as? FlowStep<Any> ?: return@registerForNavigationResult
if (step.stepId == steps.lastOrNull()?.stepId) {
resultManager.clear(step)
steps = steps.dropLast(1)
}
resultManager.clear(step)
steps = steps
.dropLastWhile { step.stepId != it.stepId }
.dropLast(1)
.dropLastWhile { it.isTransient }
},
onResult = { key, result ->
val step = key as? FlowStep<Any> ?: return@registerForNavigationResult
resultManager.set(step, result)
next()
update()
},
)

Expand All @@ -60,19 +63,19 @@ public class NavigationFlow<T> internal constructor(
}
}

public fun next() {
val flowScope = NavigationFlowScope(resultManager)
runCatching { return@next onCompleted(flowScope.flow()) }
internal fun update() {
val flowScope = NavigationFlowScope(this, resultManager, reference)
runCatching { return@update onCompleted(flowScope.flow()) }
.recover {
when(it) {
when (it) {
is NavigationFlowScope.NoResult -> {}
is NavigationFlowScope.Escape -> return
else -> throw it
}
}
.getOrThrow()

val resultChannelId = (resultChannel as ResultChannelImpl<*,*>).id
val resultChannelId = (resultChannel as ResultChannelImpl<*, *>).id
val oldSteps = steps
steps = flowScope.steps
navigation.onActiveContainer {
Expand All @@ -85,30 +88,28 @@ public class NavigationFlow<T> internal constructor(
.groupBy { it.first.stepId }
.mapValues { it.value.lastOrNull() }

val instructions = steps.map { step ->
val existingStep = existingInstructions[step.stepId]?.second?.takeIf {
oldSteps
.firstOrNull { it.stepId == step.stepId }
?.dependsOn == step.dependsOn
val instructions = steps
.filterIndexed { index, flowStep ->
if (index == steps.lastIndex) return@filterIndexed true
!flowStep.isTransient
}
existingStep ?: NavigationInstruction.Open.OpenInternal(
navigationDirection = step.direction,
navigationKey = step.key,
resultKey = step,
resultId = resultChannelId,
extras = mutableMapOf(
IS_PUSHED_IN_FLOW to (step.direction is NavigationDirection.Push)
.map { step ->
val existingStep = existingInstructions[step.stepId]?.second?.takeIf {
oldSteps
.firstOrNull { it.stepId == step.stepId }
?.dependsOn == step.dependsOn
}
existingStep ?: NavigationInstruction.Open.OpenInternal(
navigationDirection = step.direction,
navigationKey = step.key,
resultKey = step,
resultId = resultChannelId,
extras = mutableMapOf(
IS_PUSHED_IN_FLOW to (step.direction is NavigationDirection.Push)
)
)
)
}
val finalInstructions = instructions
.filter { it.navigationDirection == NavigationDirection.Push }
.plus(
instructions.lastOrNull().takeIf { it?.navigationDirection == NavigationDirection.Present }
)
.filterNotNull()

setBackstack(finalInstructions.toBackstack())
}
setBackstack(instructions.toBackstack())
}
}

Expand All @@ -121,6 +122,8 @@ public class NavigationFlow<T> internal constructor(
internal companion object {
const val IS_PUSHED_IN_FLOW = "NavigationFlow.IS_PUSHED_IN_FLOW"
const val STEPS_KEY = "NavigationFlow.STEPS_KEY"
const val RESULT_FLOW_ID = "NavigationFlow.RESULT_FLOW_ID"
const val RESULT_FLOW = "NavigationFlow.RESULT_FLOW"
}
}

Expand All @@ -131,9 +134,17 @@ public fun <T> ViewModel.registerForFlowResult(
): PropertyDelegateProvider<ViewModel, ReadOnlyProperty<ViewModel, NavigationFlow<T>>> {
return PropertyDelegateProvider { thisRef, property ->
val navigationHandle = getNavigationHandle()
val resultManager = FlowResultManager.create(navigationHandle, savedStateHandle)

val resultFlowId = property.name
val boundResultFlowId = navigationHandle.extras[NavigationFlow.RESULT_FLOW_ID]
require(boundResultFlowId == null || boundResultFlowId == resultFlowId) {
"Only one registerForFlowResult can be created per NavigationHandle. Found an existing result flow for $boundResultFlowId."
}
navigationHandle.extras[NavigationFlow.RESULT_FLOW_ID] = resultFlowId

val resultManager = FlowResultManager.create(navigationHandle, savedStateHandle)
val navigationFlow = NavigationFlow(
reference = NavigationFlowReference(resultFlowId),
savedStateHandle = savedStateHandle,
navigation = navigationHandle,
resultManager = resultManager,
Expand All @@ -146,6 +157,8 @@ public fun <T> ViewModel.registerForFlowResult(
flow = flow,
onCompleted = onCompleted,
)
navigationHandle.extras[NavigationFlow.RESULT_FLOW] = navigationFlow
navigationFlow.update()
ReadOnlyProperty { _, _ -> navigationFlow }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.enro.core.result.flows

import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import dev.enro.annotations.ExperimentalEnroApi
import dev.enro.core.NavigationHandle
import dev.enro.core.compose.navigationHandle
import dev.enro.core.controller.usecase.extras
import dev.enro.core.getParentNavigationHandle
import kotlinx.parcelize.Parcelize

/**
* NavigationFlowReference is a reference to a NavigationFlow, and is available in NavigationFlowScope when building a
* NavigationFlow. It can be passed to a NavigationKey to allow the screen that the NavigationKey represents to interact
* with the navigation flow and perform actions such as returning to previous steps within the flow to edit items.
*/
@Parcelize
@ExperimentalEnroApi
public class NavigationFlowReference internal constructor(
internal val id: String,
) : Parcelable

@ExperimentalEnroApi
public fun NavigationHandle.getNavigationFlow(reference: NavigationFlowReference): NavigationFlow<*> {
val parent = getParentNavigationHandle() ?: error("No parent navigation handle found")
val flow = parent.extras[NavigationFlow.RESULT_FLOW] as NavigationFlow<*>
require(flow.reference.id == reference.id) {
"NavigationFlowReference does not match the current flow"
}
return flow
}

@Composable
@ExperimentalEnroApi
public fun rememberNavigationFlowReference(
reference: NavigationFlowReference,
): NavigationFlow<*> {
val navigationHandle = navigationHandle()
return remember(navigationHandle) {
navigationHandle.getNavigationFlow(reference)
}
}
25 changes: 19 additions & 6 deletions enro-core/src/main/java/dev/enro/core/result/flows/FlowStep.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package dev.enro.core.result.flows

import android.os.Parcelable
import dev.enro.core.NavigationDirection
import dev.enro.core.NavigationKey
import kotlinx.parcelize.Parcelize

public sealed interface FlowStepConfiguration : Parcelable{
@Parcelize
public data object Transient : FlowStepConfiguration
}

@Parcelize
public data class FlowStep<Result: Any> private constructor(
val stepId: String,
val key: NavigationKey,
val dependsOn: Long,
val direction: NavigationDirection,
public class FlowStep<Result: Any> private constructor(
@PublishedApi internal val stepId: String,
@PublishedApi internal val key: NavigationKey,
@PublishedApi internal val dependsOn: Long,
@PublishedApi internal val direction: NavigationDirection,
@PublishedApi internal val configuration: Set<FlowStepConfiguration>,
) : NavigationKey.SupportsPush.WithResult<Result>,
NavigationKey.SupportsPresent.WithResult<Result> {

Expand All @@ -18,11 +25,13 @@ public data class FlowStep<Result: Any> private constructor(
key: NavigationKey,
dependsOn: List<Any?>,
direction: NavigationDirection,
configuration: Set<FlowStepConfiguration>,
) : this(
stepId = stepId,
key = key,
dependsOn = dependsOn.contentHash(),
direction = direction,
configuration = configuration,
)

override fun equals(other: Any?): Boolean {
Expand All @@ -35,6 +44,7 @@ public data class FlowStep<Result: Any> private constructor(
if (key != other.key) return false
if (dependsOn != other.dependsOn) return false
if (direction != other.direction) return false
if (configuration != other.configuration) return false

return true
}
Expand All @@ -44,9 +54,12 @@ public data class FlowStep<Result: Any> private constructor(
result = 31 * result + key.hashCode()
result = 31 * result + dependsOn.hashCode()
result = 31 * result + direction.hashCode()
result = 31 * result + configuration.hashCode()
return result
}

}

private fun List<Any?>.contentHash(): Long = fold(0L) { result, it -> 31L * result + it.hashCode() }

internal val FlowStep<*>.isTransient: Boolean
get() = configuration.contains(FlowStepConfiguration.Transient)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.enro.core.NavigationKey

@AdvancedEnroApi
public class FlowStepActions<T: NavigationKey.WithResult<*>>(
private val flow: NavigationFlow<*>,
private val resultManager: FlowResultManager,
private val step: FlowStep<out Any>
) {
Expand All @@ -24,6 +25,11 @@ public class FlowStepActions<T: NavigationKey.WithResult<*>>(
.clear(step)
}

public fun editStep() {
clearResult()
flow.update()
}

public companion object {
public fun <R : Any> FlowStepActions<out NavigationKey.WithResult<in R>>.setResult(result: R) {
setResultUnsafe(result)
Expand All @@ -46,7 +52,7 @@ public inline fun <reified T : NavigationKey.WithResult<*>> NavigationFlow<*>.ge
it.key is T && block(it.key)
}
?.let {
FlowStepActions(getResultManager(), it)
FlowStepActions(this, getResultManager(), it)
}
}

Expand All @@ -66,7 +72,7 @@ public inline fun <reified T : NavigationKey.WithResult<*>> NavigationFlowScope.
it.key is T && block(it.key)
}
?.let {
FlowStepActions(resultManager, it)
FlowStepActions(flow, resultManager, it)
}
}

Expand Down
Loading

0 comments on commit 3c925bb

Please sign in to comment.