Issue#84 Ensure BackgroundThread work is synced for DiffUtil

This commit is contained in:
Gergely Hegedus 2022-05-17 15:10:29 +03:00
parent b8a044ac3d
commit 27f1276cc1
6 changed files with 111 additions and 1 deletions

View file

@ -1,6 +1,7 @@
package org.fnives.test.showcase.ui.home package org.fnives.test.showcase.ui.home
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import org.fnives.test.showcase.R 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.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.ui.shared.ViewBindingAdapter 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.layoutInflater
import org.fnives.test.showcase.ui.shared.loadRoundedImage import org.fnives.test.showcase.ui.shared.loadRoundedImage
class FavouriteContentAdapter( class FavouriteContentAdapter(
private val listener: OnFavouriteItemClicked, private val listener: OnFavouriteItemClicked,
) : ListAdapter<FavouriteContent, ViewBindingAdapter<ItemFavouriteContentBinding>>( ) : ListAdapter<FavouriteContent, ViewBindingAdapter<ItemFavouriteContentBinding>>(
DiffUtilItemCallback() AsyncDifferConfig.Builder(DiffUtilItemCallback())
.setBackgroundThreadExecutor(AsyncTaskExecutor.iOThreadExecutor)
.build()
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingAdapter<ItemFavouriteContentBinding> = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingAdapter<ItemFavouriteContentBinding> =

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule 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.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.loopMainThreadFor import org.fnives.test.showcase.testutils.idling.loopMainThreadFor
import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources
@ -37,6 +38,7 @@ class MainActivityInstrumentedTest : KoinTest {
@JvmField @JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule) .around(mainDispatcherTestRule)
.around(AsyncDiffUtilInstantTestRule())
@Before @Before
fun setup() { fun setup() {