Skip to content

Commit 134394c

Browse files
rysh88psteiger
authored andcommitted
For Interactor, replace the shared flow with the stateflow to improve the ANR stacktraces and reduce ANRs
1 parent 4a9bbbc commit 134394c

File tree

5 files changed

+165
-11
lines changed

5 files changed

+165
-11
lines changed

libraries/rib-base/src/main/kotlin/com/uber/rib/core/FlowAsScope.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ import kotlinx.coroutines.rx2.rxCompletable
3838
* [range].
3939
*/
4040
@CoreFriendModuleApi
41-
public fun <T : Comparable<T>> SharedFlow<T>.asScopeCompletable(
41+
public fun <T : Comparable<T>> SharedFlow<T?>.asScopeCompletable(
4242
range: ClosedRange<T>,
4343
context: CoroutineContext = EmptyCoroutineContext,
4444
): CompletableSource {
4545
ensureAlive(range)
4646
return rxCompletable(RibDispatchers.Unconfined + context) {
47-
takeWhile { it < range.endInclusive }.collect()
47+
takeWhile { it == null || it < range.endInclusive }.collect()
4848
}
4949
}
5050

51-
private fun <T : Comparable<T>> SharedFlow<T>.ensureAlive(range: ClosedRange<T>) {
51+
private fun <T : Comparable<T>> SharedFlow<T?>.ensureAlive(range: ClosedRange<T>) {
5252
val lastEmitted = replayCache.lastOrNull()
5353
when {
5454
lastEmitted == null || lastEmitted < range.start -> throw LifecycleNotStartedException()

libraries/rib-base/src/main/kotlin/com/uber/rib/core/Interactor.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import javax.inject.Inject
2828
import kotlin.properties.ReadWriteProperty
2929
import kotlin.reflect.KProperty
3030
import kotlinx.coroutines.channels.BufferOverflow
31+
import kotlinx.coroutines.flow.Flow
3132
import kotlinx.coroutines.flow.MutableSharedFlow
32-
import kotlinx.coroutines.flow.SharedFlow
33+
import kotlinx.coroutines.flow.MutableStateFlow
34+
import kotlinx.coroutines.flow.filterNotNull
3335
import kotlinx.coroutines.rx2.asObservable
3436

3537
/**
@@ -42,9 +44,23 @@ public abstract class Interactor<P : Any, R : Router<*>>() : InteractorType, Rib
4244
@Inject public lateinit var injectedPresenter: P
4345

4446
@CoreFriendModuleApi public var actualPresenter: P? = null
45-
private val _lifecycleFlow = MutableSharedFlow<InteractorEvent>(1, 0, BufferOverflow.DROP_OLDEST)
46-
public open val lifecycleFlow: SharedFlow<InteractorEvent>
47-
get() = _lifecycleFlow
47+
48+
private val useStateFlow
49+
get() = RibEvents.useStateFlowInteractorEvent
50+
51+
@VisibleForTesting
52+
internal val _lifecycleFlow: MutableSharedFlow<InteractorEvent?> =
53+
if (useStateFlow) {
54+
MutableStateFlow(null)
55+
} else {
56+
MutableSharedFlow(
57+
1,
58+
0,
59+
BufferOverflow.DROP_OLDEST,
60+
)
61+
}
62+
public open val lifecycleFlow: Flow<InteractorEvent>
63+
get() = _lifecycleFlow.filterNotNull()
4864

4965
@Volatile private var _lifecycleObservable: Observable<InteractorEvent>? = null
5066

@@ -70,11 +86,11 @@ public abstract class Interactor<P : Any, R : Router<*>>() : InteractorType, Rib
7086
final override fun correspondingEvents(): CorrespondingEventsFunction<InteractorEvent> =
7187
LIFECYCLE_MAP_FUNCTION
7288

73-
final override fun peekLifecycle(): InteractorEvent? = lifecycleFlow.replayCache.lastOrNull()
89+
final override fun peekLifecycle(): InteractorEvent? = _lifecycleFlow.replayCache.lastOrNull()
7490

7591
@OptIn(CoreFriendModuleApi::class)
7692
final override fun requestScope(): CompletableSource =
77-
lifecycleFlow.asScopeCompletable(lifecycleRange)
93+
_lifecycleFlow.asScopeCompletable(lifecycleRange)
7894

7995
// ---- InteractorType overrides ---- //
8096

libraries/rib-base/src/main/kotlin/com/uber/rib/core/RibEvents.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public object RibEvents {
3535
@JvmStatic
3636
public val ribActionEvents: Observable<RibActionInfo> = mutableRibDurationEvents.asObservable()
3737

38+
internal var useStateFlowInteractorEvent: Boolean = false
39+
private set
40+
3841
/** Indicates if [ribActionEvents] will be emitting. */
3942
public var areRibActionEmissionsAllowed: Boolean = false
4043
@VisibleForTesting internal set
@@ -48,6 +51,12 @@ public object RibEvents {
4851
this.areRibActionEmissionsAllowed = true
4952
}
5053

54+
/** If true, the [Interactor] will use [MutableStateFlow] for the interactor events. */
55+
@JvmStatic
56+
public fun useStateFlowInteractorEvent() {
57+
this.useStateFlowInteractorEvent = true
58+
}
59+
5160
/**
5261
* @param eventType [RibEventType]
5362
* @param child [Router]

libraries/rib-base/src/main/kotlin/com/uber/rib/core/WorkerBinder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import kotlinx.coroutines.CoroutineDispatcher
3131
import kotlinx.coroutines.CoroutineStart
3232
import kotlinx.coroutines.DelicateCoroutinesApi
3333
import kotlinx.coroutines.GlobalScope
34-
import kotlinx.coroutines.flow.SharedFlow
34+
import kotlinx.coroutines.flow.Flow
3535
import kotlinx.coroutines.flow.onCompletion
3636
import kotlinx.coroutines.flow.takeWhile
3737
import kotlinx.coroutines.launch
@@ -264,7 +264,7 @@ private fun getJobCoroutineContext(
264264
}
265265

266266
private fun <T : Comparable<T>> Worker.bind(
267-
lifecycle: SharedFlow<T>,
267+
lifecycle: Flow<T>,
268268
lifecycleRange: ClosedRange<T>,
269269
): WorkerUnbinder {
270270
val dispatcherAtBinder = RibCoroutinesConfig.deprecatedWorkerDispatcher

libraries/rib-base/src/test/kotlin/com/uber/rib/core/InteractorAndRouterTest.kt

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ package com.uber.rib.core
1818
import com.google.common.truth.Truth
1919
import com.google.common.truth.Truth.assertThat
2020
import com.uber.autodispose.lifecycle.LifecycleEndedException
21+
import com.uber.autodispose.lifecycle.LifecycleNotStartedException
2122
import com.uber.rib.core.RibEvents.ribActionEvents
2223
import com.uber.rib.core.RibEventsUtils.assertRibActionInfo
2324
import com.uber.rib.core.RibRefWatcher.Companion.getInstance
2425
import com.uber.rib.core.lifecycle.InteractorEvent
26+
import io.reactivex.Completable
2527
import io.reactivex.observers.TestObserver
28+
import kotlinx.coroutines.flow.MutableSharedFlow
29+
import kotlinx.coroutines.flow.MutableStateFlow
30+
import kotlinx.coroutines.test.runTest
31+
import kotlinx.coroutines.withTimeoutOrNull
32+
import org.junit.Assert.fail
2633
import org.junit.Before
2734
import org.junit.Test
2835
import org.mockito.kotlin.any
@@ -216,6 +223,128 @@ class InteractorAndRouterTest {
216223
verify(ribRefWatcher, times(2)).watchDeletedObject(any())
217224
}
218225

226+
@Test
227+
fun init_useStateFlow_eventIsNotFired() = runTest {
228+
// Given
229+
setupInteractorForStateFlow()
230+
var eventCount = 0
231+
val lifecycleFlow = interactor.lifecycleFlow
232+
val _lifecycleFlow = interactor._lifecycleFlow
233+
234+
// When
235+
withTimeoutOrNull(10) { lifecycleFlow.collect { eventCount++ } }
236+
237+
// Then
238+
assertThat(_lifecycleFlow).isInstanceOf(MutableStateFlow::class.java)
239+
assertThat(eventCount).isEqualTo(0)
240+
}
241+
242+
@Test
243+
fun init_eventIsNotFired() = runTest {
244+
// When
245+
var eventCount = 0
246+
val lifecycleFlow = interactor.lifecycleFlow
247+
val _lifecycleFlow = interactor._lifecycleFlow
248+
249+
// When
250+
withTimeoutOrNull(10) { lifecycleFlow.collect { eventCount++ } }
251+
252+
// Then
253+
assertThat(_lifecycleFlow).isInstanceOf(MutableSharedFlow::class.java)
254+
assertThat(eventCount).isEqualTo(0)
255+
}
256+
257+
@Test
258+
fun requestScope_initAndUseStateFlow_throwsAnError() {
259+
// Given
260+
setupInteractorForStateFlow()
261+
262+
// When
263+
try {
264+
interactor.requestScope()
265+
fail("Expected LifecycleEndedException")
266+
} catch (e: LifecycleNotStartedException) {
267+
// Then
268+
// Expected exception, do nothing
269+
}
270+
}
271+
272+
@Test
273+
fun requestScope_initWith_throwsAnError() {
274+
// When
275+
try {
276+
interactor.requestScope()
277+
fail("Expected LifecycleEndedException")
278+
} catch (e: LifecycleNotStartedException) {
279+
// Then
280+
// Expected exception, do nothing
281+
}
282+
}
283+
284+
@Test
285+
fun requestScope_useStateFlowAndAttached_requestScopeIsNotCompleted() {
286+
// Given
287+
setupInteractorForStateFlow()
288+
interactor.dispatchAttach(null)
289+
290+
// When
291+
Completable.wrap(interactor.requestScope())
292+
.test()
293+
// Then
294+
.assertNotComplete()
295+
}
296+
297+
@Test
298+
fun requestScope_attached_requestScopeIsNotCompleted() {
299+
// Given
300+
interactor.dispatchAttach(null)
301+
302+
// When
303+
Completable.wrap(interactor.requestScope())
304+
.test()
305+
// Then
306+
.assertNotComplete()
307+
}
308+
309+
@Test
310+
fun requestScope_useStateFlowAndDetached_requestScopeIsCompleted() {
311+
// Given
312+
setupInteractorForStateFlow()
313+
interactor.dispatchAttach(null)
314+
val requestScope = Completable.wrap(interactor.requestScope())
315+
interactor.dispatchDetach()
316+
317+
// When
318+
requestScope
319+
.test()
320+
// Then
321+
.assertComplete()
322+
}
323+
324+
@Test
325+
fun requestScope_detached_requestScopeIsCompleted() {
326+
// Given
327+
interactor.dispatchAttach(null)
328+
val requestScope = Completable.wrap(interactor.requestScope())
329+
interactor.dispatchDetach()
330+
331+
// When
332+
requestScope
333+
.test()
334+
// Then
335+
.assertComplete()
336+
}
337+
338+
private fun setupInteractorForStateFlow() {
339+
RibEvents.useStateFlowInteractorEvent()
340+
val presenter: TestPresenter = mock()
341+
val component: InteractorComponent<TestPresenter, TestInteractor> = mock {
342+
on { presenter() } doReturn (presenter)
343+
}
344+
interactor = TestInteractor(childInteractor)
345+
router = TestRouter(interactor, component)
346+
}
347+
219348
private fun addTwoNestedChildInteractors(): Router<TestInteractorB> {
220349
val component: InteractorComponent<TestPresenter, TestInteractorB> =
221350
object : InteractorComponent<TestPresenter, TestInteractorB> {

0 commit comments

Comments
 (0)