Skip to content

Commit 4cc61bc

Browse files
simonschillerdanysantiago
authored andcommitted
[GH] Add fromSavedState handle method to generated nav args
## Proposed Changes - Generate a `fromSavedState` method for all nav args classes, similar to the `fromBundle` one. ## Testing Test: Adapted existing tests to also include the new method ## Issues Fixed Fixes: 136967621 This is an imported pull request from #122. Resolves #122 Github-Pr-Head-Sha: d235f9a GitOrigin-RevId: aa9caad Change-Id: I74ef156e29b8955ef2422f59c8cc69c86424a43d
1 parent 8788b9e commit 4cc61bc

15 files changed

Lines changed: 376 additions & 35 deletions

File tree

‎buildSrc/src/main/kotlin/androidx/build/SdkResourceGenerator.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ abstract class SdkResourceGenerator : DefaultTask() {
5353
val agpDependency: String = AGP_LATEST
5454

5555
@get:Input
56-
val navigationCommon: String = "androidx.navigation:navigation-common:2.0.0"
56+
val navigationRuntime: String = "androidx.navigation:navigation-runtime:2.2.0"
5757

5858
@get:Input
5959
val kotlinStdlib: String = KOTLIN_STDLIB
@@ -77,7 +77,7 @@ abstract class SdkResourceGenerator : DefaultTask() {
7777
writer.write("minSdkVersion=$minSdkVersion\n")
7878
writer.write("debugKeystore=${debugKeystore.get().asFile.canonicalPath}\n")
7979
writer.write("agpDependency=$agpDependency\n")
80-
writer.write("navigationCommon=$navigationCommon\n")
80+
writer.write("navigationRuntime=$navigationRuntime\n")
8181
writer.write("kotlinStdlib=$kotlinStdlib\n")
8282
writer.write("gradleVersion=$gradleVersion\n")
8383
writer.write("rootProjectPath=$rootProjectPath\n")

‎lifecycle/lifecycle-viewmodel-savedstate/build.gradle‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ dependencies {
5858
androidTestImplementation(ANDROIDX_TEST_RULES)
5959
}
6060

61+
//used by testImplementation safe-args-generator
62+
android.libraryVariants.all { variant ->
63+
def name = variant.name
64+
def suffix = name.capitalize()
65+
project.tasks.register("jar${suffix}", Copy).configure {
66+
dependsOn ("assemble$suffix")
67+
from(zipTree("${project.buildDir}/outputs/aar/lifecycle-viewmodel-savedstate-${name}.aar")) {
68+
include "classes.jar"
69+
}
70+
destinationDir new File(project.buildDir, "libJar")
71+
}
72+
}
73+
6174
androidx {
6275
name = "Android Lifecycle ViewModel with SavedState"
6376
publish = Publish.SNAPSHOT_AND_RELEASE

‎navigation/navigation-safe-args-generator/build.gradle‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ dependencies {
4444
dir: "${new File(project(":navigation:navigation-common").buildDir, "libJar")}",
4545
include : "*.jar"
4646
)
47+
testImplementation fileTree(
48+
dir: "${new File(project(":lifecycle:lifecycle-viewmodel-savedstate").buildDir, "libJar")}",
49+
include : "*.jar"
50+
)
4751
}
4852

4953
tasks.findByName("test").doFirst {
@@ -55,6 +59,7 @@ tasks.findByName("test").doFirst {
5559
}
5660

5761
tasks.findByName("compileKotlin").dependsOn(":navigation:navigation-common:jarDebug")
62+
tasks.findByName("compileKotlin").dependsOn(":lifecycle:lifecycle-viewmodel-savedstate:jarDebug")
5863

5964
androidx {
6065
name = 'Android Navigation TypeSafe Arguments Generator'

‎navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/java/JavaNavWriter.kt‎

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,7 @@ class JavaNavWriter(private val useAndroidX: Boolean = true) : NavWriter<JavaCod
198198
?: throw IllegalStateException("Destination with arguments must have name")
199199
val className = ClassName.get(destName.packageName(), "${destName.simpleName()}Args")
200200
val args = destination.args
201-
val specs =
202-
ClassWithArgsSpecs(args, annotations)
201+
val specs = ClassWithArgsSpecs(args, annotations)
203202

204203
val fromBundleMethod = MethodSpec.methodBuilder("fromBundle").apply {
205204
addAnnotation(annotations.NONNULL_CLASSNAME)
@@ -216,34 +215,30 @@ class JavaNavWriter(private val useAndroidX: Boolean = true) : NavWriter<JavaCod
216215
addStatement("$T $N = new $T()", className, result, className)
217216
addStatement("$N.setClassLoader($T.class.getClassLoader())", bundle, className)
218217
args.forEach { arg ->
219-
beginControlFlow("if ($N.containsKey($S))", bundle, arg.name).apply {
220-
addStatement("$T $N", arg.type.typeName(), arg.sanitizedName)
218+
addReadSingleArgBlock("containsKey", bundle, result, arg, specs) {
221219
arg.type.addBundleGetStatement(this, arg, arg.sanitizedName, bundle)
222-
addNullCheck(arg, arg.sanitizedName)
223-
addStatement(
224-
"$result.$N.put($S, $N)",
225-
specs.hashMapFieldSpec,
226-
arg.name,
227-
arg.sanitizedName
228-
)
229220
}
230-
if (arg.defaultValue == null) {
231-
nextControlFlow("else")
232-
addStatement(
233-
"throw new $T($S)", IllegalArgumentException::class.java,
234-
"Required argument \"${arg.name}\" is missing and does " +
235-
"not have an android:defaultValue"
236-
)
237-
} else {
238-
nextControlFlow("else")
239-
addStatement(
240-
"$result.$N.put($S, $L)",
241-
specs.hashMapFieldSpec,
242-
arg.name,
243-
arg.defaultValue.write()
244-
)
221+
}
222+
addStatement("return $N", result)
223+
}.build()
224+
225+
val fromSavedStateHandleMethod = MethodSpec.methodBuilder("fromSavedStateHandle").apply {
226+
addAnnotation(annotations.NONNULL_CLASSNAME)
227+
addModifiers(Modifier.PUBLIC, Modifier.STATIC)
228+
addAnnotation(specs.suppressAnnotationSpec)
229+
val savedStateHandle = "savedStateHandle"
230+
addParameter(
231+
ParameterSpec.builder(SAVED_STATE_HANDLE_CLASSNAME, savedStateHandle)
232+
.addAnnotation(specs.androidAnnotations.NONNULL_CLASSNAME)
233+
.build()
234+
)
235+
returns(className)
236+
val result = "__result"
237+
addStatement("$T $N = new $T()", className, result, className)
238+
args.forEach { arg ->
239+
addReadSingleArgBlock("contains", savedStateHandle, result, arg, specs) {
240+
addStatement("$N = $N.get($S)", arg.sanitizedName, savedStateHandle, arg.name)
245241
}
246-
endControlFlow()
247242
}
248243
addStatement("return $N", result)
249244
}.build()
@@ -298,6 +293,7 @@ class JavaNavWriter(private val useAndroidX: Boolean = true) : NavWriter<JavaCod
298293
.addMethod(constructor)
299294
.addMethod(fromMapConstructor)
300295
.addMethod(fromBundleMethod)
296+
.addMethod(fromSavedStateHandleMethod)
301297
.addMethods(specs.getters())
302298
.addMethod(specs.toBundleMethod("toBundle"))
303299
.addMethod(specs.equalsMethod(className))
@@ -308,6 +304,42 @@ class JavaNavWriter(private val useAndroidX: Boolean = true) : NavWriter<JavaCod
308304

309305
return JavaFile.builder(className.packageName(), typeSpec).build().toCodeFile()
310306
}
307+
308+
private fun MethodSpec.Builder.addReadSingleArgBlock(
309+
containsMethodName: String,
310+
sourceVariableName: String,
311+
targetVariableName: String,
312+
arg: Argument,
313+
specs: ClassWithArgsSpecs,
314+
addGetStatement: MethodSpec.Builder.() -> Unit
315+
) {
316+
beginControlFlow("if ($N.$containsMethodName($S))", sourceVariableName, arg.name)
317+
addStatement("$T $N", arg.type.typeName(), arg.sanitizedName)
318+
addGetStatement()
319+
addNullCheck(arg, arg.sanitizedName)
320+
addStatement(
321+
"$targetVariableName.$N.put($S, $N)",
322+
specs.hashMapFieldSpec,
323+
arg.name,
324+
arg.sanitizedName
325+
)
326+
nextControlFlow("else")
327+
if (arg.defaultValue == null) {
328+
addStatement(
329+
"throw new $T($S)", IllegalArgumentException::class.java,
330+
"Required argument \"${arg.name}\" is missing and does not have an " +
331+
"android:defaultValue"
332+
)
333+
} else {
334+
addStatement(
335+
"$targetVariableName.$N.put($S, $L)",
336+
specs.hashMapFieldSpec,
337+
arg.name,
338+
arg.defaultValue.write()
339+
)
340+
}
341+
endControlFlow()
342+
}
311343
}
312344

313345
private class ClassWithArgsSpecs(

‎navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/java/JavaTypes.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ internal val ACTION_ONLY_NAV_DIRECTION_CLASSNAME: ClassName =
5656
internal val NAV_ARGS_CLASSNAME: ClassName = ClassName.get("androidx.navigation", "NavArgs")
5757
internal val HASHMAP_CLASSNAME: ClassName = ClassName.get("java.util", "HashMap")
5858
internal val BUNDLE_CLASSNAME: ClassName = ClassName.get("android.os", "Bundle")
59+
internal val SAVED_STATE_HANDLE_CLASSNAME: ClassName =
60+
ClassName.get("androidx.lifecycle", "SavedStateHandle")
5961
internal val PARCELABLE_CLASSNAME = ClassName.get("android.os", "Parcelable")
6062
internal val SERIALIZABLE_CLASSNAME = ClassName.get("java.io", "Serializable")
6163
internal val SYSTEM_CLASSNAME = ClassName.get("java.lang", "System")

‎navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinNavWriter.kt‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,52 @@ class KotlinNavWriter(private val useAndroidX: Boolean = true) : NavWriter<Kotli
267267
addStatement("return·%T(${tempVariables.joinToString(", ") { it }})", className)
268268
}.build()
269269

270+
val fromSavedStateHandleFunSpec = FunSpec.builder("fromSavedStateHandle").apply {
271+
addAnnotation(JvmStatic::class)
272+
returns(className)
273+
val savedStateParamName = "savedStateHandle"
274+
addParameter(savedStateParamName, SAVED_STATE_HANDLE_CLASSNAME)
275+
val tempVariables = destination.args.map { arg ->
276+
val tempVal = "__${arg.sanitizedName}"
277+
addStatement(
278+
"val %L : %T",
279+
tempVal,
280+
arg.type.typeName().copy(nullable = true)
281+
)
282+
beginControlFlow("if (%L.contains(%S))", savedStateParamName, arg.name)
283+
addStatement("%L = %L[%S]", tempVal, savedStateParamName, arg.name)
284+
if (!arg.isNullable) {
285+
beginControlFlow("if (%L == null)", tempVal)
286+
val errorMessage = if (arg.type.allowsNullable()) {
287+
"Argument \"${arg.name}\" is marked as non-null but was passed a null value"
288+
} else {
289+
"Argument \"${arg.name}\" of type ${arg.type} does not support null values"
290+
}
291+
addStatement(
292+
"throw·%T(%S)",
293+
IllegalArgumentException::class.asTypeName(),
294+
errorMessage
295+
)
296+
endControlFlow()
297+
}
298+
nextControlFlow("else")
299+
val defaultValue = arg.defaultValue
300+
if (defaultValue != null) {
301+
addStatement("%L = %L", tempVal, arg.defaultValue.write())
302+
} else {
303+
addStatement(
304+
"throw·%T(%S)",
305+
IllegalArgumentException::class.asTypeName(),
306+
"Required argument \"${arg.name}\" is missing and does not have an " +
307+
"android:defaultValue"
308+
)
309+
}
310+
endControlFlow()
311+
return@map tempVal
312+
}
313+
addStatement("return·%T(${tempVariables.joinToString(", ") { it }})", className)
314+
}.build()
315+
270316
val typeSpec = TypeSpec.classBuilder(className)
271317
.addSuperinterface(NAV_ARGS_CLASSNAME)
272318
.addModifiers(KModifier.DATA)
@@ -283,6 +329,7 @@ class KotlinNavWriter(private val useAndroidX: Boolean = true) : NavWriter<Kotli
283329
.addType(
284330
TypeSpec.companionObjectBuilder()
285331
.addFunction(fromBundleFunSpec)
332+
.addFunction(fromSavedStateHandleFunSpec)
286333
.build()
287334
)
288335
.build()

‎navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ internal val ACTION_ONLY_NAV_DIRECTION_CLASSNAME: ClassName =
6262
ClassName("androidx.navigation", "ActionOnlyNavDirections")
6363
internal val NAV_ARGS_CLASSNAME: ClassName = ClassName("androidx.navigation", "NavArgs")
6464
internal val BUNDLE_CLASSNAME: ClassName = ClassName("android.os", "Bundle")
65+
internal val SAVED_STATE_HANDLE_CLASSNAME: ClassName =
66+
ClassName("androidx.lifecycle", "SavedStateHandle")
6567

6668
internal val PARCELABLE_CLASSNAME = ClassName("android.os", "Parcelable")
6769
internal val SERIALIZABLE_CLASSNAME = ClassName("java.io", "Serializable")

‎navigation/navigation-safe-args-generator/src/test/test-data/expected/java_nav_writer_test/MainFragment$InnerFragmentArgs.java‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import android.os.Bundle;
2020
import androidx.annotation.NonNull;
21+
import androidx.lifecycle.SavedStateHandle;
2122
import androidx.navigation.NavArgs;
2223
import java.lang.IllegalArgumentException;
2324
import java.lang.Object;
@@ -54,6 +55,24 @@ public class MainFragment$InnerFragmentArgs implements NavArgs {
5455
return __result;
5556
}
5657

58+
@NonNull
59+
@SuppressWarnings("unchecked")
60+
public static MainFragment$InnerFragmentArgs fromSavedStateHandle(
61+
@NonNull SavedStateHandle savedStateHandle) {
62+
MainFragment$InnerFragmentArgs __result = new MainFragment$InnerFragmentArgs();
63+
if (savedStateHandle.contains("mainArg")) {
64+
String mainArg;
65+
mainArg = savedStateHandle.get("mainArg");
66+
if (mainArg == null) {
67+
throw new IllegalArgumentException("Argument \"mainArg\" is marked as non-null but was passed a null value.");
68+
}
69+
__result.arguments.put("mainArg", mainArg);
70+
} else {
71+
throw new IllegalArgumentException("Required argument \"mainArg\" is missing and does not have an android:defaultValue");
72+
}
73+
return __result;
74+
}
75+
5776
@SuppressWarnings("unchecked")
5877
@NonNull
5978
public String getMainArg() {

‎navigation/navigation-safe-args-generator/src/test/test-data/expected/java_nav_writer_test/MainFragmentArgs.java‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.os.Parcelable;
66
import androidx.annotation.NonNull;
77
import androidx.annotation.Nullable;
8+
import androidx.lifecycle.SavedStateHandle;
89
import androidx.navigation.NavArgs;
910
import java.io.Serializable;
1011
import java.lang.IllegalArgumentException;
@@ -130,6 +131,95 @@ public static MainFragmentArgs fromBundle(@NonNull Bundle bundle) {
130131
return __result;
131132
}
132133

134+
@NonNull
135+
@SuppressWarnings("unchecked")
136+
public static MainFragmentArgs fromSavedStateHandle(@NonNull SavedStateHandle savedStateHandle) {
137+
MainFragmentArgs __result = new MainFragmentArgs();
138+
if (savedStateHandle.contains("main")) {
139+
String main;
140+
main = savedStateHandle.get("main");
141+
if (main == null) {
142+
throw new IllegalArgumentException("Argument \"main\" is marked as non-null but was passed a null value.");
143+
}
144+
__result.arguments.put("main", main);
145+
} else {
146+
throw new IllegalArgumentException("Required argument \"main\" is missing and does not have an android:defaultValue");
147+
}
148+
if (savedStateHandle.contains("optional")) {
149+
int optional;
150+
optional = savedStateHandle.get("optional");
151+
__result.arguments.put("optional", optional);
152+
} else {
153+
__result.arguments.put("optional", -1);
154+
}
155+
if (savedStateHandle.contains("reference")) {
156+
int reference;
157+
reference = savedStateHandle.get("reference");
158+
__result.arguments.put("reference", reference);
159+
} else {
160+
__result.arguments.put("reference", R.drawable.background);
161+
}
162+
if (savedStateHandle.contains("referenceZeroDefaultValue")) {
163+
int referenceZeroDefaultValue;
164+
referenceZeroDefaultValue = savedStateHandle.get("referenceZeroDefaultValue");
165+
__result.arguments.put("referenceZeroDefaultValue", referenceZeroDefaultValue);
166+
} else {
167+
__result.arguments.put("referenceZeroDefaultValue", 0);
168+
}
169+
if (savedStateHandle.contains("floatArg")) {
170+
float floatArg;
171+
floatArg = savedStateHandle.get("floatArg");
172+
__result.arguments.put("floatArg", floatArg);
173+
} else {
174+
__result.arguments.put("floatArg", 1F);
175+
}
176+
if (savedStateHandle.contains("floatArrayArg")) {
177+
float[] floatArrayArg;
178+
floatArrayArg = savedStateHandle.get("floatArrayArg");
179+
if (floatArrayArg == null) {
180+
throw new IllegalArgumentException("Argument \"floatArrayArg\" is marked as non-null but was passed a null value.");
181+
}
182+
__result.arguments.put("floatArrayArg", floatArrayArg);
183+
} else {
184+
throw new IllegalArgumentException("Required argument \"floatArrayArg\" is missing and does not have an android:defaultValue");
185+
}
186+
if (savedStateHandle.contains("objectArrayArg")) {
187+
ActivityInfo[] objectArrayArg;
188+
objectArrayArg = savedStateHandle.get("objectArrayArg");
189+
if (objectArrayArg == null) {
190+
throw new IllegalArgumentException("Argument \"objectArrayArg\" is marked as non-null but was passed a null value.");
191+
}
192+
__result.arguments.put("objectArrayArg", objectArrayArg);
193+
} else {
194+
throw new IllegalArgumentException("Required argument \"objectArrayArg\" is missing and does not have an android:defaultValue");
195+
}
196+
if (savedStateHandle.contains("boolArg")) {
197+
boolean boolArg;
198+
boolArg = savedStateHandle.get("boolArg");
199+
__result.arguments.put("boolArg", boolArg);
200+
} else {
201+
__result.arguments.put("boolArg", true);
202+
}
203+
if (savedStateHandle.contains("optionalParcelable")) {
204+
ActivityInfo optionalParcelable;
205+
optionalParcelable = savedStateHandle.get("optionalParcelable");
206+
__result.arguments.put("optionalParcelable", optionalParcelable);
207+
} else {
208+
__result.arguments.put("optionalParcelable", null);
209+
}
210+
if (savedStateHandle.contains("enumArg")) {
211+
AccessMode enumArg;
212+
enumArg = savedStateHandle.get("enumArg");
213+
if (enumArg == null) {
214+
throw new IllegalArgumentException("Argument \"enumArg\" is marked as non-null but was passed a null value.");
215+
}
216+
__result.arguments.put("enumArg", enumArg);
217+
} else {
218+
__result.arguments.put("enumArg", AccessMode.READ);
219+
}
220+
return __result;
221+
}
222+
133223
@SuppressWarnings("unchecked")
134224
@NonNull
135225
public String getMain() {

0 commit comments

Comments
 (0)