Skip to content

Commit

Permalink
* Updated dependency versions
Browse files Browse the repository at this point in the history
* Added `instruction` property directly to `NavigationContext`, to provide easy access to the instruction
* Added extensions `getViewModel` and `requireViewModel` to `NavigationContext` to access `ViewModels` directly from a context reference
* Added extensions for `findContext` and `findActiveContext` to `NavigationContext` to allow for finding other NavigationContexts from a context reference
* Updated `NavigationContainer` to add `getChildContext` which allows finding specific Active/ActivePushed/ActivePresented/Specific contexts from a container reference
* Added `instruction` property to `NavigationContext`, and marked `NavigationContext` as `@AdvancedEnroApi`
  • Loading branch information
isaac-udy committed Apr 25, 2024
1 parent 2ab25fe commit 470177a
Show file tree
Hide file tree
Showing 19 changed files with 1,280 additions and 58 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased
* Updated dependency versions
* Added `instruction` property directly to `NavigationContext`, to provide easy access to the instruction
* Added extensions `getViewModel` and `requireViewModel` to `NavigationContext` to access `ViewModels` directly from a context reference
* Added extensions for `findContext` and `findActiveContext` to `NavigationContext` to allow for finding other NavigationContexts from a context reference
* Updated `NavigationContainer` to add `getChildContext` which allows finding specific Active/ActivePushed/ActivePresented/Specific contexts from a container reference
* Added `instruction` property to `NavigationContext`, and marked `NavigationContext` as `@AdvancedEnroApi`

## 2.3.0
* Updated NavigationFlow to return from `next` after `onCompleted` is called, rather than continuing to set the backstack from the flow
* Updated NavigationContainer to take a `filter` of type NavigationContainerFilter instead of an `accept: (NavigationKey) -> Boolean` lambda. This allows for more advanced filtering of NavigationKeys, and this API will likely be expanded in the future.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public fun NavigationHandle.forward(key: NavigationKey) {
executeInstruction(NavigationInstruction.Forward(key))
}

