Skip to content

Commit e88e8be

Browse files
authored
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
1 parent 02a35a6 commit e88e8be

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

mvrx-compose/src/main/kotlin/com/airbnb/mvrx/compose/MavericksComposeExtensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ inline fun <reified VM : MavericksViewModel<S>, reified S : MavericksState> mave
7575

7676
if (parentFragment != null) {
7777
val args = argsFactory?.invoke() ?: parentFragment.arguments?.get(Mavericks.KEY_ARG)
78-
FragmentViewModelContext(activity, args, parentFragment)
78+
FragmentViewModelContext(activity, args, parentFragment, viewModelStoreOwner, savedStateRegistry)
7979
} else {
8080
val args = argsFactory?.invoke() ?: activity.intent.extras?.get(Mavericks.KEY_ARG)
8181
ActivityViewModelContext(activity, args, viewModelStoreOwner, savedStateRegistry)

mvrx-compose/src/test/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
<category android:name="android.intent.category.LAUNCHER" />
1515
</intent-filter>
1616
</activity>
17+
<activity android:name="com.airbnb.mvrx.compose.CustomLifecycleOwnerScopeTestActivity" android:exported="false" >
18+
<intent-filter>
19+
<action android:name="android.intent.action.MAIN" />
20+
21+
<category android:name="android.intent.category.LAUNCHER" />
22+
</intent-filter>
23+
</activity>
1724
<activity android:name="com.airbnb.mvrx.compose.FragmentArgsTestActivity" android:exported="false" >
1825
<intent-filter>
1926
<action android:name="android.intent.action.MAIN" />
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.airbnb.mvrx.compose
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import android.widget.LinearLayout
8+
import androidx.appcompat.app.AppCompatActivity
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.DisposableEffect
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.ui.platform.ComposeView
13+
import androidx.compose.ui.platform.LocalLifecycleOwner
14+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
15+
import androidx.fragment.app.Fragment
16+
import androidx.fragment.app.FragmentContainerView
17+
import androidx.lifecycle.Lifecycle
18+
import androidx.lifecycle.LifecycleOwner
19+
import androidx.lifecycle.LifecycleRegistry
20+
import androidx.lifecycle.ViewModelStore
21+
import androidx.lifecycle.ViewModelStoreOwner
22+
import androidx.savedstate.SavedStateRegistry
23+
import androidx.savedstate.SavedStateRegistryOwner
24+
import com.airbnb.mvrx.Mavericks
25+
import org.junit.Assert.assertNotNull
26+
import org.junit.Before
27+
import org.junit.Rule
28+
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import org.robolectric.RobolectricTestRunner
31+
32+
@RunWith(RobolectricTestRunner::class)
33+
class CustomLifecycleOwnerScopeTest {
34+
@get:Rule
35+
val composeTestRule = createAndroidComposeRule<CustomLifecycleOwnerScopeTestActivity>()
36+
37+
@Before
38+
fun setUp() {
39+
Mavericks.initialize(composeTestRule.activity)
40+
}
41+
42+
@Test
43+
fun `activity_customScope_viewModel1 and activity_customScope_viewModel2 are different`() {
44+
assertNotNull(composeTestRule.activity.fragment.viewModel1)
45+
assertNotNull(composeTestRule.activity.fragment.viewModel2)
46+
assert(composeTestRule.activity.fragment.viewModel1 !== composeTestRule.activity.fragment.viewModel2)
47+
}
48+
49+
@Test
50+
fun `fragment_customScope_viewModel1 and fragment_customScope_viewModel2 are different`() {
51+
assertNotNull(composeTestRule.activity.viewModel1)
52+
assertNotNull(composeTestRule.activity.viewModel2)
53+
assert(composeTestRule.activity.viewModel1 !== composeTestRule.activity.viewModel2)
54+
}
55+
}
56+
57+
@Composable
58+
private fun CustomViewModelScope(content: @Composable (LifecycleOwner) -> Unit) {
59+
val originLifecycleOwner = LocalLifecycleOwner.current
60+
val customLifecycleRegistry = remember { LifecycleRegistry(originLifecycleOwner) }
61+
val customScope = remember {
62+
CustomLifecycleOwner(
63+
customLifecycleRegistry,
64+
ViewModelStore(),
65+
(originLifecycleOwner as SavedStateRegistryOwner).savedStateRegistry
66+
)
67+
}
68+
69+
customLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
70+
71+
content(customScope)
72+
73+
DisposableEffect(Unit) {
74+
onDispose {
75+
customLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
76+
}
77+
}
78+
}
79+
80+
class CustomLifecycleOwner(
81+
override val lifecycle: Lifecycle,
82+
override val viewModelStore: ViewModelStore,
83+
override val savedStateRegistry: SavedStateRegistry,
84+
) : LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner
85+
86+
class CustomLifecycleOwnerScopeTestActivity : AppCompatActivity() {
87+
lateinit var fragment: CustomLifecycleOwnerScopeTestFragment
88+
lateinit var viewModel1: CounterViewModel
89+
lateinit var viewModel2: CounterViewModel
90+
91+
override fun onCreate(savedInstanceState: Bundle?) {
92+
super.onCreate(savedInstanceState)
93+
94+
val fragmentId = 123
95+
val fragmentContainerView = FragmentContainerView(this).apply {
96+
id = fragmentId
97+
}
98+
val composeView = ComposeView(this).apply {
99+
setContent {
100+
CustomViewModelScope { scope ->
101+
this@CustomLifecycleOwnerScopeTestActivity.viewModel1 = mavericksViewModel<CounterViewModel, CounterState>(scope = scope)
102+
}
103+
CustomViewModelScope { scope ->
104+
this@CustomLifecycleOwnerScopeTestActivity.viewModel2 = mavericksViewModel<CounterViewModel, CounterState>(scope = scope)
105+
}
106+
}
107+
}
108+
109+
setContentView(
110+
LinearLayout(this).apply {
111+
addView(fragmentContainerView)
112+
addView(composeView)
113+
}
114+
)
115+
116+
fragment = CustomLifecycleOwnerScopeTestFragment()
117+
118+
supportFragmentManager.beginTransaction()
119+
.add(
120+
fragmentId,
121+
fragment
122+
)
123+
.commit()
124+
}
125+
}
126+
127+
class CustomLifecycleOwnerScopeTestFragment : Fragment() {
128+
lateinit var viewModel1: CounterViewModel
129+
lateinit var viewModel2: CounterViewModel
130+
131+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
132+
return ComposeView(requireContext()).apply {
133+
setContent {
134+
CustomViewModelScope { scope ->
135+
this@CustomLifecycleOwnerScopeTestFragment.viewModel1 = mavericksViewModel<CounterViewModel, CounterState>(scope = scope)
136+
}
137+
CustomViewModelScope { scope ->
138+
this@CustomLifecycleOwnerScopeTestFragment.viewModel2 = mavericksViewModel<CounterViewModel, CounterState>(scope = scope)
139+
}
140+
}
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)