From d9beeba8015eabeb5814073630676d5acb3fe34c Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 11:26:50 +0200 Subject: [PATCH 01/11] fix(core-test): improve outcome assertion error messages --- .../core/test/outcomes/OutcomeAssertions.kt | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/core/test/src/commonMain/kotlin/si/inova/kotlinova/core/test/outcomes/OutcomeAssertions.kt b/core/test/src/commonMain/kotlin/si/inova/kotlinova/core/test/outcomes/OutcomeAssertions.kt index 49c41186..306b61e5 100644 --- a/core/test/src/commonMain/kotlin/si/inova/kotlinova/core/test/outcomes/OutcomeAssertions.kt +++ b/core/test/src/commonMain/kotlin/si/inova/kotlinova/core/test/outcomes/OutcomeAssertions.kt @@ -24,41 +24,45 @@ import io.kotest.assertions.errorCollector import io.kotest.assertions.intellijFormatError import io.kotest.assertions.print.Printed import io.kotest.assertions.withClue +import io.kotest.matchers.should import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.beInstanceOf import io.kotest.matchers.types.shouldBeInstanceOf import si.inova.kotlinova.core.outcome.CauseException import si.inova.kotlinova.core.outcome.Outcome infix fun Outcome.shouldBeSuccessWithData(expectedData: T) { assertSoftly { - this - .shouldBeInstanceOf>() - .data - .let { - withClue("Outcome's data does not match") { - it.shouldBe(expectedData) - } - } + this should beInstanceOf>() - if (this is Outcome.Error) { - errorCollector.collectOrThrow(exception) + if (this is Outcome.Success) { + this + .data + .let { + withClue("Outcome's data does not match") { + it.shouldBe(expectedData) + } + } + } else if (this is Outcome.Error) { + reportContainingError() } } } infix fun Outcome.shouldBeProgressWithData(expectedData: T?) { assertSoftly { - this - .shouldBeInstanceOf>() - .data - .let { - withClue("Outcome's data does not match") { - it.shouldBe(expectedData) - } - } + this should beInstanceOf>() - if (this is Outcome.Error) { - errorCollector.collectOrThrow(exception) + if (this is Outcome.Progress) { + this + .data + .let { + withClue("Outcome's data does not match") { + it.shouldBe(expectedData) + } + } + } else if (this is Outcome.Error) { + reportContainingError() } } } @@ -68,29 +72,28 @@ fun Outcome.shouldBeProgressWith( expectedProgress: Float? = null ) { assertSoftly { - this - .shouldBeInstanceOf>() - .apply { - data - .let { - withClue("Outcome's data does not match") { - it.shouldBe(expectedData) - } + this should beInstanceOf>() + + if (this is Outcome.Progress) { + this + .data + .let { + withClue("Outcome's data does not match") { + it.shouldBe(expectedData) } - } - .apply { - if (expectedProgress != null) { - progress - .let { - withClue("Outcome's progress does not match") { - it.shouldBe(expectedProgress) + } + .apply { + if (expectedProgress != null) { + progress + .let { + withClue("Outcome's progress does not match") { + it.shouldBe(expectedProgress) + } } - } + } } - } - - if (this is Outcome.Error) { - errorCollector.collectOrThrow(exception) + } else if (this is Outcome.Error) { + reportContainingError() } } } @@ -143,3 +146,7 @@ fun Outcome.shouldBeErrorWith( return returnException } + +private fun Outcome.Error<*>.reportContainingError() { + errorCollector.collectOrThrow(AssertionError("Reported error: $exception", exception)) +} From 04545f81dead6b04ccda866497e7d6cad8006037 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 11:56:41 +0200 Subject: [PATCH 02/11] feat(core): add USER_REQUESTED_REFRESH LoadingStyle --- .../kotlin/si/inova/kotlinova/core/outcome/Mapping.kt | 6 ++++-- .../kotlin/si/inova/kotlinova/core/outcome/Outcome.kt | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Mapping.kt b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Mapping.kt index 429403ca..1204c24b 100644 --- a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Mapping.kt +++ b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Mapping.kt @@ -117,8 +117,10 @@ fun Outcome.downgradeTo( this is Outcome.Progress -> { if (targetType is Outcome.Progress) { val combinedProgress = targetType.progress?.let { progress?.times(it) } - val style = if (targetType.style == LoadingStyle.ADDITIONAL_DATA || this.style == LoadingStyle.ADDITIONAL_DATA) { - LoadingStyle.ADDITIONAL_DATA + val style = if (targetType.style != LoadingStyle.NORMAL) { + targetType.style + } else if (this.style != LoadingStyle.NORMAL) { + this.style } else { LoadingStyle.NORMAL } diff --git a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt index 1db6b606..99d1e8c4 100644 --- a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt +++ b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt @@ -34,5 +34,6 @@ sealed class Outcome { enum class LoadingStyle { NORMAL, - ADDITIONAL_DATA + ADDITIONAL_DATA, + USER_REQUESTED_REFRESH } From e6ed73785f44fc20fc4bbb5ecdf3492caa717055 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 11:59:07 +0200 Subject: [PATCH 03/11] feat(navigation): add screenWrapper to RootNavigationContainer --- .../navigation/simplestack/RootNavigatorContainer.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/simplestack/RootNavigatorContainer.kt b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/simplestack/RootNavigatorContainer.kt index bd314121..22497691 100644 --- a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/simplestack/RootNavigatorContainer.kt +++ b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/simplestack/RootNavigatorContainer.kt @@ -29,14 +29,17 @@ import si.inova.kotlinova.navigation.screenkeys.ScreenKey */ @SuppressLint("ComposableNaming") // Backstack return is only there as a convenience, it's mostly meant to be used without return @Composable -fun NavigationInjection.Factory.RootNavigationContainer(initialHistory: () -> List): Backstack { +fun NavigationInjection.Factory.RootNavigationContainer( + screenWrapper: @Composable (key: ScreenKey, screen: @Composable () -> Unit) -> Unit = { _, screen -> screen() }, + initialHistory: () -> List +): Backstack { val composeStateChanger = remember { ComposeStateChanger() } val asyncStateChanger = remember(composeStateChanger) { AsyncStateChanger(composeStateChanger) } val backstack = this.rememberBackstack(asyncStateChanger) { initialHistory() } BackstackProvider(backstack) { - composeStateChanger.Content() + composeStateChanger.Content(screenWrapper) } return backstack From 32ed4ea931029006a71af7f27198755d8ce0bb07 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 12:17:23 +0200 Subject: [PATCH 04/11] feat(navigation): allow boundType = Screen::class in @ContributesScreenBinding --- .../compiler/ScreenInjectionGenerator.kt | 11 +++++- .../navigation/tests/AbstractScreens.kt | 39 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/navigation/navigation-compiler/src/main/kotlin/si/inova/kotlinova/navigation/compiler/ScreenInjectionGenerator.kt b/navigation/navigation-compiler/src/main/kotlin/si/inova/kotlinova/navigation/compiler/ScreenInjectionGenerator.kt index 313372e5..143f98fb 100644 --- a/navigation/navigation-compiler/src/main/kotlin/si/inova/kotlinova/navigation/compiler/ScreenInjectionGenerator.kt +++ b/navigation/navigation-compiler/src/main/kotlin/si/inova/kotlinova/navigation/compiler/ScreenInjectionGenerator.kt @@ -40,6 +40,7 @@ import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeSpec @@ -197,13 +198,21 @@ class ScreenInjectionGenerator : CodeGenerator { } val screenBindingFunction = if (contributeScreenBindingAnnotation != null) { - val boundType = contributeScreenBindingAnnotation + var boundType = contributeScreenBindingAnnotation .argumentAt("boundType", 1) ?.value() ?.asTypeName() ?: clas.getFirstScreenParent()?.asTypeName() ?: error("Invalid @ContributesScreenBinding annotation: $clas does not extend Screen") + if (boundType is ParameterizedTypeName && + boundType.rawType == SCREEN_BASE_CLASS && + boundType.typeArguments.firstOrNull() !is ClassName + ) { + val screenKey = clas.getScreenKeyIfItExists() ?: error("Unknown screen key for $clas") + boundType = SCREEN_BASE_CLASS.parameterizedBy(screenKey.asTypeName()) + } + val returnType = SCREEN_FACTORY.parameterizedBy(boundType) FunSpec.builder("bindsScreenFactoryToParentType") .returns(returnType) diff --git a/navigation/tests/src/androidTest/kotlin/si/inova/kotlinova/navigation/tests/AbstractScreens.kt b/navigation/tests/src/androidTest/kotlin/si/inova/kotlinova/navigation/tests/AbstractScreens.kt index 5d4e8d80..7d17c42c 100644 --- a/navigation/tests/src/androidTest/kotlin/si/inova/kotlinova/navigation/tests/AbstractScreens.kt +++ b/navigation/tests/src/androidTest/kotlin/si/inova/kotlinova/navigation/tests/AbstractScreens.kt @@ -57,6 +57,13 @@ class AbstractScreens { rule.onNodeWithText("Hello from Inner Screen").assertIsDisplayed() } + @Test + internal fun showScreenContainingGenericScreenReferenceWithCustomKeyAndExplicitBoundType() { + rule.insertTestNavigation(OuterScreenReferencingGenericScreenWithExplicitBoundTypeKey) + + rule.onNodeWithText("Hello from Inner Screen").assertIsDisplayed() + } + @Parcelize object OuterScreenReferencingAbstractScreenKey : NoArgsScreenKey() @@ -127,7 +134,18 @@ class AbstractScreens { } @Parcelize - object InnerScreenKey : NoArgsScreenKey() + object OuterScreenReferencingGenericScreenWithExplicitBoundTypeKey : NoArgsScreenKey() + + class OuterScreenReferencingGenericScreenWithExplicitBoundType( + private val innerScreen: Screen + ) : Screen() { + @Composable + override fun Content(key: OuterScreenReferencingGenericScreenWithExplicitBoundTypeKey) { + Column { + innerScreen.Content(InnerScreenKeyWithExplicitBoundType) + } + } + } @Suppress("unused") @ContributesScreenBinding @@ -141,4 +159,23 @@ class AbstractScreens { } } } + + @Parcelize + object InnerScreenKey : NoArgsScreenKey() + + @Suppress("unused") + @ContributesScreenBinding(boundType = Screen::class) + class TestAbstractScreenReferencingCustomKeyWithExplicitBoundType @Inject constructor( + private val service: ServiceScopes.SharedService + ) : Screen() { + @Composable + override fun Content(key: InnerScreenKeyWithExplicitBoundType) { + Column { + Text("Hello from Inner Screen") + } + } + } + + @Parcelize + object InnerScreenKeyWithExplicitBoundType : NoArgsScreenKey() } From 4f941d34f6996d445de94b90215685686aac1dfb Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 12:32:05 +0200 Subject: [PATCH 05/11] feat(core): add currentMonotonicTimeMillis to TimeProvider --- .../core/time/DefaultAndroidTimeProvider.kt | 4 ++++ .../kotlinova/core/time/FakeAndroidTimeProvider.kt | 4 ++++ .../inova/kotlinova/core/time/DefaultTimeProvider.kt | 5 +++++ .../si/inova/kotlinova/core/time/TimeProvider.kt | 12 ++++++++++++ .../kotlinova/core/test/time/FakeTimeProvider.kt | 4 ++++ 5 files changed, 29 insertions(+) diff --git a/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/DefaultAndroidTimeProvider.kt b/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/DefaultAndroidTimeProvider.kt index 767fc157..c6a48858 100644 --- a/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/DefaultAndroidTimeProvider.kt +++ b/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/DefaultAndroidTimeProvider.kt @@ -29,6 +29,10 @@ object DefaultAndroidTimeProvider : AndroidTimeProvider { return System.currentTimeMillis() } + override fun currentMonotonicTimeMillis(): Long { + return SystemClock.elapsedRealtime() + } + override fun currentLocalDate(): LocalDate { return LocalDate.now() } diff --git a/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/FakeAndroidTimeProvider.kt b/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/FakeAndroidTimeProvider.kt index 9b37b48d..904b5137 100644 --- a/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/FakeAndroidTimeProvider.kt +++ b/core/src/androidMain/kotlin/si/inova/kotlinova/core/time/FakeAndroidTimeProvider.kt @@ -38,6 +38,10 @@ class FakeAndroidTimeProvider( return currentMilliseconds() } + override fun currentMonotonicTimeMillis(): Long { + return currentMilliseconds() + } + override fun elapsedRealtimeNanos(): Long { return currentMilliseconds() } diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt index 8cb5eb13..6e0ac813 100644 --- a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt +++ b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt @@ -22,12 +22,17 @@ import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.time.ZonedDateTime +import kotlin.time.Duration.Companion.nanoseconds object DefaultTimeProvider : TimeProvider { override fun currentTimeMillis(): Long { return System.currentTimeMillis() } + override fun currentMonotonicTimeMillis(): Long { + return System.nanoTime().nanoseconds.inWholeMilliseconds + } + override fun currentLocalDate(): LocalDate { return LocalDate.now() } diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt index b04a5d18..c20320bc 100644 --- a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt +++ b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt @@ -34,6 +34,18 @@ interface TimeProvider { */ fun currentTimeMillis(): Long + /** + * The value returned represents milliseconds since some fixed but arbitrary origin time + * (perhaps in the future, so values may be negative). + * + * This is not related to any other notion of system or wall-clock time, + * meaning the value will not change if user changes system time settings. That's why + * it is recommended to use this to measure elapsed time instead of [currentTimeMillis]. + * + * This value should not be stored persistently between reboots / process instances. It should be kept in-memory only. + */ + fun currentMonotonicTimeMillis(): Long + fun currentLocalDate(): LocalDate fun currentLocalDateTime(): LocalDateTime diff --git a/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt b/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt index c9787a55..a4625460 100644 --- a/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt +++ b/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt @@ -39,6 +39,10 @@ class FakeTimeProvider( return currentMilliseconds() } + override fun currentMonotonicTimeMillis(): Long { + return currentMilliseconds() + } + override fun currentLocalDate(): LocalDate { return currentLocalDate.invoke() } From b56e1bf6d2b2099045be24b1580535bbf6940f57 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 13:11:38 +0200 Subject: [PATCH 06/11] refactor: move common JVM and Android code to jvmCommon source set --- buildSrc/src/main/kotlin/multiplatform-module.gradle.kts | 7 +++++-- .../kotlin/si/inova/kotlinova/core/containers/List.kt | 0 .../kotlin/si/inova/kotlinova/core/containers/Map.kt | 0 .../kotlin/si/inova/kotlinova/core/containers/WeakList.kt | 0 .../kotlin/si/inova/kotlinova/core/data/Debouncer.kt | 0 .../si/inova/kotlinova/core/locale/MeasurementSystems.kt | 0 .../kotlin/si/inova/kotlinova/core/logging/Throwables.kt | 0 .../si/inova/kotlinova/core/time/DefaultTimeProvider.kt | 0 .../kotlin/si/inova/kotlinova/core/time/TimeProvider.kt | 0 .../si/inova/kotlinova/core/test/time/FakeTimeProvider.kt | 0 .../kotlin/si/inova/kotlinova/core/test/time/Time.kt | 0 retrofit/build.gradle.kts | 2 +- retrofit/retrofit-test/build.gradle.kts | 2 +- .../kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt | 0 .../si/inova/kotlinova/retrofit/ServiceTestingHelper.kt | 0 .../kotlin/retrofit2/RetrofitAccessors.kt | 0 .../kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt | 0 .../inova/kotlinova/retrofit/callfactory/ErrorHandler.kt | 0 .../retrofit/callfactory/ErrorHandlingAdapterFactory.kt | 0 .../kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt | 0 .../callfactory/StaleWhileRevalidateCallAdapterFactory.kt | 0 .../retrofit/converter/LazyRetrofitConverterFactory.kt | 0 .../retrofit/interceptors/BypassCacheInterceptor.kt | 0 .../kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt | 0 .../retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt | 0 .../si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt | 0 26 files changed, 7 insertions(+), 4 deletions(-) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/containers/List.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/containers/Map.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/containers/WeakList.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/data/Debouncer.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/locale/MeasurementSystems.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/logging/Throwables.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt (100%) rename core/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt (100%) rename core/test/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt (100%) rename core/test/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/core/test/time/Time.kt (100%) rename retrofit/retrofit-test/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt (100%) rename retrofit/retrofit-test/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/ServiceTestingHelper.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/retrofit2/RetrofitAccessors.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandler.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingAdapterFactory.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/converter/LazyRetrofitConverterFactory.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/interceptors/BypassCacheInterceptor.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt (100%) rename retrofit/src/{jvmMain => jvmCommon}/kotlin/si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt (100%) diff --git a/buildSrc/src/main/kotlin/multiplatform-module.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-module.gradle.kts index 5b6a67f7..7abde3a0 100644 --- a/buildSrc/src/main/kotlin/multiplatform-module.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-module.gradle.kts @@ -48,7 +48,10 @@ kotlin { } } val commonMain by getting - val jvmMain by getting + val jvmCommon by creating + val jvmMain by getting { + dependsOn(jvmCommon) + } val jvmTest by getting { dependencies { implementation(libs.turbine) @@ -59,7 +62,7 @@ kotlin { } } val androidMain by getting { - dependsOn(jvmMain) + dependsOn(jvmCommon) } val androidUnitTest by getting { dependsOn(jvmTest) diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/List.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/List.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/List.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/List.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/Map.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/Map.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/Map.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/Map.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/WeakList.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/WeakList.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/containers/WeakList.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/containers/WeakList.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Debouncer.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Debouncer.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/locale/MeasurementSystems.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/locale/MeasurementSystems.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/locale/MeasurementSystems.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/locale/MeasurementSystems.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/logging/Throwables.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/logging/Throwables.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/logging/Throwables.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/logging/Throwables.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/time/DefaultTimeProvider.kt diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt similarity index 100% rename from core/src/jvmMain/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt rename to core/src/jvmCommon/kotlin/si/inova/kotlinova/core/time/TimeProvider.kt diff --git a/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt b/core/test/src/jvmCommon/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt similarity index 100% rename from core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt rename to core/test/src/jvmCommon/kotlin/si/inova/kotlinova/core/test/time/FakeTimeProvider.kt diff --git a/core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/Time.kt b/core/test/src/jvmCommon/kotlin/si/inova/kotlinova/core/test/time/Time.kt similarity index 100% rename from core/test/src/jvmMain/kotlin/si/inova/kotlinova/core/test/time/Time.kt rename to core/test/src/jvmCommon/kotlin/si/inova/kotlinova/core/test/time/Time.kt diff --git a/retrofit/build.gradle.kts b/retrofit/build.gradle.kts index 4c9c6edc..26e3d2ae 100644 --- a/retrofit/build.gradle.kts +++ b/retrofit/build.gradle.kts @@ -40,7 +40,7 @@ kotlin { compileOnly(libs.androidx.compose.runtime) } } - jvmMain { + jvmCommon { dependencies { api(libs.okhttp) api(libs.moshi) diff --git a/retrofit/retrofit-test/build.gradle.kts b/retrofit/retrofit-test/build.gradle.kts index 51881caa..a73898d5 100644 --- a/retrofit/retrofit-test/build.gradle.kts +++ b/retrofit/retrofit-test/build.gradle.kts @@ -33,7 +33,7 @@ publishLibrary( kotlin { sourceSets { - jvmMain { + jvmCommon { dependencies { api(projects.retrofit) api(libs.okhttp.mockWebServer) diff --git a/retrofit/retrofit-test/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt b/retrofit/retrofit-test/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt similarity index 100% rename from retrofit/retrofit-test/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt rename to retrofit/retrofit-test/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/MockWebServer.kt diff --git a/retrofit/retrofit-test/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/ServiceTestingHelper.kt b/retrofit/retrofit-test/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/ServiceTestingHelper.kt similarity index 100% rename from retrofit/retrofit-test/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/ServiceTestingHelper.kt rename to retrofit/retrofit-test/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/ServiceTestingHelper.kt diff --git a/retrofit/src/jvmMain/kotlin/retrofit2/RetrofitAccessors.kt b/retrofit/src/jvmCommon/kotlin/retrofit2/RetrofitAccessors.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/retrofit2/RetrofitAccessors.kt rename to retrofit/src/jvmCommon/kotlin/retrofit2/RetrofitAccessors.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/SyntheticHeaders.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandler.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandler.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandler.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandler.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingAdapterFactory.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingAdapterFactory.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingAdapterFactory.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingAdapterFactory.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/ErrorHandlingUtils.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/converter/LazyRetrofitConverterFactory.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/converter/LazyRetrofitConverterFactory.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/converter/LazyRetrofitConverterFactory.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/converter/LazyRetrofitConverterFactory.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/interceptors/BypassCacheInterceptor.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/interceptors/BypassCacheInterceptor.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/interceptors/BypassCacheInterceptor.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/interceptors/BypassCacheInterceptor.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/moshi/Moshi.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/moshi/adapters/JavaTimeMoshiAdapter.kt diff --git a/retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt similarity index 100% rename from retrofit/src/jvmMain/kotlin/si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt rename to retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/okhttp/OkHttpCoroutines.kt From 934a03cce57a8aac876555c5b436ee47438ae683 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 13:27:32 +0200 Subject: [PATCH 07/11] feat(core): add multiplatform Immutable annotation This will allow non-Android code to mark data classes as Jetpack Compose Stable --- .../si/inova/kotlinova/core/data/Immutable.kt | 26 +++++++++++++++++++ .../si/inova/kotlinova/core/data/Immutable.kt | 23 ++++++++++++++++ .../si/inova/kotlinova/core/data/Immutable.kt | 24 +++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 core/src/androidMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt create mode 100644 core/src/commonMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt create mode 100644 core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt diff --git a/core/src/androidMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt b/core/src/androidMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt new file mode 100644 index 00000000..ce206952 --- /dev/null +++ b/core/src/androidMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 INOVA IT d.o.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package si.inova.kotlinova.core.data + +import androidx.compose.runtime.StableMarker + +/** + * Multiplatform marker that marks this class as Immutable. On some platforms (such as Android's Jetpack Compose) + * this can be used for extra optimizations. + */ +@StableMarker +actual annotation class Immutable() diff --git a/core/src/commonMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt b/core/src/commonMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt new file mode 100644 index 00000000..47b01f57 --- /dev/null +++ b/core/src/commonMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 INOVA IT d.o.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package si.inova.kotlinova.core.data + +/** + * Multiplatform marker that marks this class as Immutable. On some platforms (such as Android's Jetpack Compose) + * this can be used for extra optimizations. + */ +expect annotation class Immutable() diff --git a/core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt new file mode 100644 index 00000000..e79a060f --- /dev/null +++ b/core/src/jvmMain/kotlin/si/inova/kotlinova/core/data/Immutable.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 INOVA IT d.o.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package si.inova.kotlinova.core.data + +/** + * Multiplatform marker that marks this class as Immutable. On some platforms (such as Android's Jetpack Compose) + * this can be used for extra optimizations. + */ +// No-op for JVM +actual annotation class Immutable From d4dc95d2c7857a196629a71fa8dd2a54344b286f Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 19 Jun 2023 13:28:28 +0200 Subject: [PATCH 08/11] feat(core): mark Outcome as Immutable this will make it stable in Jetpack Compose projects --- .../kotlin/si/inova/kotlinova/core/outcome/Outcome.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt index 99d1e8c4..3f3c86a6 100644 --- a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt +++ b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Outcome.kt @@ -16,19 +16,29 @@ package si.inova.kotlinova.core.outcome +import si.inova.kotlinova.core.data.Immutable +import si.inova.kotlinova.core.outcome.Outcome.Error +import si.inova.kotlinova.core.outcome.Outcome.Progress +import si.inova.kotlinova.core.outcome.Outcome.Success + /** * Standard wrapper for an operation. It can be either [Progress], [Success] or [Error]. */ +@Immutable sealed class Outcome { abstract val data: T? + @Immutable data class Progress( override val data: T? = null, val progress: Float? = null, val style: LoadingStyle = LoadingStyle.NORMAL ) : Outcome() + @Immutable data class Success(override val data: T) : Outcome() + + @Immutable data class Error(val exception: CauseException, override val data: T? = null) : Outcome() } From 4047e00162f9143cb8b2d121ac0e0de3d56a7f47 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Tue, 20 Jun 2023 12:28:21 +0200 Subject: [PATCH 09/11] fix(navigation-fragment): onSaveInstanceState fragment crash --- .../inova/kotlinova/navigation/fragment/FragmentScreen.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/navigation/navigation-fragment/src/main/kotlin/si/inova/kotlinova/navigation/fragment/FragmentScreen.kt b/navigation/navigation-fragment/src/main/kotlin/si/inova/kotlinova/navigation/fragment/FragmentScreen.kt index 14c91432..6f0b801d 100644 --- a/navigation/navigation-fragment/src/main/kotlin/si/inova/kotlinova/navigation/fragment/FragmentScreen.kt +++ b/navigation/navigation-fragment/src/main/kotlin/si/inova/kotlinova/navigation/fragment/FragmentScreen.kt @@ -53,6 +53,13 @@ abstract class FragmentScreen( DisposableEffect(key, fragmentViewId) { val fragmentManager = activity.supportFragmentManager + if (fragmentManager.isStateSaved) { + // Fragment manager's state has already been saved. If we do anything, we will crash + // Exit here, activity is likely being closed anyway, so Fragment will re-appear + // on next open + return@DisposableEffect onDispose { } + } + var currentFragment = fragmentManager.findFragmentByTag(key.tag) if (currentFragment == null) { From 5eec5f4ca898bbdca4177a9ae8c6d30499d10646 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Tue, 20 Jun 2023 14:14:32 +0200 Subject: [PATCH 10/11] feat(navigation): improve duplicate screen error message --- .../si/inova/kotlinova/navigation/instructions/OpenScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/instructions/OpenScreen.kt b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/instructions/OpenScreen.kt index 6674f189..81b6ca5c 100644 --- a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/instructions/OpenScreen.kt +++ b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/instructions/OpenScreen.kt @@ -36,7 +36,7 @@ data class OpenScreen(val screen: ScreenKey) : NavigationInstruction() { if (backstack.lastOrNull() == screen) { error( "Cannot add $screen to the backstack twice back to back. If you want same screen on the" + - " backstack twice, add a random identifier to the key, such as an UUID." + " backstack twice, add a random identifier to the key, such as an UUID. Current backstack $backstack" ) } NavigationResult(backstack + screen, StateChange.FORWARD) From bc14a545c368e102bc9f630b959b72dcc5db63ec Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Mon, 26 Jun 2023 10:14:12 +0200 Subject: [PATCH 11/11] fix(navigation): disable navigation while animating this fixes an issue where user could potentially add single screen to the same backstack multiple times --- .../kotlinova/navigation/navigator/SimpleStackNavigator.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/navigator/SimpleStackNavigator.kt b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/navigator/SimpleStackNavigator.kt index 00c4edaa..8ba48a5d 100644 --- a/navigation/src/main/kotlin/si/inova/kotlinova/navigation/navigator/SimpleStackNavigator.kt +++ b/navigation/src/main/kotlin/si/inova/kotlinova/navigation/navigator/SimpleStackNavigator.kt @@ -30,6 +30,10 @@ class SimpleStackNavigator @Inject constructor( private val navigationContext: Lazy ) : Navigator { override fun navigate(navigationInstruction: NavigationInstruction) { + if (backstack.isStateChangePending) { + return + } + val res = navigationInstruction.performNavigation(backstack.getHistory(), navigationContext.get()) backstack.setHistory(res.newBackstack, res.direction)