@Deprecated("You should use a close instruction followed by a push or present")
@Deprecated("You should use a push or present followed by a close instruction")
public fun NavigationHandle.replace(key: NavigationKey) {
executeInstruction(NavigationInstruction.Replace(key))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dev.enro.core

import dev.enro.core.container.NavigationContainer
import kotlin.reflect.KClass


/**
* Finds a NavigationContext that matches the predicate. This will search the hierarchy of active NavigationContexts starting
* at the context it is invoked on (not the root). Only active child contexts are considered (e.g. if there are
* two containers visible, only the active container will be searched).
*
* If you want to search the entire hierarchy, including the root, you should call this function on the root NavigationContext,
* which can be accessed from any NavigationContext by using the [rootContext] function.
*/
public fun NavigationContext<*>.findActiveContext(predicate: (NavigationContext<*>) -> Boolean): NavigationContext<*>? {
val contexts = mutableListOf(this)
while (contexts.isNotEmpty()) {
val context = contexts.removeAt(0)
if (predicate(context)) {
return context
}
val children = context.containerManager.activeContainer?.let {
setOfNotNull(
it.getChildContext(NavigationContainer.ContextFilter.ActivePushed),
it.getChildContext(NavigationContainer.ContextFilter.ActivePresented),
)
}.orEmpty()
contexts.addAll(children)
}
return null
}

/**
* Requires an active NavigationContext that matches the predicate. A wrapper for [findActiveContext] that throws an exception if
* no matching context is found.
*
* @see [findActiveContext]
*/
public fun NavigationContext<*>.requireActiveContext(predicate: (NavigationContext<*>) -> Boolean): NavigationContext<*> {
return requireNotNull(findActiveContext(predicate))
}

/**
* Finds an active NavigationContext that has a NavigationKey of type [keyType].
*
* @see [findActiveContext]
*/
public fun NavigationContext<*>.findActiveContextWithKey(keyType: KClass<*>): NavigationContext<*>? {
return findActiveContext {
val key = it.instruction?.navigationKey ?: return@findActiveContext false
key::class == keyType
}
}

/**
* Requires an active NavigationContext that has a NavigationKey of type [keyType].
*
* @see [findActiveContext]
*/
public fun NavigationContext<*>.requireActiveContextWithKey(keyType: KClass<*>): NavigationContext<*> {
return requireContext {
val key = it.instruction?.navigationKey ?: return@requireContext false
key::class == keyType
}
}

/**
* Finds an active NavigationContext that has a NavigationKey of type [T].
*
* @see [findActiveContext]
*/
public inline fun <reified T> NavigationContext<*>.findActiveContextWithKey(): NavigationContext<*>? {
return findActiveContext { it.instruction?.navigationKey is T }
}

/**
* Requires an active NavigationContext that has a NavigationKey of type [T].
*
* @see [findActiveContext]
*/
public inline fun <reified T> NavigationContext<*>.requireActiveContextWithKey(): NavigationContext<*> {
return requireContext { it.instruction?.navigationKey is T }
}

/**
* Finds an active NavigationContext that has a NavigationKey of matching [predicate].
*
* @see [findActiveContext]
*/
public inline fun <reified T> NavigationContext<*>.findActiveContextWithKey(crossinline predicate: (NavigationKey) -> Boolean): NavigationContext<*>? {
return findActiveContext { it.instruction?.navigationKey?.let(predicate) ?: false }
}

/**
* Requires an active NavigationContext that has a NavigationKey of matching [predicate].
*
* @see [findActiveContext]
*/
public inline fun <reified T> NavigationContext<*>.requireActiveContextWithKey(crossinline predicate: (NavigationKey) -> Boolean): NavigationContext<*> {
return requireContext { it.instruction?.navigationKey?.let(predicate) ?: false }
}
100 changes: 100 additions & 0 deletions enro-core/src/main/java/dev/enro/core/NavigationContext.findContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package dev.enro.core

import dev.enro.core.container.NavigationContainer
import kotlin.reflect.KClass

/**
* Finds a NavigationContext that matches the predicate. This will search the entire hierarchy of NavigationContexts starting
* at the context it is invoked on (not the root). All child contexts are considered, including contexts which are not in the
* active NavigationContainer (e.g. if there are two containers visible, both the active and non-active container will be searched).
*
* If you want to search the entire hierarchy, including the root, you should call this function on the root NavigationContext,
* which can be accessed from any NavigationContext by using the [rootContext] function.
*/
public fun NavigationContext<*>.findContext(predicate: (NavigationContext<*>) -> Boolean): NavigationContext<*>? {
val contexts = mutableListOf(this)
while (contexts.isNotEmpty()) {
val context = contexts.removeAt(0)
if (predicate(context)) {
return context
}
val children = context.containerManager.containers.flatMap {
setOfNotNull(
it.getChildContext(NavigationContainer.ContextFilter.ActivePushed),
it.getChildContext(NavigationContainer.ContextFilter.ActivePresented),
)
}
contexts.addAll(children)
}
return null
}

/**
* Requires a NavigationContext that matches the predicate. A wrapper for [findContext] that throws an exception if
* no matching context is found.
*
* @see [findContext]
*/
public fun NavigationContext<*>.requireContext(predicate: (NavigationContext<*>) -> Boolean): NavigationContext<*> {
return requireNotNull(findContext(predicate))
}

/**
* Finds a NavigationContext that has a NavigationKey of type [keyType].
*
* @see [findContext]
*/
public fun NavigationContext<*>.findContextWithKey(keyType: KClass<*>): NavigationContext<*>? {
return findContext {
val key = it.instruction?.navigationKey ?: return@findContext false
key::class == keyType
}
}

/**
* Requires a NavigationContext that has a NavigationKey of type [keyType].
*
* @see [findContext]
*/
public fun NavigationContext<*>.requireContextWithKey(keyType: KClass<*>): NavigationContext<*> {
return requireContext {
val key = it.instruction?.navigationKey ?: return@requireContext false
key::class == keyType
}
}

/**
* Finds a NavigationContext that has a NavigationKey of type [T].
*
* @see [findContext]
*/
public inline fun <reified T> NavigationContext<*>.findContextWithKey(): NavigationContext<*>? {
return findContext { it.instruction?.navigationKey is T }
}

/**
* Requires a NavigationContext that has a NavigationKey of type [T].
*
* @see [findContext]
*/
public inline fun <reified T> NavigationContext<*>.requireContextWithKey(): NavigationContext<*> {
return requireContext { it.instruction?.navigationKey is T }
}

/**
* Finds a NavigationContext that has a NavigationKey of matching [predicate].
*
* @see [findContext]
*/
public inline fun <reified T> NavigationContext<*>.findContextWithKey(crossinline predicate: (NavigationKey) -> Boolean): NavigationContext<*>? {
return findContext { it.instruction?.navigationKey?.let(predicate) ?: false }
}

/**
* Requires a NavigationContext that has a NavigationKey of matching [predicate].
*
* @see [findContext]
*/
public inline fun <reified T> NavigationContext<*>.requireContextWithKey(crossinline predicate: (NavigationKey) -> Boolean): NavigationContext<*> {
return requireContext { it.instruction?.navigationKey?.let(predicate) ?: false }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package dev.enro.core

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import kotlin.reflect.KClass

/**
* When attempting to find a ViewModel in a NavigationContext, we don't want to create a new ViewModel, rather we want to
* get an existing instance of that ViewModel, if it exists, so this ViewModelProvider.Factory always throws an exception
* if it is ever asked to actually create a ViewModel.
*/
private class NavigationContextViewModelFactory(
private val context: NavigationContext<*>,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
viewModelNotFoundError(context, modelClass)
}

override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
viewModelNotFoundError(context, modelClass)
}
}

