From f7502326e7fc9f7c65fd24b54d8e017e52e3e48f Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Mon, 28 Nov 2022 18:03:45 +0900 Subject: [PATCH 1/6] Rename PropertyDelegator to NullablePropertyDelegates --- savedstate-ktx/src/androidTest/AndroidManifest.xml | 2 +- ...ropertyDelegatorTest.kt => NullablePropertyDelegatorTest.kt} | 0 .../{PropertyDelegator.kt => NullablePropertyDelegates.kt} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename savedstate-ktx/src/androidTest/java/io/woong/savedstate/{PropertyDelegatorTest.kt => NullablePropertyDelegatorTest.kt} (100%) rename savedstate-ktx/src/main/java/io/woong/savedstate/{PropertyDelegator.kt => NullablePropertyDelegates.kt} (100%) diff --git a/savedstate-ktx/src/androidTest/AndroidManifest.xml b/savedstate-ktx/src/androidTest/AndroidManifest.xml index 8ef0325..6600be9 100644 --- a/savedstate-ktx/src/androidTest/AndroidManifest.xml +++ b/savedstate-ktx/src/androidTest/AndroidManifest.xml @@ -3,6 +3,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="io.woong.savedstate.test"> - + diff --git a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/PropertyDelegatorTest.kt b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt similarity index 100% rename from savedstate-ktx/src/androidTest/java/io/woong/savedstate/PropertyDelegatorTest.kt rename to savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/PropertyDelegator.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt similarity index 100% rename from savedstate-ktx/src/main/java/io/woong/savedstate/PropertyDelegator.kt rename to savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt From 4aa4aeb2419c1d330eeadc0e0780c17e107855bf Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Mon, 28 Nov 2022 21:52:01 +0900 Subject: [PATCH 2/6] Improve documents of getValue and setValue --- .../savedstate/NullablePropertyDelegates.kt | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt index 10507fd..6596a13 100644 --- a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt @@ -4,41 +4,45 @@ import androidx.lifecycle.SavedStateHandle import kotlin.reflect.KProperty /** - * Delegates [SavedStateHandle.getValue] operation to property. - * The delegated property returns a nullable value that stored in [SavedStateHandle] with a key. - * The key of the property is the name of the property. + * Returns a property delegate for reading a value from [SavedStateHandle]. + * + * Reading the property equals to read value from [SavedStateHandle]. + * The key to read value from handle is its property name. + * + * To define property delegate, use `by` keyword of Kotlin: * * ``` * class ExampleViewModel(savedStateHandle: SavedStateHandle) { - * // Basic way to get value from saved state handle. - * val foo: String? - * get() = savedStateHandle["foo"] - * - * // New way using this library. - * val bar: String? by savedStateHandle + * // This code equals to below code: + * // val foo: String? + * // get() = savedStateHandle["foo"] + * val foo: String? by savedStateHandle * } * ``` + * + * The type of property must nullable because [SavedStateHandle] can returns `null`. */ public operator fun SavedStateHandle.getValue(self: Any?, property: KProperty<*>): T? { return this[property.name] } /** - * Delegates [SavedStateHandle.setValue] operation to property. - * The delegated property takes a nullable value and stores in [SavedStateHandle] with a key. - * The key of the property is the name of the property. + * Returns a property delegate for writing a value to [SavedStateHandle]. + * + * Writing the property equals to write value to [SavedStateHandle]. + * The key to write value to handle is its property name. * * ``` * class ExampleViewModel(savedStateHandle: SavedStateHandle) { - * // Basic way to get/set value from saved state handle. - * var foo: String? - * get() = savedStateHandle["foo"] - * set(value) { savedStateHandle["foo"] = value } - * - * // New way using this library. + * // This code equals to below code: + * // var foo: String? + * // get() = savedStateHandle["foo"] + * // set(value) { savedStateHandle["foo"] = value } * var foo: String? by savedStateHandle * } * ``` + * + * The type of property must nullable because [SavedStateHandle] can store `null`. */ public operator fun SavedStateHandle.setValue(self: Any?, property: KProperty<*>, value: T?) { this[property.name] = value From 004a6a85bb8fc3607604bf7e59f6a78d8e08d52c Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Mon, 28 Nov 2022 21:59:02 +0900 Subject: [PATCH 3/6] Add `SavedStateHandle.nullable` function --- .../savedstate/NullablePropertyDelegates.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt index 6596a13..a1f12eb 100644 --- a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt @@ -47,3 +47,58 @@ public operator fun SavedStateHandle.getValue(self: Any?, property: KPropert public operator fun SavedStateHandle.setValue(self: Any?, property: KProperty<*>, value: T?) { this[property.name] = value } + +/** + * Returns a property delegate for reading and writing a value into [SavedStateHandle] + * with initial value. + * + * Reading the property equals to read value from [SavedStateHandle] + * and writing equals to write value to [SavedStateHandle]. + * + * To define initialized property delegate, use `by` keyword of Kotlin: + * + * ``` + * class ExampleViewModel(savedStateHandle: SavedStateHandle) { + * // This code equals to below code: + * // var foo: String? + * // get() = savedStateHandle["foo"] + * // set(value) { savedStateHandle["foo"] = value } + * // + * // init { + * // savedStateHandle["foo"] = "init" + * // } + * var foo: String? by savedStateHandle.nullable("init") + * } + * ``` + * + * @param initialValue The initial value of this property. + */ +public fun SavedStateHandle.nullable(initialValue: T?): NullablePropertyDelegateProvider { + return NullablePropertyDelegateProvider(savedStateHandle = this, initialValue) +} + +public class NullablePropertyDelegateProvider( + private val savedStateHandle: SavedStateHandle, + private val initialValue: T? +) { + public operator fun provideDelegate(self: Any?, property: KProperty<*>): NullablePropertyDelegate { + val key = property.name + if (!savedStateHandle.contains(key)) { + savedStateHandle[key] = initialValue + } + return NullablePropertyDelegate(savedStateHandle, key) + } +} + +public class NullablePropertyDelegate( + private val savedStateHandle: SavedStateHandle, + private val key: String +) { + public operator fun getValue(self: Any?, property: KProperty<*>): T? { + return savedStateHandle[key] + } + + public operator fun setValue(self: Any?, property: KProperty<*>, value: T?) { + savedStateHandle[key] = value + } +} From 40475f82baa993252f2470862df0b86f46cd54a1 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Tue, 29 Nov 2022 20:36:40 +0900 Subject: [PATCH 4/6] Improve comments --- .../savedstate/NullablePropertyDelegates.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt index a1f12eb..6f1da2b 100644 --- a/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/NullablePropertyDelegates.kt @@ -9,7 +9,8 @@ import kotlin.reflect.KProperty * Reading the property equals to read value from [SavedStateHandle]. * The key to read value from handle is its property name. * - * To define property delegate, use `by` keyword of Kotlin: + * To define property delegate, use `by` keyword of Kotlin. + * The property must nullable type. * * ``` * class ExampleViewModel(savedStateHandle: SavedStateHandle) { @@ -19,8 +20,6 @@ import kotlin.reflect.KProperty * val foo: String? by savedStateHandle * } * ``` - * - * The type of property must nullable because [SavedStateHandle] can returns `null`. */ public operator fun SavedStateHandle.getValue(self: Any?, property: KProperty<*>): T? { return this[property.name] @@ -32,6 +31,9 @@ public operator fun SavedStateHandle.getValue(self: Any?, property: KPropert * Writing the property equals to write value to [SavedStateHandle]. * The key to write value to handle is its property name. * + * To define property delegate, use `by` keyword of Kotlin. + * The property must nullable type. + * * ``` * class ExampleViewModel(savedStateHandle: SavedStateHandle) { * // This code equals to below code: @@ -49,13 +51,14 @@ public operator fun SavedStateHandle.setValue(self: Any?, property: KPropert } /** - * Returns a property delegate for reading and writing a value into [SavedStateHandle] + * Returns a property delegate for reading and writing a nullable value into [SavedStateHandle] * with initial value. * * Reading the property equals to read value from [SavedStateHandle] * and writing equals to write value to [SavedStateHandle]. * - * To define initialized property delegate, use `by` keyword of Kotlin: + * To define initialized property delegate, use `by` keyword of Kotlin. + * The property must nullable type. * * ``` * class ExampleViewModel(savedStateHandle: SavedStateHandle) { @@ -65,7 +68,9 @@ public operator fun SavedStateHandle.setValue(self: Any?, property: KPropert * // set(value) { savedStateHandle["foo"] = value } * // * // init { - * // savedStateHandle["foo"] = "init" + * // if (!savedStateHandle.contains("foo")) { + * // savedStateHandle["foo"] = "init" + * // } * // } * var foo: String? by savedStateHandle.nullable("init") * } From 65de75aefe3a6d8155e4279c62ff76490ff14d91 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Tue, 29 Nov 2022 20:58:16 +0900 Subject: [PATCH 5/6] Rewrite test code --- .../NullablePropertyDelegatorTest.kt | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt index 3411330..e26993e 100644 --- a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt +++ b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NullablePropertyDelegatorTest.kt @@ -1,7 +1,6 @@ package io.woong.savedstate import android.content.Context -import android.content.Intent import androidx.activity.ComponentActivity import androidx.activity.viewModels import androidx.lifecycle.SavedStateHandle @@ -15,7 +14,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -public class PropertyDelegatorTest { +public class NullablePropertyDelegatorTest { private lateinit var context: Context @Before @@ -24,61 +23,51 @@ public class PropertyDelegatorTest { } @Test - public fun testReadingAndWriting() { + public fun simplestDelegate() { val scenario = launchActivity() scenario.onActivity { activity -> val viewModel = activity.viewModel - assertThat(viewModel.string).isNull() - assertThat(viewModel.doubleArray).isNull() - assertThat(viewModel.integerList).isNull() + assertThat(viewModel.simpleValue).isNull() + assertThat(viewModel.simpleVariable).isNull() - viewModel.string = "test" - viewModel.doubleArray = doubleArrayOf(0.1, 0.2, 0.3) - viewModel.doubleArray?.set(0, -0.1) - viewModel.integerList = listOf(1, 2, 3) - assertThat(viewModel.string).isEqualTo("test") - assertThat(viewModel.doubleArray) - .usingExactEquality() - .containsExactlyElementsIn(arrayOf(-0.1, 0.2, 0.3)) - assertThat(viewModel.integerList).containsExactlyElementsIn(listOf(1, 2, 3)) + viewModel.simpleVariable = "test" + assertThat(viewModel.simpleVariable).isEqualTo("test") } } @Test - public fun testAutomaticSettingFromBundle() { - val intent = Intent(context, TestActivity::class.java) - intent.putExtra("string", "test") - intent.putExtra("doubleArray", doubleArrayOf(0.9, 0.8, 0.7)) - intent.putExtra("integerList", arrayListOf(9, 8, 7)) - - val scenario = launchActivity(intent) + public fun nullableWithInitialValue() { + val scenario = launchActivity() scenario.onActivity { activity -> val viewModel = activity.viewModel - assertThat(viewModel.string).isEqualTo("test") - assertThat(viewModel.doubleArray) - .usingExactEquality() - .containsExactlyElementsIn(arrayOf(0.9, 0.8, 0.7)) - assertThat(viewModel.integerList).containsExactlyElementsIn(listOf(9, 8, 7)) + assertThat(viewModel.initializedValue).isEqualTo("a") + assertThat(viewModel.initializedVariable).isEqualTo("b") + + viewModel.initializedVariable = "c" + assertThat(viewModel.initializedVariable).isEqualTo("c") } } @Test - public fun testSavedStateAfterRecreation() { + public fun stateSaving() { val scenario = launchActivity() scenario.onActivity { activity -> val viewModel = activity.viewModel - viewModel.string = "test" - viewModel.doubleArray = doubleArrayOf(-0.5, 0.0, 0.5) - viewModel.integerList = listOf(2, 4, 6, 8) + viewModel.simpleVariable = "aaa" + viewModel.initializedVariable = "bbb" + + assertThat(viewModel.simpleValue).isNull() + assertThat(viewModel.simpleVariable).isEqualTo("aaa") + assertThat(viewModel.initializedValue).isEqualTo("a") + assertThat(viewModel.initializedVariable).isEqualTo("bbb") } scenario.recreate() scenario.onActivity { activity -> val viewModel = activity.viewModel - assertThat(viewModel.string).isEqualTo("test") - assertThat(viewModel.doubleArray) - .usingExactEquality() - .containsExactlyElementsIn(arrayOf(-0.5, 0.0, 0.5)) - assertThat(viewModel.integerList).containsExactlyElementsIn(listOf(2, 4, 6, 8)) + assertThat(viewModel.simpleValue).isNull() + assertThat(viewModel.simpleVariable).isEqualTo("aaa") + assertThat(viewModel.initializedValue).isEqualTo("a") + assertThat(viewModel.initializedVariable).isEqualTo("bbb") } } @@ -87,8 +76,10 @@ public class PropertyDelegatorTest { } public class TestViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { - public var string: String? by savedStateHandle - public var doubleArray: DoubleArray? by savedStateHandle - public var integerList: List? by savedStateHandle + public val simpleValue: String? by savedStateHandle + public var simpleVariable: String? by savedStateHandle + + public val initializedValue: String? by savedStateHandle.nullable("a") + public var initializedVariable: String? by savedStateHandle.nullable("b") } } From 56e0e626cd613a9da5f7eb58b6eff33a38a8d003 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Tue, 29 Nov 2022 21:22:01 +0900 Subject: [PATCH 6/6] Add `SavedStateHandle.notNull` function --- .../src/androidTest/AndroidManifest.xml | 1 + .../NotNullPropertyDelegatorTest.kt | 64 +++++++++++++++++++ .../savedstate/NotNullPropertyDelegates.kt | 62 ++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 savedstate-ktx/src/androidTest/java/io/woong/savedstate/NotNullPropertyDelegatorTest.kt create mode 100644 savedstate-ktx/src/main/java/io/woong/savedstate/NotNullPropertyDelegates.kt diff --git a/savedstate-ktx/src/androidTest/AndroidManifest.xml b/savedstate-ktx/src/androidTest/AndroidManifest.xml index 6600be9..2771fb0 100644 --- a/savedstate-ktx/src/androidTest/AndroidManifest.xml +++ b/savedstate-ktx/src/androidTest/AndroidManifest.xml @@ -4,5 +4,6 @@ package="io.woong.savedstate.test"> + diff --git a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NotNullPropertyDelegatorTest.kt b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NotNullPropertyDelegatorTest.kt new file mode 100644 index 0000000..8e721c9 --- /dev/null +++ b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/NotNullPropertyDelegatorTest.kt @@ -0,0 +1,64 @@ +package io.woong.savedstate + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.launchActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +public class NotNullPropertyDelegatorTest { + private lateinit var context: Context + + @Before + public fun init() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + public fun notNullWithInitialValue() { + val scenario = launchActivity() + scenario.onActivity { activity -> + val viewModel = activity.viewModel + assertThat(viewModel.notNullValue).isEqualTo("a") + assertThat(viewModel.notNullVariable).isEqualTo("b") + + viewModel.notNullVariable = "c" + assertThat(viewModel.notNullVariable).isEqualTo("c") + } + } + + @Test + public fun stateSaving() { + val scenario = launchActivity() + scenario.onActivity { activity -> + val viewModel = activity.viewModel + viewModel.notNullVariable = "bbb" + + assertThat(viewModel.notNullValue).isEqualTo("a") + assertThat(viewModel.notNullVariable).isEqualTo("bbb") + } + scenario.recreate() + scenario.onActivity { activity -> + val viewModel = activity.viewModel + assertThat(viewModel.notNullValue).isEqualTo("a") + assertThat(viewModel.notNullVariable).isEqualTo("bbb") + } + } + + public class TestActivity : ComponentActivity() { + public val viewModel: TestViewModel by viewModels() + } + + public class TestViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { + public val notNullValue: String by savedStateHandle.notNull("a") + public var notNullVariable: String by savedStateHandle.notNull("b") + } +} diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/NotNullPropertyDelegates.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/NotNullPropertyDelegates.kt new file mode 100644 index 0000000..77acf40 --- /dev/null +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/NotNullPropertyDelegates.kt @@ -0,0 +1,62 @@ +package io.woong.savedstate + +import androidx.lifecycle.SavedStateHandle +import kotlin.reflect.KProperty + +/** + * Returns a property delegate for reading and writing a not-null value into [SavedStateHandle] + * with initial value. + * + * Reading the property equals to read value from [SavedStateHandle] + * and writing equals to write value to [SavedStateHandle]. + * + * To define not-null property delegate, use `by` keyword of Kotlin: + * The property must not-null type. + * + * ``` + * class ExampleViewModel(savedStateHandle: SavedStateHandle) { + * // This code equals to below code: + * // var foo: String + * // get() = savedStateHandle["foo"]!! + * // set(value) { savedStateHandle["foo"] = value } + * // + * // init { + * // if (!savedStateHandle.contains("foo")) { + * // savedStateHandle["foo"] = "init" + * // } + * // } + * var foo: String by savedStateHandle.notNull("init") + * } + * ``` + * + * @param initialValue The initial value of this property. + */ +public fun SavedStateHandle.notNull(initialValue: T): NotNullPropertyDelegateProvider { + return NotNullPropertyDelegateProvider(savedStateHandle = this, initialValue) +} + +public class NotNullPropertyDelegateProvider( + private val savedStateHandle: SavedStateHandle, + private val initialValue: T +) { + public operator fun provideDelegate(self: Any?, property: KProperty<*>): NotNullPropertyDelegate { + val key = property.name + if (!savedStateHandle.contains(key)) { + savedStateHandle[key] = initialValue + } + return NotNullPropertyDelegate(savedStateHandle, key) + } +} + +public class NotNullPropertyDelegate( + private val savedStateHandle: SavedStateHandle, + private val key: String +) { + public operator fun getValue(self: Any?, property: KProperty<*>): T { + return savedStateHandle[key]!! + } + + public operator fun setValue(self: Any?, property: KProperty<*>, value: T) { + savedStateHandle[key] = value + } +}