From 64f52c694ef99772748782d690a2d3f4a9dbc61f Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 9 Sep 2023 16:35:55 +0100 Subject: [PATCH 01/21] Added support for kotlinx-serialization, deprecated Parcelable APIs. --- build.gradle.kts | 1 + decompose/api/android/decompose.api | 18 +++++ decompose/api/jvm/decompose.api | 10 +++ decompose/build.gradle.kts | 3 + .../decompose/ParcelizeDeprecatedMessage.kt | 4 ++ .../arkivanov/decompose/router/SavedData.kt | 32 +++++++++ .../router/children/ChildrenFactory.kt | 49 +++++++++++++ .../router/pages/ChildPagesFactory.kt | 72 +++++++++++++++++++ .../decompose/router/slot/ChildSlotFactory.kt | 51 +++++++++++++ .../router/stack/ChildStackFactory.kt | 71 ++++++++++++++++++ deps.versions.toml | 10 ++- sample/shared/shared/build.gradle.kts | 2 +- .../shared/cards/DefaultCardsComponent.kt | 8 +-- .../shared/cards/card/DefaultCardComponent.kt | 12 ++-- .../counters/DefaultCountersComponent.kt | 8 +-- .../counter/DefaultCounterComponent.kt | 18 +++-- .../DefaultCustomNavigationComponent.kt | 28 ++++---- .../DefaultKittenComponent.kt | 12 ++-- .../DefaultDynamicFeaturesComponent.kt | 11 +-- .../DefaultDynamicFeatureComponent.kt | 13 ++-- .../multipane/DefaultMultiPaneComponent.kt | 27 +++---- .../shared/root/DefaultRootComponent.kt | 17 ++--- 22 files changed, 395 insertions(+), 82 deletions(-) create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/ParcelizeDeprecatedMessage.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/SavedData.kt diff --git a/build.gradle.kts b/build.gradle.kts index 19c309e05..780291ffb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ buildscript { classpath(deps.jetbrains.compose.composeGradlePlug) classpath(deps.jetbrains.kotlinx.binaryCompatibilityValidator) classpath(deps.parcelizeDarwin.gradlePlug) + classpath(deps.jetbrains.kotlin.serializationGradlePlug) } } diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api index b0509a57b..ffcf8d4b7 100644 --- a/decompose/api/android/decompose.api +++ b/decompose/api/android/decompose.api @@ -82,6 +82,14 @@ public final class com/arkivanov/decompose/errorhandler/ErrorHandlersKt { public static final fun setOnDecomposeError (Lkotlin/jvm/functions/Function1;)V } +public final class com/arkivanov/decompose/router/ParcelableJson$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arkivanov/decompose/router/ParcelableJson; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/arkivanov/decompose/router/ParcelableJson; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public abstract interface class com/arkivanov/decompose/router/children/ChildNavState { public abstract fun getConfiguration ()Ljava/lang/Object; public abstract fun getStatus ()Lcom/arkivanov/decompose/router/children/ChildNavState$Status; @@ -98,7 +106,9 @@ public final class com/arkivanov/decompose/router/children/ChildNavState$Status public final class com/arkivanov/decompose/router/children/ChildrenFactoryKt { public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun children$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun children$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/children/NavState { @@ -208,8 +218,10 @@ public final class com/arkivanov/decompose/router/pages/ChildPages { public final class com/arkivanov/decompose/router/pages/ChildPagesFactoryKt { public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static final fun getDefaultPageStatus (ILcom/arkivanov/decompose/router/pages/Pages;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status; } @@ -288,8 +300,10 @@ public final class com/arkivanov/decompose/router/slot/ChildSlot { public final class com/arkivanov/decompose/router/slot/ChildSlotFactoryKt { public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/slot/SlotNavigation : com/arkivanov/decompose/router/slot/SlotNavigationSource, com/arkivanov/decompose/router/slot/SlotNavigator { @@ -344,8 +358,12 @@ public final class com/arkivanov/decompose/router/stack/ChildStack { public final class com/arkivanov/decompose/router/stack/ChildStackFactoryKt { public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/stack/StackNavigation : com/arkivanov/decompose/router/stack/StackNavigationSource, com/arkivanov/decompose/router/stack/StackNavigator { diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api index a088b2da1..1b2244013 100644 --- a/decompose/api/jvm/decompose.api +++ b/decompose/api/jvm/decompose.api @@ -84,7 +84,9 @@ public final class com/arkivanov/decompose/router/children/ChildNavState$Status public final class com/arkivanov/decompose/router/children/ChildrenFactoryKt { public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun children$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun children$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/children/NavState { @@ -186,8 +188,10 @@ public final class com/arkivanov/decompose/router/pages/ChildPages { public final class com/arkivanov/decompose/router/pages/ChildPagesFactoryKt { public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childPages (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childPages$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/pages/PagesNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static final fun getDefaultPageStatus (ILcom/arkivanov/decompose/router/pages/Pages;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status; } @@ -258,8 +262,10 @@ public final class com/arkivanov/decompose/router/slot/ChildSlot { public final class com/arkivanov/decompose/router/slot/ChildSlotFactoryKt { public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childSlot (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlin/reflect/KClass;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childSlot$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/slot/SlotNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/slot/SlotNavigation : com/arkivanov/decompose/router/slot/SlotNavigationSource, com/arkivanov/decompose/router/slot/SlotNavigator { @@ -314,8 +320,12 @@ public final class com/arkivanov/decompose/router/stack/ChildStack { public final class com/arkivanov/decompose/router/stack/ChildStackFactoryKt { public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function0;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; } public abstract interface class com/arkivanov/decompose/router/stack/StackNavigation : com/arkivanov/decompose/router/stack/StackNavigationSource, com/arkivanov/decompose/router/stack/StackNavigator { diff --git a/decompose/build.gradle.kts b/decompose/build.gradle.kts index f63ca0681..9b3838c50 100644 --- a/decompose/build.gradle.kts +++ b/decompose/build.gradle.kts @@ -9,6 +9,7 @@ import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") + id("kotlinx-serialization") id("kotlin-parcelize") id("com.arkivanov.parcelize.darwin") id("com.arkivanov.gradle.setup") @@ -51,6 +52,8 @@ kotlin { api(deps.essenty.stateKeeper) api(deps.essenty.instanceKeeper) api(deps.essenty.backHandler) + api(deps.jetbrains.kotlinx.kotlinxSerializationCore) + implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson) } common.test.dependencies { diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ParcelizeDeprecatedMessage.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ParcelizeDeprecatedMessage.kt new file mode 100644 index 000000000..5c7d269bb --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ParcelizeDeprecatedMessage.kt @@ -0,0 +1,4 @@ +package com.arkivanov.decompose + +internal const val PARCELIZE_DEPRECATED_MESSAGE = + "Parcelize compiler plugin will not work with KMP and K2 compiler. Please use similar API based on kotlinx-serialization." diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/SavedData.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/SavedData.kt new file mode 100644 index 000000000..5d89739d7 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/SavedData.kt @@ -0,0 +1,32 @@ +package com.arkivanov.decompose.router + +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.ParcelableContainer +import com.arkivanov.essenty.parcelable.Parcelize +import com.arkivanov.essenty.parcelable.consumeRequired +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json + +private val json = + Json { + allowStructuredMapKeys = true + } + +@Parcelize +private class ParcelableJson(val json: String) : Parcelable + +// Temporary interop until v3.0 +internal fun T.toParcelableContainer(strategy: SerializationStrategy): ParcelableContainer = + ParcelableContainer( + value = ParcelableJson( + json = json.encodeToString(serializer = strategy, value = this), + ), + ) + +// Temporary interop until v3.0 +internal fun ParcelableContainer.consumeRequired(strategy: DeserializationStrategy): T = + json.decodeFromString( + deserializer = strategy, + string = consumeRequired().json, + ) diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt index 3e5136894..9558edb81 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt @@ -2,7 +2,10 @@ package com.arkivanov.decompose.router.children import com.arkivanov.decompose.Child import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.PARCELIZE_DEPRECATED_MESSAGE import com.arkivanov.decompose.backhandler.child +import com.arkivanov.decompose.router.consumeRequired +import com.arkivanov.decompose.router.toParcelableContainer import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback @@ -13,6 +16,50 @@ import com.arkivanov.essenty.parcelable.ParcelableContainer import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.essenty.parcelable.consumeRequired import com.arkivanov.essenty.statekeeper.consume +import kotlinx.serialization.KSerializer + +/** + * A convenience method for the main [children] method. Allows having `Serializable` navigation state [N], + * so it's automatically saved and restored. This method can be used if the custom save/restore logic + * is not required. + */ +fun ComponentContext.children( + source: NavigationSource, + stateSerializer: KSerializer?, + initialState: () -> N, + key: String, + navTransformer: (state: N, event: E) -> N, + stateMapper: (state: N, children: List>) -> S, + onStateChanged: (newState: N, oldState: N?) -> Unit = { _, _ -> }, + onEventComplete: (event: E, newState: N, oldState: N) -> Unit = { _, _, _ -> }, + backTransformer: (state: N) -> (() -> N)? = { null }, + childFactory: (configuration: C, componentContext: ComponentContext) -> T, +): Value where N : NavState, N : Any = + children( + source = source, + saveState = { state -> + if (stateSerializer != null) { + state.toParcelableContainer(strategy = stateSerializer) + } else { + null + } + }, + restoreState = { container -> + if (stateSerializer != null) { + container.consumeRequired(strategy = stateSerializer) + } else { + null + } + }, + initialState = initialState, + key = key, + navTransformer = navTransformer, + stateMapper = stateMapper, + onStateChanged = onStateChanged, + onEventComplete = onEventComplete, + backTransformer = backTransformer, + childFactory = childFactory, + ) /** * Initialised and manages a generic list of components. This is an API for custom navigation models. @@ -151,6 +198,8 @@ fun , S : Any> ComponentContext.child * so it's automatically saved and restored. This method can be used if the custom save/restore logic * is not required. */ +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated(message = PARCELIZE_DEPRECATED_MESSAGE) inline fun ComponentContext.children( source: NavigationSource, key: String, diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPagesFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPagesFactory.kt index 9e9a61364..49792ee67 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPagesFactory.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPagesFactory.kt @@ -2,18 +2,87 @@ package com.arkivanov.decompose.router.pages import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.PARCELIZE_DEPRECATED_MESSAGE import com.arkivanov.decompose.router.children.ChildNavState import com.arkivanov.decompose.router.children.ChildNavState.Status import com.arkivanov.decompose.router.children.NavState import com.arkivanov.decompose.router.children.SimpleChildNavState import com.arkivanov.decompose.router.children.children +import com.arkivanov.decompose.router.consumeRequired +import com.arkivanov.decompose.router.toParcelableContainer import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.ParcelableContainer import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.essenty.parcelable.consumeRequired +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable import kotlin.reflect.KClass +/** + * Initializes and manages a list of components with one selected (active) component. + * The list can be empty. + * + * @param source a source of navigation events. + * @param serializer an optional [KSerializer] to be used for serializing and deserializing configurations. + * If `null` then the navigation state will not be preserved. + * @param initialPages an initial state of Child Pages that should be set + * if there is no saved state. See [Pages] for more information. + * @param key a key of the list, must be unique if there are multiple Child Pages used in + * the same component. + * @param pageStatus a function that returns a [Status] of a page at a given index. + * By default, the currently selected page is [Status.ACTIVE], its two neighbours + * are [Status.INACTIVE], and the rest are [Status.DESTROYED]. You can implement your own + * logic, for example with circular behaviour. + * @param handleBackButton determines whether the previous component should be automatically + * selected on back button press or not, default is `false`. + * @param childFactory a factory function that creates new child instances. + * @return an observable [Value] of [ChildPages]. + */ +@ExperimentalDecomposeApi +fun ComponentContext.childPages( + source: PagesNavigationSource, + serializer: KSerializer?, + initialPages: () -> Pages = { Pages() }, + key: String = "DefaultChildPages", + pageStatus: (index: Int, Pages) -> Status = ::getDefaultPageStatus, + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T, +): Value> = + childPages( + source = source, + savePages = { pages -> + if (serializer != null) { + SerializablePages(items = pages.items, selectedIndex = pages.selectedIndex) + .toParcelableContainer(strategy = SerializablePages.serializer(serializer)) + } else { + null + } + }, + restorePages = { container -> + if (serializer != null) { + val pages = container.consumeRequired(strategy = SerializablePages.serializer(serializer)) + Pages( + items = pages.items, + selectedIndex = pages.selectedIndex, + ) + } else { + null + } + }, + initialPages = initialPages, + key = key, + pageStatus = pageStatus, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) + +@Serializable +private class SerializablePages( + val items: List, + val selectedIndex: Int, +) + /** * Initializes and manages a list of components with one selected (active) component. * The list can be empty. @@ -34,6 +103,8 @@ import kotlin.reflect.KClass * @param childFactory a factory function that creates new child instances. * @return an observable [Value] of [ChildPages]. */ +@Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION") +@Deprecated(message = PARCELIZE_DEPRECATED_MESSAGE) @ExperimentalDecomposeApi inline fun ComponentContext.childPages( source: PagesNavigationSource, @@ -76,6 +147,7 @@ inline fun ComponentContext.childPages( * @param childFactory a factory function that creates new child instances. * @return an observable [Value] of [ChildPages]. */ +@Deprecated(message = PARCELIZE_DEPRECATED_MESSAGE) @ExperimentalDecomposeApi fun ComponentContext.childPages( source: PagesNavigationSource, diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt index 7383aa901..0c82404fa 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt @@ -2,15 +2,64 @@ package com.arkivanov.decompose.router.slot import com.arkivanov.decompose.Child import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.PARCELIZE_DEPRECATED_MESSAGE import com.arkivanov.decompose.router.children.ChildNavState.Status import com.arkivanov.decompose.router.children.NavState import com.arkivanov.decompose.router.children.SimpleChildNavState import com.arkivanov.decompose.router.children.children +import com.arkivanov.decompose.router.consumeRequired +import com.arkivanov.decompose.router.toParcelableContainer import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.ParcelableContainer +import kotlinx.serialization.KSerializer import kotlin.reflect.KClass +/** + * Initializes and manages a slot for one child component. + * The child component can be either active or dismissed (destroyed). + * + * @param source a source of navigation events. + * @param serializer an optional [KSerializer] to be used for serializing and deserializing configurations. + * If `null` then the navigation state will not be preserved. + * @param key a key of the slot, must be unique within the parent (hosting) component. + * @param initialConfiguration a component configuration that should be shown if there is + * no saved state, return `null` to show nothing. + * @param handleBackButton determines whether the child component should be automatically dismissed + * on back button press or not, default is `false`. + * @param childFactory a factory function that creates new child instances. + * @return an observable [Value] of [ChildSlot]. + */ +fun ComponentContext.childSlot( + source: SlotNavigationSource, + serializer: KSerializer?, + initialConfiguration: () -> C? = { null }, + key: String = "DefaultChildSlot", + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T, +): Value> = + childSlot( + source = source, + saveConfiguration = { configuration -> + if ((serializer != null) && (configuration != null)) { + configuration.toParcelableContainer(strategy = serializer) + } else { + null + } + }, + restoreConfiguration = { container -> + if (serializer != null) { + container.consumeRequired(strategy = serializer) + } else { + null + } + }, + key = key, + initialConfiguration = initialConfiguration, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) + /** * Initializes and manages a slot for one child component. * The child component can be either active or dismissed (destroyed). @@ -27,6 +76,8 @@ import kotlin.reflect.KClass * @param childFactory a factory function that creates new child instances. * @return an observable [Value] of [ChildSlot]. */ +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated(message = PARCELIZE_DEPRECATED_MESSAGE) fun ComponentContext.childSlot( source: SlotNavigationSource, configurationClass: KClass, diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt index 63740fb46..675beba0a 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt @@ -2,17 +2,87 @@ package com.arkivanov.decompose.router.stack import com.arkivanov.decompose.Child import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.PARCELIZE_DEPRECATED_MESSAGE import com.arkivanov.decompose.router.children.ChildNavState.Status import com.arkivanov.decompose.router.children.NavState import com.arkivanov.decompose.router.children.SimpleChildNavState import com.arkivanov.decompose.router.children.children +import com.arkivanov.decompose.router.consumeRequired +import com.arkivanov.decompose.router.toParcelableContainer import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.ParcelableContainer import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.essenty.parcelable.consumeRequired +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer import kotlin.reflect.KClass +/** + * Initializes and manages a stack of components. + * + * @param source a source of navigation events. + * @param serializer an optional [KSerializer] to be used for serializing and deserializing configurations. + * If `null` then the navigation state will not be preserved. + * @param initialStack a stack of component configurations (ordered from tail to head) that should be set + * if there is no saved state, must be not empty and unique. + * @param key a key of the stack, must be unique if there are multiple stacks in the same component. + * @param handleBackButton determines whether the stack should be automatically popped on back button press or not, + * default is `false`. + * @param childFactory a factory function that creates new child instances. + * @return an observable [Value] of [ChildStack]. + */ +fun ComponentContext.childStack( + source: StackNavigationSource, + serializer: KSerializer?, + initialStack: () -> List, + key: String = "DefaultChildStack", + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T, +): Value> = + childStack( + source = source, + saveStack = { stack -> + if (serializer != null) { + stack.toParcelableContainer(strategy = ListSerializer(serializer)) + } else { + null + } + }, + restoreStack = { container -> + if (serializer != null) { + container.consumeRequired(strategy = ListSerializer(serializer)) + } else { + null + } + }, + initialStack = initialStack, + key = key, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) + +/** + * A convenience extension function for [ComponentContext.childStack]. + */ +@Suppress("DeprecatedCallableAddReplaceWith") +fun ComponentContext.childStack( + source: StackNavigationSource, + serializer: KSerializer?, + initialConfiguration: C, + key: String = "DefaultChildStack", + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T +): Value> = + childStack( + source = source, + serializer = serializer, + initialStack = { listOf(initialConfiguration) }, + key = key, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) + /** * Initializes and manages a stack of components. * @@ -28,6 +98,7 @@ import kotlin.reflect.KClass * @param childFactory a factory function that creates new child instances. * @return an observable [Value] of [ChildStack]. */ +@Deprecated(message = PARCELIZE_DEPRECATED_MESSAGE) fun ComponentContext.childStack( source: StackNavigationSource, initialStack: () -> List, diff --git a/deps.versions.toml b/deps.versions.toml index b1df4e0e4..b077f18dd 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,14 +1,15 @@ [versions] -decompose = "2.1.0" +decompose = "2.2.0-dev02" kotlin = "1.9.10" -essenty = "1.2.0" -parcelizeDarwin = "0.2.1" +essenty = "1.3.0-alpha01" +parcelizeDarwin = "0.2.2" reaktive = "1.2.3" junit = "4.13.2" jetbrainsCompose = "1.5.1" jetbrainsKotlinWrappers = "1.0.0-pre.608" jetbrainsKotlinxCoroutines = "1.6.4" +jetbrainsKotlinxSerialization = "1.6.0" jetbrainsBinaryCompatibilityValidator = "0.13.2" jetpackCompose = "1.5.0" jetpackComposeCompiler = "1.5.3" @@ -46,6 +47,9 @@ jetbrains-kotlinWrappers-kotlinWrappersBom = { group = "org.jetbrains.kotlin-wra jetbrains-kotlinx-binaryCompatibilityValidator = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version.ref = "jetbrainsBinaryCompatibilityValidator" } jetbrains-kotlinx-kotlinxCoroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "jetbrainsKotlinxCoroutines" } jetbrains-kotlinx-kotlinxCoroutinesSwing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "jetbrainsKotlinxCoroutines" } +jetbrains-kotlin-serializationGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } +jetbrains-kotlinx-kotlinxSerializationCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "jetbrainsKotlinxSerialization" } +jetbrains-kotlinx-kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jetbrainsKotlinxSerialization" } androidx-compose-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "jetpackCompose" } androidx-compose-ui-uiTestJunit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "jetpackCompose" } diff --git a/sample/shared/shared/build.gradle.kts b/sample/shared/shared/build.gradle.kts index bfa6e918f..a85ef6885 100644 --- a/sample/shared/shared/build.gradle.kts +++ b/sample/shared/shared/build.gradle.kts @@ -10,7 +10,7 @@ plugins { id("kotlin-multiplatform") id("com.android.library") id("kotlin-parcelize") - id("com.arkivanov.parcelize.darwin") // Optional, only if you need state preservation on Darwin (Apple) targets + id("kotlinx-serialization") id("com.arkivanov.gradle.setup") } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/DefaultCardsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/DefaultCardsComponent.kt index cd42866d0..b3230cdff 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/DefaultCardsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/DefaultCardsComponent.kt @@ -9,10 +9,9 @@ import com.arkivanov.decompose.router.stack.navigate import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.cards.card.CardComponent import com.arkivanov.sample.shared.cards.card.DefaultCardComponent +import kotlinx.serialization.Serializable class DefaultCardsComponent( componentContext: ComponentContext, @@ -23,6 +22,7 @@ class DefaultCardsComponent( private val _stack: Value> = childStack( source = navigation, + serializer = Config.serializer(), initialStack = { COLORS.mapIndexed { index, color -> Config(color = color, number = index + 1) @@ -76,9 +76,9 @@ class DefaultCardsComponent( ) } - @Parcelize + @Serializable private data class Config( val color: Long, val number: Int, - ) : Parcelable + ) } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/DefaultCardComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/DefaultCardComponent.kt index 73306f058..a26be906d 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/DefaultCardComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/DefaultCardComponent.kt @@ -7,9 +7,6 @@ import com.arkivanov.decompose.value.update import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.essenty.lifecycle.subscribe -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize -import com.arkivanov.essenty.statekeeper.consume import com.arkivanov.sample.shared.cards.card.CardComponent.Model import com.badoo.reaktive.disposable.Disposable import com.badoo.reaktive.disposable.scope.DisposableScope @@ -19,6 +16,7 @@ import com.badoo.reaktive.scheduler.Scheduler import com.badoo.reaktive.scheduler.mainScheduler import com.badoo.reaktive.subject.behavior.BehaviorObservable import com.badoo.reaktive.subject.behavior.BehaviorSubject +import kotlinx.serialization.Serializable class DefaultCardComponent( componentContext: ComponentContext, @@ -30,7 +28,7 @@ class DefaultCardComponent( private val handler = instanceKeeper.getOrCreate { Handler( - initialCount = stateKeeper.consume(key = KEY_SAVED_STATE)?.count ?: 0, + initialCount = stateKeeper.consume(key = KEY_SAVED_STATE, strategy = SavedState.serializer())?.count ?: 0, tickScheduler = tickScheduler, ) } @@ -39,7 +37,7 @@ class DefaultCardComponent( override val model: Value = _model init { - stateKeeper.register(KEY_SAVED_STATE) { SavedState(count = handler.count.value) } + stateKeeper.register(key = KEY_SAVED_STATE, strategy = SavedState.serializer()) { SavedState(count = handler.count.value) } handler.count.subscribeScoped { count -> _model.update { it.copy(text = "Count: $count") } @@ -92,6 +90,6 @@ class DefaultCardComponent( } } - @Parcelize - private class SavedState(val count: Int) : Parcelable + @Serializable + private class SavedState(val count: Int) } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt index 8b24c2514..1edfd29b3 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt @@ -8,10 +8,9 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.popTo import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.counters.counter.CounterComponent import com.arkivanov.sample.shared.counters.counter.DefaultCounterComponent +import kotlinx.serialization.Serializable internal class DefaultCountersComponent( componentContext: ComponentContext, @@ -22,6 +21,7 @@ internal class DefaultCountersComponent( private val _childStack = childStack( source = navigation, + serializer = Config.serializer(), initialConfiguration = Config(index = 0, isBackEnabled = false), childFactory = ::child, ) @@ -48,9 +48,9 @@ internal class DefaultCountersComponent( navigation.popTo(index = toIndex) } - @Parcelize + @Serializable private data class Config( val index: Int, val isBackEnabled: Boolean, - ) : Parcelable + ) } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/DefaultCounterComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/DefaultCounterComponent.kt index c0ded2381..f3ef8ba34 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/DefaultCounterComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/DefaultCounterComponent.kt @@ -12,9 +12,6 @@ import com.arkivanov.decompose.value.operator.map import com.arkivanov.decompose.value.update import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize -import com.arkivanov.essenty.statekeeper.consume import com.arkivanov.sample.shared.counters.counter.CounterComponent.Model import com.arkivanov.sample.shared.dialog.DefaultDialogComponent import com.arkivanov.sample.shared.dialog.DialogComponent @@ -22,6 +19,7 @@ import com.badoo.reaktive.disposable.scope.DisposableScope import com.badoo.reaktive.observable.observableInterval import com.badoo.reaktive.scheduler.Scheduler import com.badoo.reaktive.scheduler.mainScheduler +import kotlinx.serialization.Serializable internal class DefaultCounterComponent( componentContext: ComponentContext, @@ -35,7 +33,7 @@ internal class DefaultCounterComponent( private val handler = instanceKeeper.getOrCreate(KEY_STATE) { Handler( - initialState = stateKeeper.consume(KEY_STATE) ?: State(), + initialState = stateKeeper.consume(key = KEY_STATE, strategy = State.serializer()) ?: State(), tickScheduler = tickScheduler, ) } @@ -47,7 +45,7 @@ internal class DefaultCounterComponent( private val _dialogSlot = childSlot( source = dialogNavigation, - persistent = false, + serializer = null, handleBackButton = true, childFactory = { config, _ -> DefaultDialogComponent( @@ -65,7 +63,7 @@ internal class DefaultCounterComponent( } init { - stateKeeper.register(KEY_STATE) { handler.state.value } + stateKeeper.register(key = KEY_STATE, strategy = State.serializer()) { handler.state.value } } private fun State.toModel(): Model = @@ -90,15 +88,15 @@ internal class DefaultCounterComponent( private const val KEY_STATE = "STATE" } - @Parcelize + @Serializable private data class State( val count: Int = 0, - ) : Parcelable + ) - @Parcelize + @Serializable private data class DialogConfig( val count: Int, - ) : Parcelable + ) private class Handler( initialState: State, diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultCustomNavigationComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultCustomNavigationComponent.kt index 28ea73f39..07897b816 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultCustomNavigationComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultCustomNavigationComponent.kt @@ -8,11 +8,10 @@ import com.arkivanov.decompose.router.children.SimpleChildNavState import com.arkivanov.decompose.router.children.SimpleNavigation import com.arkivanov.decompose.router.children.children import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.customnavigation.CustomNavigationComponent.Children import com.arkivanov.sample.shared.customnavigation.CustomNavigationComponent.Mode import com.arkivanov.sample.shared.customnavigation.KittenComponent.ImageType +import kotlinx.serialization.Serializable class DefaultCustomNavigationComponent( componentContext: ComponentContext, @@ -23,6 +22,7 @@ class DefaultCustomNavigationComponent( private val _children: Value> = children( source = navigation, + stateSerializer = NavigationState.serializer(), key = "carousel", initialState = { NavigationState( @@ -87,25 +87,25 @@ class DefaultCustomNavigationComponent( } } - @Parcelize + @Serializable private data class Config( val imageType: ImageType, - ) : Parcelable + ) - @Parcelize + @Serializable private data class NavigationState( val configurations: List, val index: Int, val mode: Mode, - ) : NavState, Parcelable { + ) : NavState { - override val children: List> - get() = - configurations.mapIndexed { index, config -> - SimpleChildNavState( - configuration = config, - status = if (index == this.index) ChildNavState.Status.ACTIVE else ChildNavState.Status.INACTIVE, - ) - } + override val children: List> by lazy { + configurations.mapIndexed { index, config -> + SimpleChildNavState( + configuration = config, + status = if (index == this.index) ChildNavState.Status.ACTIVE else ChildNavState.Status.INACTIVE, + ) + } + } } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultKittenComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultKittenComponent.kt index ea997c406..47156142c 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultKittenComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/DefaultKittenComponent.kt @@ -8,15 +8,13 @@ import com.arkivanov.decompose.value.update import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.essenty.lifecycle.subscribe -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize -import com.arkivanov.essenty.statekeeper.consume import com.arkivanov.sample.shared.customnavigation.KittenComponent.ImageType import com.arkivanov.sample.shared.customnavigation.KittenComponent.Model import com.badoo.reaktive.disposable.Disposable import com.badoo.reaktive.observable.observableInterval import com.badoo.reaktive.observable.subscribe import com.badoo.reaktive.scheduler.mainScheduler +import kotlinx.serialization.Serializable class DefaultKittenComponent( componentContext: ComponentContext, @@ -25,7 +23,7 @@ class DefaultKittenComponent( private val handler = instanceKeeper.getOrCreate(KEY_STATE) { - Handler(initialState = stateKeeper.consume(KEY_STATE) ?: State()) + Handler(initialState = stateKeeper.consume(key = KEY_STATE, strategy = State.serializer()) ?: State()) } override val model: Value = handler.state.map { it.toModel() } @@ -36,7 +34,7 @@ class DefaultKittenComponent( onStop = handler::pause, ) - stateKeeper.register(KEY_STATE) { handler.state.value } + stateKeeper.register(key = KEY_STATE, strategy = State.serializer()) { handler.state.value } } private fun State.toModel(): Model = @@ -49,10 +47,10 @@ class DefaultKittenComponent( private const val KEY_STATE = "STATE" } - @Parcelize + @Serializable private data class State( val count: Int = 0, - ) : Parcelable + ) private class Handler(initialState: State) : InstanceKeeper.Instance { val state: MutableValue = MutableValue(initialState) diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DefaultDynamicFeaturesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DefaultDynamicFeaturesComponent.kt index e27163dc6..879c9eef2 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DefaultDynamicFeaturesComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DefaultDynamicFeaturesComponent.kt @@ -7,8 +7,6 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeaturesComponent.Child import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeaturesComponent.Child.Feature1Child import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeaturesComponent.Child.Feature2Child @@ -17,6 +15,7 @@ import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeature import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.FeatureInstaller import com.arkivanov.sample.shared.dynamicfeatures.feature1.Feature1 import com.arkivanov.sample.shared.dynamicfeatures.feature2.Feature2 +import kotlinx.serialization.Serializable import kotlin.random.Random internal class DefaultDynamicFeaturesComponent( @@ -29,6 +28,7 @@ internal class DefaultDynamicFeaturesComponent( private val stack = childStack( source = navigation, + serializer = Config.serializer(), initialConfiguration = Config.Feature1, handleBackButton = true, childFactory = ::child, @@ -69,11 +69,12 @@ internal class DefaultDynamicFeaturesComponent( } ) - private sealed interface Config : Parcelable { - @Parcelize + @Serializable + private sealed interface Config { + @Serializable data object Feature1 : Config - @Parcelize + @Serializable data class Feature2(val magicNumber: Int) : Config } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DefaultDynamicFeatureComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DefaultDynamicFeatureComponent.kt index e7ff01f36..606572071 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DefaultDynamicFeatureComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DefaultDynamicFeatureComponent.kt @@ -7,14 +7,13 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.replaceCurrent import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.lifecycle.subscribe -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.ErrorChild import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.FeatureChild import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.LoadingChild import com.badoo.reaktive.disposable.scope.DisposableScope import com.badoo.reaktive.disposable.scope.disposableScope +import kotlinx.serialization.Serializable internal class DefaultDynamicFeatureComponent( componentContext: ComponentContext, @@ -28,6 +27,7 @@ internal class DefaultDynamicFeatureComponent( private val stack = childStack( source = navigation, + serializer = Config.serializer(), initialConfiguration = Config.Loading, childFactory = ::child, ) @@ -62,14 +62,15 @@ internal class DefaultDynamicFeatureComponent( } } - private sealed interface Config : Parcelable { - @Parcelize + @Serializable + private sealed interface Config { + @Serializable data object Loading : Config - @Parcelize + @Serializable data object Feature : Config - @Parcelize + @Serializable data object Error : Config } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt index d14967342..e3ff541c4 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt @@ -9,8 +9,6 @@ import com.arkivanov.decompose.router.children.SimpleChildNavState import com.arkivanov.decompose.router.children.SimpleNavigation import com.arkivanov.decompose.router.children.children import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.multipane.MultiPaneComponent.Children import com.arkivanov.sample.shared.multipane.database.DefaultArticleDatabase import com.arkivanov.sample.shared.multipane.details.ArticleDetailsComponent @@ -22,6 +20,7 @@ import com.badoo.reaktive.disposable.scope.DisposableScope import com.badoo.reaktive.observable.map import com.badoo.reaktive.observable.notNull import com.badoo.reaktive.subject.behavior.BehaviorSubject +import kotlinx.serialization.Serializable internal class DefaultMultiPaneComponent( componentContext: ComponentContext @@ -34,6 +33,7 @@ internal class DefaultMultiPaneComponent( override val children: Value = children( source = navigation, + stateSerializer = NavigationState.serializer(), key = "children", initialState = ::NavigationState, navTransformer = { navState, event -> event(navState) }, @@ -76,24 +76,25 @@ internal class DefaultMultiPaneComponent( navigation.navigate { it.copy(isMultiPane = isMultiPane) } } - private sealed interface Config : Parcelable { - @Parcelize + @Serializable + private sealed interface Config { + @Serializable data object List : Config - @Parcelize + @Serializable data class Details(val articleId: Long) : Config } - @Parcelize + @Serializable private data class NavigationState( val isMultiPane: Boolean = false, val articleId: Long? = null, - ) : NavState, Parcelable { - override val children: List> - get() = - listOfNotNull( - SimpleChildNavState(Config.List, if (isMultiPane || (articleId == null)) Status.ACTIVE else Status.INACTIVE), - if (articleId != null) SimpleChildNavState(Config.Details(articleId), Status.ACTIVE) else null, - ) + ) : NavState { + override val children: List> by lazy { + listOfNotNull( + SimpleChildNavState(Config.List, if (isMultiPane || (articleId == null)) Status.ACTIVE else Status.INACTIVE), + if (articleId != null) SimpleChildNavState(Config.Details(articleId), Status.ACTIVE) else null, + ) + } } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt index 93c5c1778..ac286bcd6 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt @@ -8,8 +8,6 @@ import com.arkivanov.decompose.router.stack.bringToFront import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.webhistory.WebHistoryController import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.sample.shared.cards.DefaultCardsComponent import com.arkivanov.sample.shared.counters.DefaultCountersComponent import com.arkivanov.sample.shared.customnavigation.DefaultCustomNavigationComponent @@ -21,6 +19,7 @@ import com.arkivanov.sample.shared.root.RootComponent.Child.CountersChild import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.MultiPaneChild +import kotlinx.serialization.Serializable @OptIn(ExperimentalDecomposeApi::class) class DefaultRootComponent( @@ -35,6 +34,7 @@ class DefaultRootComponent( private val stack = childStack( source = navigation, + serializer = Config.serializer(), initialStack = { getInitialStack(webHistoryPaths = webHistoryController?.historyPaths, deepLink = deepLink) }, childFactory = ::child, ) @@ -118,20 +118,21 @@ class DefaultRootComponent( } } - private sealed interface Config : Parcelable { - @Parcelize + @Serializable + private sealed interface Config { + @Serializable data object Counters : Config - @Parcelize + @Serializable data object Cards : Config - @Parcelize + @Serializable data object MultiPane : Config - @Parcelize + @Serializable data object DynamicFeatures : Config - @Parcelize + @Serializable data object CustomNavigation : Config } From 5e0a097663bdf12e1bd9af23abe099e89efd926c Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 24 Sep 2023 09:35:11 +0100 Subject: [PATCH 02/21] Bumped version to 2.2.0-alpha01 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index b077f18dd..bd0c2f525 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-dev02" +decompose = "2.2.0-alpha01" kotlin = "1.9.10" essenty = "1.3.0-alpha01" parcelizeDarwin = "0.2.2" From ac558087f6cbcf93b45fa987d0e747f80e9d1811 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 30 Sep 2023 23:55:00 +0100 Subject: [PATCH 03/21] Added key argument to Pages --- .../android/extensions-compose-jetbrains.api | 4 ++-- .../api/jvm/extensions-compose-jetbrains.api | 4 ++-- .../compose/jetbrains/pages/Pages.kt | 19 +++++++++++++------ .../api/extensions-compose-jetpack.api | 4 ++-- .../extensions/compose/jetpack/pages/Pages.kt | 15 ++++++++++----- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api index 27669037c..3615f18d1 100644 --- a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api @@ -12,8 +12,8 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/Co } public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesKt { - public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V - public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V public static final fun defaultHorizontalPager ()Lkotlin/jvm/functions/Function6; public static final fun defaultVerticalPager ()Lkotlin/jvm/functions/Function6; } diff --git a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api index 7e4f92391..e459843bb 100644 --- a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api @@ -24,8 +24,8 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/Co } public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesKt { - public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V - public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V public static final fun defaultHorizontalPager ()Lkotlin/jvm/functions/Function6; public static final fun defaultVerticalPager ()Lkotlin/jvm/functions/Function6; } diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pages.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pages.kt index 872db1481..b5998e699 100644 --- a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pages.kt +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pages.kt @@ -12,7 +12,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.arkivanov.decompose.Child import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.InternalDecomposeApi import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import com.arkivanov.decompose.hashString import com.arkivanov.decompose.router.pages.ChildPages @@ -21,15 +23,17 @@ import com.arkivanov.decompose.value.Value /** * Displays a list of pages represented by [ChildPages]. */ +@OptIn(InternalDecomposeApi::class) @ExperimentalFoundationApi @ExperimentalDecomposeApi @Composable -fun Pages( - pages: Value>, +fun Pages( + pages: Value>, onPageSelected: (index: Int) -> Unit, modifier: Modifier = Modifier, scrollAnimation: PagesScrollAnimation = PagesScrollAnimation.Disabled, pager: Pager = defaultHorizontalPager(), + key: (Child) -> Any = { it.configuration.hashString() }, pageContent: @Composable PagerScope.(index: Int, page: T) -> Unit, ) { val state = pages.subscribeAsState() @@ -40,6 +44,7 @@ fun Pages( modifier = modifier, scrollAnimation = scrollAnimation, pager = pager, + key = key, pageContent = pageContent, ) } @@ -47,18 +52,20 @@ fun Pages( /** * Displays a list of pages represented by [ChildPages]. */ +@OptIn(InternalDecomposeApi::class) @ExperimentalFoundationApi @ExperimentalDecomposeApi @Composable -fun Pages( - pages: State>, +fun Pages( + pages: State>, onPageSelected: (index: Int) -> Unit, modifier: Modifier = Modifier, scrollAnimation: PagesScrollAnimation = PagesScrollAnimation.Disabled, pager: Pager = defaultHorizontalPager(), + key: (Child) -> Any = { it.configuration.hashString() }, pageContent: @Composable PagerScope.(index: Int, page: T) -> Unit, ) { - val childPages by pages + val childPages by pages val selectedIndex = childPages.selectedIndex val state = rememberPagerState( initialPage = selectedIndex, @@ -83,7 +90,7 @@ fun Pages( pager( modifier, state, - { childPages.items[it].configuration.hashString() }, + { key(childPages.items[it]) }, ) { pageIndex -> childPages.items[pageIndex].instance?.also { page -> pageContent(pageIndex, page) diff --git a/extensions-compose-jetpack/api/extensions-compose-jetpack.api b/extensions-compose-jetpack/api/extensions-compose-jetpack.api index c012e5c82..58f335a5c 100644 --- a/extensions-compose-jetpack/api/extensions-compose-jetpack.api +++ b/extensions-compose-jetpack/api/extensions-compose-jetpack.api @@ -12,8 +12,8 @@ public final class com/arkivanov/decompose/extensions/compose/jetpack/pages/Comp } public final class com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesKt { - public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V - public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V + public static final fun Pages (Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V public static final fun defaultHorizontalPager ()Lkotlin/jvm/functions/Function6; public static final fun defaultVerticalPager ()Lkotlin/jvm/functions/Function6; } diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/Pages.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/Pages.kt index fcbb13d30..a223606c1 100644 --- a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/Pages.kt +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/Pages.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.arkivanov.decompose.Child import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.InternalDecomposeApi import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState @@ -22,15 +23,17 @@ import com.arkivanov.decompose.value.Value /** * Displays a list of pages represented by [ChildPages]. */ +@OptIn(InternalDecomposeApi::class) @ExperimentalFoundationApi @ExperimentalDecomposeApi @Composable -fun Pages( - pages: Value>, +fun Pages( + pages: Value>, onPageSelected: (index: Int) -> Unit, modifier: Modifier = Modifier, scrollAnimation: PagesScrollAnimation = PagesScrollAnimation.Disabled, pager: Pager = defaultHorizontalPager(), + key: (Child) -> Any = { it.configuration.hashString() }, pageContent: @Composable PagerScope.(index: Int, page: T) -> Unit, ) { val state = pages.subscribeAsState() @@ -41,6 +44,7 @@ fun Pages( modifier = modifier, scrollAnimation = scrollAnimation, pager = pager, + key = key, pageContent = pageContent, ) } @@ -52,12 +56,13 @@ fun Pages( @ExperimentalFoundationApi @ExperimentalDecomposeApi @Composable -fun Pages( - pages: State>, +fun Pages( + pages: State>, onPageSelected: (index: Int) -> Unit, modifier: Modifier = Modifier, scrollAnimation: PagesScrollAnimation = PagesScrollAnimation.Disabled, pager: Pager = defaultHorizontalPager(), + key: (Child) -> Any = { it.configuration.hashString() }, pageContent: @Composable PagerScope.(index: Int, page: T) -> Unit, ) { val childPages by pages @@ -85,7 +90,7 @@ fun Pages( pager( modifier, state, - { childPages.items[it].configuration.hashString() }, + { key(childPages.items[it]) }, ) { pageIndex -> childPages.items[pageIndex].instance?.also { page -> pageContent(pageIndex, page) From 18cbbc399291e221c411a9e52b88096a52a35b25 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 1 Oct 2023 10:38:19 +0100 Subject: [PATCH 04/21] Bumped version to 2.2.0-alpha02 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index bd0c2f525..7957a2c9e 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-alpha01" +decompose = "2.2.0-alpha02" kotlin = "1.9.10" essenty = "1.3.0-alpha01" parcelizeDarwin = "0.2.2" From 695cbf8c59bd77a85ec248fe877495b80dc13a62 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Thu, 5 Oct 2023 14:42:08 +0100 Subject: [PATCH 05/21] Added pushNew extension function for StackNavigator --- decompose/api/android/decompose.api | 2 + decompose/api/jvm/decompose.api | 2 + .../router/stack/StackNavigatorExt.kt | 28 +++++++++++ .../router/stack/RouterPushNewTest.kt | 46 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPushNewTest.kt diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api index ffcf8d4b7..23172c210 100644 --- a/decompose/api/android/decompose.api +++ b/decompose/api/android/decompose.api @@ -399,6 +399,8 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt { public static final fun popWhile (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public static final fun push (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V public static synthetic fun push$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static final fun pushNew (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun pushNew$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun replaceAll (Lcom/arkivanov/decompose/router/stack/StackNavigator;[Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V public static synthetic fun replaceAll$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;[Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api index 1b2244013..38396ab4d 100644 --- a/decompose/api/jvm/decompose.api +++ b/decompose/api/jvm/decompose.api @@ -361,6 +361,8 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt { public static final fun popWhile (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public static final fun push (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V public static synthetic fun push$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static final fun pushNew (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun pushNew$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun replaceAll (Lcom/arkivanov/decompose/router/stack/StackNavigator;[Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V public static synthetic fun replaceAll$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;[Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt index 378bd3196..5839087bd 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt @@ -1,5 +1,7 @@ package com.arkivanov.decompose.router.stack +import com.arkivanov.decompose.ExperimentalDecomposeApi + /** * A convenience method for [StackNavigator.navigate]. */ @@ -10,12 +12,38 @@ fun StackNavigator.navigate(transformer: (stack: List) -> List StackNavigator.push(configuration: C, onComplete: () -> Unit = {}) { navigate(transformer = { it + configuration }, onComplete = { _, _ -> onComplete() }) } +/** + * Pushes the provided [configuration] at the top of the stack. Does nothing if the provided + * [configuration] is already on top of the stack. + * + * Decompose will throw an exception if the provided [configuration] is already present in the + * back stack (not at the top of the stack). + * + * This can be useful when pushing a component on button click, to avoid pushing the same component + * if the user clicks the same button quickly multiple times. + * + * @param onComplete called when the navigation is finished (either synchronously or asynchronously). + * The `isSuccess` argument is `true` if the component was pushed, `false` otherwise. + */ +@ExperimentalDecomposeApi +fun StackNavigator.pushNew( + configuration: C, + onComplete: (isSuccess: Boolean) -> Unit = {}, +) { + navigate( + transformer = { stack -> if (stack.last() == configuration) stack else stack + configuration }, + onComplete = { newStack, oldStack -> onComplete(newStack.size > oldStack.size) }, + ) +} + /** * Pops the latest configuration at the top of the stack. * diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPushNewTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPushNewTest.kt new file mode 100644 index 000000000..4cce7726c --- /dev/null +++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPushNewTest.kt @@ -0,0 +1,46 @@ +package com.arkivanov.decompose.router.stack + +import kotlin.test.Test +import kotlin.test.assertEquals + +@Suppress("TestFunctionName") +class RouterPushNewTest { + + @Test + fun GIVEN_configuration_not_present_WHEN_pushNew_THEN_pushed() { + val navigator = TestStackNavigator(listOf(1, 2)) + + navigator.pushNew(3) + + assertEquals(listOf(1, 2, 3), navigator.configurations) + } + + @Test + fun GIVEN_configuration_present_on_top_WHEN_pushNew_THEN_not_pushed() { + val navigator = TestStackNavigator(listOf(1, 2)) + + navigator.pushNew(2) + + assertEquals(listOf(1, 2), navigator.configurations) + } + + @Test + fun GIVEN_configuration_not_present_WHEN_pushNew_THEN_onComplete_success() { + val navigator = TestStackNavigator(listOf(1, 2)) + var result: Boolean? = null + + navigator.pushNew(3) { result = it } + + assertEquals(true, result) + } + + @Test + fun GIVEN_configuration_present_on_top_WHEN_pushNew_THEN_onComplete_not_success() { + val navigator = TestStackNavigator(listOf(1, 2)) + var result: Boolean? = null + + navigator.pushNew(2) { result = it } + + assertEquals(false, result) + } +} From 099717ecbcb8354361e8dadf3530ab78aa805dc6 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Tue, 19 Sep 2023 19:47:23 +0100 Subject: [PATCH 06/21] Added eventTransformer argument and refactored predictiveBackAnimation --- deps.versions.toml | 2 +- .../android/extensions-compose-jetbrains.api | 22 +- .../api/jvm/extensions-compose-jetbrains.api | 22 +- .../animation/PredictiveBackAnimation.kt | 276 ------------------ .../predictiveback/BackGestureHandler.kt | 29 ++ .../DefaultPredictiveBackAnimatable.kt | 31 ++ .../PredictiveBackAnimatable.kt | 93 ++++++ .../predictiveback/PredictiveBackAnimation.kt | 175 +++++++++++ .../api/extensions-compose-jetpack.api | 22 +- .../animation/PredictiveBackAnimation.kt | 276 ------------------ .../predictiveback/BackGestureHandler.kt | 29 ++ .../DefaultPredictiveBackAnimatable.kt | 31 ++ .../PredictiveBackAnimatable.kt | 93 ++++++ .../predictiveback/PredictiveBackAnimation.kt | 175 +++++++++++ .../sample/shared/counters/CountersContent.kt | 2 +- 15 files changed, 709 insertions(+), 569 deletions(-) delete mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimation.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/BackGestureHandler.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt delete mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/PredictiveBackAnimation.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/BackGestureHandler.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt diff --git a/deps.versions.toml b/deps.versions.toml index 7957a2c9e..495c2d0b6 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -2,7 +2,7 @@ decompose = "2.2.0-alpha02" kotlin = "1.9.10" -essenty = "1.3.0-alpha01" +essenty = "1.3.0-alpha02" parcelizeDarwin = "0.2.2" reaktive = "1.2.3" junit = "4.13.2" diff --git a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api index 3615f18d1..dee65b61c 100644 --- a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api @@ -71,11 +71,6 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun fade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } -public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimationKt { - public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; - public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; -} - public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/ScaleKt { public static final fun scale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; public static synthetic fun scale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; @@ -111,3 +106,20 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } +public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable { + public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getEnterModifier ()Landroidx/compose/ui/Modifier; + public abstract fun getExitModifier ()Landroidx/compose/ui/Modifier; +} + +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt { + public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; +} + +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt { + public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; + public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; +} + diff --git a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api index e459843bb..140987c0b 100644 --- a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api @@ -83,11 +83,6 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun fade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } -public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimationKt { - public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; - public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; -} - public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/ScaleKt { public static final fun scale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; public static synthetic fun scale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; @@ -123,3 +118,20 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } +public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable { + public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getEnterModifier ()Landroidx/compose/ui/Modifier; + public abstract fun getExitModifier ()Landroidx/compose/ui/Modifier; +} + +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt { + public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; +} + +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt { + public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; + public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation; +} + diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimation.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimation.kt deleted file mode 100644 index 56b214c10..000000000 --- a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/PredictiveBackAnimation.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.movableContentOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.Child -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.essenty.backhandler.BackCallback -import com.arkivanov.essenty.backhandler.BackEvent -import com.arkivanov.essenty.backhandler.BackHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds - -/** - * Wraps the provided [animation], handles the predictive back gesture and animates - * the transition from the current [Child] to the previous one. - * Calls [onBack] when the animation is finished. - * - * @param backHandler a source of the predictive back gesture events, see [BackHandler]. - * @param animation a [StackAnimation] for regular transitions. - * @param exitModifier a function that returns a [Modifier] for every gesture event, for - * the child being removed (the currently active child). - * @param enterModifier a function that returns a [Modifier] for every gesture event, for - * the previous child (behind the currently active child). - * @param onBack a callback that is called when the gesture is finished. - */ -@ExperimentalDecomposeApi -fun predictiveBackAnimation( - backHandler: BackHandler, - animation: StackAnimation? = null, - exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> - Modifier.exitModifier(progress = progress, edge = edge) - }, - enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> - Modifier.enterModifier(progress = progress) - }, - onBack: () -> Unit, -): StackAnimation = - PredictiveBackAnimation( - backHandler = backHandler, - animation = animation ?: emptyStackAnimation(), - exitModifier = exitModifier, - enterModifier = enterModifier, - onBack = onBack, - ) - -private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = - scale(1F - progress * 0.25F) - .absoluteOffset( - x = when (edge) { - BackEvent.SwipeEdge.LEFT -> 32.dp * progress - BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress - BackEvent.SwipeEdge.UNKNOWN -> 0.dp - }, - ) - .alpha(((1F - progress) * 2F).coerceAtMost(1F)) - .clip(RoundedCornerShape(size = 64.dp * progress)) - -private fun Modifier.enterModifier(progress: Float): Modifier = - drawWithContent { - drawContent() - drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) - } - -private class PredictiveBackAnimation( - private val backHandler: BackHandler, - private val animation: StackAnimation, - private val exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val onBack: () -> Unit, -) : StackAnimation { - - @Composable - override fun invoke(stack: ChildStack, modifier: Modifier, content: @Composable (child: Child.Created) -> Unit) { - var activeConfigurations: Set by remember { mutableStateOf(emptySet()) } - - val childContent = - remember(content) { - movableContentOf> { child -> - key(child.configuration) { - content(child) - - DisposableEffect(Unit) { - activeConfigurations += child.configuration - onDispose { activeConfigurations -= child.configuration } - } - } - } - } - - val currentKey = remember { Holder(value = 0) } - - var items: List> by rememberMutableStateWithLatest( - key = stack, - onReplaced = { latestItems -> currentKey.value = latestItems.maxOf(Item<*, *>::key) }, - getValue = { listOf(Item(stack = stack, key = currentKey.value)) }, - ) - - Box(modifier = modifier) { - items.forEach { item -> - key(item.key) { - animation( - stack = item.stack, - modifier = Modifier.fillMaxSize().then(item.modifier), - content = childContent, - ) - } - } - } - - val isBackEnabled = stack.backStack.isNotEmpty() - val isBackGestureEnabled = isBackEnabled && ((items.size > 1) || (items.size == 1) && (activeConfigurations.size == 1)) - - DisposableEffect(stack, isBackEnabled, isBackGestureEnabled) { - if (!isBackEnabled) { - return@DisposableEffect onDispose {} - } - - val scope = CoroutineScope(Dispatchers.Main.immediate) - - val callback = - if (isBackGestureEnabled) { - GestureBackCallback( - scope = scope, - stack = stack, - currentKey = currentKey.value, - exitModifier = exitModifier, - enterModifier = enterModifier, - setItems = { items = it }, - onFinished = { newKey -> - currentKey.value = newKey - onBack() - }, - ) - } else { - BackCallback(onBack = onBack) - } - - backHandler.register(callback) - - onDispose { - scope.cancel() - backHandler.unregister(callback) - } - } - } - - @Composable - private fun rememberMutableStateWithLatest( - key: Any, - onReplaced: (latestValue: T) -> Unit, - getValue: () -> T, - ): MutableState { - val latestValue: Holder = remember { Holder(value = null) } - - val state = - remember(key) { - latestValue.value?.also(onReplaced) - mutableStateOf(getValue()) - } - - latestValue.value = state.value - - return state - } - - private data class BackData( - val progress: Float, - val edge: BackEvent.SwipeEdge, - val exitItem: Item, - val enterItem: Item, - ) - - private data class Item( - val stack: ChildStack, - val key: Int, - val modifier: Modifier = Modifier, - ) - - private class Holder(var value: T) - - private class GestureBackCallback( - private val scope: CoroutineScope, - stack: ChildStack, - currentKey: Int, - private val exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val setItems: (List>) -> Unit, - private val onFinished: (newKey: Int) -> Unit, - ) : BackCallback() { - private var backData: BackData = - BackData( - progress = 0F, - edge = BackEvent.SwipeEdge.UNKNOWN, - exitItem = Item(stack = stack, key = currentKey), - enterItem = Item(stack = stack.dropLast(), key = currentKey + 1), - ) - - private fun ChildStack.dropLast(): ChildStack = - ChildStack(active = backStack.last(), backStack = backStack.dropLast(1)) - - override fun onBackStarted(backEvent: BackEvent) { - updateData(backEvent) - } - - private fun updateData(backEvent: BackEvent) { - backData = backData.withProgress(progress = backEvent.progress, edge = backEvent.swipeEdge) - setItems(listOf(backData.enterItem, backData.exitItem)) - } - - override fun onBackProgressed(backEvent: BackEvent) { - updateData(backEvent) - } - - override fun onBackCancelled() { - setItems(listOf(backData.exitItem.copy(modifier = Modifier))) - } - - override fun onBack() { - if (backData.progress > 0F) { - scope.launch { continueGesture() } - } else { - onFinished(backData.exitItem.key) - } - } - - private suspend fun CoroutineScope.continueGesture() { - var progress = backData.progress - while ((progress <= 1F) && isActive) { - delay(16.milliseconds) - progress += 0.075F - backData = backData.withProgress(progress = progress) - setItems(listOf(backData.enterItem, backData.exitItem)) - } - - if (isActive) { - setItems(listOf(backData.enterItem.copy(modifier = Modifier))) - onFinished(backData.enterItem.key) - } - } - - private fun BackData.withProgress( - progress: Float = this.progress, - edge: BackEvent.SwipeEdge = this.edge, - ): BackData = - copy( - progress = progress.coerceIn(0F..1F), - edge = edge, - exitItem = exitItem.copy(modifier = exitModifier(progress, edge)), - enterItem = enterItem.copy(modifier = enterModifier(progress, edge)), - ) - } -} diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/BackGestureHandler.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/BackGestureHandler.kt new file mode 100644 index 000000000..b85a5688a --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/BackGestureHandler.kt @@ -0,0 +1,29 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.backhandler.BackEvent +import com.arkivanov.essenty.backhandler.BackHandler + +@Composable +internal fun BackGestureHandler( + backHandler: BackHandler, + onBackStarted: (BackEvent) -> Unit = {}, + onBackProgressed: (BackEvent) -> Unit = {}, + onBackCancelled: () -> Unit = {}, + onBack: () -> Unit, +) { + DisposableEffect(backHandler) { + val callback = + BackCallback( + onBackStarted = onBackStarted, + onBackProgressed = onBackProgressed, + onBackCancelled = onBackCancelled, + onBack = onBack, + ) + + backHandler.register(callback) + onDispose { backHandler.unregister(callback) } + } +} diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt new file mode 100644 index 000000000..11fe9beef --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt @@ -0,0 +1,31 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.animation.core.Animatable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent + +@ExperimentalDecomposeApi +internal class DefaultPredictiveBackAnimatable( + initialBackEvent: BackEvent, + private val getExitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, + private val getEnterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, +) : PredictiveBackAnimatable { + + private val progressAnimatable = Animatable(initialValue = initialBackEvent.progress) + private var swipeEdge by mutableStateOf(initialBackEvent.swipeEdge) + override val exitModifier: Modifier get() = getExitModifier(progressAnimatable.value, swipeEdge) + override val enterModifier: Modifier get() = getEnterModifier(progressAnimatable.value, swipeEdge) + + override suspend fun animate(event: BackEvent) { + swipeEdge = event.swipeEdge + progressAnimatable.snapTo(targetValue = event.progress) + } + + override suspend fun finish() { + progressAnimatable.animateTo(targetValue = 1F) + } +} diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt new file mode 100644 index 000000000..95ca88b62 --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt @@ -0,0 +1,93 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent + +/** + * Animates [exitModifier] and [enterModifier] according to incoming [BackEvent] events. + * + * [Animatable][androidx.compose.animation.core.Animatable] can be used for animations. + */ +@ExperimentalDecomposeApi +interface PredictiveBackAnimatable { + + /** + * Returns a [Modifier] for the child being removed (the currently active child). + * The property must be Compose-observable, e.g. be backed by a Compose state. + */ + val exitModifier: Modifier + + /** + * Returns a [Modifier] for the child being shown (the previous child, behind the currently active child). + * The property must be Compose-observable, e.g. be backed by a Compose state. + */ + val enterModifier: Modifier + + /** + * Animates both [exitModifier] and [enterModifier] according to [event]. + * Any previous animation must be cancelled. + * + * @see androidx.compose.animation.core.Animatable + */ + suspend fun animate(event: BackEvent) + + /** + * Animates both [exitModifier] and [enterModifier] towards the final state. + * Any previous animation must be cancelled. + * + * @see androidx.compose.animation.core.Animatable + */ + suspend fun finish() +} + +/** + * Creates a default implementation of [PredictiveBackAnimatable] with customisable exit and enter [Modifier]s. + * + * @param initialBackEvent an initial [BackEvent] of the predictive back gesture. + * @param exitModifier a function that returns a [Modifier] for every gesture event, for + * the child being removed (the currently active child). + * @param enterModifier a function that returns a [Modifier] for every gesture event, for + * the previous child (behind the currently active child). + */ +@ExperimentalDecomposeApi +fun predictiveBackAnimatable( + initialBackEvent: BackEvent, + exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> + Modifier.exitModifier(progress = progress, edge = edge) + }, + enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> + Modifier.enterModifier(progress = progress) + }, +): PredictiveBackAnimatable = + DefaultPredictiveBackAnimatable( + initialBackEvent = initialBackEvent, + getExitModifier = exitModifier, + getEnterModifier = enterModifier, + ) + +private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = + scale(1F - progress * 0.25F) + .absoluteOffset( + x = when (edge) { + BackEvent.SwipeEdge.LEFT -> 32.dp * progress + BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress + BackEvent.SwipeEdge.UNKNOWN -> 0.dp + }, + ) + .alpha(((1F - progress) * 2F).coerceAtMost(1F)) + .clip(RoundedCornerShape(size = 64.dp * progress)) + +private fun Modifier.enterModifier(progress: Float): Modifier = + drawWithContent { + drawContent() + drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) + } diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt new file mode 100644 index 000000000..a14edfe99 --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt @@ -0,0 +1,175 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.emptyStackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.essenty.backhandler.BackEvent +import com.arkivanov.essenty.backhandler.BackHandler +import kotlinx.coroutines.launch + +/** + * Wraps the provided [animation], handles the predictive back gesture and animates + * the transition from the current [Child] to the previous one. + * Calls [onBack] when the animation is finished. + * + * @param backHandler a source of the predictive back gesture events, see [BackHandler]. + * @param animation a [StackAnimation] for regular transitions. + * @param selector a selector function that is called when the predictive back gesture begins, + * returns [PredictiveBackAnimatable] responsible for animations. + * @param onBack a callback that is called when the gesture is finished. + */ +@ExperimentalDecomposeApi +fun predictiveBackAnimation( + backHandler: BackHandler, + animation: StackAnimation? = null, + selector: ( + initialBackEvent: BackEvent, + exitChild: Child.Created, + enterChild: Child.Created, + ) -> PredictiveBackAnimatable = { initialBackEvent, _, _ -> + predictiveBackAnimatable(initialBackEvent = initialBackEvent) + }, + onBack: () -> Unit, +): StackAnimation = + PredictiveBackAnimation( + backHandler = backHandler, + animation = animation ?: emptyStackAnimation(), + selector = selector, + onBack = onBack, + ) + +@OptIn(ExperimentalDecomposeApi::class) +private class PredictiveBackAnimation( + private val backHandler: BackHandler, + private val animation: StackAnimation, + private val selector: (BackEvent, exitChild: Child.Created, enterChild: Child.Created) -> PredictiveBackAnimatable, + private val onBack: () -> Unit, +) : StackAnimation { + + @Composable + override fun invoke(stack: ChildStack, modifier: Modifier, content: @Composable (child: Child.Created) -> Unit) { + var activeConfigurations: Set by remember { mutableStateOf(emptySet()) } + + val childContent = + remember(content) { + movableContentOf> { child -> + key(child.configuration) { + content(child) + + DisposableEffect(Unit) { + activeConfigurations += child.configuration + onDispose { activeConfigurations -= child.configuration } + } + } + } + } + + var data: Data by rememberMutableStateWithLatest(key = stack) { latestData -> + Data(stack = stack, key = latestData?.nextKey ?: 0) + } + + val (dataStack, dataKey, dataAnimatable) = data + + val items = + if (dataAnimatable == null) { + listOf(Item(stack = dataStack, key = dataKey, modifier = Modifier)) + } else { + listOf( + Item(stack = dataStack.dropLast(), key = dataKey + 1, modifier = dataAnimatable.enterModifier), + Item(stack = dataStack, key = dataKey, modifier = dataAnimatable.exitModifier), + ) + } + + Box(modifier = modifier) { + items.forEach { item -> + key(item.key) { + animation( + stack = item.stack, + modifier = Modifier.fillMaxSize().then(item.modifier), + content = childContent, + ) + } + } + } + + val isBackEnabled = dataStack.backStack.isNotEmpty() + val isBackGestureEnabled = isBackEnabled && ((dataAnimatable != null) || (activeConfigurations.size == 1)) + + if (isBackEnabled) { + if (isBackGestureEnabled) { + val scope = rememberCoroutineScope() + + BackGestureHandler( + backHandler = backHandler, + onBackStarted = { + data = data.copy(animatable = selector(it, data.stack.active, data.stack.backStack.last())) + }, + onBackProgressed = { + scope.launch { data.animatable?.animate(it) } + }, + onBackCancelled = { + data = data.copy(animatable = null) + }, + onBack = { + if (data.animatable == null) { + onBack() + } else { + scope.launch { + data.animatable?.finish() + onBack() + } + } + } + ) + } else { + BackGestureHandler(backHandler = backHandler, onBack = onBack) + } + } + } + + @Composable + private fun rememberMutableStateWithLatest( + key: Any, + getValue: (latestValue: T?) -> T, + ): MutableState { + val latestValue: Holder = remember { Holder(value = null) } + val state = remember(key) { mutableStateOf(getValue(latestValue.value)) } + latestValue.value = state.value + + return state + } + + private fun ChildStack.dropLast(): ChildStack = + ChildStack(active = backStack.last(), backStack = backStack.dropLast(1)) + + private data class Data( + val stack: ChildStack, + val key: Int, + val animatable: PredictiveBackAnimatable? = null, + ) { + val nextKey: Int get() = if (animatable == null) key else key + 1 + } + + private data class Item( + val stack: ChildStack, + val key: Int, + val modifier: Modifier, + ) + + private class Holder(var value: T) +} diff --git a/extensions-compose-jetpack/api/extensions-compose-jetpack.api b/extensions-compose-jetpack/api/extensions-compose-jetpack.api index 58f335a5c..cd8fc4e34 100644 --- a/extensions-compose-jetpack/api/extensions-compose-jetpack.api +++ b/extensions-compose-jetpack/api/extensions-compose-jetpack.api @@ -71,11 +71,6 @@ public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/anim public static synthetic fun fade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimator; } -public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/PredictiveBackAnimationKt { - public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation; - public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation; -} - public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/ScaleKt { public static final fun scale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimator; public static synthetic fun scale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimator; @@ -111,3 +106,20 @@ public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/anim public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimator; } +public abstract interface class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable { + public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getEnterModifier ()Landroidx/compose/ui/Modifier; + public abstract fun getExitModifier ()Landroidx/compose/ui/Modifier; +} + +public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatableKt { + public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; +} + +public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimationKt { + public static final fun predictiveBackAnimation (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation; + public static synthetic fun predictiveBackAnimation$default (Lcom/arkivanov/essenty/backhandler/BackHandler;Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimation; +} + diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/PredictiveBackAnimation.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/PredictiveBackAnimation.kt deleted file mode 100644 index 27c6ff414..000000000 --- a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/PredictiveBackAnimation.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.movableContentOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.Child -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.essenty.backhandler.BackCallback -import com.arkivanov.essenty.backhandler.BackEvent -import com.arkivanov.essenty.backhandler.BackHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds - -/** - * Wraps the provided [animation], handles the predictive back gesture and animates - * the transition from the current [Child] to the previous one. - * Calls [onBack] when the animation is finished. - * - * @param backHandler a source of the predictive back gesture events, see [BackHandler]. - * @param animation a [StackAnimation] for regular transitions. - * @param exitModifier a function that returns a [Modifier] for every gesture event, for - * the child being removed (the currently active child). - * @param enterModifier a function that returns a [Modifier] for every gesture event, for - * the previous child (behind the currently active child). - * @param onBack a callback that is called when the gesture is finished. - */ -@ExperimentalDecomposeApi -fun predictiveBackAnimation( - backHandler: BackHandler, - animation: StackAnimation? = null, - exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> - Modifier.exitModifier(progress = progress, edge = edge) - }, - enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> - Modifier.enterModifier(progress = progress) - }, - onBack: () -> Unit, -): StackAnimation = - PredictiveBackAnimation( - backHandler = backHandler, - animation = animation ?: emptyStackAnimation(), - exitModifier = exitModifier, - enterModifier = enterModifier, - onBack = onBack, - ) - -private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = - scale(1F - progress * 0.25F) - .absoluteOffset( - x = when (edge) { - BackEvent.SwipeEdge.LEFT -> 32.dp * progress - BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress - BackEvent.SwipeEdge.UNKNOWN -> 0.dp - }, - ) - .alpha(((1F - progress) * 2F).coerceAtMost(1F)) - .clip(RoundedCornerShape(size = 64.dp * progress)) - -private fun Modifier.enterModifier(progress: Float): Modifier = - drawWithContent { - drawContent() - drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) - } - -private class PredictiveBackAnimation( - private val backHandler: BackHandler, - private val animation: StackAnimation, - private val exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val onBack: () -> Unit, -) : StackAnimation { - - @Composable - override fun invoke(stack: ChildStack, modifier: Modifier, content: @Composable (child: Child.Created) -> Unit) { - var activeConfigurations: Set by remember { mutableStateOf(emptySet()) } - - val childContent = - remember(content) { - movableContentOf> { child -> - key(child.configuration) { - content(child) - - DisposableEffect(Unit) { - activeConfigurations += child.configuration - onDispose { activeConfigurations -= child.configuration } - } - } - } - } - - val currentKey = remember { Holder(value = 0) } - - var items: List> by rememberMutableStateWithLatest( - key = stack, - onReplaced = { latestItems -> currentKey.value = latestItems.maxOf(Item<*, *>::key) }, - getValue = { listOf(Item(stack = stack, key = currentKey.value)) }, - ) - - Box(modifier = modifier) { - items.forEach { item -> - key(item.key) { - animation( - stack = item.stack, - modifier = Modifier.fillMaxSize().then(item.modifier), - content = childContent, - ) - } - } - } - - val isBackEnabled = stack.backStack.isNotEmpty() - val isBackGestureEnabled = isBackEnabled && ((items.size > 1) || (items.size == 1) && (activeConfigurations.size == 1)) - - DisposableEffect(stack, isBackEnabled, isBackGestureEnabled) { - if (!isBackEnabled) { - return@DisposableEffect onDispose {} - } - - val scope = CoroutineScope(Dispatchers.Main.immediate) - - val callback = - if (isBackGestureEnabled) { - GestureBackCallback( - scope = scope, - stack = stack, - currentKey = currentKey.value, - exitModifier = exitModifier, - enterModifier = enterModifier, - setItems = { items = it }, - onFinished = { newKey -> - currentKey.value = newKey - onBack() - }, - ) - } else { - BackCallback(onBack = onBack) - } - - backHandler.register(callback) - - onDispose { - scope.cancel() - backHandler.unregister(callback) - } - } - } - - @Composable - private fun rememberMutableStateWithLatest( - key: Any, - onReplaced: (latestValue: T) -> Unit, - getValue: () -> T, - ): MutableState { - val latestValue: Holder = remember { Holder(value = null) } - - val state = - remember(key) { - latestValue.value?.also(onReplaced) - mutableStateOf(getValue()) - } - - latestValue.value = state.value - - return state - } - - private data class BackData( - val progress: Float, - val edge: BackEvent.SwipeEdge, - val exitItem: Item, - val enterItem: Item, - ) - - private data class Item( - val stack: ChildStack, - val key: Int, - val modifier: Modifier = Modifier, - ) - - private class Holder(var value: T) - - private class GestureBackCallback( - private val scope: CoroutineScope, - stack: ChildStack, - currentKey: Int, - private val exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, - private val setItems: (List>) -> Unit, - private val onFinished: (newKey: Int) -> Unit, - ) : BackCallback() { - private var backData: BackData = - BackData( - progress = 0F, - edge = BackEvent.SwipeEdge.UNKNOWN, - exitItem = Item(stack = stack, key = currentKey), - enterItem = Item(stack = stack.dropLast(), key = currentKey + 1), - ) - - private fun ChildStack.dropLast(): ChildStack = - ChildStack(active = backStack.last(), backStack = backStack.dropLast(1)) - - override fun onBackStarted(backEvent: BackEvent) { - updateData(backEvent) - } - - private fun updateData(backEvent: BackEvent) { - backData = backData.withProgress(progress = backEvent.progress, edge = backEvent.swipeEdge) - setItems(listOf(backData.enterItem, backData.exitItem)) - } - - override fun onBackProgressed(backEvent: BackEvent) { - updateData(backEvent) - } - - override fun onBackCancelled() { - setItems(listOf(backData.exitItem.copy(modifier = Modifier))) - } - - override fun onBack() { - if (backData.progress > 0F) { - scope.launch { continueGesture() } - } else { - onFinished(backData.exitItem.key) - } - } - - private suspend fun CoroutineScope.continueGesture() { - var progress = backData.progress - while ((progress <= 1F) && isActive) { - delay(16.milliseconds) - progress += 0.075F - backData = backData.withProgress(progress = progress) - setItems(listOf(backData.enterItem, backData.exitItem)) - } - - if (isActive) { - setItems(listOf(backData.enterItem.copy(modifier = Modifier))) - onFinished(backData.enterItem.key) - } - } - - private fun BackData.withProgress( - progress: Float = this.progress, - edge: BackEvent.SwipeEdge = this.edge, - ): BackData = - copy( - progress = progress.coerceIn(0F..1F), - edge = edge, - exitItem = exitItem.copy(modifier = exitModifier(progress, edge)), - enterItem = enterItem.copy(modifier = enterModifier(progress, edge)), - ) - } -} diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/BackGestureHandler.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/BackGestureHandler.kt new file mode 100644 index 000000000..c120afae3 --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/BackGestureHandler.kt @@ -0,0 +1,29 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.backhandler.BackEvent +import com.arkivanov.essenty.backhandler.BackHandler + +@Composable +internal fun BackGestureHandler( + backHandler: BackHandler, + onBackStarted: (BackEvent) -> Unit = {}, + onBackProgressed: (BackEvent) -> Unit = {}, + onBackCancelled: () -> Unit = {}, + onBack: () -> Unit, +) { + DisposableEffect(backHandler) { + val callback = + BackCallback( + onBackStarted = onBackStarted, + onBackProgressed = onBackProgressed, + onBackCancelled = onBackCancelled, + onBack = onBack, + ) + + backHandler.register(callback) + onDispose { backHandler.unregister(callback) } + } +} diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt new file mode 100644 index 000000000..71fe7fc23 --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/DefaultPredictiveBackAnimatable.kt @@ -0,0 +1,31 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import androidx.compose.animation.core.Animatable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent + +@ExperimentalDecomposeApi +internal class DefaultPredictiveBackAnimatable( + initialBackEvent: BackEvent, + private val getExitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, + private val getEnterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, +) : PredictiveBackAnimatable { + + private val progressAnimatable = Animatable(initialValue = initialBackEvent.progress) + private var swipeEdge by mutableStateOf(initialBackEvent.swipeEdge) + override val exitModifier: Modifier get() = getExitModifier(progressAnimatable.value, swipeEdge) + override val enterModifier: Modifier get() = getEnterModifier(progressAnimatable.value, swipeEdge) + + override suspend fun animate(event: BackEvent) { + swipeEdge = event.swipeEdge + progressAnimatable.snapTo(targetValue = event.progress) + } + + override suspend fun finish() { + progressAnimatable.animateTo(targetValue = 1F) + } +} diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt new file mode 100644 index 000000000..a6dc4e1be --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt @@ -0,0 +1,93 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent + +/** + * Animates [exitModifier] and [enterModifier] according to incoming [BackEvent] events. + * + * [Animatable][androidx.compose.animation.core.Animatable] can be used for animations. + */ +@ExperimentalDecomposeApi +interface PredictiveBackAnimatable { + + /** + * Returns a [Modifier] for the child being removed (the currently active child). + * The property must be Compose-observable, e.g. be backed by a Compose state. + */ + val exitModifier: Modifier + + /** + * Returns a [Modifier] for the child being shown (the previous child, behind the currently active child). + * The property must be Compose-observable, e.g. be backed by a Compose state. + */ + val enterModifier: Modifier + + /** + * Animates both [exitModifier] and [enterModifier] according to [event]. + * Any previous animation must be cancelled. + * + * @see androidx.compose.animation.core.Animatable + */ + suspend fun animate(event: BackEvent) + + /** + * Animates both [exitModifier] and [enterModifier] towards the final state. + * Any previous animation must be cancelled. + * + * @see androidx.compose.animation.core.Animatable + */ + suspend fun finish() +} + +/** + * Creates a default implementation of [PredictiveBackAnimatable] with customisable exit and enter [Modifier]s. + * + * @param initialBackEvent an initial [BackEvent] of the predictive back gesture. + * @param exitModifier a function that returns a [Modifier] for every gesture event, for + * the child being removed (the currently active child). + * @param enterModifier a function that returns a [Modifier] for every gesture event, for + * the previous child (behind the currently active child). + */ +@ExperimentalDecomposeApi +fun predictiveBackAnimatable( + initialBackEvent: BackEvent, + exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> + Modifier.exitModifier(progress = progress, edge = edge) + }, + enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> + Modifier.enterModifier(progress = progress) + }, +): PredictiveBackAnimatable = + DefaultPredictiveBackAnimatable( + initialBackEvent = initialBackEvent, + getExitModifier = exitModifier, + getEnterModifier = enterModifier, + ) + +private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = + scale(1F - progress * 0.25F) + .absoluteOffset( + x = when (edge) { + BackEvent.SwipeEdge.LEFT -> 32.dp * progress + BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress + BackEvent.SwipeEdge.UNKNOWN -> 0.dp + }, + ) + .alpha(((1F - progress) * 2F).coerceAtMost(1F)) + .clip(RoundedCornerShape(size = 64.dp * progress)) + +private fun Modifier.enterModifier(progress: Float): Modifier = + drawWithContent { + drawContent() + drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) + } diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt new file mode 100644 index 000000000..d3cc6624a --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt @@ -0,0 +1,175 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.StackAnimation +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.emptyStackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.essenty.backhandler.BackEvent +import com.arkivanov.essenty.backhandler.BackHandler +import kotlinx.coroutines.launch + +/** + * Wraps the provided [animation], handles the predictive back gesture and animates + * the transition from the current [Child] to the previous one. + * Calls [onBack] when the animation is finished. + * + * @param backHandler a source of the predictive back gesture events, see [BackHandler]. + * @param animation a [StackAnimation] for regular transitions. + * @param selector a selector function that is called when the predictive back gesture begins, + * returns [PredictiveBackAnimatable] responsible for animations. + * @param onBack a callback that is called when the gesture is finished. + */ +@ExperimentalDecomposeApi +fun predictiveBackAnimation( + backHandler: BackHandler, + animation: StackAnimation? = null, + selector: ( + initialBackEvent: BackEvent, + exitChild: Child.Created, + enterChild: Child.Created, + ) -> PredictiveBackAnimatable = { initialBackEvent, _, _ -> + predictiveBackAnimatable(initialBackEvent = initialBackEvent) + }, + onBack: () -> Unit, +): StackAnimation = + PredictiveBackAnimation( + backHandler = backHandler, + animation = animation ?: emptyStackAnimation(), + selector = selector, + onBack = onBack, + ) + +@OptIn(ExperimentalDecomposeApi::class) +private class PredictiveBackAnimation( + private val backHandler: BackHandler, + private val animation: StackAnimation, + private val selector: (BackEvent, exitChild: Child.Created, enterChild: Child.Created) -> PredictiveBackAnimatable, + private val onBack: () -> Unit, +) : StackAnimation { + + @Composable + override fun invoke(stack: ChildStack, modifier: Modifier, content: @Composable (child: Child.Created) -> Unit) { + var activeConfigurations: Set by remember { mutableStateOf(emptySet()) } + + val childContent = + remember(content) { + movableContentOf> { child -> + key(child.configuration) { + content(child) + + DisposableEffect(Unit) { + activeConfigurations += child.configuration + onDispose { activeConfigurations -= child.configuration } + } + } + } + } + + var data: Data by rememberMutableStateWithLatest(key = stack) { latestData -> + Data(stack = stack, key = latestData?.nextKey ?: 0) + } + + val (dataStack, dataKey, dataAnimatable) = data + + val items = + if (dataAnimatable == null) { + listOf(Item(stack = dataStack, key = dataKey, modifier = Modifier)) + } else { + listOf( + Item(stack = dataStack.dropLast(), key = dataKey + 1, modifier = dataAnimatable.enterModifier), + Item(stack = dataStack, key = dataKey, modifier = dataAnimatable.exitModifier), + ) + } + + Box(modifier = modifier) { + items.forEach { item -> + key(item.key) { + animation( + stack = item.stack, + modifier = Modifier.fillMaxSize().then(item.modifier), + content = childContent, + ) + } + } + } + + val isBackEnabled = dataStack.backStack.isNotEmpty() + val isBackGestureEnabled = isBackEnabled && ((dataAnimatable != null) || (activeConfigurations.size == 1)) + + if (isBackEnabled) { + if (isBackGestureEnabled) { + val scope = rememberCoroutineScope() + + BackGestureHandler( + backHandler = backHandler, + onBackStarted = { + data = data.copy(animatable = selector(it, data.stack.active, data.stack.backStack.last())) + }, + onBackProgressed = { + scope.launch { data.animatable?.animate(it) } + }, + onBackCancelled = { + data = data.copy(animatable = null) + }, + onBack = { + if (data.animatable == null) { + onBack() + } else { + scope.launch { + data.animatable?.finish() + onBack() + } + } + } + ) + } else { + BackGestureHandler(backHandler = backHandler, onBack = onBack) + } + } + } + + @Composable + private fun rememberMutableStateWithLatest( + key: Any, + getValue: (latestValue: T?) -> T, + ): MutableState { + val latestValue: Holder = remember { Holder(value = null) } + val state = remember(key) { mutableStateOf(getValue(latestValue.value)) } + latestValue.value = state.value + + return state + } + + private fun ChildStack.dropLast(): ChildStack = + ChildStack(active = backStack.last(), backStack = backStack.dropLast(1)) + + private data class Data( + val stack: ChildStack, + val key: Int, + val animatable: PredictiveBackAnimatable? = null, + ) { + val nextKey: Int get() = if (animatable == null) key else key + 1 + } + + private data class Item( + val stack: ChildStack, + val key: Int, + val modifier: Modifier, + ) + + private class Holder(var value: T) +} diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt index 7673531c5..0d9d42f1e 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.fade import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.plus -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveBackAnimation +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback.predictiveBackAnimation import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.scale import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack From 4fdbacc90519da5527882b98a8197dc2f3921d0b Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Tue, 31 Oct 2023 21:33:22 +0000 Subject: [PATCH 07/21] Bumped version to 2.2.0-alpha03 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 495c2d0b6..9a1e794b7 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-alpha02" +decompose = "2.2.0-alpha03" kotlin = "1.9.10" essenty = "1.3.0-alpha02" parcelizeDarwin = "0.2.2" From af2b535a0ab7c18c2324a4b4551898307a273ff2 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 5 Nov 2023 13:20:53 +0000 Subject: [PATCH 08/21] Updated Essenty to 1.3.0-alpha03 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index af66dfabe..7278e7dad 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -2,7 +2,7 @@ decompose = "2.2.0-alpha03" kotlin = "1.9.10" -essenty = "1.3.0-alpha02" +essenty = "1.3.0-alpha03" parcelizeDarwin = "0.2.2" reaktive = "1.2.3" junit = "4.13.2" From 716142e8a12a707bd6336c00cf9d8f1970c66326 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 5 Nov 2023 14:15:24 +0000 Subject: [PATCH 09/21] Bumped version to 2.2.0-alpha04 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 7278e7dad..bdcf30c6b 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-alpha03" +decompose = "2.2.0-alpha04" kotlin = "1.9.10" essenty = "1.3.0-alpha03" parcelizeDarwin = "0.2.2" From 16e6bcaa153ee02c7965ea9d27765e0ba6c7ae63 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Tue, 7 Nov 2023 23:12:44 +0000 Subject: [PATCH 10/21] Updated Kotlin to 1.9.20 and other dependencies --- decompose/build.gradle.kts | 4 +-- .../kotlin/com/arkivanov/decompose/Relay.kt | 4 --- .../kotlin/com/arkivanov/decompose/Utils.kt | 2 -- .../decompose/value/MutableValueBuilder.kt | 5 --- .../backhandler/TestBackDispatcher.kt | 5 --- .../backhandler/TestChildBackHandler.kt | 32 ------------------- .../kotlin/com/arkivanov/decompose/Utils.kt | 7 ---- .../kotlin/com/arkivanov/decompose/Utils.kt | 9 ------ deps.versions.toml | 10 +++--- .../android/extensions-compose-jetbrains.api | 6 ++++ .../api/jvm/extensions-compose-jetbrains.api | 6 ++++ .../jetbrains/pages/PagesScrollAnimation.kt | 4 +-- .../api/extensions-compose-jetpack.api | 6 ++++ .../jetpack/pages/PagesScrollAnimation.kt | 4 +-- gradle.properties | 1 + .../feature1Impl/build.gradle.kts | 21 ++---------- .../feature2Impl/build.gradle.kts | 21 ++---------- settings.gradle.kts | 2 +- 18 files changed, 34 insertions(+), 115 deletions(-) delete mode 100644 decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestChildBackHandler.kt delete mode 100644 decompose/src/nativeMain/kotlin/com/arkivanov/decompose/Utils.kt delete mode 100644 decompose/src/nonNativeMain/kotlin/com/arkivanov/decompose/Utils.kt diff --git a/decompose/build.gradle.kts b/decompose/build.gradle.kts index 9b3838c50..f0d300da3 100644 --- a/decompose/build.gradle.kts +++ b/decompose/build.gradle.kts @@ -27,17 +27,15 @@ kotlin { setupSourceSets { val android by bundle() val nonAndroid by bundle() - val native by bundle() val nonNative by bundle() val darwin by bundle() val js by bundle() val nonJs by bundle() - (nonAndroid + native + nonNative + nonJs) dependsOn common + (nonAndroid + darwin + nonNative + nonJs) dependsOn common (allSet - android) dependsOn nonAndroid (allSet - nativeSet) dependsOn nonNative (allSet - js) dependsOn nonJs - (nativeSet + darwin) dependsOn native darwinSet dependsOn darwin all { diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt index ec0f41fe7..ffd3e5404 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt @@ -6,10 +6,6 @@ internal class Relay( private val isMainThreadCheckEnabled: Boolean = false, ) { - init { - ensureNeverFrozen() - } - private val lock = Lock() private val queue = ArrayDeque() private var isDraining = false diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Utils.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Utils.kt index cec8ed846..c63206ebd 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Utils.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Utils.kt @@ -9,6 +9,4 @@ fun Any.hashString(): String = internal expect val KClass<*>.uniqueName: String? -internal expect fun Any.ensureNeverFrozen() - internal val Lifecycle.isDestroyed: Boolean get() = state == Lifecycle.State.DESTROYED diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt index 7e2f0dad5..622db28b5 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt @@ -1,7 +1,6 @@ package com.arkivanov.decompose.value import com.arkivanov.decompose.Lock -import com.arkivanov.decompose.ensureNeverFrozen import com.arkivanov.decompose.synchronized /** @@ -12,10 +11,6 @@ fun MutableValue(initialValue: T): MutableValue = MutableValueImpl( private class MutableValueImpl(initialValue: T) : MutableValue() { - init { - ensureNeverFrozen() - } - private val lock = Lock() private var _value: T = initialValue private var isEmitting = false diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestBackDispatcher.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestBackDispatcher.kt index 1133cd042..1587dc9fb 100644 --- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestBackDispatcher.kt +++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestBackDispatcher.kt @@ -1,15 +1,10 @@ package com.arkivanov.decompose.backhandler -import com.arkivanov.decompose.ensureNeverFrozen import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.backhandler.BackDispatcher internal class TestBackDispatcher : BackDispatcher { - init { - ensureNeverFrozen() - } - private var set = emptySet() val size: Int get() = set.size diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestChildBackHandler.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestChildBackHandler.kt deleted file mode 100644 index 3015bc12e..000000000 --- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestChildBackHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.arkivanov.decompose.backhandler - -import com.arkivanov.decompose.ensureNeverFrozen -import com.arkivanov.essenty.backhandler.BackCallback - -class TestChildBackHandler( - var isStarted: Boolean = false, - override var isEnabled: Boolean = false, -) : ChildBackHandler { - - init { - ensureNeverFrozen() - } - - override fun start() { - check(!isStarted) - isStarted = true - } - - override fun stop() { - check(isStarted) - isStarted = false - } - - override fun register(callback: BackCallback) { - TODO("Not yet implemented") - } - - override fun unregister(callback: BackCallback) { - TODO("Not yet implemented") - } -} diff --git a/decompose/src/nativeMain/kotlin/com/arkivanov/decompose/Utils.kt b/decompose/src/nativeMain/kotlin/com/arkivanov/decompose/Utils.kt deleted file mode 100644 index e413ee3d2..000000000 --- a/decompose/src/nativeMain/kotlin/com/arkivanov/decompose/Utils.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.arkivanov.decompose - -import kotlin.native.concurrent.ensureNeverFrozen as ensureNeverFrozenNative - -internal actual fun Any.ensureNeverFrozen() { - ensureNeverFrozenNative() -} diff --git a/decompose/src/nonNativeMain/kotlin/com/arkivanov/decompose/Utils.kt b/decompose/src/nonNativeMain/kotlin/com/arkivanov/decompose/Utils.kt deleted file mode 100644 index b06b4b90b..000000000 --- a/decompose/src/nonNativeMain/kotlin/com/arkivanov/decompose/Utils.kt +++ /dev/null @@ -1,9 +0,0 @@ -@file:JvmName("UtilsJvm") - -package com.arkivanov.decompose - -import kotlin.jvm.JvmName - -internal actual fun Any.ensureNeverFrozen() { - // no-op -} diff --git a/deps.versions.toml b/deps.versions.toml index bdcf30c6b..6dcdcbab6 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,18 +1,18 @@ [versions] decompose = "2.2.0-alpha04" -kotlin = "1.9.10" -essenty = "1.3.0-alpha03" -parcelizeDarwin = "0.2.2" +kotlin = "1.9.20" +essenty = "1.3.0-alpha04" +parcelizeDarwin = "0.2.3" reaktive = "1.2.3" junit = "4.13.2" -jetbrainsCompose = "1.5.1" +jetbrainsCompose = "1.5.10" jetbrainsKotlinWrappers = "1.0.0-pre.608" jetbrainsKotlinxCoroutines = "1.6.4" jetbrainsKotlinxSerialization = "1.6.0" jetbrainsBinaryCompatibilityValidator = "0.13.2" jetpackCompose = "1.5.0" -jetpackComposeCompiler = "1.5.3" +jetpackComposeCompiler = "1.5.4" androidGradle = "8.0.2" androidMaterial = "1.6.1" androidPlay = "1.10.3" diff --git a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api index dee65b61c..26ca4566c 100644 --- a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api @@ -30,11 +30,17 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pa public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Default : com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Default; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Disabled : com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Disabled; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/ChildrenKt { diff --git a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api index 140987c0b..a519761cf 100644 --- a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api @@ -42,11 +42,17 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pa public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Default : com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Default; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Disabled : com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation$Disabled; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/ChildrenKt { diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation.kt index 1d20c1aa2..9aba9b04c 100644 --- a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation.kt +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation.kt @@ -8,7 +8,7 @@ import com.arkivanov.decompose.ExperimentalDecomposeApi @ExperimentalDecomposeApi sealed interface PagesScrollAnimation { - object Disabled : PagesScrollAnimation - object Default : PagesScrollAnimation + data object Disabled : PagesScrollAnimation + data object Default : PagesScrollAnimation class Custom(val spec: AnimationSpec) : PagesScrollAnimation } diff --git a/extensions-compose-jetpack/api/extensions-compose-jetpack.api b/extensions-compose-jetpack/api/extensions-compose-jetpack.api index cd8fc4e34..a4063a739 100644 --- a/extensions-compose-jetpack/api/extensions-compose-jetpack.api +++ b/extensions-compose-jetpack/api/extensions-compose-jetpack.api @@ -30,11 +30,17 @@ public final class com/arkivanov/decompose/extensions/compose/jetpack/pages/Page public final class com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation$Default : com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation$Default; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation$Disabled : com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation { public static final field $stable I public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation$Disabled; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/ChildrenKt { diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation.kt index 9f81aab20..0a443f91b 100644 --- a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation.kt +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/pages/PagesScrollAnimation.kt @@ -8,7 +8,7 @@ import com.arkivanov.decompose.ExperimentalDecomposeApi @ExperimentalDecomposeApi sealed interface PagesScrollAnimation { - object Disabled : PagesScrollAnimation - object Default : PagesScrollAnimation + data object Disabled : PagesScrollAnimation + data object Default : PagesScrollAnimation class Custom(val spec: AnimationSpec) : PagesScrollAnimation } diff --git a/gradle.properties b/gradle.properties index d40fe3df8..da8ed906d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ org.gradle.parallel=true org.gradle.caching=true systemProp.org.gradle.internal.publish.checksums.insecure=true kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.mpp.applyDefaultHierarchyTemplate=false # For compatibility with Kotlin 1.9.0 android.experimental.lint.version=8.1.0 diff --git a/sample/shared/dynamic-features/feature1Impl/build.gradle.kts b/sample/shared/dynamic-features/feature1Impl/build.gradle.kts index 3f069689a..b20b10a63 100644 --- a/sample/shared/dynamic-features/feature1Impl/build.gradle.kts +++ b/sample/shared/dynamic-features/feature1Impl/build.gradle.kts @@ -2,9 +2,6 @@ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.iosCompat import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupSourceSets -import org.jetbrains.compose.ComposeCompilerKotlinSupportPlugin -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType plugins { @@ -60,20 +57,6 @@ kotlin { } } -compose.web.targets() - - -plugins.removeAll { it is ComposeCompilerKotlinSupportPlugin } - -class ComposeNoNativePlugin : KotlinCompilerPluginSupportPlugin by ComposeCompilerKotlinSupportPlugin( - buildEventsListenerRegistry = {}, -) { - override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { - return when (kotlinCompilation.target.platformType) { - KotlinPlatformType.native -> false - else -> ComposeCompilerKotlinSupportPlugin(buildEventsListenerRegistry = {}).isApplicable(kotlinCompilation) - } - } +compose { + platformTypes.set(platformTypes.get() - KotlinPlatformType.js - KotlinPlatformType.native) } - -apply() diff --git a/sample/shared/dynamic-features/feature2Impl/build.gradle.kts b/sample/shared/dynamic-features/feature2Impl/build.gradle.kts index 9ccc60f8a..01d5924ab 100644 --- a/sample/shared/dynamic-features/feature2Impl/build.gradle.kts +++ b/sample/shared/dynamic-features/feature2Impl/build.gradle.kts @@ -2,9 +2,6 @@ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.iosCompat import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupSourceSets -import org.jetbrains.compose.ComposeCompilerKotlinSupportPlugin -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType plugins { @@ -60,20 +57,6 @@ kotlin { } } -compose.web.targets() - - -plugins.removeAll { it is ComposeCompilerKotlinSupportPlugin } - -class ComposeNoNativePlugin : KotlinCompilerPluginSupportPlugin by ComposeCompilerKotlinSupportPlugin( - buildEventsListenerRegistry = {}, -) { - override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { - return when (kotlinCompilation.target.platformType) { - KotlinPlatformType.native -> false - else -> ComposeCompilerKotlinSupportPlugin(buildEventsListenerRegistry = {}).isApplicable(kotlinCompilation) - } - } +compose { + platformTypes.set(platformTypes.get() - KotlinPlatformType.js - KotlinPlatformType.native) } - -apply() diff --git a/settings.gradle.kts b/settings.gradle.kts index 32e172e95..c443475b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,7 +15,7 @@ pluginManagement { resolutionStrategy { eachPlugin { if (requested.id.toString() == "com.arkivanov.gradle.setup") { - useModule("com.github.arkivanov:gradle-setup-plugin:2571f348ff") + useModule("com.github.arkivanov:gradle-setup-plugin:655aedff78") } } } From aba0059a0be6572dfff5304d79061812421b3818 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Thu, 16 Nov 2023 19:46:07 +0000 Subject: [PATCH 11/21] Added ApplicationLifecycle for iOS and tvOS targets --- decompose/build.gradle.kts | 4 +- .../lifecycle/ApplicationLifecycle.kt | 84 +++++++++++++++++++ sample/app-ios/app-ios/app_iosApp.swift | 70 ++++------------ 3 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt diff --git a/decompose/build.gradle.kts b/decompose/build.gradle.kts index f0d300da3..5b92f687f 100644 --- a/decompose/build.gradle.kts +++ b/decompose/build.gradle.kts @@ -29,6 +29,7 @@ kotlin { val nonAndroid by bundle() val nonNative by bundle() val darwin by bundle() + val itvos by bundle() val js by bundle() val nonJs by bundle() @@ -36,7 +37,8 @@ kotlin { (allSet - android) dependsOn nonAndroid (allSet - nativeSet) dependsOn nonNative (allSet - js) dependsOn nonJs - darwinSet dependsOn darwin + (iosSet + tvosSet) dependsOn itvos + (darwinSet - iosSet - tvosSet + itvos) dependsOn darwin all { languageSettings { diff --git a/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt b/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt new file mode 100644 index 000000000..0217e3c57 --- /dev/null +++ b/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt @@ -0,0 +1,84 @@ +package com.arkivanov.decompose.lifecycle + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.destroy +import com.arkivanov.essenty.lifecycle.pause +import com.arkivanov.essenty.lifecycle.resume +import com.arkivanov.essenty.lifecycle.start +import com.arkivanov.essenty.lifecycle.stop +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNotificationName +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIApplicationDidBecomeActiveNotification +import platform.UIKit.UIApplicationDidEnterBackgroundNotification +import platform.UIKit.UIApplicationWillEnterForegroundNotification +import platform.UIKit.UIApplicationWillResignActiveNotification +import platform.UIKit.UIApplicationWillTerminateNotification + +/** + * An implementation of [Lifecycle] that follows the [UIApplication][platform.UIKit.UIApplication] lifecycle notifications. + */ +@ExperimentalDecomposeApi +class ApplicationLifecycle private constructor( + private val lifecycle: LifecycleRegistry, +) : Lifecycle by lifecycle { + + constructor() : this(lifecycle = LifecycleRegistry()) + + init { + addObserver(name = UIApplicationWillEnterForegroundNotification, selectorName = "willEnterForeground") + addObserver(name = UIApplicationDidBecomeActiveNotification, selectorName = "didBecomeActive") + addObserver(name = UIApplicationWillResignActiveNotification, selectorName = "willResignActive") + addObserver(name = UIApplicationDidEnterBackgroundNotification, selectorName = "didEnterBackground") + addObserver(name = UIApplicationWillTerminateNotification, selectorName = "willTerminate") + } + + @OptIn(ExperimentalForeignApi::class) + private fun addObserver(name: NSNotificationName, selectorName: String) { + NSNotificationCenter.defaultCenter.addObserver( + name = name, + `object` = null, + observer = this, + selector = NSSelectorFromString(selectorName), + ) + } + + @Suppress("unused") + @OptIn(BetaInteropApi::class) + @ObjCAction + fun willEnterForeground() { + lifecycle.start() + } + + @Suppress("unused") + @OptIn(BetaInteropApi::class) + @ObjCAction + fun didBecomeActive() { + lifecycle.resume() + } + + @Suppress("unused") + @OptIn(BetaInteropApi::class) + @ObjCAction + fun willResignActive() { + lifecycle.pause() + } + + @Suppress("unused") + @OptIn(BetaInteropApi::class) + @ObjCAction + fun didEnterBackground() { + lifecycle.stop() + } + + @OptIn(BetaInteropApi::class) + @ObjCAction + fun willTerminate() { + lifecycle.destroy() + } +} diff --git a/sample/app-ios/app-ios/app_iosApp.swift b/sample/app-ios/app-ios/app_iosApp.swift index 5cb7def1a..bd20440a3 100644 --- a/sample/app-ios/app-ios/app_iosApp.swift +++ b/sample/app-ios/app-ios/app_iosApp.swift @@ -13,76 +13,40 @@ struct app_iosApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate - private var rootHolder: RootHolder { appDelegate.getRootHolder() } - var body: some Scene { WindowGroup { - RootView(rootHolder.root) - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in - LifecycleRegistryExtKt.resume(rootHolder.lifecycle) - } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in - LifecycleRegistryExtKt.pause(rootHolder.lifecycle) - } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in - LifecycleRegistryExtKt.stop(rootHolder.lifecycle) - } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in - LifecycleRegistryExtKt.destroy(rootHolder.lifecycle) - } + RootView(appDelegate.root) } } } class AppDelegate: NSObject, UIApplicationDelegate { - private var rootHolder: RootHolder? - + private var stateKeeper = StateKeeperDispatcherKt.StateKeeperDispatcher(savedState: nil) + + lazy var root: RootComponent = DefaultRootComponent( + componentContext: DefaultComponentContext( + lifecycle: ApplicationLifecycle(), + stateKeeper: stateKeeper, + instanceKeeper: nil, + backHandler: nil + ), + featureInstaller: DefaultFeatureInstaller.shared, + deepLink: DefaultRootComponentDeepLinkNone.shared, + webHistoryController: nil + ) + func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool { - let savedState = rootHolder!.stateKeeper.save() - CodingKt.encodeParcelable(coder, value: savedState, key: "savedState") + CodingKt.encodeParcelable(coder, value: stateKeeper.save(), key: "savedState") return true } func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool { do { let savedState = try CodingKt.decodeParcelable(coder, key: "savedState") as! ParcelableParcelableContainer - rootHolder = RootHolder(savedState: savedState) + stateKeeper = StateKeeperDispatcherKt.StateKeeperDispatcher(savedState: savedState) return true } catch { return false } } - - fileprivate func getRootHolder() -> RootHolder { - if (rootHolder == nil) { - rootHolder = RootHolder(savedState: nil) - } - - return rootHolder! - } -} - -private class RootHolder { - let lifecycle: LifecycleRegistry - let stateKeeper: StateKeeperDispatcher - let root: RootComponent - - init(savedState: ParcelableParcelableContainer?) { - lifecycle = LifecycleRegistryKt.LifecycleRegistry() - stateKeeper = StateKeeperDispatcherKt.StateKeeperDispatcher(savedState: savedState) - - root = DefaultRootComponent( - componentContext: DefaultComponentContext( - lifecycle: lifecycle, - stateKeeper: stateKeeper, - instanceKeeper: nil, - backHandler: nil - ), - featureInstaller: DefaultFeatureInstaller.shared, - deepLink: DefaultRootComponentDeepLinkNone.shared, - webHistoryController: nil - ) - - LifecycleRegistryExtKt.create(lifecycle) - } } From d16bbc96e53ad2877e3db0de651f639da770ae79 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Fri, 17 Nov 2023 19:19:42 +0000 Subject: [PATCH 12/21] Bumped version to 2.2.0-alpha05 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 6dcdcbab6..e07189d98 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-alpha04" +decompose = "2.2.0-alpha05" kotlin = "1.9.20" essenty = "1.3.0-alpha04" parcelizeDarwin = "0.2.3" From 7df29b2822918397e7276ae95438c8f462de8b83 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Fri, 17 Nov 2023 20:15:46 +0000 Subject: [PATCH 13/21] Downgrade create-nexus-staging-repo to v1.2.0 --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 902087637..5b1a57e6c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,11 +13,11 @@ jobs: repository-id: ${{ steps.create.outputs.repository-id }} steps: - id: create - uses: nexus-actions/create-nexus-staging-repo@v1 + uses: nexus-actions/create-nexus-staging-repo@v1.2.0 with: username: arkivanov password: ${{ secrets.SONATYPE_PASSWORD }} - staging-profile-id: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + staging_profile_id: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} description: Created by GitHub Actions base_url: https://oss.sonatype.org/service/local/ publish: From 4013a7db9bb4cb240debd9083fdb8005ba51e179 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Fri, 17 Nov 2023 20:48:20 +0000 Subject: [PATCH 14/21] Fixed publish.yml --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5b1a57e6c..ebf3cf1ae 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest name: Create staging repository outputs: - repository-id: ${{ steps.create.outputs.repository-id }} + repository_id: ${{ steps.create.outputs.repository_id }} steps: - id: create uses: nexus-actions/create-nexus-staging-repo@v1.2.0 @@ -33,7 +33,7 @@ jobs: java-version: 17 - name: Publish env: - SONATYPE_REPOSITORY_ID: ${{ needs.create-staging-repository.outputs.repository-id }} + SONATYPE_REPOSITORY_ID: ${{ needs.create-staging-repository.outputs.repository_id }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} @@ -48,7 +48,7 @@ jobs: with: username: arkivanov password: ${{ secrets.SONATYPE_PASSWORD }} - staging_repository_id: ${{ needs.create-staging-repository.outputs.repository-id }} + staging_repository_id: ${{ needs.create-staging-repository.outputs.repository_id }} base_url: https://oss.sonatype.org/service/local/ close_only: 'true' check-publication: From 5612d520c57a3d5fc908cdfbebe5c919d4c224c4 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Tue, 21 Nov 2023 22:47:54 +0000 Subject: [PATCH 15/21] Added materialPredictiveBackAnimatable as default --- docs/extensions/compose.md | 4 +- .../android/extensions-compose-jetbrains.api | 6 +- .../api/jvm/extensions-compose-jetbrains.api | 6 +- .../animation/predictiveback/LayoutCorners.kt | 122 ++++++++++++++++ .../animation/predictiveback/LayoutCorners.kt | 19 +++ .../MaterialPredictiveBackAnimatable.kt | 134 +++++++++++++++++ .../PredictiveBackAnimatable.kt | 39 +---- .../predictiveback/PredictiveBackAnimation.kt | 2 +- .../animation/predictiveback/LayoutCorners.kt | 6 + .../api/extensions-compose-jetpack.api | 6 +- .../animation/predictiveback/LayoutCorners.kt | 135 ++++++++++++++++++ .../MaterialPredictiveBackAnimatable.kt | 134 +++++++++++++++++ .../PredictiveBackAnimatable.kt | 34 +---- .../predictiveback/PredictiveBackAnimation.kt | 2 +- 14 files changed, 579 insertions(+), 70 deletions(-) create mode 100644 extensions-compose-jetbrains/src/androidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt create mode 100644 extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt create mode 100644 extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/LayoutCorners.kt create mode 100644 extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt diff --git a/docs/extensions/compose.md b/docs/extensions/compose.md index bebb10fc0..20970b633 100644 --- a/docs/extensions/compose.md +++ b/docs/extensions/compose.md @@ -357,7 +357,9 @@ Please refer to the predefined animators (`fade`, `slide`, etc.) for implementat !!!warning Predictive Back Gesture support is experimental, the API is subject to change. For now, please use version 2.1.x. -`Child Stack` supports the new [Android Predictive Back Gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) on all platforms. To enable the gesture, first implement `BackHandlerOwner` interface in your component with `Child Stack`, then just pass `predictiveBackAnimation` to the `Children` function. +`Child Stack` supports the new [Android Predictive Back Gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) on all platforms. By default, the gesture animation resembles the [predictive back design for Android](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back), but it's customizable. + +To enable the gesture, first implement `BackHandlerOwner` interface in your component with `Child Stack`, then just pass `predictiveBackAnimation` to the `Children` function. ```kotlin title="RootComponent" interface RootComponent : BackHandlerOwner { diff --git a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api index 26ca4566c..ef01c0bf3 100644 --- a/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api @@ -112,6 +112,11 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatableKt { + public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun materialPredictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; +} + public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable { public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -121,7 +126,6 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetbr public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt { public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; - public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt { diff --git a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api index a519761cf..f6203c05b 100644 --- a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api @@ -124,6 +124,11 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator; } +public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatableKt { + public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun materialPredictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; +} + public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable { public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -133,7 +138,6 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetbr public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt { public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; - public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable; } public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt { diff --git a/extensions-compose-jetbrains/src/androidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt b/extensions-compose-jetbrains/src/androidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt new file mode 100644 index 000000000..a0711ae3f --- /dev/null +++ b/extensions-compose-jetbrains/src/androidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt @@ -0,0 +1,122 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import android.content.Context +import android.os.Build +import android.view.RoundedCorner +import android.view.View +import android.view.WindowManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.core.content.ContextCompat.getSystemService + +internal actual fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier = + composed { + val context = LocalContext.current + val density = LocalDensity.current + val screenInfo = remember(context) { context.getScreenInfo(density) } + + if (screenInfo != null) { + val rootView = LocalView.current + val layoutDirection = LocalLayoutDirection.current + var positionOnScreen by remember { mutableStateOf(null) } + val corners = getLayoutCorners(screenInfo, positionOnScreen, layoutDirection) + + onGloballyPositioned { coords -> + positionOnScreen = getBoundsOnScreen(rootView = rootView, boundsInRoot = coords.boundsInRoot()) + }.block(corners) + } else { + block(LayoutCorners()) + } + } + +private fun Context.getScreenInfo(density: Density): ScreenInfo? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return null + } + + val windowMetrics = requireNotNull(getSystemService(this, WindowManager::class.java)).maximumWindowMetrics + val insets = windowMetrics.windowInsets + + return with(density) { + ScreenInfo( + cornerRadii = CornerRadii( + topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius?.toDp(), + topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.radius?.toDp(), + bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius?.toDp(), + bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.radius?.toDp(), + ), + width = windowMetrics.bounds.width(), + height = windowMetrics.bounds.height(), + ) + } +} + +private fun getLayoutCorners( + screenInfo: ScreenInfo, + positionOnScreen: Rect?, + layoutDirection: LayoutDirection, +): LayoutCorners { + if (positionOnScreen == null) { + return LayoutCorners() + } + + val (cornerRadii, screenWidth, screenHeight) = screenInfo + val (left, top, right, bottom) = positionOnScreen + + val topLeft = getLayoutCorner(radius = cornerRadii.topLeft, isFixed = (left <= 0) && (top <= 0)) + val topRight = getLayoutCorner(radius = cornerRadii.topRight, isFixed = (right >= screenWidth) && (top <= 0)) + val bottomRight = getLayoutCorner(radius = cornerRadii.bottomRight, isFixed = (right >= screenWidth) && (bottom >= screenHeight)) + val bottomLeft = getLayoutCorner(radius = cornerRadii.bottomLeft, isFixed = (left <= 0) && (bottom >= screenHeight)) + + return when (layoutDirection) { + LayoutDirection.Ltr -> LayoutCorners(topStart = topLeft, topEnd = topRight, bottomEnd = bottomRight, bottomStart = bottomLeft) + LayoutDirection.Rtl -> LayoutCorners(topStart = topRight, topEnd = topLeft, bottomEnd = bottomLeft, bottomStart = bottomRight) + } +} + +private fun getLayoutCorner(radius: Dp?, isFixed: Boolean): LayoutCorner = + if (radius == null) { + LayoutCorner() + } else { + LayoutCorner(radius = radius, isFixed = isFixed) + } + +private fun getBoundsOnScreen(rootView: View, boundsInRoot: Rect): Rect { + val rootViewLeftTopOnScreen = IntArray(2) + rootView.getLocationOnScreen(rootViewLeftTopOnScreen) + val (rootViewLeftOnScreen, rootViewTopOnScreen) = rootViewLeftTopOnScreen + + return Rect( + left = rootViewLeftOnScreen + boundsInRoot.left, + top = rootViewTopOnScreen + boundsInRoot.top, + right = rootViewLeftOnScreen + boundsInRoot.right, + bottom = rootViewTopOnScreen + boundsInRoot.bottom, + ) +} + +private data class ScreenInfo( + val cornerRadii: CornerRadii, + val width: Int, + val height: Int, +) + +private data class CornerRadii( + val topLeft: Dp? = null, + val topRight: Dp? = null, + val bottomRight: Dp? = null, + val bottomLeft: Dp? = null, +) diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt new file mode 100644 index 000000000..9a1ef6122 --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt @@ -0,0 +1,19 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal expect fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier + +internal data class LayoutCorners( + val topStart: LayoutCorner = LayoutCorner(), + val topEnd: LayoutCorner = LayoutCorner(), + val bottomEnd: LayoutCorner = LayoutCorner(), + val bottomStart: LayoutCorner = LayoutCorner(), +) + +internal data class LayoutCorner( + val radius: Dp = 16.dp, + val isFixed: Boolean = true, +) diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt new file mode 100644 index 000000000..967cfa53d --- /dev/null +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt @@ -0,0 +1,134 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch + +/** + * Creates an implementation of [PredictiveBackAnimatable] that resembles the + * [predictive back design for Android](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back). + * + * @param initialBackEvent an initial [BackEvent] of the predictive back gesture. + * @param shape an optional clipping shape of the child being removed (the currently active child). + * If not supplied then a [RoundedCornerShape][androidx.compose.foundation.shape.RoundedCornerShape] will be applied. + */ +@ExperimentalDecomposeApi +fun materialPredictiveBackAnimatable( + initialBackEvent: BackEvent, + shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null, +): PredictiveBackAnimatable = + MaterialPredictiveBackAnimatable( + initialEvent = initialBackEvent, + shape = shape, + ) + +@ExperimentalDecomposeApi +private class MaterialPredictiveBackAnimatable( + private val initialEvent: BackEvent, + private val shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null, +) : PredictiveBackAnimatable { + + private val finishProgressAnimatable = Animatable(initialValue = 1F) + private val finishProgress by derivedStateOf { finishProgressAnimatable.value } + private val progressAnimatable = Animatable(initialValue = initialEvent.progress) + private val progress by derivedStateOf { progressAnimatable.value } + private var edge by mutableStateOf(initialEvent.swipeEdge) + private var touchY by mutableFloatStateOf(initialEvent.touchY) + + override val exitModifier: Modifier + get() = + if (shape == null) { + Modifier.withLayoutCorners { corners -> + graphicsLayer { setupExitGraphicLayer(corners.toShape()) } + } + } else { + Modifier.graphicsLayer { + setupExitGraphicLayer(this@MaterialPredictiveBackAnimatable.shape.invoke(progress, edge)) + } + } + + override val enterModifier: Modifier + get() = + Modifier.drawWithContent { + drawContent() + drawRect(color = Color.Black.copy(alpha = finishProgress * 0.25F)) + } + + private fun GraphicsLayerScope.setupExitGraphicLayer(layoutShape: Shape) { + val pivotFractionX = + when (edge) { + BackEvent.SwipeEdge.LEFT -> 1F + BackEvent.SwipeEdge.RIGHT -> 0F + BackEvent.SwipeEdge.UNKNOWN -> 0.5F + } + + transformOrigin = TransformOrigin(pivotFractionX = pivotFractionX, pivotFractionY = 0.5F) + + val scale = 1F - progress / 10F + scaleX = scale + scaleY = scale + + val translationXLimit = + when (edge) { + BackEvent.SwipeEdge.LEFT -> -8.dp.toPx() + BackEvent.SwipeEdge.RIGHT -> 8.dp.toPx() + BackEvent.SwipeEdge.UNKNOWN -> 0F + } + + translationX = translationXLimit * progress + + val translationYLimit = size.height / 20F - 8.dp.toPx() + val translationYFactor = ((touchY - initialEvent.touchY) / size.height) * (progress * 3F).coerceAtMost(1f) + translationY = translationYLimit * translationYFactor + + alpha = finishProgress + shape = layoutShape + clip = true + } + + private fun LayoutCorners.toShape(): RoundedCornerShape = + RoundedCornerShape( + topStart = topStart.getProgressRadius(), + topEnd = topEnd.getProgressRadius(), + bottomEnd = bottomEnd.getProgressRadius(), + bottomStart = bottomStart.getProgressRadius(), + ) + + private fun LayoutCorner.getProgressRadius(): Dp = + if (isFixed) radius else radius * progress + + override suspend fun animate(event: BackEvent) { + edge = event.swipeEdge + touchY = event.touchY + progressAnimatable.animateTo(event.progress) + } + + override suspend fun finish() { + val velocityFactor = progressAnimatable.velocity.coerceAtMost(1F) / 1F + val progress = progressAnimatable.value + coroutineScope { + joinAll( + launch { progressAnimatable.animateTo(progress + (1F - progress) * velocityFactor) }, + launch { finishProgressAnimatable.animateTo(targetValue = 0F) }, + ) + } + } +} diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt index 95ca88b62..874305104 100644 --- a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable.kt @@ -1,14 +1,6 @@ package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.essenty.backhandler.BackEvent @@ -51,6 +43,11 @@ interface PredictiveBackAnimatable { /** * Creates a default implementation of [PredictiveBackAnimatable] with customisable exit and enter [Modifier]s. + * The gesture progress follows the events from the system. Automatically animates the progress towards 1.0 + * once the gesture is confirmed. + * + * If the behaviour of the returned default [PredictiveBackAnimatable] is undesired or the API is not suitable for + * your use case, then consider implementing [PredictiveBackAnimatable] manually. * * @param initialBackEvent an initial [BackEvent] of the predictive back gesture. * @param exitModifier a function that returns a [Modifier] for every gesture event, for @@ -61,33 +58,11 @@ interface PredictiveBackAnimatable { @ExperimentalDecomposeApi fun predictiveBackAnimatable( initialBackEvent: BackEvent, - exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> - Modifier.exitModifier(progress = progress, edge = edge) - }, - enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> - Modifier.enterModifier(progress = progress) - }, + exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, + enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, ): PredictiveBackAnimatable = DefaultPredictiveBackAnimatable( initialBackEvent = initialBackEvent, getExitModifier = exitModifier, getEnterModifier = enterModifier, ) - -private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = - scale(1F - progress * 0.25F) - .absoluteOffset( - x = when (edge) { - BackEvent.SwipeEdge.LEFT -> 32.dp * progress - BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress - BackEvent.SwipeEdge.UNKNOWN -> 0.dp - }, - ) - .alpha(((1F - progress) * 2F).coerceAtMost(1F)) - .clip(RoundedCornerShape(size = 64.dp * progress)) - -private fun Modifier.enterModifier(progress: Float): Modifier = - drawWithContent { - drawContent() - drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) - } diff --git a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt index a14edfe99..b941d1e6f 100644 --- a/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt +++ b/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimation.kt @@ -42,7 +42,7 @@ fun predictiveBackAnimation( exitChild: Child.Created, enterChild: Child.Created, ) -> PredictiveBackAnimatable = { initialBackEvent, _, _ -> - predictiveBackAnimatable(initialBackEvent = initialBackEvent) + materialPredictiveBackAnimatable(initialBackEvent = initialBackEvent) }, onBack: () -> Unit, ): StackAnimation = diff --git a/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt b/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt new file mode 100644 index 000000000..ee14e7b17 --- /dev/null +++ b/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/LayoutCorners.kt @@ -0,0 +1,6 @@ +package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback + +import androidx.compose.ui.Modifier + +internal actual fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier = + block(LayoutCorners()) diff --git a/extensions-compose-jetpack/api/extensions-compose-jetpack.api b/extensions-compose-jetpack/api/extensions-compose-jetpack.api index a4063a739..bed924ad9 100644 --- a/extensions-compose-jetpack/api/extensions-compose-jetpack.api +++ b/extensions-compose-jetpack/api/extensions-compose-jetpack.api @@ -112,6 +112,11 @@ public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/anim public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/StackAnimator; } +public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/MaterialPredictiveBackAnimatableKt { + public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; + public static synthetic fun materialPredictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; +} + public abstract interface class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable { public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -121,7 +126,6 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetpa public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatableKt { public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; - public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable; } public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimationKt { diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/LayoutCorners.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/LayoutCorners.kt new file mode 100644 index 000000000..6e4d1209a --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/LayoutCorners.kt @@ -0,0 +1,135 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import android.content.Context +import android.os.Build +import android.view.RoundedCorner +import android.view.View +import android.view.WindowManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getSystemService + +internal fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier = + composed { + val context = LocalContext.current + val density = LocalDensity.current + val screenInfo = remember(context) { context.getScreenInfo(density) } + + if (screenInfo != null) { + val rootView = LocalView.current + val layoutDirection = LocalLayoutDirection.current + var positionOnScreen by remember { mutableStateOf(null) } + val corners = getLayoutCorners(screenInfo, positionOnScreen, layoutDirection) + + onGloballyPositioned { coords -> + positionOnScreen = getBoundsOnScreen(rootView = rootView, boundsInRoot = coords.boundsInRoot()) + }.block(corners) + } else { + block(LayoutCorners()) + } + } + +private fun Context.getScreenInfo(density: Density): ScreenInfo? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return null + } + + val windowMetrics = requireNotNull(getSystemService(this, WindowManager::class.java)).maximumWindowMetrics + val insets = windowMetrics.windowInsets + + return with(density) { + ScreenInfo( + cornerRadii = CornerRadii( + topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius?.toDp(), + topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.radius?.toDp(), + bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius?.toDp(), + bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.radius?.toDp(), + ), + width = windowMetrics.bounds.width(), + height = windowMetrics.bounds.height(), + ) + } +} + +private fun getLayoutCorners( + screenInfo: ScreenInfo, + positionOnScreen: Rect?, + layoutDirection: LayoutDirection, +): LayoutCorners { + if (positionOnScreen == null) { + return LayoutCorners() + } + + val (cornerRadii, screenWidth, screenHeight) = screenInfo + val (left, top, right, bottom) = positionOnScreen + + val topLeft = getLayoutCorner(radius = cornerRadii.topLeft, isFixed = (left <= 0) && (top <= 0)) + val topRight = getLayoutCorner(radius = cornerRadii.topRight, isFixed = (right >= screenWidth) && (top <= 0)) + val bottomRight = getLayoutCorner(radius = cornerRadii.bottomRight, isFixed = (right >= screenWidth) && (bottom >= screenHeight)) + val bottomLeft = getLayoutCorner(radius = cornerRadii.bottomLeft, isFixed = (left <= 0) && (bottom >= screenHeight)) + + return when (layoutDirection) { + LayoutDirection.Ltr -> LayoutCorners(topStart = topLeft, topEnd = topRight, bottomEnd = bottomRight, bottomStart = bottomLeft) + LayoutDirection.Rtl -> LayoutCorners(topStart = topRight, topEnd = topLeft, bottomEnd = bottomLeft, bottomStart = bottomRight) + } +} + +private fun getLayoutCorner(radius: Dp?, isFixed: Boolean): LayoutCorner = + if (radius == null) { + LayoutCorner() + } else { + LayoutCorner(radius = radius, isFixed = isFixed) + } + +private fun getBoundsOnScreen(rootView: View, boundsInRoot: Rect): Rect { + val rootViewLeftTopOnScreen = IntArray(2) + rootView.getLocationOnScreen(rootViewLeftTopOnScreen) + val (rootViewLeftOnScreen, rootViewTopOnScreen) = rootViewLeftTopOnScreen + + return Rect( + left = rootViewLeftOnScreen + boundsInRoot.left, + top = rootViewTopOnScreen + boundsInRoot.top, + right = rootViewLeftOnScreen + boundsInRoot.right, + bottom = rootViewTopOnScreen + boundsInRoot.bottom, + ) +} + +internal data class LayoutCorners( + val topStart: LayoutCorner = LayoutCorner(), + val topEnd: LayoutCorner = LayoutCorner(), + val bottomEnd: LayoutCorner = LayoutCorner(), + val bottomStart: LayoutCorner = LayoutCorner(), +) + +internal data class LayoutCorner( + val radius: Dp = 16.dp, + val isFixed: Boolean = true, +) + +private data class ScreenInfo( + val cornerRadii: CornerRadii, + val width: Int, + val height: Int, +) + +private data class CornerRadii( + val topLeft: Dp? = null, + val topRight: Dp? = null, + val bottomRight: Dp? = null, + val bottomLeft: Dp? = null, +) diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt new file mode 100644 index 000000000..f4537872a --- /dev/null +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/MaterialPredictiveBackAnimatable.kt @@ -0,0 +1,134 @@ +package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.essenty.backhandler.BackEvent +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch + +/** + * Creates an implementation of [PredictiveBackAnimatable] that resembles the + * [predictive back design for Android](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back). + * + * @param initialBackEvent an initial [BackEvent] of the predictive back gesture. + * @param shape an optional clipping shape of the child being removed (the currently active child). + * If not supplied then a [RoundedCornerShape][androidx.compose.foundation.shape.RoundedCornerShape] will be applied. + */ +@ExperimentalDecomposeApi +fun materialPredictiveBackAnimatable( + initialBackEvent: BackEvent, + shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null, +): PredictiveBackAnimatable = + MaterialPredictiveBackAnimatable( + initialEvent = initialBackEvent, + shape = shape, + ) + +@ExperimentalDecomposeApi +private class MaterialPredictiveBackAnimatable( + private val initialEvent: BackEvent, + private val shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null, +) : PredictiveBackAnimatable { + + private val finishProgressAnimatable = Animatable(initialValue = 1F) + private val finishProgress by derivedStateOf { finishProgressAnimatable.value } + private val progressAnimatable = Animatable(initialValue = initialEvent.progress) + private val progress by derivedStateOf { progressAnimatable.value } + private var edge by mutableStateOf(initialEvent.swipeEdge) + private var touchY by mutableFloatStateOf(initialEvent.touchY) + + override val exitModifier: Modifier + get() = + if (shape == null) { + Modifier.withLayoutCorners { corners -> + graphicsLayer { setupExitGraphicLayer(corners.toShape()) } + } + } else { + Modifier.graphicsLayer { + setupExitGraphicLayer(this@MaterialPredictiveBackAnimatable.shape.invoke(progress, edge)) + } + } + + override val enterModifier: Modifier + get() = + Modifier.drawWithContent { + drawContent() + drawRect(color = Color.Black.copy(alpha = finishProgress * 0.25F)) + } + + private fun GraphicsLayerScope.setupExitGraphicLayer(layoutShape: Shape) { + val pivotFractionX = + when (edge) { + BackEvent.SwipeEdge.LEFT -> 1F + BackEvent.SwipeEdge.RIGHT -> 0F + BackEvent.SwipeEdge.UNKNOWN -> 0.5F + } + + transformOrigin = TransformOrigin(pivotFractionX = pivotFractionX, pivotFractionY = 0.5F) + + val scale = 1F - progress / 10F + scaleX = scale + scaleY = scale + + val translationXLimit = + when (edge) { + BackEvent.SwipeEdge.LEFT -> -8.dp.toPx() + BackEvent.SwipeEdge.RIGHT -> 8.dp.toPx() + BackEvent.SwipeEdge.UNKNOWN -> 0F + } + + translationX = translationXLimit * progress + + val translationYLimit = size.height / 20F - 8.dp.toPx() + val translationYFactor = ((touchY - initialEvent.touchY) / size.height) * (progress * 3F).coerceAtMost(1f) + translationY = translationYLimit * translationYFactor + + alpha = finishProgress + shape = layoutShape + clip = true + } + + private fun LayoutCorners.toShape(): RoundedCornerShape = + RoundedCornerShape( + topStart = topStart.getProgressRadius(), + topEnd = topEnd.getProgressRadius(), + bottomEnd = bottomEnd.getProgressRadius(), + bottomStart = bottomStart.getProgressRadius(), + ) + + private fun LayoutCorner.getProgressRadius(): Dp = + if (isFixed) radius else radius * progress + + override suspend fun animate(event: BackEvent) { + edge = event.swipeEdge + touchY = event.touchY + progressAnimatable.animateTo(event.progress) + } + + override suspend fun finish() { + val velocityFactor = progressAnimatable.velocity.coerceAtMost(1F) / 1F + val progress = progressAnimatable.value + coroutineScope { + joinAll( + launch { progressAnimatable.animateTo(progress + (1F - progress) * velocityFactor) }, + launch { finishProgressAnimatable.animateTo(targetValue = 0F) }, + ) + } + } +} diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt index a6dc4e1be..a1e51eac8 100644 --- a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable.kt @@ -1,14 +1,6 @@ package com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.essenty.backhandler.BackEvent @@ -61,33 +53,11 @@ interface PredictiveBackAnimatable { @ExperimentalDecomposeApi fun predictiveBackAnimatable( initialBackEvent: BackEvent, - exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge -> - Modifier.exitModifier(progress = progress, edge = edge) - }, - enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ -> - Modifier.enterModifier(progress = progress) - }, + exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, + enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier, ): PredictiveBackAnimatable = DefaultPredictiveBackAnimatable( initialBackEvent = initialBackEvent, getExitModifier = exitModifier, getEnterModifier = enterModifier, ) - -private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier = - scale(1F - progress * 0.25F) - .absoluteOffset( - x = when (edge) { - BackEvent.SwipeEdge.LEFT -> 32.dp * progress - BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress - BackEvent.SwipeEdge.UNKNOWN -> 0.dp - }, - ) - .alpha(((1F - progress) * 2F).coerceAtMost(1F)) - .clip(RoundedCornerShape(size = 64.dp * progress)) - -private fun Modifier.enterModifier(progress: Float): Modifier = - drawWithContent { - drawContent() - drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F)) - } diff --git a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt index d3cc6624a..c025a9ce9 100644 --- a/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt +++ b/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimation.kt @@ -42,7 +42,7 @@ fun predictiveBackAnimation( exitChild: Child.Created, enterChild: Child.Created, ) -> PredictiveBackAnimatable = { initialBackEvent, _, _ -> - predictiveBackAnimatable(initialBackEvent = initialBackEvent) + materialPredictiveBackAnimatable(initialBackEvent = initialBackEvent) }, onBack: () -> Unit, ): StackAnimation = From 607fb9ac51a7a5e640c97951347ad416ad09ac8f Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 25 Nov 2023 11:48:46 +0000 Subject: [PATCH 16/21] Bumped version to 2.2.0-beta01 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index e07189d98..56f6cbca5 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-alpha05" +decompose = "2.2.0-beta01" kotlin = "1.9.20" essenty = "1.3.0-alpha04" parcelizeDarwin = "0.2.3" From 5a4939c2526016b77346cc7dc014163a0fe45bc1 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Fri, 24 Nov 2023 12:26:06 +0000 Subject: [PATCH 17/21] LTR/RTL in PredictiveBackGestureOverlay (cherry picked from commit 086ba1188f732702186af663d07c4b8a026e942e) --- .../api/jvm/extensions-compose-jetbrains.api | 2 +- .../jetbrains/PredictiveBackGestureOverlay.kt | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api index f6203c05b..1c6669b78 100644 --- a/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api +++ b/extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api @@ -3,7 +3,7 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/Predicti } public final class com/arkivanov/decompose/extensions/compose/jetbrains/PredictiveBackGestureOverlayKt { - public static final fun PredictiveBackGestureOverlay (Lcom/arkivanov/essenty/backhandler/BackDispatcher;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;Ljava/util/Set;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun PredictiveBackGestureOverlay (Lcom/arkivanov/essenty/backhandler/BackDispatcher;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class com/arkivanov/decompose/extensions/compose/jetbrains/SubscribeAsStateKt { diff --git a/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/PredictiveBackGestureOverlay.kt b/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/PredictiveBackGestureOverlay.kt index 357d3840f..b315842e8 100644 --- a/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/PredictiveBackGestureOverlay.kt +++ b/extensions-compose-jetbrains/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/PredictiveBackGestureOverlay.kt @@ -18,7 +18,9 @@ import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.essenty.backhandler.BackCallback @@ -35,8 +37,10 @@ import com.arkivanov.essenty.backhandler.BackEvent.SwipeEdge * @param backIcon an icon to be shown while the gesture is being performed. A default icon can be shown * using [PredictiveBackGestureIcon]. * @param modifier a [Modifier] to applied to the overlay. - * @param swipeEdges a [Set] of [SwipeEdge] where swipe gestures can be detected. Absolute, e.g. RTL mode - * should be handled manually. + * @param startEdgeEnabled controls whether the start edge is enabled or not, + * left in RTL mode and right in LTR mode. + * @param endEdgeEnabled controls whether the end edge is enabled or not, + * right in RTL mode and left in LTR mode. * @param onClose If supplied, then the back gesture is also handled when there are no other enabled back * callbacks registered in [backDispatcher], can be used to close the application. * @param content a content to be shown under the overlay. @@ -47,16 +51,25 @@ fun PredictiveBackGestureOverlay( backDispatcher: BackDispatcher, backIcon: (@Composable (progress: Float, edge: SwipeEdge) -> Unit)?, modifier: Modifier = Modifier, - swipeEdges: Set = setOf(SwipeEdge.LEFT, SwipeEdge.RIGHT), + startEdgeEnabled: Boolean = true, + endEdgeEnabled: Boolean = true, onClose: (() -> Unit)? = null, content: @Composable () -> Unit, ) { val iconState: MutableState = remember { mutableStateOf(IconState()) } + val layoutDirection = LocalLayoutDirection.current Box( modifier = modifier.handleBackGestures( backDispatcher = backDispatcher, - swipeEdges = swipeEdges, + leftEdgeEnabled = when (layoutDirection) { + LayoutDirection.Ltr -> startEdgeEnabled + LayoutDirection.Rtl -> endEdgeEnabled + }, + rightEdgeEnabled = when (layoutDirection) { + LayoutDirection.Ltr -> endEdgeEnabled + LayoutDirection.Rtl -> startEdgeEnabled + }, onIconMoved = { position, progress, edge -> iconState.value = IconState( @@ -95,19 +108,20 @@ fun PredictiveBackGestureOverlay( private fun Modifier.handleBackGestures( backDispatcher: BackDispatcher, - swipeEdges: Set, + leftEdgeEnabled: Boolean, + rightEdgeEnabled: Boolean, onIconMoved: (position: Offset, progress: Float, BackGestureHandler.Edge) -> Unit, onIconHidden: () -> Unit, ): Modifier = - pointerInput(backDispatcher, swipeEdges) { + pointerInput(backDispatcher, leftEdgeEnabled, rightEdgeEnabled) { awaitEachGesture { onIconHidden() val down = awaitFirstDown(pass = PointerEventPass.Initial) val startPosition = down.position - val isLeftInvalid = (SwipeEdge.LEFT !in swipeEdges) || (startPosition.x > 16.dp.toPx()) - val isRightInvalid = (SwipeEdge.RIGHT !in swipeEdges) || (startPosition.x < size.width - 16.dp.toPx()) + val isLeftInvalid = !leftEdgeEnabled || (startPosition.x > 16.dp.toPx()) + val isRightInvalid = !rightEdgeEnabled || (startPosition.x < size.width - 16.dp.toPx()) if (isLeftInvalid && isRightInvalid) { return@awaitEachGesture From fa184af9348bebd0664d7e7b1cf487bc29856d58 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 25 Nov 2023 18:21:00 +0000 Subject: [PATCH 18/21] Updated Essenty to 1.3.0-beta01 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 56f6cbca5..477bce3b8 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -2,7 +2,7 @@ decompose = "2.2.0-beta01" kotlin = "1.9.20" -essenty = "1.3.0-alpha04" +essenty = "1.3.0-beta01" parcelizeDarwin = "0.2.3" reaktive = "1.2.3" junit = "4.13.2" From d8e1f88e3af198fc0729f810264925a77817e128 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 25 Nov 2023 18:50:55 +0000 Subject: [PATCH 19/21] 2.2.0-beta02 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 477bce3b8..5e27c31bf 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -1,6 +1,6 @@ [versions] -decompose = "2.2.0-beta01" +decompose = "2.2.0-beta02" kotlin = "1.9.20" essenty = "1.3.0-beta01" parcelizeDarwin = "0.2.3" From 3f428dc7f35369cc440a9f9198356f503bd4079d Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Fri, 1 Dec 2023 10:15:03 +0000 Subject: [PATCH 20/21] Use addObserverForName instead of addObserver in ApplicationLifecycle --- .../lifecycle/ApplicationLifecycle.kt | 84 ++++++++----------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt b/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt index 0217e3c57..a02f80837 100644 --- a/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt +++ b/decompose/src/itvosMain/kotlin/com/arkivanov/decompose/lifecycle/ApplicationLifecycle.kt @@ -3,22 +3,25 @@ package com.arkivanov.decompose.lifecycle import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.create import com.arkivanov.essenty.lifecycle.destroy +import com.arkivanov.essenty.lifecycle.doOnDestroy import com.arkivanov.essenty.lifecycle.pause import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.essenty.lifecycle.start import com.arkivanov.essenty.lifecycle.stop -import kotlinx.cinterop.BetaInteropApi -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSNotification import platform.Foundation.NSNotificationCenter import platform.Foundation.NSNotificationName -import platform.Foundation.NSSelectorFromString +import platform.Foundation.NSOperationQueue +import platform.UIKit.UIApplication import platform.UIKit.UIApplicationDidBecomeActiveNotification import platform.UIKit.UIApplicationDidEnterBackgroundNotification +import platform.UIKit.UIApplicationState import platform.UIKit.UIApplicationWillEnterForegroundNotification import platform.UIKit.UIApplicationWillResignActiveNotification import platform.UIKit.UIApplicationWillTerminateNotification +import platform.darwin.NSObjectProtocol /** * An implementation of [Lifecycle] that follows the [UIApplication][platform.UIKit.UIApplication] lifecycle notifications. @@ -30,55 +33,38 @@ class ApplicationLifecycle private constructor( constructor() : this(lifecycle = LifecycleRegistry()) + private val willEnterForegroundObserver = addObserver(UIApplicationWillEnterForegroundNotification) { lifecycle.start() } + private val didBecomeActiveObserver = addObserver(UIApplicationDidBecomeActiveNotification) { lifecycle.resume() } + private val willResignActiveObserver = addObserver(UIApplicationWillResignActiveNotification) { lifecycle.pause() } + private val didEnterBackgroundObserver = addObserver(UIApplicationDidEnterBackgroundNotification) { lifecycle.stop() } + private val willTerminateObserver = addObserver(UIApplicationWillTerminateNotification) { lifecycle.destroy() } + init { - addObserver(name = UIApplicationWillEnterForegroundNotification, selectorName = "willEnterForeground") - addObserver(name = UIApplicationDidBecomeActiveNotification, selectorName = "didBecomeActive") - addObserver(name = UIApplicationWillResignActiveNotification, selectorName = "willResignActive") - addObserver(name = UIApplicationDidEnterBackgroundNotification, selectorName = "didEnterBackground") - addObserver(name = UIApplicationWillTerminateNotification, selectorName = "willTerminate") + NSOperationQueue.mainQueue.addOperationWithBlock { + if (lifecycle.state == Lifecycle.State.INITIALIZED) { + when (UIApplication.sharedApplication.applicationState) { + UIApplicationState.UIApplicationStateActive -> lifecycle.resume() + UIApplicationState.UIApplicationStateInactive -> lifecycle.start() + UIApplicationState.UIApplicationStateBackground -> lifecycle.create() + else -> lifecycle.create() + } + } + } + + doOnDestroy { + NSNotificationCenter.defaultCenter.removeObserver(willEnterForegroundObserver) + NSNotificationCenter.defaultCenter.removeObserver(didBecomeActiveObserver) + NSNotificationCenter.defaultCenter.removeObserver(willResignActiveObserver) + NSNotificationCenter.defaultCenter.removeObserver(didEnterBackgroundObserver) + NSNotificationCenter.defaultCenter.removeObserver(willTerminateObserver) + } } - @OptIn(ExperimentalForeignApi::class) - private fun addObserver(name: NSNotificationName, selectorName: String) { - NSNotificationCenter.defaultCenter.addObserver( + private fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = + NSNotificationCenter.defaultCenter.addObserverForName( name = name, `object` = null, - observer = this, - selector = NSSelectorFromString(selectorName), + queue = NSOperationQueue.mainQueue, + usingBlock = block, ) - } - - @Suppress("unused") - @OptIn(BetaInteropApi::class) - @ObjCAction - fun willEnterForeground() { - lifecycle.start() - } - - @Suppress("unused") - @OptIn(BetaInteropApi::class) - @ObjCAction - fun didBecomeActive() { - lifecycle.resume() - } - - @Suppress("unused") - @OptIn(BetaInteropApi::class) - @ObjCAction - fun willResignActive() { - lifecycle.pause() - } - - @Suppress("unused") - @OptIn(BetaInteropApi::class) - @ObjCAction - fun didEnterBackground() { - lifecycle.stop() - } - - @OptIn(BetaInteropApi::class) - @ObjCAction - fun willTerminate() { - lifecycle.destroy() - } } From 8b03f2527d17df89e1f4478545704c0243d3d0c3 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 3 Dec 2023 12:22:47 +0000 Subject: [PATCH 21/21] Updated Essenty to 1.3.0 --- deps.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.versions.toml b/deps.versions.toml index 5e27c31bf..485a2fa0d 100644 --- a/deps.versions.toml +++ b/deps.versions.toml @@ -2,7 +2,7 @@ decompose = "2.2.0-beta02" kotlin = "1.9.20" -essenty = "1.3.0-beta01" +essenty = "1.3.0" parcelizeDarwin = "0.2.3" reaktive = "1.2.3" junit = "4.13.2"