From 8b390e2deef6835061280e8202d5301b5f07bd2d Mon Sep 17 00:00:00 2001 From: Csaba Kozak Date: Fri, 8 Nov 2024 15:44:15 +0100 Subject: [PATCH] Add support for List arguments in typesafe navigation Test: ./gradlew navigation:navigation-common:test Test: ./gradlew navigation:navigation-common:cC Test: ./gradlew navigation:navigation-runtime:cC Bug: 375559962 --- .../serialization/RouteFilledTest.kt | 31 +++++++++++++ .../serialization/NavTypeConverter.kt | 45 +++++++++++++++++++ .../serialization/NavArgumentGeneratorTest.kt | 28 ++++++++++++ .../navigation/NavControllerRouteTest.kt | 34 ++++++++++++++ 4 files changed, 138 insertions(+) diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt index 6268f33a53b7d..e4de30a4e15d5 100644 --- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt +++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt @@ -841,6 +841,37 @@ class RouteFilledTest { } assertThatRouteFilledFrom(clazz, listOf(arg)).isEqualTo("$PATH_SERIAL_NAME") } + + @Test + fun encodeEnumList() { + @Serializable + @SerialName(PATH_SERIAL_NAME) + class TestClass(val arg: List) + + val clazz = TestClass(listOf(TestEnum.ONE, TestEnum.TWO)) + val arg = + navArgument("arg") { + type = InternalNavType.EnumListType(TestEnum::class.java) + nullable = true + } + assertThatRouteFilledFrom(clazz, listOf(arg)) + .isEqualTo("$PATH_SERIAL_NAME?arg=ONE&arg=TWO") + } + + @Test + fun encodeEnumListNullable() { + @Serializable + @SerialName(PATH_SERIAL_NAME) + class TestClass(val arg: List?) + + val clazz = TestClass(null) + val arg = + navArgument("arg") { + type = InternalNavType.EnumListType(TestEnum::class.java) + nullable = true + } + assertThatRouteFilledFrom(clazz, listOf(arg)).isEqualTo(PATH_SERIAL_NAME) + } } private fun assertThatRouteFilledFrom( diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt index 444b8ced9e6c6..ef65bac963a60 100644 --- a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt +++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt @@ -101,6 +101,9 @@ internal fun SerialDescriptor.getNavType(): NavType<*> { InternalType.LONG -> NavType.LongListType InternalType.STRING -> NavType.StringListType InternalType.STRING_NULLABLE -> InternalNavType.StringNullableListType + InternalType.ENUM -> + @Suppress("UNCHECKED_CAST") + InternalNavType.EnumListType(getElementDescriptor(0).getClass() as Class>) else -> UNKNOWN } } @@ -471,6 +474,48 @@ internal object InternalNavType { override fun emptyCollection(): List = emptyList() } + class EnumListType>(type: Class): CollectionNavType?>(true) { + private val enumNavType = EnumType(type) + + override val name: String + get() = "List<${enumNavType.name}}>" + + override fun put(bundle: Bundle, key: String, value: List?) { + bundle.putSerializable(key, value?.let { ArrayList(value) }) + } + + @Suppress("DEPRECATION", "UNCHECKED_CAST") + override fun get(bundle: Bundle, key: String): List? = + (bundle[key] as? List?) + + override fun parseValue(value: String): List = + listOf(enumNavType.parseValue(value)) + + override fun parseValue(value: String, previousValue: List?): List? = + previousValue?.plus(parseValue(value)) ?: parseValue(value) + + override fun valueEquals(value: List?, other: List?): Boolean { + val valueArrayList = value?.let { ArrayList(value) } + val otherArrayList = other?.let { ArrayList(other) } + return valueArrayList == otherArrayList + } + + override fun serializeAsValues(value: List?): List = + value?.map { it.toString() } ?: emptyList() + + override fun emptyCollection(): List = emptyList() + + public override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EnumListType<*>) return false + return enumNavType == other.enumNavType + } + + public override fun hashCode(): Int { + return enumNavType.hashCode() + } + } + class EnumNullableType?>(type: Class) : SerializableNullableType(type) { private val type: Class diff --git a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt index 5fc3f778fd1d9..818d0450f2705 100644 --- a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt +++ b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt @@ -652,6 +652,34 @@ class NavArgumentGeneratorTest { assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() } + @Test + fun convertToEnumList() { + @Serializable class TestClass(val arg: List) + + val converted = serializer().generateNavArguments() + val expected = + navArgument("arg") { + type = InternalNavType.EnumListType(TestEnum::class.java) + nullable = false + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + + @Test + fun convertToEnumListNullable() { + @Serializable class TestClass(val arg: List?) + + val converted = serializer().generateNavArguments() + val expected = + navArgument("arg") { + type = InternalNavType.EnumListType(TestEnum::class.java) + nullable = true + } + assertThat(converted).containsExactlyInOrder(expected) + assertThat(converted[0].argument.isDefaultValueUnknown).isFalse() + } + @Test fun convertToParcelable() { @Serializable diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt index 22c047f11eeea..907e6b8ea565a 100644 --- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt +++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt @@ -5250,6 +5250,40 @@ class NavControllerRouteTest { assertThat(route2!!.arg).containsExactly(11E123, 11.11) } + @UiThreadTest + @Test + fun testNavigateWithObjectEnumList() { + @Serializable @SerialName("test") class TestClass(val arg: List) + + val navController = createNavController() + navController.graph = + navController.createGraph(startDestination = TestClass(listOf(TestEnum.ONE, TestEnum.TWO))) { + test() + } + assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}") + val route = navController.currentBackStackEntry?.toRoute() + assertThat(route!!.arg).containsExactly(TestEnum.ONE, TestEnum.TWO) + } + + @UiThreadTest + @Test + fun testNavigateWithObjectNullEnumList() { + @Serializable @SerialName("test") class TestClass(val arg: List? = null) + + val navController = createNavController() + navController.graph = + navController.createGraph(startDestination = TestClass(null)) { test() } + + assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}") + val route = navController.currentBackStackEntry?.toRoute() + assertThat(route!!.arg).isNull() + + navController.navigate(TestClass(listOf(TestEnum.ONE, TestEnum.TWO))) + assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}") + val route2 = navController.currentBackStackEntry?.toRoute() + assertThat(route2!!.arg).containsExactly(TestEnum.ONE, TestEnum.TWO) + } + @UiThreadTest @Test fun testNavigateWithObjectValueClass() {