private fun viewModelNotFoundError(context: NavigationContext<*>, modelClass: Class<*>): Nothing {
val key = context.instruction?.navigationKey
error("ViewModel ${modelClass.simpleName} was not found in NavigationContext with navigation key $key")
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested, or null if the ViewModel does not exist in the NavigationContext's ViewModelStore
*/
public fun <T : ViewModel> NavigationContext<*>.getViewModel(
cls: Class<T>,
key: String? = null,
): T? {
val provider = ViewModelProvider(
store = viewModelStoreOwner.viewModelStore,
factory = NavigationContextViewModelFactory(this)
)
val result = kotlin.runCatching {
when (key) {
null -> provider[cls]
else -> provider[key, cls]
}
}
return result.getOrNull()
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested
*
* @throws IllegalStateException if the ViewModel does not already exist in the NavigationContext
*/
public fun <T : ViewModel> NavigationContext<*>.requireViewModel(
cls: Class<T>,
key: String? = null,
): T {
return getViewModel(cls, key)
?: viewModelNotFoundError(this, cls)
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested, or null if the ViewModel does not exist in the NavigationContext's ViewModelStore
*/
public fun <T : ViewModel> NavigationContext<*>.getViewModel(
cls: KClass<T>,
key: String? = null,
): T? {
return getViewModel(cls.java, key)
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested
*
* @throws IllegalStateException if the ViewModel does not already exist in the NavigationContext
*/
public fun <T : ViewModel> NavigationContext<*>.requireViewModel(
cls: KClass<T>,
key: String? = null,
): T {
return requireViewModel(cls.java, key)
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested, or null if the ViewModel does not exist in the NavigationContext's ViewModelStore
*/
public inline fun <reified T : ViewModel> NavigationContext<*>.getViewModel(
key: String? = null,
): T? {
return getViewModel(T::class.java, key)
}

/**
* Attempt to get a ViewModel of a certain type from a NavigationContext.
*
* @return The ViewModel requested
*
* @throws IllegalStateException if the ViewModel does not already exist in the NavigationContext
*/
public inline fun <reified T : ViewModel> NavigationContext<*>.requireViewModel(
key: String? = null,
): T {
return requireViewModel(T::class.java, key)
}
11 changes: 11 additions & 0 deletions enro-core/src/main/java/dev/enro/core/NavigationContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ public class NavigationContext<ContextType : Any> internal constructor(
public val controller: NavigationController get() = getController()
public val parentContext: NavigationContext<*>? get() = getParentContext()

/**
* The arguments provided to this NavigationContext. It is possible to read the open instruction from these arguments,
* but it may be different than the open instruction attached to the NavigationHandle. If the arguments do not contain
* a NavigationInstruction, a NavigationInstruction is still provided to the NavigationHandle, which will be either a
* default key (if one is provided with the destination) or a "NoNavigationKey" NavigationKey.
*
* Generally it should be preferred to read the instruction property, rather than read the instruction from the arguments.
*/
@AdvancedEnroApi
public val arguments: Bundle get() = getArguments()

public val instruction: NavigationInstruction.Open<*> by lazy { getNavigationHandle().instruction }
public val viewModelStoreOwner: ViewModelStoreOwner get() = getViewModelStoreOwner()
public val savedStateRegistryOwner: SavedStateRegistryOwner get() = getSavedStateRegistryOwner()
public val lifecycleOwner: LifecycleOwner get() = getLifecycleOwner()
Expand Down
Loading

0 comments on commit 470177a

Please sign in to comment.