Merge "Add `contentDeepEquals` to Saved State" into androidx-main
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index fe1c8f0..eae7dfc 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -13,6 +13,7 @@
@kotlin.jvm.JvmInline public final value class SavedStateReader {
ctor public SavedStateReader(android.os.Bundle source);
method public inline operator boolean contains(String key);
+ method public boolean contentDeepEquals(android.os.Bundle other);
method public inline boolean getBoolean(String key);
method public inline boolean getBooleanOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
method public inline double getDouble(String key);
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index 47dcf78..32f0d13 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -13,6 +13,7 @@
@kotlin.jvm.JvmInline public final value class SavedStateReader {
ctor public SavedStateReader(android.os.Bundle source);
method public inline operator boolean contains(String key);
+ method public boolean contentDeepEquals(android.os.Bundle other);
method public inline boolean getBoolean(String key);
method public inline boolean getBooleanOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
method public inline double getDouble(String key);
@@ -45,6 +46,10 @@
property public final android.os.Bundle source;
}
+ public final class SavedStateReader_androidKt {
+ method @kotlin.PublishedApi internal static boolean contentDeepEquals(android.os.Bundle, android.os.Bundle other);
+ }
+
public final class SavedStateRegistry {
method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String key);
method public androidx.savedstate.SavedStateRegistry.SavedStateProvider? getSavedStateProvider(String key);
diff --git a/savedstate/savedstate/bcv/native/current.txt b/savedstate/savedstate/bcv/native/current.txt
index 1a70510..4625c67 100644
--- a/savedstate/savedstate/bcv/native/current.txt
+++ b/savedstate/savedstate/bcv/native/current.txt
@@ -51,6 +51,7 @@
final val source // androidx.savedstate/SavedStateReader.source|{}source[0]
final fun <get-source>(): androidx.savedstate/SavedState // androidx.savedstate/SavedStateReader.source.<get-source>|<get-source>(){}[0]
+ final fun contentDeepEquals(androidx.savedstate/SavedState): kotlin/Boolean // androidx.savedstate/SavedStateReader.contentDeepEquals|contentDeepEquals(androidx.savedstate.SavedState){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.savedstate/SavedStateReader.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // androidx.savedstate/SavedStateReader.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // androidx.savedstate/SavedStateReader.toString|toString(){}[0]
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt
index c17f0fa..120eff2 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt
@@ -171,6 +171,8 @@
actual inline operator fun contains(key: String): Boolean = source.containsKey(key)
+ actual fun contentDeepEquals(other: SavedState): Boolean = source.contentDeepEquals(other)
+
@PublishedApi
internal inline fun <reified T> getSingleResultOrThrow(
key: String,
@@ -221,3 +223,23 @@
defaultValue = { defaultValue() },
)
}
+
+@PublishedApi
+internal fun SavedState.contentDeepEquals(other: SavedState): Boolean {
+ if (this === other) return true
+ if (this.size() != other.size()) return false
+
+ for (k in this.keySet()) {
+ @Suppress("DEPRECATION") val v1 = this[k]
+ @Suppress("DEPRECATION") val v2 = other[k]
+
+ when {
+ v1 === v2 -> continue
+ v1 == null || v2 == null -> return false
+ v1 is SavedState && v2 is SavedState -> if (!v1.contentDeepEquals(v2)) return false
+ v1 is Array<*> && v2 is Array<*> -> if (!v1.contentDeepEquals(v2)) return false
+ else -> if (v1 != v2) return false
+ }
+ }
+ return true
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt
index e3f37ac..611d5d8 100644
--- a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt
@@ -245,4 +245,16 @@
* @return `true` if the [SavedState] contains the [key], `false` otherwise.
*/
public inline operator fun contains(key: String): Boolean
+
+ /**
+ * Checks if the two specified [SavedState] are *deeply* equal to one another.
+ *
+ * Two [SavedState] are considered deeply equal if they have the same size, and elements at
+ * corresponding keys are deeply equal. That is, if two corresponding elements are nested
+ * [SavedState], they are also compared deeply.
+ *
+ * @param other the object to compare deeply with this.
+ * @return `true` if the two are deeply equal, `false` otherwise.
+ */
+ public fun contentDeepEquals(other: SavedState): Boolean
}
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt
index b40f557..715575f 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt
@@ -95,6 +95,89 @@
assertThat(underTest.read { isEmpty() }).isTrue()
}
+ @Test
+ fun contentDeepEquals_withEqualContent_returnsTrue() {
+ val sharedState = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+ val state1 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+ val state2 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+
+ val contentDeepEquals = state1.read { contentDeepEquals(state2) }
+
+ assertThat(contentDeepEquals).isTrue()
+ }
+
+ @Test
+ fun contentDeepEquals_withMissingKey_returnsFalse() {
+ val sharedState = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+ val state1 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+ val state2 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+
+ val contentDeepEquals = state1.read { contentDeepEquals(state2) }
+
+ assertThat(contentDeepEquals).isFalse()
+ }
+
+ @Test
+ fun contentDeepEquals_withDifferentContent_returnsFalse() {
+ val sharedState = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+ val state1 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+ val state2 = savedState {
+ putFloat(KEY_1, Float.MAX_VALUE)
+ putFloat(KEY_2, Float.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+
+ val contentDeepEquals = state1.read { contentDeepEquals(state2) }
+
+ assertThat(contentDeepEquals).isFalse()
+ }
+
+ @Test
+ fun contentDeepEquals_withEmptyContent_returnsFalse() {
+ val sharedState = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+ val state1 = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ putSavedState(KEY_3, sharedState)
+ }
+ val state2 = savedState()
+
+ val contentDeepEquals = state1.read { contentDeepEquals(state2) }
+
+ assertThat(contentDeepEquals).isFalse()
+ }
+
// region getters and setters
@Test
fun getBoolean_whenSet_returns() {
@@ -436,6 +519,7 @@
private companion object {
const val KEY_1 = "KEY_1"
const val KEY_2 = "KEY_2"
+ const val KEY_3 = "KEY_3"
const val STRING_VALUE = "string-value"
val LIST_INT_VALUE = List(size = 5) { idx -> idx }
val LIST_STRING_VALUE = List(size = 5) { idx -> "index=$idx" }
diff --git a/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt
index 01e3cd9..a7aaf90 100644
--- a/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt
+++ b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt
@@ -163,4 +163,9 @@
currentValue = { currentValue() },
defaultValue = { defaultValue() },
)
+
+ actual fun contentDeepEquals(other: SavedState): Boolean {
+ // Map implements `equals` as a content deep, there is no need to do anything else.
+ return source.map == other.map
+ }
}