Skip to content

Commit

Permalink
Use main dispatcher for fragment flow store. (#367)
Browse files Browse the repository at this point in the history
* Use main dispatcher for fragment flow store.

* Fix tests.
  • Loading branch information
Laimiux authored Apr 25, 2024
1 parent a61dcbd commit 96df3e0
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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<Looper?>()
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<Looper?>()
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!")
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,6 +145,9 @@ class FragmentFlowStore @PublishedApi internal constructor(
}

internal fun state(environment: FragmentEnvironment): Observable<FragmentFlowState> {
return toObservable(environment)
val config = RuntimeConfig(
defaultDispatcher = MainThreadDispatcher(),
)
return toObservable(environment, config)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -193,7 +192,7 @@ class FragmentFlowStoreTest {

val updates = mutableListOf<Map<FragmentKey, Any>>()
val updateThreads = linkedSetOf<Thread>()
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)

Expand Down

0 comments on commit 96df3e0

Please sign in to comment.