Skip to content

Commit

Permalink
Merge branch 'enro-2.8.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
isaac-udy committed Nov 15, 2024
2 parents fab4af2 + 2de8c1c commit c4c3c60
Show file tree
Hide file tree
Showing 98 changed files with 3,246 additions and 701 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ jobs:
- name: Compile example (KAPT)
run: ./gradlew :example:assembleDebug -PenroExampleUseKapt

# Compile test application with KAPT; we don't need to compile :tests:application with KSP,
# because it will be compiled with KSP as part of the "Run tests" job.
compile-test-application-kapt:
name: Compile test application (KAPT)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/[email protected]
with:
distribution: 'zulu'
java-version: 17

- name: Setup gradle
uses: gradle/gradle-build-action@v2

- name: Compile test application (KAPT)
run: ./gradlew :tests:application:assembleDebug -PenroExampleUseKapt

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .run/Enro [disableConnectedDeviceAnimations].run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>
1 change: 1 addition & 0 deletions .run/Enro [enableConnectedDeviceAnimations].run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
# Changelog

## 2.8.0
* Updated Compose to 1.7.1
* Added support for NavigationKey.WithExtras to `NavigationResultChannel` and `NavigationFlowScope`
* Updated `enro-test` methods to provide more descriptive error messages when assert/expect methods fail, and added kdoc comments to many of the functions
* Updated Composable navigation animations to use SeekableTransitionState, as a step towards supporting predictive back navigation animations
* Fixed a bug where managed flows (`registerForFlowResult`) that launch embedded flows (`deliverResultFromPush/Present`) were not correctly handling the result of the embedded flow
* Added `FragmentSharedElements` to provide a way to define shared elements for Fragment navigation, including a compatibility layer for Composable NavigationDestinations that want to use AndroidViews as shared elements with Fragments. See `FragmentsWithSharedElements.kt` in the test application for examples of how to use `FragmentSharedElements`
* Added `acceptFromFlow` as a `NavigationContainerFilter` for use on screens that build managed flows using `registerForFlowResult`. This filter will cause the `NavigationContainer` to only accept instructions that have been created as part a managed flow, and will reject instructions that are not part of a managed flow.
* Removed `isAnimating` from `ComposableNavigationContainer`, as it was unused internally, did not appear to be useful for external use cases, and was complicating Compose animation code. If this functionality *was* important to your use case, please create a Github issue to discuss your use case.
* Removed the requirement to provide a SavedStateHandle to `registerForFlowResult`. This should not affect any existing code, but if you were passing a SavedStateHandle to `registerForFlowResult`, you can now remove this parameter.
* NavigationHandles now have access to a SavedStateHandle internally, which removes the requirement to pass this through to `registerForFlowResult`
* Added `managedFlowDestination` as a way to create a managed flow as a standalone destination
* `managedFlowDestination` works in the same way you'd use `registerForFlowResult` to create a managed flow, but allows you to define the flow as a standalone destination that can be pushed or presented from other destinations, without the need to define a ViewModel and regular destination for the flow.
* `managedFlowDestination` is currently marked as an `@ExperimentalEnroApi`, and may be subject to change in future versions of Enro.
* For an example of a `managedFlowDestination`, see `dev.enro.tests.application.managedflow.UserInformationFlow` in the test application

* ⚠️ Updated result channel identifiers in preparation for Kotlin 2.0 ⚠️
* Kotlin 2.0 changes the way that lambdas are compiled, which has implications for `registerForNavigationResult` and how result channels are uniquely identified. Activites, Fragments, Composables and ViewModels that use `by registerForNavigationResult` directly will not be affected by this change. However, if you are creating result channels inside of other objects, such as delegates, helper objects, or extension functions, you should verify that these cases continue to work as expected. It is not expected that there will be issues, but if this does result in bugs in your application, please raise them on the Enro GitHub repository.

* ⚠️ Updated NavigationContainer handling of NavigationInstructionFilter ⚠️
* In versions of Enro before 2.8.0, NavigationContainers would always accept destinations that were presented (`NavigationInstruction.Present(...)`, `navigationHandle.present(...)`, etc), and would only enforce their instructionFilter for pushed instructions (`NavigationInstruction.Push(...)`, `navigationHandle.push(...)`, etc). This is no longer the default behavior, and NavigationContainers will apply their instructionFilter to all instructions.
* This behavior can be reverted to the previous behavior by setting `useLegacyContainerPresentBehavior` when creating a NavigationController for your application using `createNavigationController`.
* `useLegacyContainerPresentBehavior` will be removed in a future version of Enro, and it is recommended that you update your NavigationContainers to explicitly declare their instructionFilter for all instructions, not just pushed instructions.

## 2.7.0
* ⚠️ Updated to androidx.lifecycle 2.8.1 ⚠️
Expand Down
7 changes: 7 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

repositories {
mavenLocal()
google()
Expand All @@ -12,6 +14,11 @@ java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

dependencies {
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
Expand Down
11 changes: 6 additions & 5 deletions buildSrc/src/main/kotlin/configureAndroid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.the
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.FileInputStream
import java.util.Properties
Expand Down Expand Up @@ -72,17 +73,17 @@ private fun Project.commonAndroidConfig(
}

tasks.withType<KotlinCompile>() {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)

// We want to disable the automatic inclusion of the `dev.enro.annotations.AdvancedEnroApi` and `dev.enro.annotations.ExperimentalEnroApi`
// opt-ins when we're compiling the test application, so that we're not accidentally making changes that might break the public API by
// requiring the opt-ins.
if (path.startsWith(":tests:application")) {
return@kotlinOptions
return@compilerOptions
}
freeCompilerArgs += "-Xopt-in=dev.enro.annotations.AdvancedEnroApi"
freeCompilerArgs += "-Xopt-in=dev.enro.annotations.ExperimentalEnroApi"
freeCompilerArgs.add("-Xopt-in=dev.enro.annotations.AdvancedEnroApi")
freeCompilerArgs.add("-Xopt-in=dev.enro.annotations.ExperimentalEnroApi")
}
}

Expand Down
6 changes: 4 additions & 2 deletions buildSrc/src/main/kotlin/configureExplicitApi.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

