From 7a3bedb9d266b9b08724d898e48e1e8ed7424e0d Mon Sep 17 00:00:00 2001 From: JSpiner Date: Mon, 18 Mar 2024 14:13:20 +0900 Subject: [PATCH 1/3] Create a failure test for custom compose fragment lifecycle scope. - Even if create and pass a custom lifecycle scope in Fragment, `mavericksViewModel` function uses the fragment's lifecycleScope. --- mvrx-compose/src/test/AndroidManifest.xml | 7 + .../compose/CustomLifecycleOwnerScopeTest.kt | 143 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt diff --git a/mvrx-compose/src/test/AndroidManifest.xml b/mvrx-compose/src/test/AndroidManifest.xml index bcd868b6..72d2c219 100644 --- a/mvrx-compose/src/test/AndroidManifest.xml +++ b/mvrx-compose/src/test/AndroidManifest.xml @@ -14,6 +14,13 @@ + + + + + + + diff --git a/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt b/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt new file mode 100644 index 00000000..8a935c97 --- /dev/null +++ b/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt @@ -0,0 +1,143 @@ +package com.airbnb.mvrx.compose + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +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.SavedStateRegistryOwner +import com.airbnb.mvrx.Mavericks +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CustomLifecycleOwnerScopeTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Before + fun setUp() { + Mavericks.initialize(composeTestRule.activity) + } + + @Test + fun `activity_customScope_viewModel1 and activity_customScope_viewModel2 are different`() { + assertNotNull(composeTestRule.activity.fragment.viewModel1) + assertNotNull(composeTestRule.activity.fragment.viewModel2) + assert(composeTestRule.activity.fragment.viewModel1 !== composeTestRule.activity.fragment.viewModel2) + } + + @Test + fun `fragment_customScope_viewModel1 and fragment_customScope_viewModel2 are different`() { + assertNotNull(composeTestRule.activity.viewModel1) + assertNotNull(composeTestRule.activity.viewModel2) + assert(composeTestRule.activity.viewModel1 !== composeTestRule.activity.viewModel2) + } +} + +@Composable +private fun CustomViewModelScope(content: @Composable (LifecycleOwner) -> Unit) { + val originLifecycleOwner = LocalLifecycleOwner.current + val customLifecycleRegistry = remember { LifecycleRegistry(originLifecycleOwner) } + val customScope = remember { + CustomLifecycleOwner( + customLifecycleRegistry, + ViewModelStore(), + (originLifecycleOwner as SavedStateRegistryOwner).savedStateRegistry + ) + } + + customLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + + content(customScope) + + DisposableEffect(Unit) { + onDispose { + customLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + } +} + +class CustomLifecycleOwner( + override val lifecycle: Lifecycle, + override val viewModelStore: ViewModelStore, + override val savedStateRegistry: SavedStateRegistry, +) : LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner + +class CustomLifecycleOwnerScopeTestActivity : AppCompatActivity() { + lateinit var fragment: CustomLifecycleOwnerScopeTestFragment + lateinit var viewModel1: CounterViewModel + lateinit var viewModel2: CounterViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val fragmentId = 123 + val fragmentContainerView = FragmentContainerView(this).apply { + id = fragmentId + } + val composeView = ComposeView(this).apply { + setContent { + CustomViewModelScope { scope -> + this@CustomLifecycleOwnerScopeTestActivity.viewModel1 = mavericksViewModel(scope = scope) + } + CustomViewModelScope { scope -> + this@CustomLifecycleOwnerScopeTestActivity.viewModel2 = mavericksViewModel(scope = scope) + } + } + } + + setContentView( + LinearLayout(this).apply { + addView(fragmentContainerView) + addView(composeView) + } + ) + + fragment = CustomLifecycleOwnerScopeTestFragment() + + supportFragmentManager.beginTransaction() + .add( + fragmentId, + fragment + ) + .commit() + } +} + +class CustomLifecycleOwnerScopeTestFragment : Fragment() { + lateinit var viewModel1: CounterViewModel + lateinit var viewModel2: CounterViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + setContent { + CustomViewModelScope { scope -> + this@CustomLifecycleOwnerScopeTestFragment.viewModel1 = mavericksViewModel(scope = scope) + } + CustomViewModelScope { scope -> + this@CustomLifecycleOwnerScopeTestFragment.viewModel2 = mavericksViewModel(scope = scope) + } + } + } + } +} \ No newline at end of file From 63bfac07cebd8ecc1bd342518d2746165838bbda Mon Sep 17 00:00:00 2001 From: JSpiner Date: Mon, 18 Mar 2024 14:16:59 +0900 Subject: [PATCH 2/3] Change to create a viewModelContext using the received viewModelStoreOwner and savedStateRegistry. - Even if create and pass a custom lifecycle scope in Fragment, `mavericksViewModel` function uses the fragment's lifecycleScope. --- .../com/airbnb/mvrx/compose/MavericksComposeExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvrx-compose/src/main/kotlin/com/airbnb/mvrx/compose/MavericksComposeExtensions.kt b/mvrx-compose/src/main/kotlin/com/airbnb/mvrx/compose/MavericksComposeExtensions.kt index cacbaed9..9d276695 100644 --- a/mvrx-compose/src/main/kotlin/com/airbnb/mvrx/compose/MavericksComposeExtensions.kt +++ b/mvrx-compose/src/main/kotlin/com/airbnb/mvrx/compose/MavericksComposeExtensions.kt @@ -75,7 +75,7 @@ inline fun , reified S : MavericksState> mave if (parentFragment != null) { val args = argsFactory?.invoke() ?: parentFragment.arguments?.get(Mavericks.KEY_ARG) - FragmentViewModelContext(activity, args, parentFragment) + FragmentViewModelContext(activity, args, parentFragment, viewModelStoreOwner, savedStateRegistry) } else { val args = argsFactory?.invoke() ?: activity.intent.extras?.get(Mavericks.KEY_ARG) ActivityViewModelContext(activity, args, viewModelStoreOwner, savedStateRegistry) From 991d23cdaa0bae9fc2ba098febdaf888454110be Mon Sep 17 00:00:00 2001 From: JSpiner Date: Tue, 19 Mar 2024 09:09:36 +0900 Subject: [PATCH 3/3] Add newline at EOF to fix detekt warning --- .../com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt b/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt index 8a935c97..0ac58f71 100644 --- a/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt +++ b/mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt @@ -140,4 +140,4 @@ class CustomLifecycleOwnerScopeTestFragment : Fragment() { } } } -} \ No newline at end of file +}