diff --git a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt new file mode 100644 index 00000000..82138784 --- /dev/null +++ b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt @@ -0,0 +1,67 @@ +package com.sebaslogen.resacaapp.sample + +import android.content.Intent +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.sebaslogen.resacaapp.sample.ui.main.ComposeActivity +import com.sebaslogen.resacaapp.sample.ui.main.rememberScopedDestination +import com.sebaslogen.resacaapp.sample.utils.ComposeTestUtils +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.ref.WeakReference +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@RunWith(AndroidJUnit4::class) +class MemoryLeakTests : ComposeTestUtils { + + private lateinit var scenario: ActivityScenario + + @get:Rule + override val composeTestRule = createEmptyComposeRule() + + @Before + fun setUp() { + scenario = ActivityScenario.launch( + Intent(ApplicationProvider.getApplicationContext(), ComposeActivity::class.java).apply { + putExtra(ComposeActivity.START_DESTINATION, rememberScopedDestination) + }) + } + + @Test + fun givenComposeActivityWithComposablesInANestedNavigationComposable_whenTheActivityIsRecreated_thenTheOriginalComposeActivityObjectIsGarbageCollected() { + var weakActivityReference: WeakReference? = null + // Given I create the Activity + composeTestRule.waitForIdle() + scenario.onActivity { activity: ComposeActivity -> + + // And we grab a WeakReference to the Activity + weakActivityReference = WeakReference(activity) + } + printComposeUiTreeToLog() + + // And I click "Navigate to rememberScoped" to get to a nested screen in the same Activity + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("Navigate to rememberScoped").performClick() + printComposeUiTreeToLog() + composeTestRule.waitForIdle() + + // When we recreate the activity + scenario.recreate() + composeTestRule.waitForIdle() + + // And trigger Garbage Collection to make sure old ComposeActivity is collected + Runtime.getRuntime().gc() + printComposeUiTreeToLog() + + // Then the original Activity object is garbage collected + assertNotNull(weakActivityReference) { "WeakReference container for initial ComposeActivity should not be null because it was created" } + assertNull(weakActivityReference?.get(), "Initial ComposeActivity should have been garbage collected but it wasn't, so it's leaking") + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt index 3c38724b..3ef91860 100644 --- a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt +++ b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @@ -122,7 +123,7 @@ fun ScreensWithNavigation(navController: NavHostController = rememberNavControll */ @Composable fun NavigationButtons(navController: NavHostController) { - Button(modifier = Modifier.padding(top = 16.dp, bottom = 2.dp), + Button(modifier = Modifier.padding(top = 16.dp, bottom = 2.dp).testTag("Navigate to rememberScoped"), onClick = { navController.navigate(rememberScopedDestination) }) { Text(text = "Push rememberScoped destination") } diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt new file mode 100644 index 00000000..74558fec --- /dev/null +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/MemoryLeakTests.kt @@ -0,0 +1,62 @@ +package com.sebaslogen.resacaapp.sample + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.sebaslogen.resacaapp.sample.ui.main.ComposeActivity +import com.sebaslogen.resacaapp.sample.ui.main.rememberScopedDestination +import com.sebaslogen.resacaapp.sample.utils.ComposeTestUtils +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.ref.WeakReference +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@RunWith(AndroidJUnit4::class) +class MemoryLeakTests : ComposeTestUtils { + init { + callFromTestInit() + } + + override fun callFromTestInit() { + ComposeActivity.defaultDestination = rememberScopedDestination // This is needed to reset the destination to the default one on the release app + } + + @get:Rule + override val composeTestRule = createComposeRule() + + @Test + fun `given ComposeActivity with Composables in a nested Navigation Composable, when the activity is recreated, then the original ComposeActivity object is garbage collected`() { + ActivityScenario.launch(ComposeActivity::class.java).use { scenario -> + var weakActivityReference: WeakReference? = null + // Given I create the Activity and navigate to a nested screen + scenario.onActivity { activity: ComposeActivity -> + + // Given the Activity shows a screen with scoped objects + printComposeUiTreeToLog() + + // And I grab a WeakReference to the Activity + weakActivityReference = WeakReference(activity) + + // And I click "Navigate to rememberScoped" to get to a nested screen in the same Activity + onNodeWithTestTag("Navigate to rememberScoped").performClick() + printComposeUiTreeToLog() + } + + // When we recreate the activity + scenario.recreate().onActivity { + + // And trigger Garbage Collection to make sure old ComposeActivity is collected + System.gc() + printComposeUiTreeToLog() + + // Then the original Activity object is garbage collected + assertNotNull(weakActivityReference) { "WeakReference container for initial ComposeActivity should not be null because it was created" } + assertNull(weakActivityReference?.get(), "Initial ComposeActivity should have been garbage collected but it wasn't, so it's leaking") + } + + } + } +} \ No newline at end of file diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/ScopeKeysTest.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/ScopeKeysTest.kt index 741ca8d3..9696efc3 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/ScopeKeysTest.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/ScopeKeysTest.kt @@ -23,7 +23,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class ScopeKeysTest : ComposeTestUtils { init {