Created
February 14, 2019 10:10
-
-
Save tieorange/74f4fcdcd1f42d909c6c37cbc2a732f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.westwingnow.android.base | |
import java.lang.reflect.Method | |
import java.lang.reflect.Modifier | |
import java.util.* | |
object StateDiffUtil { | |
private val COMPARABLE_PACKAGES = listOf( | |
"java.lang", | |
"java.util" | |
) | |
private val methodCache = mutableMapOf<Class<*>, List<Method>>() | |
fun calculate(old: Any?, new: Any?): List<Change> { | |
return compare( | |
createMap(old), | |
createMap(new) | |
) | |
} | |
private fun compare( | |
old: Map<String, Any?>?, | |
new: Map<String, Any?>?): List<Change> { | |
val oldKeys = old?.keys.orEmpty() | |
val newKeys = new?.keys.orEmpty() | |
val additions = newKeys | |
.subtract(oldKeys) | |
.map { Change.Addition(it, new?.get(it)) } | |
val deletions = oldKeys | |
.subtract(newKeys) | |
.map { Change.Deletion(it, old?.get(it)) } | |
val modifications = oldKeys | |
.intersect(newKeys) | |
.filter { old?.get(it) != new?.get(it) } | |
.map { | |
val oldValue = old?.get(it) | |
val newValue = new?.get(it) | |
val comparable = oldValue != null && newValue != null | |
&& Map::class.java.isAssignableFrom(oldValue.javaClass) | |
&& Map::class.java.isAssignableFrom(newValue.javaClass) | |
@Suppress("UNCHECKED_CAST") | |
when { | |
comparable -> compare( | |
oldValue as Map<String, Any?>, | |
newValue as Map<String, Any?> | |
) | |
else -> listOf(Change.Modification(it, old?.get(it), new?.get(it))) | |
} | |
} | |
.flatten() | |
return additions + deletions + modifications | |
} | |
private fun createMap(obj: Any?, parentName: String = ""): Map<String, Any?>? { | |
return getProperties(obj) | |
?.map { | |
val name = parentName + it.name | |
.replace(Regex("^(get|is)"), "") // Kotlin property prefix | |
.decapitalize() | |
val value = it.invoke(obj) | |
val comparable = if (value != null) { | |
val type = value.javaClass | |
type.isPrimitive | |
|| type.isArray | |
|| type.isEnum | |
|| type.`package`.name in COMPARABLE_PACKAGES | |
} | |
else true | |
name to if (comparable) value else createMap(value, "$name.") | |
} | |
?.toMap() | |
} | |
private fun getProperties(obj: Any?): List<Method>? { | |
if (obj == null) return null | |
return methodCache[obj.javaClass] ?: obj | |
.javaClass | |
.declaredMethods | |
.filter { Modifier.isPublic(it.modifiers) } | |
.filter { it.parameterTypes.isEmpty() } | |
.filter { it.returnType != Void.TYPE } | |
.filter { it.name.matches(Regex("^(get|is).*+$")) } // Kotlin properties | |
.filter { it.name != "getClass" } | |
.apply { methodCache.put(obj.javaClass, this) } | |
} | |
private fun Any?.isEqualTo(obj: Any?): Boolean { | |
return when { | |
this is Array<*> && obj is Array<*> -> Arrays.deepEquals(this, obj) | |
else -> this == obj | |
} | |
} | |
sealed class Change { | |
class Modification(val name: String, val oldValue: Any?, val newValue: Any?) : Change() { | |
override fun equals(other: Any?): Boolean { | |
if (this === other) return true | |
if (other?.javaClass != javaClass) return false | |
other as Modification | |
if (name != other.name) return false | |
if (!oldValue.isEqualTo(other.oldValue)) return false | |
if (!newValue.isEqualTo(other.newValue)) return false | |
return true | |
} | |
override fun hashCode(): Int { | |
var result = name.hashCode() | |
result = 31 * result + (oldValue?.hashCode() ?: 0) | |
result = 31 * result + (newValue?.hashCode() ?: 0) | |
return result | |
} | |
override fun toString(): String { | |
val sb = StringBuilder() | |
sb.append("\n∆ $name: ") | |
sb.append(if (oldValue is Array<*>) Arrays.toString(oldValue) else "$oldValue") | |
sb.append(" ---->>> ") | |
sb.append(if (newValue is Array<*>) Arrays.toString(newValue) else "$newValue") | |
return sb.toString() | |
} | |
} | |
class Addition(val name: String, val value: Any?) : Change() { | |
override fun equals(other: Any?): Boolean { | |
if (this === other) return true | |
if (other?.javaClass != javaClass) return false | |
other as Addition | |
if (name != other.name) return false | |
if (!value.isEqualTo(other.value)) return false | |
return true | |
} | |
override fun hashCode(): Int { | |
var result = name.hashCode() | |
result = 31 * result + (value?.hashCode() ?: 0) | |
return result | |
} | |
override fun toString(): String { | |
val sb = StringBuilder() | |
sb.append("ADDED: \t$name: ") | |
sb.append(if (value is Array<*>) Arrays.toString(value) else "$value") | |
return sb.toString() | |
} | |
} | |
class Deletion(val name: String, val value: Any?) : Change() { | |
override fun equals(other: Any?): Boolean { | |
if (this === other) return true | |
if (other?.javaClass != javaClass) return false | |
other as Deletion | |
if (name != other.name) return false | |
if (!value.isEqualTo(other.value)) return false | |
return true | |
} | |
override fun hashCode(): Int { | |
var result = name.hashCode() | |
result = 31 * result + (value?.hashCode() ?: 0) | |
return result | |
} | |
override fun toString(): String { | |
val sb = StringBuilder() | |
sb.append("REMOVED: \t$name: ") | |
sb.append(if (value is Array<*>) Arrays.toString(value) else "$value") | |
return sb.toString() | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment