From e88e8bed20531838f97ecae004c5b90467f57927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=84=B1=EB=AF=BC?= Date: Tue, 19 Mar 2024 13:30:58 +0900 Subject: [PATCH] Fixes a bug that cannot use custom scope viewModel in mvrx-compose. (#712) * 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. * 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. * Add newline at EOF to fix detekt warning --- .../compose/MavericksComposeExtensions.kt | 2 +- mvrx-compose/src/test/AndroidManifest.xml | 7 + .../compose/CustomLifecycleOwnerScopeTest.kt | 143 ++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 mvrx-compose/src/test/kotlin/com/airbnb/mvrx/compose/CustomLifecycleOwnerScopeTest.kt 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) 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..0ac58f71 --- /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) + } + } + } + } +}