diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api index 1a0d3e6a1..54a9c63a3 100644 --- a/decompose/api/android/decompose.api +++ b/decompose/api/android/decompose.api @@ -70,10 +70,10 @@ public abstract interface annotation class com/arkivanov/decompose/InternalDecom } public final class com/arkivanov/decompose/RetainedComponentKt { - public static final fun retainedComponent (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object; - public static final fun retainedComponent (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object; - public static synthetic fun retainedComponent$default (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun retainedComponent$default (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun retainedComponent (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun retainedComponent (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun retainedComponent$default (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun retainedComponent$default (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; } public final class com/arkivanov/decompose/UtilsKt { diff --git a/decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt index 139b3ecad..017029b8d 100644 --- a/decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt +++ b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt @@ -25,6 +25,7 @@ import com.arkivanov.essenty.lifecycle.subscribe import com.arkivanov.essenty.statekeeper.SerializableContainer import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher import com.arkivanov.essenty.statekeeper.stateKeeper +import kotlinx.serialization.builtins.serializer /** * Returns (creating if needed) a component that is retained over configuration changes. @@ -34,17 +35,26 @@ import com.arkivanov.essenty.statekeeper.stateKeeper * * @param key a key of the component, must be unique within the `Activity`. * @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`. + * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, + * default value is `false`. Can be useful for handling deep links in `onCreate`, so that the navigation state + * is not restored and initial state from the deep link is applied instead. + * @param isStateSavingAllowed called before saving the state. When `true` then the state will be saved, + * otherwise it won't. Default value is `true`. * @param factory a function that returns a new instance of the component. */ @ExperimentalDecomposeApi fun ComponentActivity.retainedComponent( key: String = "RootRetainedComponent", handleBackButton: Boolean = true, + discardSavedState: Boolean = false, + isStateSavingAllowed: () -> Boolean = { true }, factory: (ComponentContext) -> T, ): T = retainedComponent( key = key, onBackPressedDispatcher = if (handleBackButton) onBackPressedDispatcher else null, + discardSavedState = discardSavedState, + isStateSavingAllowed = isStateSavingAllowed, isChangingConfigurations = ::isChangingConfigurations, factory = factory, ) @@ -57,30 +67,43 @@ fun ComponentActivity.retainedComponent( * * @param key a key of the component, must be unique within the `Fragment`. * @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`. + * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, + * default value is `false`. Can be useful for handling deep links in `onCreate`, so that the navigation state + * is not restored and initial state from the deep link is applied instead. + * @param isStateSavingAllowed called before saving the state. When `true` then the state will be saved, + * otherwise it won't. Default value is `true`. * @param factory a function that returns a new instance of the component. */ @ExperimentalDecomposeApi fun Fragment.retainedComponent( key: String = "RootRetainedComponent", handleBackButton: Boolean = true, + discardSavedState: Boolean = false, + isStateSavingAllowed: () -> Boolean = { true }, factory: (ComponentContext) -> T, ): T = retainedComponent( key = key, onBackPressedDispatcher = if (handleBackButton) requireActivity().onBackPressedDispatcher else null, + discardSavedState = discardSavedState, + isStateSavingAllowed = isStateSavingAllowed, isChangingConfigurations = { activity?.isChangingConfigurations ?: false }, factory = factory, ) -private fun O.retainedComponent( +internal fun O.retainedComponent( key: String, onBackPressedDispatcher: OnBackPressedDispatcher?, + discardSavedState: Boolean, + isStateSavingAllowed: () -> Boolean, isChangingConfigurations: () -> Boolean, factory: (ComponentContext) -> T, ): T where O : LifecycleOwner, O : SavedStateRegistryOwner, O : ViewModelStoreOwner { val lifecycle = essentyLifecycle() - val stateKeeper = stateKeeper() - val instanceKeeper = instanceKeeper() + val stateKeeper = stateKeeper(discardSavedState = discardSavedState, isSavingAllowed = isStateSavingAllowed) + val marker = stateKeeper.consume(key = KEY_STATE_MARKER, strategy = String.serializer()) + stateKeeper.register(key = KEY_STATE_MARKER, strategy = String.serializer()) { "marker" } + val instanceKeeper = instanceKeeper(discardRetainedInstances = marker == null) check(!stateKeeper.isRegistered(key = key)) { "Another retained component is already registered with the key: $key" } @@ -124,6 +147,8 @@ private fun O.retainedComponent( return holder.component } +private const val KEY_STATE_MARKER = "RetainedComponent_state_marker" + private class DelegateOnBackPressedCallback( private val dispatcher: OnBackPressedDispatcher, ) : OnBackPressedCallback(enabled = dispatcher.hasEnabledCallbacks()) { diff --git a/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/AndroidTestUtils.kt b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/AndroidTestUtils.kt new file mode 100644 index 000000000..5c7069e36 --- /dev/null +++ b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/AndroidTestUtils.kt @@ -0,0 +1,16 @@ +package com.arkivanov.decompose + +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.subscribe + +fun Lifecycle.logEvents(): MutableList = + ArrayList().apply { + subscribe( + onCreate = { add("onCreate") }, + onStart = { add("onStart") }, + onResume = { add("onResume") }, + onPause = { add("onPause") }, + onStop = { add("onStop") }, + onDestroy = { add("onDestroy") }, + ) + } diff --git a/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/DefaultComponentContextBuilderTest.kt b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/DefaultComponentContextBuilderTest.kt index ae33f473c..117415bab 100644 --- a/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/DefaultComponentContextBuilderTest.kt +++ b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/DefaultComponentContextBuilderTest.kt @@ -1,23 +1,12 @@ package com.arkivanov.decompose -import android.os.Bundle -import android.os.Parcel -import androidx.activity.OnBackPressedDispatcher -import androidx.activity.OnBackPressedDispatcherOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.ViewModelStore -import androidx.lifecycle.ViewModelStoreOwner -import androidx.savedstate.SavedStateRegistry -import androidx.savedstate.SavedStateRegistryController -import androidx.savedstate.SavedStateRegistryOwner import com.arkivanov.decompose.router.TestInstance import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.instancekeeper.getOrCreate import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotSame @@ -30,12 +19,59 @@ import kotlin.test.assertTrue class DefaultComponentContextBuilderTest { @Test - fun saves_and_restores_state() { + fun WHEN_created_THEN_lifecycle_resumed() { + val owner = TestOwner() + val ctx = owner.defaultComponentContext() + val events = ctx.lifecycle.logEvents() + + assertContentEquals(listOf("onCreate", "onStart", "onResume"), events) + } + + @Test + fun WHEN_recreated_THEN_old_lifecycle_destroyed() { + var owner = TestOwner() + val ctx = owner.defaultComponentContext() + val events = ctx.lifecycle.logEvents() + events.clear() + + owner = owner.recreate(isChangingConfigurations = false) + owner.defaultComponentContext() + + assertContentEquals(listOf("onPause", "onStop", "onDestroy"), events) + } + + @Test + fun WHEN_recreated_THEN_new_lifecycle_resumed() { + var owner = TestOwner() + owner.defaultComponentContext() + + owner = owner.recreate(isChangingConfigurations = false) + val ctx = owner.defaultComponentContext() + val events = ctx.lifecycle.logEvents() + + assertContentEquals(listOf("onCreate", "onStart", "onResume"), events) + } + + @Test + fun WHEN_recreated_THEN_saves_and_restores_state() { var owner = TestOwner() var ctx = owner.defaultComponentContext() ctx.stateKeeper.register(key = "key") { "saved_state" } - owner = owner.recreate() + owner = owner.recreate(isChangingConfigurations = false) + ctx = owner.defaultComponentContext() + val restoredState = ctx.stateKeeper.consume(key = "key") + + assertEquals("saved_state", restoredState) + } + + @Test + fun WHEN_configuration_changed_THEN_saves_and_restores_state() { + var owner = TestOwner() + var ctx = owner.defaultComponentContext() + ctx.stateKeeper.register(key = "key") { "saved_state" } + + owner = owner.recreate(isChangingConfigurations = true) ctx = owner.defaultComponentContext() val restoredState = ctx.stateKeeper.consume(key = "key") @@ -56,7 +92,7 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_isStateSavingAllowed_is_false_on_save_THEN_state_not_saved() { + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_state_not_saved() { var owner = TestOwner() var ctx = owner.defaultComponentContext(isStateSavingAllowed = { false }) ctx.stateKeeper.register(key = "key") { "saved_state" } @@ -69,7 +105,7 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_isStateSavingAllowed_is_false_on_save_THEN_instances_not_retained() { + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_instances_not_retained() { var owner = TestOwner() var ctx = owner.defaultComponentContext(isStateSavingAllowed = { false }) val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) @@ -82,9 +118,9 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_isStateSavingAllowed_is_false_on_save_THEN_old_instances_destroyed() { + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_old_instances_destroyed() { var owner = TestOwner() - var ctx = owner.defaultComponentContext(isStateSavingAllowed = { false }) + val ctx = owner.defaultComponentContext(isStateSavingAllowed = { false }) val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) owner = owner.recreate() @@ -94,7 +130,7 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_discardSavedState_is_true_on_restore_THEN_discards_saved_state() { + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_discards_saved_state() { var owner = TestOwner() var ctx = owner.defaultComponentContext() ctx.stateKeeper.register(key = "key") { "saved_state" } @@ -107,7 +143,7 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_discardSavedState_is_true_on_restore_THEN_instances_not_retained() { + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_instances_not_retained() { var owner = TestOwner() var ctx = owner.defaultComponentContext() val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) @@ -120,7 +156,7 @@ class DefaultComponentContextBuilderTest { } @Test - fun GIVEN_discardSavedState_is_true_on_restore_THEN_old_instances_destroyed() { + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_old_instances_destroyed() { var owner = TestOwner() val ctx = owner.defaultComponentContext() val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) @@ -154,39 +190,4 @@ class DefaultComponentContextBuilderTest { assertFalse(isCalled) } - - private class TestOwner( - savedState: Bundle = Bundle(), - override val viewModelStore: ViewModelStore = ViewModelStore(), - ) : LifecycleOwner, SavedStateRegistryOwner, ViewModelStoreOwner, OnBackPressedDispatcherOwner { - private val savedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this) - override val lifecycle: Lifecycle = LifecycleRegistry(this) - override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry - override val onBackPressedDispatcher: OnBackPressedDispatcher = OnBackPressedDispatcher() - - init { - savedStateRegistryController.performRestore(savedState) - } - - fun recreate(): TestOwner { - val bundle = Bundle() - savedStateRegistryController.performSave(bundle) - - return TestOwner(savedState = bundle.parcelize().deparcelize(), viewModelStore = viewModelStore) - } - - private fun Bundle.parcelize(): ByteArray { - val parcel = Parcel.obtain() - parcel.writeBundle(this) - return parcel.marshall() - } - - private fun ByteArray.deparcelize(): Bundle { - val parcel = Parcel.obtain() - parcel.unmarshall(this, 0, size) - parcel.setDataPosition(0) - - return requireNotNull(parcel.readBundle()) - } - } } diff --git a/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentTest.kt b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentTest.kt new file mode 100644 index 000000000..7ea834162 --- /dev/null +++ b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentTest.kt @@ -0,0 +1,205 @@ +package com.arkivanov.decompose + +import com.arkivanov.decompose.router.TestInstance +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.instancekeeper.getOrCreate +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotSame +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@Suppress("TestFunctionName") +@RunWith(RobolectricTestRunner::class) +class RetainedComponentTest { + + @Test + fun WHEN_created_THEN_lifecycle_resumed() { + val owner = TestOwner() + val ctx = owner.retainedComponent() + val events = ctx.lifecycle.logEvents() + + assertContentEquals(listOf("onCreate", "onStart", "onResume"), events) + } + + @Test + fun WHEN_recreated_THEN_old_lifecycle_destroyed() { + var owner = TestOwner() + val ctx = owner.retainedComponent() + val events = ctx.lifecycle.logEvents() + events.clear() + + owner = owner.recreate(isChangingConfigurations = false) + owner.retainedComponent() + + assertContentEquals(listOf("onPause", "onStop", "onDestroy"), events) + } + + @Test + fun WHEN_recreated_THEN_new_lifecycle_resumed() { + var owner = TestOwner() + owner.retainedComponent() + + owner = owner.recreate(isChangingConfigurations = false) + val ctx = owner.retainedComponent() + val events = ctx.lifecycle.logEvents() + + assertContentEquals(listOf("onCreate", "onStart", "onResume"), events) + } + + @Test + fun WHEN_configuration_changed_THEN_lifecycle_not_called() { + var owner = TestOwner() + val ctx = owner.retainedComponent(isChangingConfigurations = { true }) + val events = ctx.lifecycle.logEvents() + events.clear() + + owner = owner.recreate(isChangingConfigurations = true) + owner.retainedComponent() + + assertContentEquals(emptyList(), events) + } + + @Test + fun WHEN_recreated_THEN_saves_and_restores_state() { + var owner = TestOwner() + var ctx = owner.retainedComponent() + ctx.stateKeeper.register(key = "key") { "saved_state" } + + owner = owner.recreate(isChangingConfigurations = false) + ctx = owner.retainedComponent() + val restoredState = ctx.stateKeeper.consume(key = "key") + + assertEquals("saved_state", restoredState) + } + + @Test + fun retains_instances() { + var owner = TestOwner() + val ctx1 = owner.retainedComponent() + + owner = owner.recreate() + val ctx2 = owner.retainedComponent() + + assertSame(ctx1, ctx2) + } + + @Test + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_recreated_THEN_state_not_saved() { + var owner = TestOwner() + var ctx = owner.retainedComponent(isStateSavingAllowed = { false }) + ctx.stateKeeper.register(key = "key") { "saved_state" } + + owner = owner.recreate(isChangingConfigurations = false) + ctx = owner.retainedComponent() + val restoredState = ctx.stateKeeper.consume(key = "key") + + assertNull(restoredState) + } + + @Test + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_instances_not_retained() { + var owner = TestOwner() + var ctx = owner.retainedComponent(isStateSavingAllowed = { false }) + val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + owner = owner.recreate(isChangingConfigurations = true) + ctx = owner.retainedComponent() + val instance2 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + assertNotSame(instance1, instance2) + } + + @Test + fun GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_old_instances_destroyed() { + var owner = TestOwner() + val ctx = owner.retainedComponent(isStateSavingAllowed = { false }) + val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + owner = owner.recreate(isChangingConfigurations = true) + owner.retainedComponent() + + assertTrue(instance1.isDestroyed) + } + + @Test + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_discards_saved_state() { + var owner = TestOwner() + var ctx = owner.retainedComponent() + ctx.stateKeeper.register(key = "key") { "saved_state" } + + owner = owner.recreate(isChangingConfigurations = true) + ctx = owner.retainedComponent(discardSavedState = true) + val restoredState = ctx.stateKeeper.consume(key = "key") + + assertNull(restoredState) + } + + @Test + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_instances_not_retained() { + var owner = TestOwner() + var ctx = owner.retainedComponent() + val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + owner = owner.recreate(isChangingConfigurations = true) + ctx = owner.retainedComponent(discardSavedState = true) + val instance2 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + assertNotSame(instance1, instance2) + } + + @Test + fun WHEN_configuration_changed_and_discardSavedState_is_true_on_restore_THEN_old_instances_destroyed() { + var owner = TestOwner() + val ctx = owner.retainedComponent() + val instance1 = ctx.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) + + owner = owner.recreate(isChangingConfigurations = true) + owner.retainedComponent(discardSavedState = true) + + assertTrue(instance1.isDestroyed) + } + + @Test + fun GIVEN_enabled_BackCallback_registered_WHEN_onBackPressed_THEN_callback_called() { + val owner = TestOwner() + val ctx = owner.retainedComponent() + var isCalled = false + ctx.backHandler.register(BackCallback { isCalled = true }) + + owner.onBackPressedDispatcher.onBackPressed() + + assertTrue(isCalled) + } + + @Test + fun GIVEN_disabled_BackCallback_registered_WHEN_onBackPressed_THEN_callback_not_called() { + val owner = TestOwner() + val ctx = owner.defaultComponentContext() + var isCalled = false + ctx.backHandler.register(BackCallback(isEnabled = false) { isCalled = true }) + + owner.onBackPressedDispatcher.onBackPressed() + + assertFalse(isCalled) + } + + private fun TestOwner.retainedComponent( + discardSavedState: Boolean = false, + isStateSavingAllowed: () -> Boolean = { true }, + isChangingConfigurations: () -> Boolean = { false }, + ): ComponentContext = + retainedComponent( + key = "key", + onBackPressedDispatcher = onBackPressedDispatcher, + discardSavedState = discardSavedState, + isStateSavingAllowed = isStateSavingAllowed, + isChangingConfigurations = isChangingConfigurations, + factory = { it }, + ) +} diff --git a/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt new file mode 100644 index 000000000..2673ed347 --- /dev/null +++ b/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt @@ -0,0 +1,57 @@ +package com.arkivanov.decompose + +import android.os.Bundle +import android.os.Parcel +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner + +class TestOwner( + savedState: Bundle = Bundle(), + override val viewModelStore: ViewModelStore = ViewModelStore(), +) : LifecycleOwner, SavedStateRegistryOwner, ViewModelStoreOwner, OnBackPressedDispatcherOwner { + private val savedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this) + override val lifecycle: LifecycleRegistry = LifecycleRegistry(this) + override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry + override val onBackPressedDispatcher: OnBackPressedDispatcher = OnBackPressedDispatcher() + + init { + savedStateRegistryController.performRestore(savedState) + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + fun recreate(isChangingConfigurations: Boolean = true): TestOwner { + val bundle = Bundle() + savedStateRegistryController.performSave(bundle) + val savedState = bundle.parcelize().deparcelize() + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + + return if (isChangingConfigurations) { + TestOwner(savedState = savedState, viewModelStore = viewModelStore) + } else { + viewModelStore.clear() + TestOwner(savedState = savedState) + } + } + + private fun Bundle.parcelize(): ByteArray { + val parcel = Parcel.obtain() + parcel.writeBundle(this) + return parcel.marshall() + } + + private fun ByteArray.deparcelize(): Bundle { + val parcel = Parcel.obtain() + parcel.unmarshall(this, 0, size) + parcel.setDataPosition(0) + + return requireNotNull(parcel.readBundle()) + } +}