Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not refresh transaction when it is not being affected. #246

Merged
merged 4 commits into from
Feb 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
}
}
}