Skip to content

Commit 76e810f

Browse files
MickeyMozpocmo
andcommitted
4143: CCloses mozilla-mobile#4125: Fragment.consumeFrom(): Temporarily check whether fragment is added. r=Amejia481 a=pocmo Co-authored-by: Sebastian Kaspari <[email protected]>
2 parents 4b47a7d + 348a545 commit 76e810f

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,33 @@ import mozilla.components.support.ktx.android.view.toScope
2828
@ExperimentalCoroutinesApi // Channel
2929
@ObsoleteCoroutinesApi // consumeEach
3030
fun <S : State, A : Action> Fragment.consumeFrom(store: Store<S, A>, block: (S) -> Unit) {
31+
val fragment = this
3132
val view = checkNotNull(view) { "Fragment has no view yet. Call from onViewCreated()." }
3233

3334
val scope = view.toScope()
3435
val channel = store.channel(owner = this)
3536

3637
scope.launch {
37-
channel.consumeEach { state -> block(state) }
38+
channel.consumeEach { state ->
39+
// We are using a scope that is bound to the view being attached here. It can happen
40+
// that the "view detached" callback gets executed *after* the fragment was detached. If
41+
// a `consumeFrom` runs in exactly this moment then we run inside a detached fragment
42+
// without a `Context` and this can cause a variety of issues/crashes.
43+
// See: https://github.com/mozilla-mobile/android-components/issues/4125
44+
//
45+
// To avoid this, we check whether the fragment is added. If it's not added then we run
46+
// in exactly that moment between fragment detach and view detach.
47+
// It would be better if we could use `viewLifecycleOwner` which is bound to
48+
// onCreateView() and onDestroyView() of the fragment. But:
49+
// - `viewLifecycleOwner` is only available in alpha versions of AndroidX currently.
50+
// - We found a bug where `viewLifecycleOwner.lifecycleScope` is not getting cancelled
51+
// causing this coroutine to run forever.
52+
// See: https://github.com/mozilla-mobile/android-components/issues/3828
53+
// Once those two issues get resolved we can remove the `isAdded` check and use
54+
// `viewLifecycleOwner.lifecycleScope` instead of the view scope.
55+
if (fragment.isAdded) {
56+
block(state)
57+
}
58+
}
3859
}
3960
}

components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class FragmentKtTest {
6565
)
6666

6767
val fragment: Fragment = mock()
68+
doReturn(true).`when`(fragment).isAdded
6869

6970
val view = View(testContext)
7071
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
@@ -114,4 +115,68 @@ class FragmentKtTest {
114115
assertFalse(latch.await(1, TimeUnit.SECONDS))
115116
assertEquals(26, receivedValue)
116117
}
118+
119+
@Test
120+
@Synchronized
121+
@ExperimentalCoroutinesApi // consumeFrom
122+
@ObsoleteCoroutinesApi // consumeFrom
123+
fun `consumeFrom does not run when fragment is not added`() {
124+
val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
125+
126+
val store = Store(
127+
TestState(counter = 23),
128+
::reducer
129+
)
130+
131+
val fragment: Fragment = mock()
132+
133+
val view = View(testContext)
134+
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
135+
activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
136+
assertTrue(view.isAttachedToWindow)
137+
doReturn(view).`when`(fragment).view
138+
doReturn(owner.lifecycle).`when`(fragment).lifecycle
139+
140+
doReturn(true).`when`(fragment).isAdded
141+
142+
var receivedValue = 0
143+
var latch = CountDownLatch(1)
144+
145+
fragment.consumeFrom(store) { state ->
146+
receivedValue = state.counter
147+
latch.countDown()
148+
}
149+
150+
assertTrue(latch.await(1, TimeUnit.SECONDS))
151+
assertEquals(23, receivedValue)
152+
153+
latch = CountDownLatch(1)
154+
store.dispatch(TestAction.IncrementAction).joinBlocking()
155+
assertTrue(latch.await(1, TimeUnit.SECONDS))
156+
assertEquals(24, receivedValue)
157+
158+
latch = CountDownLatch(1)
159+
store.dispatch(TestAction.IncrementAction).joinBlocking()
160+
assertTrue(latch.await(1, TimeUnit.SECONDS))
161+
assertEquals(25, receivedValue)
162+
163+
doReturn(false).`when`(fragment).isAdded
164+
165+
latch = CountDownLatch(1)
166+
store.dispatch(TestAction.IncrementAction).joinBlocking()
167+
assertFalse(latch.await(1, TimeUnit.SECONDS))
168+
assertEquals(25, receivedValue)
169+
170+
latch = CountDownLatch(1)
171+
store.dispatch(TestAction.IncrementAction).joinBlocking()
172+
assertFalse(latch.await(1, TimeUnit.SECONDS))
173+
assertEquals(25, receivedValue)
174+
175+
doReturn(true).`when`(fragment).isAdded
176+
177+
latch = CountDownLatch(1)
178+
store.dispatch(TestAction.IncrementAction).joinBlocking()
179+
assertTrue(latch.await(1, TimeUnit.SECONDS))
180+
assertEquals(28, receivedValue)
181+
}
117182
}

0 commit comments

Comments
 (0)