Skip to content

Commit 96d2847

Browse files
authored
Support @Preview locale metadata in thumbnail rendering (#8)
The KSP processor captures the preview's locale qualifier (direct or via a multipreview meta-annotation) into the manifest, and both render backends apply it: Layoutlib through the renderer's preview params, Robolectric through a composition-scoped configuration-context override with the matching layout direction. Also fixes the generated navgraph.version resource staying stale across a VERSION_NAME bump, which made consumers auto-wire the previous release. Fixes #7
1 parent 7f27964 commit 96d2847

20 files changed

Lines changed: 246 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **`@Preview(locale = …)` support for thumbnails** ([#7](https://github.com/skydoves/compose-nav-graph/issues/7)): the KSP processor now captures the preview's locale qualifier (declared directly or via a multipreview meta-annotation) into the manifest, and both render backends apply it (Layoutlib through the renderer's preview params, Robolectric through a composition-scoped configuration-context override), so localized previews render with the same resources Android Studio shows.
12+
13+
### Fixed
14+
- The generated `navgraph.version` resource (which pins the auto-wired `compose-nav-graph-annotations` / `compose-nav-graph-ksp` / `compose-nav-graph-testing` versions) is regenerated when `VERSION_NAME` changes instead of staying stale.
15+
816
## [0.1.0] - 2026-06-01
917

1018
### Added

compose-nav-graph-annotations/api/android/compose-nav-graph-annotations.api

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,19 @@ public final class com/github/skydoves/navgraph/model/NavPreviewParam$Companion
220220

221221
public final class com/github/skydoves/navgraph/model/NavPreviewRef {
222222
public static final field Companion Lcom/github/skydoves/navgraph/model/NavPreviewRef$Companion;
223-
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Z)V
224-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
223+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;)V
224+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
225225
public final fun component1 ()Ljava/lang/String;
226226
public final fun component2 ()Ljava/lang/String;
227227
public final fun component3 ()Ljava/lang/String;
228228
public final fun component4 ()Ljava/util/List;
229229
public final fun component5 ()Ljava/lang/String;
230230
public final fun component6 ()Z
231-
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Z)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
232-
public static synthetic fun copy$default (Lcom/github/skydoves/navgraph/model/NavPreviewRef;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZILjava/lang/Object;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
231+
public final fun component7 ()Ljava/lang/String;
232+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
233+
public static synthetic fun copy$default (Lcom/github/skydoves/navgraph/model/NavPreviewRef;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
233234
public fun equals (Ljava/lang/Object;)Z
235+
public final fun getLocale ()Ljava/lang/String;
234236
public final fun getPreviewFqn ()Ljava/lang/String;
235237
public final fun getPreviewMethodFqn ()Ljava/lang/String;
236238
public final fun getPreviewName ()Ljava/lang/String;

compose-nav-graph-annotations/api/jvm/compose-nav-graph-annotations.api

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,19 @@ public final class com/github/skydoves/navgraph/model/NavPreviewParam$Companion
220220

221221
public final class com/github/skydoves/navgraph/model/NavPreviewRef {
222222
public static final field Companion Lcom/github/skydoves/navgraph/model/NavPreviewRef$Companion;
223-
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Z)V
224-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
223+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;)V
224+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
225225
public final fun component1 ()Ljava/lang/String;
226226
public final fun component2 ()Ljava/lang/String;
227227
public final fun component3 ()Ljava/lang/String;
228228
public final fun component4 ()Ljava/util/List;
229229
public final fun component5 ()Ljava/lang/String;
230230
public final fun component6 ()Z
231-
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Z)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
232-
public static synthetic fun copy$default (Lcom/github/skydoves/navgraph/model/NavPreviewRef;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZILjava/lang/Object;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
231+
public final fun component7 ()Ljava/lang/String;
232+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
233+
public static synthetic fun copy$default (Lcom/github/skydoves/navgraph/model/NavPreviewRef;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Lcom/github/skydoves/navgraph/model/NavPreviewRef;
233234
public fun equals (Ljava/lang/Object;)Z
235+
public final fun getLocale ()Ljava/lang/String;
234236
public final fun getPreviewFqn ()Ljava/lang/String;
235237
public final fun getPreviewMethodFqn ()Ljava/lang/String;
236238
public final fun getPreviewName ()Ljava/lang/String;

compose-nav-graph-annotations/src/commonMain/kotlin/com/github/skydoves/navgraph/model/NavGraphModels.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ public data class NavArg(
126126
* The device-free renderer needs each provider to instantiate the composable's sample value — without them a
127127
* parameterized `@NavPreview` (very common in real apps) can't render and yields a blank/failed thumbnail.
128128
* @property primary whether this is the canonical thumbnail for the route (`@NavPreview.primary`).
129+
* @property locale the `@Preview(locale = …)` resource qualifier (e.g. `"ko"`, `"fr-rFR"`) the function's
130+
* `@Preview` declares, directly or via a multipreview meta-annotation; null when none is declared. Both
131+
* render backends apply it so the thumbnail matches what Android Studio's preview shows. Additive since
132+
* schema 1 (absent in older manifests → null).
129133
*/
130134
@Serializable
131135
public data class NavPreviewRef(
@@ -135,6 +139,7 @@ public data class NavPreviewRef(
135139
val previewParameters: List<NavPreviewParam> = emptyList(),
136140
val thumbnail: String? = null,
137141
val primary: Boolean = false,
142+
val locale: String? = null,
138143
)
139144

140145
/**

compose-nav-graph-gradle/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ tasks.withType<Test>().configureEach { useJUnit() }
3737
val generateNavGraphVersion = tasks.register("generateNavGraphVersion") {
3838
val outDir = layout.buildDirectory.dir("generated/navgraphversion")
3939
val versionValue = version.toString()
40+
// Without this input the task stays UP-TO-DATE across a VERSION_NAME bump, leaving a stale
41+
// resource — consumers would then auto-wire the previous release's artifacts.
42+
inputs.property("version", versionValue)
4043
outputs.dir(outDir)
4144
doLast {
4245
outDir.get().file("navgraph.version").asFile.apply {

compose-nav-graph-gradle/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# compose-nav-graph-gradle is a standalone (included) build, so it carries its own publishing coordinates
22
# rather than inheriting the root gradle.properties.
33
GROUP=com.github.skydoves
4-
VERSION_NAME=0.1.0
4+
VERSION_NAME=0.1.1-SNAPSHOT
55

66
POM_NAME=Compose Navigation Graph Gradle Plugin
77
POM_ARTIFACT_ID=compose-nav-graph-gradle

compose-nav-graph-gradle/src/main/kotlin/com/github/skydoves/navgraph/gradle/LayoutlibRenderTask.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public abstract class LayoutlibRenderTask : DefaultTask() {
114114
val methodFqn: String,
115115
val id: String,
116116
val params: List<HPreviewParam>,
117+
val locale: String?,
117118
)
118119

119120
@TaskAction
@@ -135,7 +136,10 @@ public abstract class LayoutlibRenderTask : DefaultTask() {
135136
)
136137
} else {
137138
add(
138-
Shot(node.id, pv.previewName, pv.primary, method, "nav${i++}", pv.previewParameters),
139+
Shot(
140+
node.id, pv.previewName, pv.primary, method, "nav${i++}",
141+
pv.previewParameters, pv.locale,
142+
),
139143
)
140144
}
141145
}
@@ -205,6 +209,9 @@ public abstract class LayoutlibRenderTask : DefaultTask() {
205209
putJsonObject("previewParams") {
206210
put("apiLevel", apiLevel.get())
207211
put("device", PREVIEW_DEVICE)
212+
// The @Preview(locale = …) qualifier, extracted by KSP (a multipreview's meta-annotation isn't
213+
// visible to the renderer itself, so it must be passed explicitly).
214+
s.locale?.let { put("locale", it) }
208215
}
209216
put("previewId", s.id)
210217
}

compose-nav-graph-gradle/src/main/kotlin/com/github/skydoves/navgraph/gradle/NavManifest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ internal data class HPreview(
6565
val previewParameters: List<HPreviewParam> = emptyList(),
6666
val thumbnail: String? = null,
6767
val primary: Boolean = false,
68+
val locale: String? = null,
6869
)
6970

7071
internal data class HPreviewParam(val name: String = "", val provider: String = "")
@@ -107,6 +108,7 @@ internal fun parseGraph(text: String): HGraph {
107108
},
108109
thumbnail = po.str("thumbnail"),
109110
primary = po.bool("primary"),
111+
locale = po.str("locale"),
110112
)
111113
},
112114
start = o.bool("start"),

compose-nav-graph-gradle/src/main/kotlin/com/github/skydoves/navgraph/gradle/RobolectricRenderListTask.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import org.gradle.api.tasks.TaskAction
2929
import org.gradle.work.DisableCachingByDefault
3030

3131
/**
32-
* Writes the TSV render-list (`methodFqn\tnodeId\tpreviewName\tprimary`, one preview per line) that the
32+
* Writes the TSV render-list (`methodFqn\tnodeId\tpreviewName\tprimary[\tlocale]`, one preview per line) that the
3333
* generated `NavGraphRobolectricRenderTest` reads via the `navgraph.renderList` system property. In `ROBOLECTRIC` mode
3434
* every `@NavPreview` is listed; in `AUTO` mode only the previews Layoutlib **failed** to render are listed —
3535
* derived from the Layoutlib `results.json` ([readLayoutlibResults] + [isLayoutlibSuccess]), the single source
@@ -81,14 +81,15 @@ public abstract class RobolectricRenderListTask : DefaultTask() {
8181
val primary: Boolean,
8282
val methodFqn: String,
8383
val id: String,
84+
val locale: String?,
8485
)
8586
val shots = buildList {
8687
var i = 0
8788
graph.nodes.forEach { node ->
8889
node.previews.forEach { pv ->
8990
val method = pv.previewMethodFqn
9091
if (!method.isNullOrBlank()) {
91-
add(Shot(node.id, pv.previewName, pv.primary, method, "nav${i++}"))
92+
add(Shot(node.id, pv.previewName, pv.primary, method, "nav${i++}", pv.locale))
9293
}
9394
}
9495
}
@@ -115,9 +116,14 @@ public abstract class RobolectricRenderListTask : DefaultTask() {
115116

116117
val out = renderList.get().asFile
117118
out.parentFile.mkdirs()
119+
// TSV fields are TRAILING-additive only: NavPreviewRenderer in compose-nav-graph-testing parses this
120+
// line format, and the plugin auto-wires that artifact at its own version, so both sides move in step.
121+
// The locale field is emitted only when present, keeping locale-less lines byte-identical to the
122+
// 4-field format an older, explicitly-pinned testing artifact still parses correctly.
118123
out.writeText(
119124
selected.joinToString("") { s ->
120-
"${s.methodFqn}\t${s.nodeId}\t${s.previewName}\t${s.primary}\n"
125+
"${s.methodFqn}\t${s.nodeId}\t${s.previewName}\t${s.primary}" +
126+
"${s.locale?.let { "\t$it" }.orEmpty()}\n"
121127
},
122128
)
123129
logger.lifecycle(

compose-nav-graph-ksp/src/main/kotlin/com/github/skydoves/navgraph/ksp/NavGraphProcessor.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ internal class NavGraphProcessor(
135135
previewMethodFqn = jvmMethodFqn(fn),
136136
previewParameters = previewParametersOf(fn),
137137
primary = ann.boolArg("primary"),
138+
locale = previewLocaleOf(fn),
138139
),
139140
)
140141
}
@@ -324,6 +325,7 @@ internal class NavGraphProcessor(
324325
previewMethodFqn = method,
325326
previewParameters = previewParametersOf(fn),
326327
primary = false,
328+
locale = previewLocaleOf(fn),
327329
)
328330
}
329331
// The slug is lossy (two packages/modules differing only by `_` vs `.`/`-`/`:` collapse to one), so append
@@ -374,6 +376,33 @@ internal class NavGraphProcessor(
374376
}
375377
}
376378

379+
/**
380+
* The `@Preview(locale = …)` qualifier the function's preview declares, or null. Walks the function's
381+
* annotations in declaration order — recursing into multipreview meta-annotations depth-first (same cycle
382+
* guard as [isPreviewAnnotation]) — and returns the FIRST non-blank locale found. A function whose
383+
* `@Preview`s disagree on locale renders one thumbnail, so one locale has to win, and declaration order is
384+
* the deterministic choice (a meta-annotation listed before a direct `@Preview` wins).
385+
*/
386+
private fun previewLocaleOf(fn: KSFunctionDeclaration): String? =
387+
firstPreviewLocale(fn.annotations, mutableSetOf())
388+
389+
private fun firstPreviewLocale(
390+
annotations: Sequence<KSAnnotation>,
391+
visited: MutableSet<String>,
392+
): String? {
393+
for (ann in annotations) {
394+
val decl = ann.annotationType.resolve().declaration as? KSClassDeclaration ?: continue
395+
val name = decl.qualifiedName?.asString() ?: continue
396+
if (name == PREVIEW) {
397+
ann.stringArg("locale")?.takeIf(String::isNotBlank)?.let { return it }
398+
continue
399+
}
400+
if (!visited.add(name)) continue
401+
firstPreviewLocale(decl.annotations, visited)?.let { return it }
402+
}
403+
return null
404+
}
405+
377406
/**
378407
* Serializable properties = the navigation arguments: primary-constructor properties (in ctor order) then
379408
* body `val/var` with backing fields (declared order), excluding `@Transient`. Uses *declared* (not

0 commit comments

Comments
 (0)