RizTech Academy logo
RizTech Academy
Jetpack Compose FundamentalsLesson 5 of 620 min

Previews and the fastest way to iterate

Every change you have made so far meant a build, an install and a wait for the emulator, which on an 8 GB laptop is long enough to lose your train of thought. For layout work there is a faster loop: Android Studio renders composables directly in the editor, without a device, and re-renders as you type. This lesson sets up previews for PriceCard that show every sample price and a large font at once, and covers the one design rule that decides whether a screen can be previewed at all.

A first preview

@Preview(showBackground = true)
@Composable
private fun PriceCardPreview() {
    MaterialTheme {
        PriceCard(price = SamplePrices.lasalgaonOnion)
    }
}

Put it at the bottom of PriceCard.kt, with two new imports, androidx.compose.ui.tooling.preview.Preview and com.riztechacademy.mandibhav.data.SamplePrices. A preview is a composable with no parameters (with one exception, below) and an @Preview annotation. It can be private: nothing calls it except the tooling, and private keeps it out of autocomplete everywhere else.

Switch the editor to its split view (the view buttons sit at the top right of the editor) and the card appears beside the code. Change titleLarge to headlineLarge and the preview updates without a build. Larger changes, such as a new parameter, need a short rebuild that Android Studio triggers itself; it is still far quicker than a run on the emulator.

Two dependencies make this work, and your starter project already has both: ui-tooling-preview, which provides the annotation, and ui-tooling, which Android Studio uses to render. The second is debugImplementation, so it is not packed into release builds.

Wrap previews in MaterialTheme. Without it, components still render, but with default colours and text styles, so the preview stops matching the app.

Parameters worth knowing

Parameter Example What it does
name name = "Large text" Label above the preview
group group = "Fonts" Lets you filter previews by group
showBackground true White background instead of transparent
backgroundColor 0xFFFFF8E1 Background colour, an ARGB Long
widthDp, heightDp widthDp = 320 Fixed size in dp; 320 is a small, cheap phone
fontScale 1.5f Simulates the user's font size setting
locale locale = "mr" Uses string resources for that language
uiMode Configuration.UI_MODE_NIGHT_YES Night mode, among others
showSystemUi true Draws a status bar and navigation bar round it
device device = "id:pixel_4" Renders at a specific device's size
apiLevel apiLevel = 30 Renders with an older Android version

Multiple previews of one composable

The large-text case is the one that catches layouts out. Many older users set the phone's font size up, and Mandi Bhav's users skew older than most apps'. Add a second preview:

@Preview(name = "Large text", showBackground = true, fontScale = 1.5f, widthDp = 320)
@Composable
private fun PriceCardLargeTextPreview() {
    MaterialTheme {
        PriceCard(price = SamplePrices.puneOnion)
    }
}

At 1.5× on a 320 dp-wide screen, "Pune (Gultekdi) APMC · 7 Nov 2026" no longer fits on one line. It wraps, which is right, but it breaks between "7" and "Nov 2026", leaving the day stranded at the end of the first line. Nothing crashes and no test fails; you only find it by looking at a large font, and the preview lets you look without changing the emulator's settings. Practice 6 fixes it.

You can also stack @Preview annotations on one function, and Compose ships ready-made sets of them, called multipreview annotations:

Annotation Renders
@PreviewFontScale The same composable at a range of font scales
@PreviewLightDark Light and dark mode
@PreviewScreenSizes Phone, foldable, tablet and desktop sizes
@PreviewDynamicColors Several wallpaper-based colour schemes

The trap: a dark preview that looks light

Put @PreviewLightDark on PriceCardPreview and you get two previews that look the same. Nothing is broken in the preview: MaterialTheme with no arguments always uses its default light colour scheme, and Mandi Bhav has no dark theme yet. The night-mode setting reaches the app; the app ignores it. Module 4 builds a theme that switches, and from then on this preview pair earns its place. Until then, a dark preview is proof only that you have not built dark mode.

Every sample price at once

Writing one preview per price does not scale. A PreviewParameterProvider feeds a sequence of values into a single preview function, which renders once per value. Create ui/SamplePriceProvider.kt:

package com.riztechacademy.mandibhav.ui

import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.riztechacademy.mandibhav.data.SamplePrices
import com.riztechacademy.mandibhav.model.CommodityPrice

/** Feeds every sample price into a preview, one preview per price. */
class SamplePriceProvider : PreviewParameterProvider<CommodityPrice> {
    private val prices = SamplePrices.all

    override val values: Sequence<CommodityPrice> = prices.asSequence()

    override fun getDisplayName(index: Int): String? {
        val price = prices.getOrNull(index) ?: return null
        return "${price.commodity}, ${price.market}"
    }
}

and the preview, in PriceCard.kt (import androidx.compose.ui.tooling.preview.PreviewParameter):

@Preview(showBackground = true, widthDp = 360)
@Composable
private fun PriceCardSamplesPreview(
    @PreviewParameter(SamplePriceProvider::class) price: CommodityPrice,
) {
    MaterialTheme {
        PriceCard(price = price)
    }
}

That is the exception to "previews take no parameters": exactly one parameter, annotated with @PreviewParameter and the provider class. Five previews appear, one per sample price. getDisplayName labels each one "Onion, Lasalgaon APMC" instead of the default index-based name; without it you get five previews you cannot tell apart by name. If a provider has hundreds of values, @PreviewParameter(SamplePriceProvider::class, limit = 3) renders only the first three.

The provider must be a class with a constructor that takes no arguments, because the tooling creates it by reflection. That is also why it reads from SamplePrices instead of receiving the list.

The trap: previews that need a ViewModel or real data

Previews are rendered inside Android Studio by a cut-down copy of the Android framework. It has no network, no file access, and only part of Context. So a composable that, inside itself, creates a ViewModel, opens a database or calls an API, cannot be previewed. Android Studio shows a render error instead, and the usual reaction is to give up on previews for that screen, which is a big loss: screens are where previews pay most.

The fix is a design rule, and it is the same rule that made PriceCard straightforward to preview: a composable that draws a screen takes the data it shows as parameters. Mandi Bhav's screen, coming in the next lesson, is PricesScreen(prices: List<CommodityPrice>). It does not know where prices come from. In the app, MainActivity hands it sample data now and real data later; in a preview, you hand it SamplePrices.all. When the ViewModel arrives in module 5, it lives one level up, in a small composable that reads from the ViewModel and calls PricesScreen with plain data. That outer composable is not previewed; everything inside it is.

You will also meet LocalInspectionMode.current, which is true inside a preview, used to swap real data for fake. It works, but it puts preview logic into production code, and it hides the real problem, so reach for it only for third-party components you cannot restructure.

What previews cost. The sample data in SamplePrices is compiled into the app, and preview functions are code you maintain. Both are small. The real cost is false confidence: a preview runs on your laptop's fonts and a simplified framework, so before a release you still look at the real screen on a real phone.

Check your work

What makes a function a preview. @Preview on a composable that takes no parameters, or one @PreviewParameter parameter.

Why previews can be private. Only the tooling calls them.

Why the PriceCardLargeTextPreview matters for Mandi Bhav. Many users set a large font size; it shows wrapping and clipping without changing device settings.

Why @PreviewLightDark shows two identical cards today. MaterialTheme() uses its default light colour scheme; the app has no dark theme until module 4.

How many previews PriceCardSamplesPreview renders. Five, one per sample price.

Why a provider needs a no-argument constructor. The tooling instantiates it by reflection.

Why a composable that creates a ViewModel cannot be previewed. The preview renderer cannot construct its dependencies (network, database, injection), so rendering fails.

The design rule that keeps screens previewable. Screens take the data they show as parameters.

Practice 2. Nothing clips. Every line wraps, including the range, and the card grows taller, because nothing in PriceCard fixes a height. That is why lesson 4 warned against height(…) on text containers.

Practice 4. The texts in PriceCard are Kotlin strings, not string resources, and there is no values-mr folder. locale only changes which resources are loaded. Translating the card means moving its words into strings.xml first.

Practice 6. Put the market and the date on separate lines (two Texts), or join the date's parts with a non-breaking space, written in code as the escape "\u00A0", so the line can only break before the date. A separate line is easier to read at large sizes; Mandi Bhav keeps one line for now and module 4 revisits the card's layout.

Practice 7.

@Preview(name = "320 dp", widthDp = 320, showBackground = true)
@Preview(name = "320 dp, large text", widthDp = 320, fontScale = 1.5f, showBackground = true)
annotation class CheapPhonePreviews

Put @CheapPhonePreviews above @Composable on any preview function.

Practice

  1. Add the three previews to PriceCard.kt and get all seven rendering (one, one large-text, five from the provider).
  2. Change the large-text preview to fontScale = 2f and widthDp = 280. Does anything clip rather than wrap?
  3. Add @PreviewFontScale above PriceCardPreview and compare the scales it renders.
  4. Add a preview with locale = "mr". Nothing changes. Explain why, using what you learnt about string resources in module 2.
  5. Break it on purpose: give SamplePriceProvider a constructor parameter. Read the render error Android Studio shows, then undo it.
  6. Fix the stranded "7": make the date always stay on one line, without changing what it says.
  7. Harder: write your own multipreview annotation @CheapPhonePreviews that renders a 320 dp-wide preview at font scale 1 and one at 1.5, and use it on PriceCardPreview.

Official documentation

Next: the Material 3 components you will use daily, and Mandi Bhav's first real screen.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship