diff --git a/Writerside/topics/ModoOverview.md b/Writerside/topics/ModoOverview.md index da20fad8..53614537 100644 --- a/Writerside/topics/ModoOverview.md +++ b/Writerside/topics/ModoOverview.md @@ -1,5 +1,3 @@ -Here's the improved version of your documentation text: - # Modo Overview [![Maven Central](https://img.shields.io/maven-central/v/com.github.terrakok/modo-compose)](https://repo1.maven.org/maven2/com/github/terrakok) diff --git a/Writerside/v.list b/Writerside/v.list index 90f3bd1b..fedc6718 100644 --- a/Writerside/v.list +++ b/Writerside/v.list @@ -1,7 +1,7 @@ - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e786be5..1d821e63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,14 @@ [versions] composeWheelPicker = "1.0.0-beta05" leakcanaryAndroid = "2.14" -modo = "0.11.0-rc2" +modo = "0.11.0" #noinspection AndroidGradlePluginVersion androidGradlePlugin = "8.13.2" nexusPublish = "2.0.0" detektComposeVersion = "0.3.20" detektVersion = "1.23.6" junit = "4.13.2" +mockk = "1.13.12" androidxComposeBomModo = "2025.11.01" androidxComposeBomApp = "2025.11.01" androidxActivityCompose = "1.8.2" @@ -49,6 +50,7 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } test-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.10.1" } test-androidx-arch-core = { group = "androidx.arch.core", name = "core-testing", version = "2.2.0" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } debug-logcat = { group = "com.squareup.logcat", name = "logcat", version = "0.1" } diff --git a/modo-compose/build.gradle.kts b/modo-compose/build.gradle.kts index 7aec9f6e..b496b63f 100644 --- a/modo-compose/build.gradle.kts +++ b/modo-compose/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { testImplementation(libs.test.junit.jupiter) testImplementation(kotlin("test")) testImplementation(libs.test.androidx.arch.core) + testImplementation(libs.mockk) } tasks.withType(Test::class) { diff --git a/modo-compose/src/main/java/com/github/terrakok/modo/Modo.kt b/modo-compose/src/main/java/com/github/terrakok/modo/Modo.kt index bc06688b..8adf87cc 100644 --- a/modo-compose/src/main/java/com/github/terrakok/modo/Modo.kt +++ b/modo-compose/src/main/java/com/github/terrakok/modo/Modo.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.github.terrakok.modo.Modo.rememberRootScreen +import com.github.terrakok.modo.Modo.rootScreens +import com.github.terrakok.modo.Modo.save import com.github.terrakok.modo.model.ScreenModelStore import com.github.terrakok.modo.util.getActivity import java.util.concurrent.ConcurrentHashMap @@ -26,58 +29,71 @@ object Modo { /** * Contains root screens to priovide stability of returned instance when use [rememberRootScreen] and return a same instance in per a process. */ - private val rootScreens: MutableMap> = ConcurrentHashMap() + @org.jetbrains.annotations.VisibleForTesting + internal val rootScreens: MutableMap> = ConcurrentHashMap() /** * Saves provided screen with nested graph to bundle for further restoration. */ + @Deprecated("Use rememberRootScreen, which handles saving and restoring automatically. Will be removed in 1.0.") fun save(outState: Bundle, rootScreen: Screen?) { outState.putInt(MODO_SCREEN_COUNTER_KEY, screenCounterKey.get()) outState.putParcelable(MODO_GRAPH, rootScreen) } /** - * Creates [RootScreen] provided by [rootScreenProvider], if there is no data in [savedState] or in-memory. - * Otherwise [RootScreen] is firstly taking from memoryr and then restored from savedState, if there is no RootScreen in memory. - * Returns same instance of [RootScreen] for same process. A new instance returned only after process death. - * @param savedState - container with modo state and graph - * @param rootScreenProvider invokes when [savedState] is null and [inMemoryScreen] is null and we need to provide root screen. + * Returns the [RootScreen] for this host (Activity/Fragment), creating it if necessary. + * Guarantees the same instance is returned within a single process — a new instance is only + * created after process death. + * + * There are three scenarios, resolved in priority order: + * + * **1. Bundle restore** (`savedState != null`): + * The host was recreated by the system (config change or process death). + * - Config change (same process): [rootScreens] already holds the live instance → returned from cache. + * - Process death: [rootScreens] is empty → the graph is deserialized from [savedState], + * stored in [rootScreens], and returned. [screenCounterKey] is restored to avoid key collisions. + * + * **2. In-memory hit** (`savedState == null`, [inMemoryScreen] != null): + * The host's view was destroyed without saving state (e.g. a Fragment going to the back stack). + * The Fragment is still alive and holds the previous [RootScreen] reference. + * [inMemoryScreen] provides the key to retrieve the same instance from [rootScreens]. + * + * **3. First initialization** (`savedState == null`, [inMemoryScreen] == null): + * Fresh start — [rootScreenProvider] is called to build the initial screen, a new [RootScreen] + * is created, stored in [rootScreens], and returned. + * + * @param savedState bundle produced by [save], or null on first launch / back-stack return. + * @param inMemoryScreen existing [RootScreen] held by a non-destroyed Fragment (scenario 2). + * Must be null for Activities and for the very first Fragment creation. + * @param rootScreenProvider called only in scenario 3 to construct the initial root screen. */ + @Deprecated("Use rememberRootScreen, which handles all lifecycle concerns automatically. Will be removed in 1.0.") fun getOrCreateRootScreen(savedState: Bundle?, inMemoryScreen: RootScreen?, rootScreenProvider: () -> T): RootScreen { - // taking saved state to obtain screenKey - val modoGraph = savedState?.getParcelable>(MODO_GRAPH) - return if (modoGraph != null) { - // If restoring after activity death, but not after process death, then [inMemoryScreen] is null, but we have cached object in rootScreens - // So we trying to restore it from memory and only if it is null - taking it from savedState. - val cachedRootScreen = rootScreens.get(modoGraph.screenKey)?.let { it as RootScreen } - if (cachedRootScreen != null) { - cachedRootScreen - } else { - restoreScreenCounter(savedState.getInt(MODO_SCREEN_COUNTER_KEY)) - modoGraph - } + val savedModoGraph = savedState?.getParcelable>(MODO_GRAPH) + return if (savedModoGraph != null) { + // Scenario 1: bundle restore. + // Config change → cache hit, process death → cache miss, savedModoGraph is stored. + restoreScreenCounterIfNeeded(savedState.getInt(MODO_SCREEN_COUNTER_KEY)) + @Suppress("UNCHECKED_CAST") + rootScreens.getOrPut(savedModoGraph.screenKey) { savedModoGraph } as RootScreen } else { - // saved state is going to be null after taking fragment from backstack, beckause in this case - // 1. onSaveInstaneState is not called - // 2. View is destroyed - // 3. Fragment is not destroyed and have inMemoryScreen != null if it is not a first call - inMemoryScreen ?: RootScreen(rootScreenProvider()) - }.also { rootScreen -> - rootScreens.put(rootScreen.screenKey, rootScreen) + // Scenarios 2 & 3: no saved state. + // savedState is null after back-stack return because onSaveInstanceState is not called + // when a Fragment is only stopped (not destroyed), so inMemoryScreen carries the reference. + val screen = inMemoryScreen ?: RootScreen(rootScreenProvider()) // scenario 3: first init + @Suppress("UNCHECKED_CAST") + rootScreens.getOrPut(screen.screenKey) { screen } as RootScreen } } - @Deprecated( - "Renamed it getOrCreateRootScreen to except misunderstanding. Use getOrCreateRootScreen instead.", - ReplaceWith("Modo.getOrCreateRootScreen(savedState, inMemoryScreen, rootScreenProvider)") - ) - fun init(savedState: Bundle?, inMemoryScreen: RootScreen?, rootScreenProvider: () -> T): RootScreen = - getOrCreateRootScreen(savedState, inMemoryScreen, rootScreenProvider) - /** * Must be called to clear all data from [ScreenModelStore], related with removed screens. */ - fun onRootScreenFinished(rootScreen: RootScreen?) { + @Deprecated("Use rememberRootScreen, which handles cleanup automatically. Will be removed in 1.0.") + fun onRootScreenFinished(rootScreen: RootScreen?) = finishRootScreen(rootScreen) + + private fun finishRootScreen(rootScreen: RootScreen?) { if (rootScreen != null) { Log.d("Modo", "rootScreen removing $rootScreen") rootScreens.remove(rootScreen.screenKey) @@ -101,7 +117,7 @@ object Modo { DisposableEffect(rootScreen, this) { onDispose { if (isFinishing) { - onRootScreenFinished(rootScreen) + finishRootScreen(rootScreen) } } } @@ -114,7 +130,7 @@ object Modo { key = MODO_SCREEN_COUNTER_KEY, saver = Saver( restore = { - restoreScreenCounter(it as Int) + restoreScreenCounterIfNeeded(it as Int) it }, save = { @@ -132,16 +148,16 @@ object Modo { val rootScreen = rememberSaveable( key = MODO_GRAPH, saver = Saver( - save = { rootScreen -> - rootScreens.put(rootScreen.screenKey, rootScreen) - rootScreen - }, - restore = { rootScreen -> - rootScreens.get(rootScreen.screenKey)?.let { it as RootScreen } ?: rootScreen + save = { it }, + restore = { saved -> + @Suppress("UNCHECKED_CAST") + rootScreens.getOrPut(saved.screenKey) { saved } as RootScreen } ) ) { - RootScreen(rootScreenFactory()) + RootScreen(rootScreenFactory()).also { newRoot -> + rootScreens[newRoot.screenKey] = newRoot + } } return rootScreen } @@ -169,17 +185,9 @@ object Modo { if (!hasAnyObserver) { hasAnyObserver = true val activity = context.getActivity()!! - val lifecycleObserver = LifecycleEventObserver { owner, event -> - // If parent activity is finishes - fragment also is finishing. - // But if activity is not finishes, then it can be just fragment removal from backstack. - // This check is not taking into account the case, when an activity is destroyed by system, but it is going to be restored. - - if (event == Lifecycle.Event.ON_DESTROY) { - val activityFinishing = activity.isFinishing - val fragmentRemovedFromBackStack = !activityFinishing && !activity.isChangingConfigurations && !isStateSaved - if (activityFinishing || fragmentRemovedFromBackStack) { - onRootScreenFinished(rootScreen) - } + val lifecycleObserver = LifecycleEventObserver { _, event -> + if (isFragmentClosing(event, activity)) { + finishRootScreen(rootScreen) } } lifecycle.addObserver(lifecycleObserver) @@ -196,4 +204,16 @@ object Modo { (screen as? ContainerScreen<*, *>)?.navigationState?.getChildScreens()?.forEach(::clearScreenModel) } -} \ No newline at end of file +} + +/** + * Returns true when the fragment is being permanently destroyed and its state should be cleaned up. + * Returns false when the fragment will be restored (config change or system-initiated process death). + */ +fun Fragment.isFragmentClosing(event: Lifecycle.Event, activity: Activity): Boolean = +// ON_DESTROY is the only moment when isStateSaved and isChangingConfigurations are reliable. +// Activity finishing covers user-initiated close (back press, finish()). +// The second condition covers fragment removal from backstack: + // no config change and state wasn't saved means the fragment won't be restored. + event == Lifecycle.Event.ON_DESTROY && + (activity.isFinishing || (!activity.isChangingConfigurations && !isStateSaved)) \ No newline at end of file diff --git a/modo-compose/src/main/java/com/github/terrakok/modo/ScreenKey.kt b/modo-compose/src/main/java/com/github/terrakok/modo/ScreenKey.kt index f4bfce62..cfbe71c3 100644 --- a/modo-compose/src/main/java/com/github/terrakok/modo/ScreenKey.kt +++ b/modo-compose/src/main/java/com/github/terrakok/modo/ScreenKey.kt @@ -18,10 +18,10 @@ internal val screenCounterKey = AtomicInteger(-1) fun generateScreenKey(): ScreenKey = ScreenKey("Screen#${screenCounterKey.incrementAndGet()}") /** - * Restores the screen counter to the given value. - * It's safe to call this multiple times, because it restores the value only if it's not already set. + * Restores the screen counter to [value] only if it hasn't been set yet (counter == -1). + * Logs a warning if the counter is already set to a different value. */ -internal fun restoreScreenCounter(value: Int) { +internal fun restoreScreenCounterIfNeeded(value: Int) { if (screenCounterKey.get() == -1 || screenCounterKey.get() == value) { screenCounterKey.set(value) } else { diff --git a/modo-compose/src/test/java/com/github/terrakok/modo/ModoRootScreenCacheTest.kt b/modo-compose/src/test/java/com/github/terrakok/modo/ModoRootScreenCacheTest.kt new file mode 100644 index 00000000..30c7c725 --- /dev/null +++ b/modo-compose/src/test/java/com/github/terrakok/modo/ModoRootScreenCacheTest.kt @@ -0,0 +1,174 @@ +package com.github.terrakok.modo + +import android.os.Bundle +import com.github.terrakok.modo.model.ScreenModelStore +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertNotSame +import kotlin.test.assertNull +import kotlin.test.assertSame + +@Suppress("DEPRECATION") +class ModoRootScreenCacheTest { + + @BeforeEach + fun setup() { + Modo.rootScreens.clear() + screenCounterKey.set(-1) + ScreenModelStore.removedScreenKeys.clear() + ScreenModelStore.screenModels.clear() + ScreenModelStore.dependencies.clear() + ScreenModelStore.dependencyCounter.set(0L) + ScreenModelStore.lastScreenModelKey.value = null + ModoDevOptions.onIllegalScreenModelStoreAccess = ModoDevOptions.ValidationFailedStrategy { } + } + + // region Scenario 3: first initialization (savedState == null, inMemoryScreen == null) + + @Test + fun `When savedState is null and inMemoryScreen is null - Then factory creates new and caches it`() { + val mockScreen = MockScreen(ScreenKey("new")) + + val result = Modo.getOrCreateRootScreen( + savedState = null, + inMemoryScreen = null, + rootScreenProvider = { mockScreen } + ) + + assertSame(result, Modo.rootScreens[result.screenKey]) + } + + // endregion + + // region Scenario 2: in-memory hit (savedState == null, inMemoryScreen != null — fragment backstack return) + + @Test + fun `When savedState is null and inMemoryScreen is provided - Then inMemoryScreen is returned`() { + val existing = RootScreen(MockScreen()) + Modo.rootScreens[existing.screenKey] = existing + + val result = Modo.getOrCreateRootScreen( + savedState = null, + inMemoryScreen = existing, + rootScreenProvider = { error("factory must not be called") } + ) + + assertSame(existing, result) + } + + @Test + fun `When savedState is null and inMemoryScreen is provided - Then factory is not called`() { + val existing = RootScreen(MockScreen()) + Modo.rootScreens[existing.screenKey] = existing + var factoryCalled = false + + Modo.getOrCreateRootScreen( + savedState = null, + inMemoryScreen = existing, + rootScreenProvider = { + factoryCalled = true + MockScreen() + } + ) + + assertSame(false, factoryCalled) + } + + // endregion + + // region Scenario 1a: bundle restore, cache hit (config change — rootScreens has live instance) + + @Test + fun `When savedState is not null and rootScreens has instance - Then cached instance returned, not bundle one`() { + // Simulate config change: cached holds the live instance, bundle holds the parcelized one. + // In practice they'd be equal by value but different objects; here we reuse the same instance + // for the bundle to share the same screenKey, then verify the cache takes priority. + val cached = RootScreen(MockScreen()) + Modo.rootScreens[cached.screenKey] = cached + val savedState = mockBundle(cached, counter = 5) + + val result = Modo.getOrCreateRootScreen( + savedState = savedState, + inMemoryScreen = null, + rootScreenProvider = { error("factory must not be called") } + ) + + assertSame(cached, result) + } + + // endregion + + // region Scenario 1b: bundle restore, cache miss (process death — rootScreens is empty) + + @Test + fun `When savedState is not null and rootScreens is empty - Then bundle instance stored and returned`() { + val fromBundle = RootScreen(MockScreen()) + screenCounterKey.set(-1) // reset: simulates new process after death, counter not yet set + val savedState = mockBundle(fromBundle, counter = 42) + + val result = Modo.getOrCreateRootScreen( + savedState = savedState, + inMemoryScreen = null, + rootScreenProvider = { error("factory must not be called") } + ) + + assertSame(fromBundle, result) + assertSame(fromBundle, Modo.rootScreens[fromBundle.screenKey]) + } + + @Test + fun `When savedState is not null and rootScreens is empty - Then screenCounter is restored`() { + val fromBundle = RootScreen(MockScreen()) + screenCounterKey.set(-1) // reset: simulates new process after death, counter not yet set + val savedState = mockBundle(fromBundle, counter = 42) + + Modo.getOrCreateRootScreen( + savedState = savedState, + inMemoryScreen = null, + rootScreenProvider = { error("factory must not be called") } + ) + + assertSame(42, screenCounterKey.get()) + } + + // endregion + + // region getOrPut cache isolation + + @Test + fun `When screen is cached and getOrPut called again - Then cached instance returned`() { + val cached = RootScreen(MockScreen(ScreenKey("k"))) + Modo.rootScreens[ScreenKey("k")] = cached + + val newCandidate = RootScreen(MockScreen(ScreenKey("k"))) + + @Suppress("UNCHECKED_CAST") + val result = Modo.rootScreens.getOrPut(ScreenKey("k")) { newCandidate } as RootScreen + + assertSame(cached, result) + assertNotSame(newCandidate, result) + } + + // endregion + + // region onRootScreenFinished cleanup + + @Test + fun `When onRootScreenFinished called - Then rootScreens entry is removed`() { + val screen = RootScreen(MockScreen(ScreenKey("fin"))) + Modo.rootScreens[screen.screenKey] = screen + + Modo.onRootScreenFinished(screen) + + assertNull(Modo.rootScreens[screen.screenKey]) + } + + // endregion + + private fun mockBundle(rootScreen: RootScreen<*>, counter: Int): Bundle = mockk { + every { getParcelable>("MODO_GRAPH") } returns rootScreen + every { getInt("MODO_SCREEN_COUNTER_KEY") } returns counter + } +} diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/ModoLegacyIntegrationActivity.kt b/sample/src/main/java/com/github/terrakok/modo/sample/ModoLegacyIntegrationActivity.kt index b494f688..5be2fa84 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/ModoLegacyIntegrationActivity.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/ModoLegacyIntegrationActivity.kt @@ -7,11 +7,21 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import com.github.terrakok.modo.Modo +import com.github.terrakok.modo.Modo.rememberRootScreen import com.github.terrakok.modo.RootScreen import com.github.terrakok.modo.sample.screens.MainScreen import com.github.terrakok.modo.sample.screens.containers.SampleStack import com.github.terrakok.modo.stack.StackScreen +/** + * Demonstrates manual (legacy) integration of Modo into an Activity without using [rememberRootScreen]. + * + * This approach requires the host to manually call [Modo.getOrCreateRootScreen], + * [Modo.save], and [Modo.onRootScreenFinished]. + * + * **This is not the recommended way to integrate Modo.** Prefer [rememberRootScreen], + * which handles all lifecycle concerns automatically. Use this only if you cannot use Compose at the Activity level. + */ class ModoLegacyIntegrationActivity : AppCompatActivity() { private var rootScreen: RootScreen? = null diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragment.kt b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragment.kt index 5ebbf651..e0001d8e 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragment.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragment.kt @@ -17,7 +17,10 @@ import com.github.terrakok.modo.sample.screens.MainScreen import com.github.terrakok.modo.sample.screens.containers.SampleStack /** - * Sample of integration Modo into the fragment + * Demonstrates the recommended way to integrate Modo into a Fragment using [rememberRootScreen]. + * + * All lifecycle concerns (state saving, screen model cleanup) are handled automatically. + * For the manual alternative, see [ModoLegacyIntegrationFragment]. */ class ModoFragment : Fragment() { diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragmentIntegrationActivity.kt b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragmentIntegrationActivity.kt index 46c5e085..487f1f4b 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragmentIntegrationActivity.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoFragmentIntegrationActivity.kt @@ -1,7 +1,10 @@ package com.github.terrakok.modo.sample.fragment +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.activity.addCallback +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity class ModoFragmentIntegrationActivity : FragmentActivity() { @@ -16,9 +19,22 @@ class ModoFragmentIntegrationActivity : FragmentActivity() { } } if (savedInstanceState == null) { + val fragment: Fragment = if (intent.getBooleanExtra(EXTRA_USE_LEGACY, false)) { + ModoLegacyIntegrationFragment() + } else { + ModoFragment() + } supportFragmentManager.beginTransaction() - .replace(android.R.id.content, ModoFragment()) + .replace(android.R.id.content, fragment) .commit() } } + + companion object { + private const val EXTRA_USE_LEGACY = "EXTRA_USE_LEGACY" + + fun createIntent(context: Context, useLegacy: Boolean = false): Intent = + Intent(context, ModoFragmentIntegrationActivity::class.java) + .putExtra(EXTRA_USE_LEGACY, useLegacy) + } } \ No newline at end of file diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoLegacyIntegrationFragment.kt b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoLegacyIntegrationFragment.kt new file mode 100644 index 00000000..daac668f --- /dev/null +++ b/sample/src/main/java/com/github/terrakok/modo/sample/fragment/ModoLegacyIntegrationFragment.kt @@ -0,0 +1,66 @@ +package com.github.terrakok.modo.sample.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.github.terrakok.modo.Modo +import com.github.terrakok.modo.Modo.rememberRootScreen +import com.github.terrakok.modo.RootScreen +import com.github.terrakok.modo.isFragmentClosing +import com.github.terrakok.modo.sample.screens.MainScreen +import com.github.terrakok.modo.sample.screens.containers.SampleStack +import com.github.terrakok.modo.stack.StackScreen + +/** + * Demonstrates manual (legacy) integration of Modo into a Fragment without using [rememberRootScreen]. + * + * This approach requires the host to manually call [Modo.getOrCreateRootScreen], + * [Modo.save], and [Modo.onRootScreenFinished]. + * + * **This is not the recommended way to integrate Modo.** Prefer [rememberRootScreen], + * which handles all lifecycle concerns automatically. Use this only if you cannot use Compose at the Fragment level. + */ +class ModoLegacyIntegrationFragment : Fragment() { + + private var rootScreen: RootScreen? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + ComposeView(inflater.context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + rootScreen = Modo.getOrCreateRootScreen(savedInstanceState, rootScreen) { + SampleStack(MainScreen(1)) + } + setContent { + Column { + Text(text = "ModoLegacyFragment", style = MaterialTheme.typography.h5) + val rootScreen = rememberRootScreen { + SampleStack(MainScreen(screenIndex = 1, canOpenFragment = true)) + } + rootScreen.Content(modifier = Modifier.fillMaxSize()) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + Modo.save(outState, rootScreen) + super.onSaveInstanceState(outState) + } + + override fun onDestroy() { + super.onDestroy() + if (isFragmentClosing(Lifecycle.Event.ON_DESTROY, requireActivity())) { + Modo.onRootScreenFinished(rootScreen) + } + } + +} diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt index 0ab33f9e..65422fb2 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt @@ -15,6 +15,7 @@ import com.github.terrakok.modo.sample.ModoLegacyIntegrationActivity import com.github.terrakok.modo.sample.ModoSampleActivity import com.github.terrakok.modo.sample.fragment.ModoFragment import com.github.terrakok.modo.sample.fragment.ModoFragmentIntegrationActivity +import com.github.terrakok.modo.sample.fragment.ModoLegacyIntegrationFragment import com.github.terrakok.modo.sample.quickstart.QuickStartActivity import com.github.terrakok.modo.sample.screens.base.ButtonsScreenContent import com.github.terrakok.modo.sample.screens.containers.CustomStackSample @@ -204,7 +205,10 @@ private fun rememberButtons( navigation?.dispatch(OpenActivityAction(context)) }, ModoButtonSpec("Fragment integration") { - navigation?.dispatch(OpenActivityAction(context)) + context.startActivity(ModoFragmentIntegrationActivity.createIntent(context)) + }, + ModoButtonSpec("Legacy Fragment integration") { + context.startActivity(ModoFragmentIntegrationActivity.createIntent(context, useLegacy = true)) }, activity?.let { activity -> if (canOpenFragment) { @@ -218,6 +222,18 @@ private fun rememberButtons( null } }, + activity?.let { activity -> + if (canOpenFragment) { + ModoButtonSpec("New legacy fragment") { + activity.supportFragmentManager.beginTransaction() + .replace(android.R.id.content, ModoLegacyIntegrationFragment()) + .addToBackStack("ModoLegacyIntegrationFragment") + .commit() + } + } else { + null + } + }, ) ), GroupedButtonsState.Group(