Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

Compose Mastery: Automating Android UI Previews

This technical guide provides an in-depth analysis of the json to jetpack compose preview engine, best practices for implementation, and data security standards.

JSON to Jetpack Compose Previews: PreviewParameterProvider, kotlinx.serialization, and Multi-State Testing

Jetpack Compose's @Preview annotation makes UI iteration fast — but preview data hardcoded directly in Kotlin drifts from real API responses and misses edge cases. Loading JSON fixtures in previews mirrors what production data actually looks like: variable string lengths, optional fields, nested structures, and localized content. The professional pattern combines PreviewParameterProvider (which generates one preview per data case) with JSON deserialization in the debug source set, ensuring preview-only code never ships to production.

Live Example: Multi-State Previews with JSON Fixtures

// Data models (main source set)
@Serializable
data class User(
    val id:           String,
    val username:     String,
    val bio:          String?       = null,
    val followerCount: Int,
    val isVerified:   Boolean       = false,
    @SerialName("joined_at")
    val joinedAt:     String,
)

@Serializable
data class UserFeed(
    val users:     List<User>,
    val totalCount: Int,
    val hasMore:   Boolean,
)

// src/debug/assets/fixtures/user-success.json
{
  "id": "usr_001",
  "username": "alice_dev",
  "bio": "Android engineer. Building with Compose and Kotlin. Open source contributor.",
  "followerCount": 1250,
  "isVerified": true,
  "joined_at": "2022-03-15T08:00:00Z"
}

// src/debug/assets/fixtures/user-minimal.json
{
  "id": "usr_002",
  "username": "bob",
  "followerCount": 3,
  "isVerified": false,
  "joined_at": "2024-11-01T08:00:00Z"
}

// src/debug/kotlin/com/example/fixtures/JsonFixture.kt
object JsonFixture {
    val json = Json {
        ignoreUnknownKeys  = true
        isLenient          = true
        coerceInputValues  = true
    }

    inline fun <reified T> load(context: Context, filename: String): T {
        val jsonString = context.assets.open("fixtures/$filename.json")
            .bufferedReader().use { it.readText() }
        return json.decodeFromString(jsonString)
    }
}

// src/debug/kotlin/com/example/previews/UserPreviewProvider.kt
class UserPreviewProvider : PreviewParameterProvider<User> {
    override val values: Sequence<User> = sequenceOf(
        Json.decodeFromString(loadFixtureString("user-success")),
        Json.decodeFromString(loadFixtureString("user-minimal")),
        // Programmatic edge cases (no JSON file needed for simple variants)
        User(id = "u3", username = "x".repeat(50), followerCount = 999_999, isVerified = true, joinedAt = "2020-01-01T00:00:00Z"),
    )
}

// View composable
@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
    Card(modifier = modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Row(horizontalArrangement = Arrangement.SpaceBetween) {
                Text(user.username, style = MaterialTheme.typography.titleMedium)
                if (user.isVerified) {
                    Icon(Icons.Default.Verified, contentDescription = "Verified",
                         tint = MaterialTheme.colorScheme.primary)
                }
            }
            user.bio?.let { bio ->
                Text(bio, style = MaterialTheme.typography.bodyMedium,
                     color = MaterialTheme.colorScheme.onSurfaceVariant)
            }
            Text("${user.followerCount} followers", style = MaterialTheme.typography.labelSmall)
        }
    }
}

// Preview using PreviewParameterProvider — generates one preview per data variant
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun UserCardPreview(@PreviewParameter(UserPreviewProvider::class) user: User) {
    MyAppTheme {
        UserCard(user = user, modifier = Modifier.padding(8.dp))
    }
}

@Preview Annotation Parameters

// Common @Preview parameters for comprehensive visual testing

@Preview(
    name           = "Large text — Accessibility",
    fontScale      = 2.0f,
    showBackground = true,
)
@Preview(
    name      = "Tablet landscape",
    device    = Devices.TABLET,
    widthDp   = 1024,
    heightDp  = 768,
    showSystemUi = true,
)
@Preview(
    name    = "Arabic (RTL)",
    locale  = "ar",
    showBackground = true,
)
@Preview(
    name    = "Dark mode",
    uiMode  = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL,
)
@Composable
fun UserCardMultiPreview(@PreviewParameter(UserPreviewProvider::class) user: User) {
    MyAppTheme {
        UserCard(user)
    }
}

