Skip to content
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

Merged
merged 6 commits into from
Jan 26, 2024

Conversation

Laimiux
Copy link
Collaborator

@Laimiux Laimiux commented Jan 25, 2024

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 thread
  • Fragment state changes now usesAndroidUpdateScheduler to ensure that they happen on the main thread
  • Activity update function now also uses AndroidUpdateScheduler to ensure updates happen on the main thread

The 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.

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from 8970422 to 8f43e10 Compare January 25, 2024 17:05
@carrotkite
Copy link

carrotkite commented Jan 25, 2024

JaCoCo Code Coverage 79.8% ✅

Class Covered Meta Status
com/instacart/formula/android/FragmentFlowStore 100% 0%
com/instacart/formula/android/internal/ActivityManager 64% 0%
com/instacart/formula/android/internal/StreamConfiguratorIml 100% 0%
com/instacart/formula/android/internal/FeatureObservableAction 100% 0%
com/instacart/formula/android/internal/AndroidUpdateScheduler 100% 0%
com/instacart/formula/android/internal/FeatureBinding 100% 0%
com/instacart/formula/android/internal/FragmentFlowRenderView 68% 0%
com/instacart/formula/android/internal/Utils 50% 0%
com/instacart/formula/android/internal/ActivityStoreContextImpl 66% 0%

Generated by 🚫 Danger

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from bb20476 to 675363d Compare January 25, 2024 19:46
if (it.binds(key)) return true
}
return false
return types.contains(key.javaClass)
Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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

} else {
Utils.mainThreadHandler.post {
startedActivity()?.effect()
}
Copy link
Member

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?

Copy link
Member

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.

Copy link
Collaborator Author

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.

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from 675363d to d4a4400 Compare January 25, 2024 20:15

if (localPending != null) {
// We will take over processing, so let's clear the message
Utils.mainThreadHandler.removeCallbacks(this)

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.

Copy link
Collaborator Author

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)

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from 6c7c17d to 25f6b12 Compare January 25, 2024 22:25
feature.state.onErrorResumeNext {
input.onScreenError(fragmentId.key, it)
Observable.empty()
val action = FeatureObservableAction(
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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)
Copy link
Collaborator Author

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.

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from 2cc53c3 to 25c4017 Compare January 26, 2024 16:30
@@ -175,6 +178,47 @@ class FragmentFlowStoreTest {
)
}

@Test fun `background feature events are moved to the main thread`() {
Copy link
Collaborator Author

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

@Laimiux Laimiux force-pushed the laimonas/formula-android-main-thread branch from 25c4017 to bb0eda5 Compare January 26, 2024 17:13
@Laimiux Laimiux merged commit 9676c10 into master Jan 26, 2024
3 checks passed
@Laimiux Laimiux deleted the laimonas/formula-android-main-thread branch January 26, 2024 17:53
@@ -199,6 +210,51 @@ class FragmentFlowRenderViewTest {
assertThat(activeContracts()).containsExactly(TestKey(), TestKeyWithId(1)).inOrder()
}

@Test fun `background feature events are moved to the main thread`() {
Copy link
Collaborator

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`() {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Copy link
Collaborator

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

Copy link
Collaborator Author

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())
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Laimiux added a commit that referenced this pull request Jan 30, 2024
Laimiux added a commit that referenced this pull request Jan 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants