Skip to content

Commit

Permalink
Do not refresh transaction when it is not being affected. (#246)
Browse files Browse the repository at this point in the history
* Do not refresh transaction when it is not being affected.
* Use correct null-aware comparison for HttpTransaction.
  • Loading branch information
MiSikora authored Feb 22, 2020
1 parent f6d2aa3 commit 667d668
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buildscript {
materialComponentsVersion = '1.1.0-rc02'
roomVersion = '2.2.3'
lifecycleVersion = '2.2.0'
androidXCore = '2.1.0'

// Publishing
androidMavenGradleVersion = '2.1'
Expand Down
1 change: 1 addition & 0 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testImplementation "io.mockk:mockk:$mockkVersion"
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp3Version"
testImplementation "androidx.arch.core:core-testing:$androidXCore"
}

apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,37 @@ internal class HttpTransaction(
scheme = url.scheme()
return this
}

// Not relying on 'equals' because comparison be long due to request and response sizes
// and it would be unwise to do this every time 'equals' is called.
@Suppress("ComplexMethod")
fun hasTheSameContent(other: HttpTransaction?): Boolean {
if (this === other) return true
if (other == null) return false

return (id == other.id) &&
(requestDate == other.requestDate) &&
(responseDate == other.responseDate) &&
(tookMs == other.tookMs) &&
(protocol == other.protocol) &&
(method == other.method) &&
(url == other.url) &&
(host == other.host) &&
(path == other.path) &&
(scheme == other.scheme) &&
(requestContentLength == other.requestContentLength) &&
(requestContentType == other.requestContentType) &&
(requestHeaders == other.requestHeaders) &&
(requestBody == other.requestBody) &&
(isRequestBodyPlainText == other.isRequestBodyPlainText) &&
(responseCode == other.responseCode) &&
(responseMessage == other.responseMessage) &&
(error == other.error) &&
(responseContentLength == other.responseContentLength) &&
(responseContentType == other.responseContentType) &&
(responseHeaders == other.responseHeaders) &&
(responseBody == other.responseBody) &&
(isResponseBodyPlainText == other.isResponseBodyPlainText) &&
responseImageData?.contentEquals(other.responseImageData ?: byteArrayOf()) != false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple
import com.chuckerteam.chucker.internal.data.room.ChuckerDatabase
import com.chuckerteam.chucker.internal.support.distinctUntilChanged
import java.util.concurrent.Executor
import java.util.concurrent.Executors

Expand All @@ -19,7 +20,7 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa
}

override fun getTransaction(transactionId: Long): LiveData<HttpTransaction> {
return transcationDao.getById(transactionId)
return transcationDao.getById(transactionId).distinctUntilChanged { old, new -> old.hasTheSameContent(new) }
}

override fun getSortedTransactionTuples(): LiveData<List<HttpTransactionTuple>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import com.chuckerteam.chucker.internal.data.entity.RecordedThrowable
import com.chuckerteam.chucker.internal.data.entity.RecordedThrowableTuple
import com.chuckerteam.chucker.internal.data.room.ChuckerDatabase
import com.chuckerteam.chucker.internal.support.distinctUntilChanged
import java.util.concurrent.Executor
import java.util.concurrent.Executors

Expand All @@ -14,7 +15,7 @@ internal class RecordedThrowableDatabaseRepository(
private val executor: Executor = Executors.newSingleThreadExecutor()

override fun getRecordedThrowable(id: Long): LiveData<RecordedThrowable> {
return database.throwableDao().getById(id)
return database.throwableDao().getById(id).distinctUntilChanged()
}

override fun deleteAllThrowables() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.chuckerteam.chucker.internal.support

import android.annotation.SuppressLint
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import java.util.concurrent.Executor

// Unlike built-in extension operation is performed on a provided thread pool.
// This is needed in our case since we compare requests and responses which can be big
// and result in frame drops.
internal fun <T> LiveData<T>.distinctUntilChanged(
executor: Executor = ioExecutor(),
areEqual: (old: T, new: T) -> Boolean = { old, new -> old == new }
): LiveData<T> {
val distinctMediator = MediatorLiveData<T>()
var old = uninitializedToken
distinctMediator.addSource(this) { new ->
executor.execute {
@Suppress("UNCHECKED_CAST")
if (old === uninitializedToken || !areEqual(old as T, new)) {
old = new
distinctMediator.postValue(new)
}
}
}
return distinctMediator
}

private val uninitializedToken: Any? = Any()

// It is lesser evil than providing a custom executor.
@SuppressLint("RestrictedApi")
private fun ioExecutor() = ArchTaskExecutor.getIOThreadExecutor()
36 changes: 36 additions & 0 deletions library/src/test/java/com/chuckerteam/chucker/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.chuckerteam.chucker

import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.io.File
import okio.Buffer
import okio.Okio
Expand All @@ -9,3 +11,37 @@ fun getResourceFile(file: String): Buffer {
writeAll(Okio.buffer(Okio.source(File("./src/test/resources/$file"))))
}
}

fun <T> LiveData<T>.test(test: LiveDataRecord<T>.() -> Unit) {
val observer = RecordingObserver<T>()
observeForever(observer)
LiveDataRecord(observer).test()
removeObserver(observer)
observer.records.clear()
}

class LiveDataRecord<T> internal constructor(
private val observer: RecordingObserver<T>
) {
fun expectData(): T {
if (observer.records.isEmpty()) {
throw AssertionError("Expected data but was empty.")
}
return observer.records.removeAt(0)
}

fun expectNoData() {
if (observer.records.isNotEmpty()) {
val data = observer.records[0]
throw AssertionError("Expected no data but was $data.")
}
}
}

internal class RecordingObserver<T> : Observer<T> {
val records = mutableListOf<T>()

override fun onChanged(data: T) {
records += data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.chuckerteam.chucker.internal.support

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import com.chuckerteam.chucker.test
import junit.framework.TestCase.assertEquals
import org.junit.Rule
import org.junit.Test

class LiveDataDistinctUntilChangedTest {
@get:Rule val instantExecutorRule = InstantTaskExecutorRule()

@Test
fun initialUpstreamData_isEmittedDownstream() {
val upstream = MutableLiveData<Any?>(null)

upstream.distinctUntilChanged().test {
assertEquals(null, expectData())
}
}

@Test
fun emptyUpstream_isNotEmittedDownstream() {
val upstream = MutableLiveData<Any?>()

upstream.distinctUntilChanged().test {
expectNoData()
}
}

@Test
fun newDistinctData_isEmittedDownstream() {
val upstream = MutableLiveData<Int?>()

upstream.distinctUntilChanged().test {
upstream.value = 1
assertEquals(1, expectData())

upstream.value = 2
assertEquals(2, expectData())

upstream.value = null
assertEquals(null, expectData())

upstream.value = 2
assertEquals(2, expectData())
}
}

@Test
fun newIndistinctData_isNotEmittedDownstream() {
val upstream = MutableLiveData<String?>()

upstream.distinctUntilChanged().test {
upstream.value = null
assertEquals(null, expectData())

upstream.value = null
expectNoData()

upstream.value = ""
assertEquals("", expectData())

upstream.value = ""
expectNoData()
}
}

@Test
fun customFunction_canBeUsedToDistinguishData() {
val upstream = MutableLiveData<Pair<Int, String>>()

upstream.distinctUntilChanged { old, new -> old.first == new.first }.test {
upstream.value = 1 to ""
assertEquals(1 to "", expectData())

upstream.value = 1 to "a"
expectNoData()

upstream.value = 2 to "b"
assertEquals(2 to "b", expectData())

upstream.value = 3 to "b"
assertEquals(3 to "b", expectData())
}
}
}

0 comments on commit 667d668

Please sign in to comment.