-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Android] Ensure all events touching Android happen on the main thread. #334
Conversation
8970422
to
8f43e10
Compare
JaCoCo Code Coverage 79.8% ✅
Generated by 🚫 Danger |
bb20476
to
675363d
Compare
if (it.binds(key)) return true | ||
} | ||
return false | ||
return types.contains(key.javaClass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have all types
in a set, so no need to iterate over bindings
manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, this actually is a problem if the fragment key is a sealed class
formula-android/src/main/java/com/instacart/formula/android/internal/FeatureBinding.kt
Outdated
Show resolved
Hide resolved
} else { | ||
Utils.mainThreadHandler.post { | ||
startedActivity()?.effect() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a chance that all effects happen before the activity has started and therefore all effects are lost and never emitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should they be stored somewhere until the activity is started and then one it is, propagated? I am not sure if this ever even would happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would miss any effect that happens before the activity is started. This has been the behavior since this API was introduced, I haven't seen any problems with this as most issues would happen if somebody emits an effect during activity initialization.
formula-android/src/main/java/com/instacart/formula/android/internal/AndroidUpdateScheduler.kt
Outdated
Show resolved
Hide resolved
675363d
to
d4a4400
Compare
|
||
if (localPending != null) { | ||
// We will take over processing, so let's clear the message | ||
Utils.mainThreadHandler.removeCallbacks(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we also want to set updateScheduled to false here as well? If emitUpdate was called during the above update, the pendingValue
would be set and updateScheduled would have been set to true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, moved it after localPending != null)
6c7c17d
to
25f6b12
Compare
feature.state.onErrorResumeNext { | ||
input.onScreenError(fragmentId.key, it) | ||
Observable.empty() | ||
val action = FeatureObservableAction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a custom action to move updates from the background to the main thread when needed.
@@ -19,24 +19,18 @@ internal class ActivityManager<Activity : FragmentActivity>( | |||
private val store: ActivityStore<Activity> | |||
) { | |||
|
|||
private val fragmentState = store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplification
|
||
uiSubscription = delegate.fragmentFlowState().subscribe { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since delegate
uses BehaviorRelay
for fragmentFlowState
, we don't need to have the replay(1)
with connect
mechanism anymore.
} | ||
|
||
// We ensure all feature state updates come on the main thread. | ||
val androidUpdateScheduler = AndroidUpdateScheduler(send) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Custom action to enable moving update from bg thread to the main one.
2cc53c3
to
25c4017
Compare
@@ -175,6 +178,47 @@ class FragmentFlowStoreTest { | |||
) | |||
} | |||
|
|||
@Test fun `background feature events are moved to the main thread`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing fragment flow store to handle background events
25c4017
to
bb0eda5
Compare
@@ -199,6 +210,51 @@ class FragmentFlowRenderViewTest { | |||
assertThat(activeContracts()).containsExactly(TestKey(), TestKeyWithId(1)).inOrder() | |||
} | |||
|
|||
@Test fun `background feature events are moved to the main thread`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice test
assertThat(computedValues).containsExactly("first", "next").inOrder() | ||
} | ||
|
||
@Test fun `when update arrives on bg thread, handle it on main thread`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite clear how this test checks if update was handled on the main thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #336
|
||
/** | ||
* Handles state update scheduling to the main thread. If update arrives on a background thread, | ||
* it will added it the main thread queue. It will throw away a pending update if a new update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it will added it the main thread queue
copy is off
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #336
import android.os.Looper | ||
|
||
internal object Utils { | ||
internal val mainThreadHandler = Handler(Looper.getMainLooper()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be lazily initialized?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into both functions, they don't do much so lazy init should not be necessary.
What
Given we are starting to support non-main threads within formula core (#332), we need to make sure that our Android module handles this change more robustly. To ensure this, I've been auditing various entries and making them thread-safe.
fun send(effect: Activity.() -> Unit)
will now ensure that an activity side-effect is executed on the main threadAndroidUpdateScheduler
to ensure that they happen on the main threadAndroidUpdateScheduler
to ensure updates happen on the main threadThe new
AndroidUpdateScheduler
class helps coordinate state updates to the main thread. If there are multiple updates before the main thread can handle them, it will only process the last update throwing all the previous values away. This mechanism should help reduce unnecessary intermediate updates.