From 96df3e062d19be91b6bc8c037993cf99077536f6 Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Thu, 25 Apr 2024 14:12:42 -0400 Subject: [PATCH] Use main dispatcher for fragment flow store. (#367) * Use main dispatcher for fragment flow store. * Fix tests. --- .../android/utils/MainThreadDispatcherTest.kt | 64 +++++++++++++++++++ .../formula/android/FragmentFlowStore.kt | 7 +- .../internal/FeatureObservableAction.kt | 4 +- .../android/utils/MainThreadDispatcher.kt | 30 +++++++++ .../android/ActivityStoreFactoryTest.kt | 3 + .../formula/android/FragmentFlowStoreTest.kt | 3 +- 6 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 formula-android-tests/src/test/java/com/instacart/formula/android/utils/MainThreadDispatcherTest.kt create mode 100644 formula-android/src/main/java/com/instacart/formula/android/utils/MainThreadDispatcher.kt diff --git a/formula-android-tests/src/test/java/com/instacart/formula/android/utils/MainThreadDispatcherTest.kt b/formula-android-tests/src/test/java/com/instacart/formula/android/utils/MainThreadDispatcherTest.kt new file mode 100644 index 00000000..923e1eb3 --- /dev/null +++ b/formula-android-tests/src/test/java/com/instacart/formula/android/utils/MainThreadDispatcherTest.kt @@ -0,0 +1,64 @@ +package com.instacart.formula.android.utils + +import android.os.Looper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Shadows +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +@RunWith(AndroidJUnit4::class) +class MainThreadDispatcherTest { + + @Test fun `isDispatchNeeded returns false when on main thread`() { + val result = MainThreadDispatcher().isDispatchNeeded() + Truth.assertThat(result).isFalse() + } + + @Test fun `isDispatchNeeded returns true when not on main thread`() { + val latch = CountDownLatch(1) + val result = AtomicBoolean() + Executors.newSingleThreadExecutor().execute { + result.set(MainThreadDispatcher().isDispatchNeeded()) + latch.countDown() + } + + if (latch.await(1, TimeUnit.SECONDS)) { + Truth.assertThat(result.get()).isTrue() + } else { + error("Latch timed out!") + } + } + + @Test fun `if dispatch is called from main thread, executable is executed immediately`() { + val dispatcher = MainThreadDispatcher() + val loopers = mutableSetOf() + dispatcher.dispatch { loopers.add(Looper.myLooper()) } + Truth.assertThat(loopers).containsExactly(Looper.getMainLooper()) + } + + @Test fun `if dispatch is called from background thread, executable is dispatched to main thread`() { + val dispatcher = MainThreadDispatcher() + val latch = CountDownLatch(1) + + val loopers = mutableSetOf() + Executors.newSingleThreadExecutor().execute { + dispatcher.dispatch { + loopers.add(Looper.myLooper()) + } + latch.countDown() + } + + if (latch.await(1, TimeUnit.SECONDS)) { + Shadows.shadowOf(Looper.getMainLooper()).idle() + Truth.assertThat(loopers).containsExactly(Looper.getMainLooper()) + } else { + error("Latch timed out!") + } + } + +} \ No newline at end of file diff --git a/formula-android/src/main/java/com/instacart/formula/android/FragmentFlowStore.kt b/formula-android/src/main/java/com/instacart/formula/android/FragmentFlowStore.kt index 1cbf759f..d979577c 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/FragmentFlowStore.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/FragmentFlowStore.kt @@ -2,10 +2,12 @@ package com.instacart.formula.android import com.instacart.formula.Evaluation import com.instacart.formula.Formula +import com.instacart.formula.RuntimeConfig import com.instacart.formula.Snapshot import com.instacart.formula.android.internal.Binding import com.instacart.formula.android.events.FragmentLifecycleEvent import com.instacart.formula.android.internal.FeatureObservableAction +import com.instacart.formula.android.utils.MainThreadDispatcher import com.instacart.formula.rxjava3.RxAction import com.instacart.formula.rxjava3.toObservable import com.jakewharton.rxrelay3.PublishRelay @@ -143,6 +145,9 @@ class FragmentFlowStore @PublishedApi internal constructor( } internal fun state(environment: FragmentEnvironment): Observable { - return toObservable(environment) + val config = RuntimeConfig( + defaultDispatcher = MainThreadDispatcher(), + ) + return toObservable(environment, config) } } diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt index 30b59e56..3a9d53e4 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt @@ -21,9 +21,7 @@ class FeatureObservableAction( Observable.empty() } - // We ensure all feature state updates come on the main thread. - val androidUpdateScheduler = AndroidUpdateScheduler(send) - val disposable = observable.subscribe(androidUpdateScheduler::emitUpdate) + val disposable = observable.subscribe(send) return Cancelable(disposable::dispose) } } \ No newline at end of file diff --git a/formula-android/src/main/java/com/instacart/formula/android/utils/MainThreadDispatcher.kt b/formula-android/src/main/java/com/instacart/formula/android/utils/MainThreadDispatcher.kt new file mode 100644 index 00000000..cff28973 --- /dev/null +++ b/formula-android/src/main/java/com/instacart/formula/android/utils/MainThreadDispatcher.kt @@ -0,0 +1,30 @@ +package com.instacart.formula.android.utils + +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Message +import com.instacart.formula.plugin.Dispatcher + +/** + * Android main thread formula dispatcher. + */ +class MainThreadDispatcher : Dispatcher { + private val handler = Handler(Looper.getMainLooper()) + + override fun dispatch(executable: () -> Unit) { + if (isDispatchNeeded()) { + val message = Message.obtain(handler, executable) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + message.isAsynchronous = true + } + handler.sendMessage(message) + } else { + executable() + } + } + + override fun isDispatchNeeded(): Boolean { + return Looper.getMainLooper().thread != Thread.currentThread() + } +} diff --git a/formula-android/src/test/java/com/instacart/formula/android/ActivityStoreFactoryTest.kt b/formula-android/src/test/java/com/instacart/formula/android/ActivityStoreFactoryTest.kt index 5f1f0679..62e7c26b 100644 --- a/formula-android/src/test/java/com/instacart/formula/android/ActivityStoreFactoryTest.kt +++ b/formula-android/src/test/java/com/instacart/formula/android/ActivityStoreFactoryTest.kt @@ -1,11 +1,14 @@ package com.instacart.formula.android import androidx.fragment.app.FragmentActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.instacart.formula.android.internal.ActivityStoreFactory import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.mock +@RunWith(AndroidJUnit4::class) class ActivityStoreFactoryTest { class FakeActivity : FragmentActivity() diff --git a/formula-android/src/test/java/com/instacart/formula/android/FragmentFlowStoreTest.kt b/formula-android/src/test/java/com/instacart/formula/android/FragmentFlowStoreTest.kt index 4c5b74aa..984c6a21 100644 --- a/formula-android/src/test/java/com/instacart/formula/android/FragmentFlowStoreTest.kt +++ b/formula-android/src/test/java/com/instacart/formula/android/FragmentFlowStoreTest.kt @@ -12,7 +12,6 @@ import com.instacart.formula.android.fakes.NoOpViewFactory import com.instacart.formula.android.fakes.TestAccountFragmentKey import com.instacart.formula.android.fakes.TestLoginFragmentKey import com.instacart.formula.android.fakes.TestSignUpFragmentKey -import com.instacart.formula.rxjava3.toObservable import io.reactivex.rxjava3.observers.TestObserver import org.junit.Test import org.junit.runner.RunWith @@ -193,7 +192,7 @@ class FragmentFlowStoreTest { val updates = mutableListOf>() val updateThreads = linkedSetOf() - val disposable = store.toObservable(FragmentEnvironment()).subscribe { + val disposable = store.state(FragmentEnvironment()).subscribe { val states = it.states.mapKeys { it.key.key }.mapValues { it.value.renderModel } updates.add(states)