fun Project.configureExplicitApi() {
tasks.withType<KotlinCompile>() {
kotlinOptions {
freeCompilerArgs += "-Xexplicit-api=strict"
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
freeCompilerArgs.add("-Xexplicit-api=strict")
}
}
}
39 changes: 26 additions & 13 deletions enro-core/src/main/java/dev/enro/animation/NavigationAnimations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Transition
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import dev.enro.core.AnyOpenInstruction
import dev.enro.core.EnroConfig
import dev.enro.core.NavigationDirection
import dev.enro.core.container.originalNavigationDirection
import dev.enro.extensions.KeepVisibleWith
import dev.enro.core.controller.NavigationApplication
import dev.enro.extensions.ResourceAnimatedVisibility
import dev.enro.extensions.getAttributeResourceId
import dev.enro.extensions.getNestedAttributeResourceId
Expand Down Expand Up @@ -51,9 +51,9 @@ public sealed interface NavigationAnimation {
internal abstract val forView: ForView

@androidx.compose.runtime.Composable
public abstract fun Animate(
internal abstract fun Animate(
visible: Transition<Boolean>,
content: @androidx.compose.runtime.Composable () -> Unit
content: @androidx.compose.runtime.Composable (Transition<EnterExitState>) -> Unit,
)

public companion object {
Expand Down Expand Up @@ -84,24 +84,34 @@ public sealed interface NavigationAnimation {
val exit: ExitTransition = ExitTransition.None,
override val forView: ForView = DefaultAnimations.ForView.noneEnter,
) : Composable(), Enter, Exit {
@OptIn(ExperimentalAnimationApi::class)
@androidx.compose.runtime.Composable
override fun Animate(visible: Transition<Boolean>, content: @androidx.compose.runtime.Composable () -> Unit) {
override fun Animate(
visible: Transition<Boolean>,
content: @androidx.compose.runtime.Composable (Transition<EnterExitState>) -> Unit,
) {
val context = LocalContext.current
val config = remember(context) {
val navigationApplication = (context.applicationContext as? NavigationApplication)
navigationApplication?.navigationController?.config ?: EnroConfig()
}

val resourceAnimation = remember(this, forView) { forView.asResource(context.theme) }
visible.AnimatedVisibility(
visible = { it },
enter = enter,
exit = exit,
) {
transition.ResourceAnimatedVisibility(
visible = { it == EnterExitState.Visible },
enter = resourceAnimation.id,
exit = resourceAnimation.id,
) {
content()
if (config.enableViewAnimationsForCompose) {
transition.ResourceAnimatedVisibility(
visible = { it == EnterExitState.Visible },
enter = resourceAnimation.id,
exit = resourceAnimation.id,
) {
content(transition)
}
} else {
content(transition)
}
KeepVisibleWith(visible)
}
}
}
Expand All @@ -112,9 +122,11 @@ public sealed interface NavigationAnimation {
is Attr -> Resource(
theme.getAttributeResourceId(attr),
)

is Theme -> Resource(
id(theme),
)

is Composable -> forView.asResource(theme)
}

Expand Down Expand Up @@ -182,6 +194,7 @@ public object DefaultAnimations {
NavigationDirection.Present -> ForView.presentCloseEnter
else -> ForView.pushCloseEnter
}

NavigationDirection.Push, NavigationDirection.Forward -> ForView.pushCloseEnter
else -> ForView.presentCloseEnter
}
Expand Down
23 changes: 23 additions & 0 deletions enro-core/src/main/java/dev/enro/core/EnroConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.enro.core

import dev.enro.core.controller.EnroBackConfiguration

public data class EnroConfig(
internal val isInTest: Boolean = false,
internal val isAnimationsDisabled: Boolean = false,
internal val isStrictMode: Boolean = false,
/**
* In versions of Enro before 2.8.0, NavigationContainers would always accept destinations that were presented, and
* would only enforce their navigation instruction filter for pushed instructions. This is no longer the default
* behavior, but can be re-enabled by setting this Boolean to true.
*/
@Deprecated("This behavior is no longer recommended, and will be removed in a future version of Enro. Please update your NavigationContainers to use a NavigationInstructionFilter that explicitly declares all instructions that are valid for the container.")
internal val useLegacyContainerPresentBehavior: Boolean = false,
internal val backConfiguration: EnroBackConfiguration = EnroBackConfiguration.Default,
/**
* This Boolean sets whether or not Composables will attempt to fallback to View based animations (Animation or Animator)
* when there are no Composable Enter/ExitTransition animations provided. This is disabled by default for tests, based
* on checking for the presence of the JUnit Test class, because these animations cause issues with ComposeTestRule tests.
*/
internal val enableViewAnimationsForCompose: Boolean = runCatching { Class.forName("org.junit.Test") }.isFailure,
)
4 changes: 2 additions & 2 deletions enro-core/src/main/java/dev/enro/core/EnroExceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public abstract class EnroException(

val message =
"Opened ${args.key::class.java.simpleName} as a ${args.instruction.navigationDirection::class.java.simpleName} instruction. Forward and Replace type instructions are deprecated, please replace these with Push and Present instructions."
if (navigationController.isStrictMode) {
if (navigationController.config.isStrictMode) {
throw LegacyNavigationDirectionUsedInStrictMode(message)
} else {
Log.w("Enro", "$message Enro would have thrown in strict mode.")
Expand All @@ -84,7 +84,7 @@ public abstract class EnroException(
) {
val message =
"Attempted to Push to ${navigationKey::class.java.simpleName}, but could not find a valid container."
if (navigationController.isStrictMode) {
if (navigationController.config.isStrictMode) {
throw MissingContainerForPushInstruction(message)
} else {
Log.w(
Expand Down
21 changes: 14 additions & 7 deletions enro-core/src/main/java/dev/enro/core/NavigationHandle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ public interface TypedNavigationHandle<T : NavigationKey> : NavigationHandle {
internal class TypedNavigationHandleImpl<T : NavigationKey>(
internal val navigationHandle: NavigationHandle,
private val type: Class<T>
): TypedNavigationHandle<T> {
) : TypedNavigationHandle<T> {
override val id: String get() = navigationHandle.id
override val instruction: NavigationInstruction.Open<*> = navigationHandle.instruction
override val dependencyScope: EnroDependencyScope get() = navigationHandle.dependencyScope

@Suppress("UNCHECKED_CAST")
override val key: T get() = navigationHandle.key as? T
?: throw EnroException.IncorrectlyTypedNavigationHandle("TypedNavigationHandle failed to cast key of type ${navigationHandle.key::class.java.simpleName} to ${type.simpleName}")
override val key: T
get() = navigationHandle.key as? T
?: throw EnroException.IncorrectlyTypedNavigationHandle("TypedNavigationHandle failed to cast key of type ${navigationHandle.key::class.java.simpleName} to ${type.simpleName}")

override val lifecycle: Lifecycle get() = navigationHandle.lifecycle

override fun executeInstruction(navigationInstruction: NavigationInstruction) = navigationHandle.executeInstruction(navigationInstruction)
override fun executeInstruction(navigationInstruction: NavigationInstruction) =
navigationHandle.executeInstruction(navigationInstruction)
}

public fun <T : NavigationKey> NavigationHandle.asTyped(type: KClass<T>): TypedNavigationHandle<T> {
Expand Down Expand Up @@ -125,9 +127,9 @@ public fun NavigationHandle.requestClose() {
internal fun NavigationHandle.runWhenHandleActive(block: () -> Unit) {
val isMainThread = runCatching {
Looper.getMainLooper() == Looper.myLooper()
}.getOrElse { dependencyScope.get<NavigationController>().isInTest } // if the controller is in a Jvm only test, the block above may fail to run
}.getOrElse { dependencyScope.get<NavigationController>().config.isInTest } // if the controller is in a Jvm only test, the block above may fail to run

if(isMainThread && lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
if (isMainThread && lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
block()
} else {
lifecycleScope.launch {
Expand All @@ -136,4 +138,9 @@ internal fun NavigationHandle.runWhenHandleActive(block: () -> Unit) {
}
}
}
}
}

internal val NavigationHandle.enroConfig: EnroConfig
get() = runCatching {
dependencyScope.get<NavigationController>().config
}.getOrElse { EnroConfig() }
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public class LazyNavigationHandleConfiguration<T : NavigationKey>(
if (handle is NavigationHandleViewModel) {
handle.internalOnCloseRequested =
{ onCloseRequested(navigationHandle.asTyped(keyType)) }
} else if (handle.dependencyScope.get<NavigationController>().isInTest) {
} else if (handle.dependencyScope.get<NavigationController>().config.isInTest) {
val field = handle::class.java.declaredFields
.firstOrNull { it.name.startsWith("internalOnCloseRequested") }
?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import dev.enro.core.controller.EnroDependencyScope
import dev.enro.core.controller.get
import dev.enro.core.controller.usecase.GetNavigationBinding

/**
* A NavigationHostFactory allows for destinations of different types to be interoperable with each other. For example,
* a Fragment destination can host a Composable destination. There are two important functions to register here:
* - supports: This function should return true if the NavigationHostFactory can host the provided NavigationInstruction.Open
* - wrap: This function should return a new NavigationInstruction.Open that is compatible with the HostType
*/
@AdvancedEnroApi
public abstract class NavigationHostFactory<HostType: Any>(
public val hostType: Class<HostType>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ public abstract class NavigationContainer(
public fun accept(
instruction: AnyOpenInstruction
): Boolean {
return (instructionFilter.accept(instruction) || instruction.navigationDirection == NavigationDirection.Present)
val isPresentedWithLegacyBehavior = context.controller.config.useLegacyContainerPresentBehavior
&& instruction.navigationDirection == NavigationDirection.Present

return (instructionFilter.accept(instruction) || isPresentedWithLegacyBehavior)
&& acceptedByContext(instruction)
&& canInstructionBeHostedAs(
hostType = contextType,
Expand Down
Loading

0 comments on commit c4c3c60

Please sign in to comment.