Loading JSON in Preview Composables

// For previews that need Context (reading assets), use LocalContext
@Preview(showBackground = true)
@Composable
fun FeedPreview() {
    val context = LocalContext.current
    val feed: UserFeed = remember {
        JsonFixture.load(context, "user-feed")
    }
    MyAppTheme {
        UserFeedScreen(
            users     = feed.users,
            hasMore   = feed.hasMore,
            onLoadMore = {}
        )
    }
}

// Multi-state: loading / empty / populated / error
@Preview(showBackground = true, name = "Loading state")
@Composable
fun FeedLoadingPreview() {
    MyAppTheme {
        UserFeedScreen(users = emptyList(), isLoading = true, onLoadMore = {})
    }
}

@Preview(showBackground = true, name = "Empty state")
@Composable
fun FeedEmptyPreview() {
    MyAppTheme {
        UserFeedScreen(users = emptyList(), isLoading = false, onLoadMore = {})
    }
}

@Preview(showBackground = true, name = "Error state")
@Composable
fun FeedErrorPreview() {
    MyAppTheme {
        ErrorScreen(message = "Network error — tap to retry")
    }
}

kotlinx.serialization vs Gson vs Moshi

// For Compose projects, prefer kotlinx.serialization:
// - First-class Kotlin support (data classes, sealed classes, nullability)
// - No reflection at runtime (generates serializers at compile time)
// - Works with Kotlin Multiplatform

// Gradle setup:
// plugins { id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" }
// dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") }

// kotlinx.serialization — annotation-driven
@Serializable
data class Product(
    val id:    String,
    val title: String,
    @SerialName("unit_price")
    val price: Double,
    val inStock: Boolean = true,    // default handles missing JSON field
)

// Decode
val product = Json.decodeFromString<Product>(jsonString)

// Gson — reflection-based (works without annotation, but less safe)
val product = Gson().fromJson(jsonString, Product::class.java)

// Moshi — annotation-based, null-safe, Kotlin adapters
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(Product::class.java)
val product = adapter.fromJson(jsonString)

Best Practices for Production

  • Keep all preview and fixture code in the debug source set: Create src/debug/kotlin/ for preview helpers and src/debug/assets/fixtures/ for JSON files. These are included only in debug builds — the release APK/AAB never contains mock data.
  • Use PreviewParameterProvider for data-driven previews: Rather than writing five separate preview composables for five states, one PreviewParameterProvider generates all five. Adding a new case is just adding another item to the values sequence — no new composable needed.
  • Mark data classes with @Immutable or @Stable: Compose's recomposition engine skips recomposition for stable/immutable values. If your data class is annotated, the compiler skips the smart recomposition check overhead. For preview purposes, this also makes preview rendering faster.
  • Test RTL layouts explicitly: Add @Preview(locale = "ar") or @Preview(locale = "he") for right-to-left languages. The same JSON fixture drives the RTL preview — you see immediately if your Row/Column directions are hardcoded or correctly use LayoutDirection.

FAQ

Q: Can I use the same JSON fixtures in instrumented UI tests?
A: Yes — place fixtures in src/androidTest/assets/ for instrumented tests and src/debug/assets/ for previews. For a single source of truth, define a shared fixtures directory and add symlinks or use a shared Gradle module. The deserialization logic is identical between previews and tests.

Q: Why does my preview not update when I change the JSON file?
A: Xcode/Android Studio caches preview renders. In Android Studio, use "Refresh Previews" (the circular arrow in the Preview panel) or invalidate caches (File → Invalidate Caches). If the JSON is loaded with remember { ... } and the composable doesn't recompose, the cache is stale — during previews, remember is evaluated once per render.

Q: How do I preview a composable that uses a ViewModel?
A: Create an interface or abstract class for your ViewModel state (UiState data class), and a preview-only implementation that returns the JSON-loaded data. Your composable should accept the state object directly rather than observing the ViewModel — this makes it both testable and previewable without a real ViewModel lifecycle.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.