From 27f1276cc12d4576fdf929ca18ce7d78a5be3a4a Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 17 May 2022 15:10:29 +0300 Subject: [PATCH] Issue#84 Ensure BackgroundThread work is synced for DiffUtil --- .../ui/home/FavouriteContentAdapter.kt | 6 +++- .../ui/shared/executor/AsyncTaskExecutor.kt | 28 +++++++++++++++ .../ui/shared/executor/DefaultTaskExecutor.kt | 34 +++++++++++++++++++ .../ui/shared/executor/TaskExecutor.kt | 10 ++++++ .../idling/AsyncDiffUtilInstantTestRule.kt | 32 +++++++++++++++++ .../ui/home/MainActivityInstrumentedTest.kt | 2 ++ 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/shared/executor/AsyncTaskExecutor.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/shared/executor/DefaultTaskExecutor.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/shared/executor/TaskExecutor.kt create mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/AsyncDiffUtilInstantTestRule.kt diff --git a/app/src/main/java/org/fnives/test/showcase/ui/home/FavouriteContentAdapter.kt b/app/src/main/java/org/fnives/test/showcase/ui/home/FavouriteContentAdapter.kt index a4e5b58..6811711 100644 --- a/app/src/main/java/org/fnives/test/showcase/ui/home/FavouriteContentAdapter.kt +++ b/app/src/main/java/org/fnives/test/showcase/ui/home/FavouriteContentAdapter.kt @@ -1,6 +1,7 @@ package org.fnives.test.showcase.ui.home import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import org.fnives.test.showcase.R @@ -8,13 +9,16 @@ import org.fnives.test.showcase.databinding.ItemFavouriteContentBinding import org.fnives.test.showcase.model.content.ContentId import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.ui.shared.ViewBindingAdapter +import org.fnives.test.showcase.ui.shared.executor.AsyncTaskExecutor import org.fnives.test.showcase.ui.shared.layoutInflater import org.fnives.test.showcase.ui.shared.loadRoundedImage class FavouriteContentAdapter( private val listener: OnFavouriteItemClicked, ) : ListAdapter>( - DiffUtilItemCallback() + AsyncDifferConfig.Builder(DiffUtilItemCallback()) + .setBackgroundThreadExecutor(AsyncTaskExecutor.iOThreadExecutor) + .build() ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingAdapter = diff --git a/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/AsyncTaskExecutor.kt b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/AsyncTaskExecutor.kt new file mode 100644 index 0000000..0206dea --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/AsyncTaskExecutor.kt @@ -0,0 +1,28 @@ +package org.fnives.test.showcase.ui.shared.executor + +import java.util.concurrent.Executor + +/** + * Basic copy of [ArchTaskExecutor][androidx.arch.core.executor.ArchTaskExecutor], needed because that is restricted to Library. + * + * Intended to be used for [AsyncDifferConfig][androidx.recyclerview.widget.AsyncDifferConfig] so it can be synchronized with Espresso. + * + * Workaround until https://github.com/android/android-test/issues/382 is fixed finally. + */ +object AsyncTaskExecutor : TaskExecutor { + + val mainThreadExecutor = Executor { command -> postToMainThread(command) } + val iOThreadExecutor = Executor { command -> executeOnDiskIO(command) } + + var delegate: TaskExecutor? = null + private val defaultExecutor by lazy { DefaultTaskExecutor() } + private val executor get() = delegate ?: defaultExecutor + + override fun executeOnDiskIO(runnable: Runnable) { + executor.executeOnDiskIO(runnable) + } + + override fun postToMainThread(runnable: Runnable) { + executor.postToMainThread(runnable) + } +} diff --git a/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/DefaultTaskExecutor.kt b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/DefaultTaskExecutor.kt new file mode 100644 index 0000000..af8fd6e --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/DefaultTaskExecutor.kt @@ -0,0 +1,34 @@ +package org.fnives.test.showcase.ui.shared.executor + +import android.os.Build +import android.os.Handler +import android.os.Looper +import java.util.concurrent.Executors + +/** + * Basic copy of [androidx.arch.core.executor.DefaultTaskExecutor], needed because that is restricted to Library. + * With a Flavour of [androidx.recyclerview.widget.AsyncDifferConfig]. + * Used within [AsyncTaskExecutor]. + * + * Intended to be used for AsyncDiffUtil so it can be synchronized with Espresso. + */ +class DefaultTaskExecutor : TaskExecutor { + + private val diskIO = Executors.newFixedThreadPool(2) + private val mMainHandler: Handler by lazy { createAsync(Looper.getMainLooper()) } + + override fun executeOnDiskIO(runnable: Runnable) { + diskIO.execute(runnable) + } + + override fun postToMainThread(runnable: Runnable) { + mMainHandler.post(runnable) + } + + private fun createAsync(looper: Looper): Handler = + if (Build.VERSION.SDK_INT >= 28) { + Handler.createAsync(looper) + } else { + Handler(looper) + } +} diff --git a/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/TaskExecutor.kt b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/TaskExecutor.kt new file mode 100644 index 0000000..469de79 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/shared/executor/TaskExecutor.kt @@ -0,0 +1,10 @@ +package org.fnives.test.showcase.ui.shared.executor + +/** + * Define TaskExecutor intended for [AsyncDifferConfig][androidx.recyclerview.widget.AsyncDifferConfig] + */ +interface TaskExecutor { + fun executeOnDiskIO(runnable: Runnable) + + fun postToMainThread(runnable: Runnable) +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/AsyncDiffUtilInstantTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/AsyncDiffUtilInstantTestRule.kt new file mode 100644 index 0000000..6352b48 --- /dev/null +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/AsyncDiffUtilInstantTestRule.kt @@ -0,0 +1,32 @@ +package org.fnives.test.showcase.testutils.idling + +import org.fnives.test.showcase.ui.shared.executor.AsyncTaskExecutor +import org.fnives.test.showcase.ui.shared.executor.TaskExecutor +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Similar Test Rule to InstantTaskExecutorRule just for the [AsyncTaskExecutor] to make AsyncDiffUtil synchronized. + */ +class AsyncDiffUtilInstantTestRule : TestRule { + override fun apply(base: Statement, description: Description): Statement = + object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + AsyncTaskExecutor.delegate = object : TaskExecutor { + override fun executeOnDiskIO(runnable: Runnable) { + runnable.run() + } + + override fun postToMainThread(runnable: Runnable) { + runnable.run() + } + } + + base.evaluate() + + AsyncTaskExecutor.delegate = null + } + } +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt index 2900305..bc412e8 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt @@ -8,6 +8,7 @@ import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule +import org.fnives.test.showcase.testutils.idling.AsyncDiffUtilInstantTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.idling.loopMainThreadFor import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources @@ -37,6 +38,7 @@ class MainActivityInstrumentedTest : KoinTest { @JvmField val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) + .around(AsyncDiffUtilInstantTestRule()) @Before fun setup() {