Jetpack Compose Preview allows developers to visualize their UI components directly within Android Studio without having to deploy the app to an emulator or physical device. Jetpack Compose Preview is a game-changer for UI development, enabling you to see real-time changes as you code, significantly speeding up the development process and allowing for rapid iteration.
As part of the broader Jetpack Compose UI toolkit, the Preview functionality stands out as one of the most productivity-enhancing features, letting you build and refine complex interfaces with immediate visual feedback. Whether you're designing simple components or intricate layouts, Jetpack Compose Preview provides an immediate visual representation of your Composable functions, helping you catch design issues early and experiment with different styles effortlessly.
At its core, Jetpack Compose Preview works by using special annotations to mark Composable functions for preview rendering. The Android Studio preview pane then interprets these annotations and renders your UI components accordingly.
The foundation of Jetpack Compose Preview is the @Preview
annotation. This simple yet powerful annotation tells Android Studio that a specific Composable function should be rendered in the preview pane.
@Preview
@Composable
fun SimpleTextPreview() {
Text("Hello, Jetpack Compose Preview!")
}
When you add this annotation to a Composable function, Android Studio will render the function's content in the preview pane, allowing you to see how your UI component looks without running the app. This immediate feedback loop is invaluable for rapid UI development.
It's important to note that preview functions must not have parameters. If your Composable requires parameters, you'll need to create a separate parameter-less function specifically for preview purposes that calls your actual Composable with appropriate default values.
While the basic @Preview
annotation is useful, Jetpack Compose Preview truly shines when you customize it to match your specific needs. The annotation supports numerous parameters that allow you to configure how your preview appears.
For better organization, especially when you have multiple previews, you can name your previews using the name
parameter:
@Preview(name = "Default Button")
@Composable
fun DefaultButtonPreview() {
MyButton(text = "Click Me", onClick = {})
}
@Preview(name = "Disabled Button")
@Composable
fun DisabledButtonPreview() {
MyButton(text = "Cannot Click", enabled = false, onClick = {})
}
This naming convention helps you identify different preview variants quickly, especially when working with multiple versions of the same component.
Jetpack Compose Preview allows you to specify the dimensions and background color of your preview:
@Preview(
name = "Custom Sized Preview",
widthDp = 200,
heightDp = 100,
backgroundColor = 0xFFE0E0E0 // Light gray background
)
@Composable
fun CustomSizedPreview() {
Surface(color = Color.White) {
Text("I'm in a 200x100dp preview with a gray background")
}
}
This customization is particularly useful when you want to ensure your components look good at specific sizes or against different background colors. The widthDp
and heightDp
parameters define the dimensions in density-independent pixels, while backgroundColor
accepts a color value in the Long format.
Accessibility is a crucial aspect of modern app development. Jetpack Compose Preview helps you test how your UI looks with different font scales:
@Preview(
name = "Large Font Scale",
fontScale = 1.5f
)
@Composable
fun LargeFontScalePreview() {
Text("This text has a larger font scale")
}
Testing your UI with different font scales ensures your layout remains usable for users who have adjusted their device's font size for better readability.
Beyond the basic customizations, Jetpack Compose Preview offers advanced features that can significantly enhance your development workflow.
To ensure your UI looks great across different devices, Jetpack Compose Preview allows you to specify device configurations:
@Preview(
name = "Pixel 4 Preview",
device = Devices.PIXEL_4
)
@Composable
fun Pixel4Preview() {
MyApp()
}
@Preview(
name = "Pixel C Preview",
device = Devices.PIXEL_C
)
@Composable
fun PixelCPreview() {
MyApp()
}
The device
parameter accepts predefined device configurations from the Devices
class, which includes popular devices like Pixel phones and tablets. This feature allows you to see how your UI adapts to different screen sizes and densities without having to run your app on multiple emulators.
With dark mode becoming increasingly popular, testing your UI in both light and dark themes is essential. Jetpack Compose Preview makes this easy:
@Preview(
name = "Light Mode",
uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
name = "Dark Mode",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun ThemePreview() {
MyTheme {
MyScreen()
}
}
By using the uiMode
parameter, you can preview your UI in both light and dark modes side by side, ensuring a consistent experience across different theme settings.
Sometimes you need to preview your Composable with different sets of data. The @PreviewParameter
annotation allows you to do just that:
class UserProvider : PreviewParameterProvider<User> {
override val values = sequenceOf(
User("John Doe", "john@example.com"),
User("Jane Smith", "jane@example.com", profilePicUrl = "https://example.com/jane.jpg")
)
}
@Preview
@Composable
fun UserCardPreview(
@PreviewParameter(UserProvider::class) user: User
) {
UserCard(user = user)
}
The PreviewParameterProvider
interface allows you to define a sequence of values that will be used to generate multiple previews of your Composable. This is particularly useful for testing how your UI handles different data scenarios.
When you need to apply the same set of preview configurations to multiple Composables, creating a custom annotation can save you time and reduce code duplication:
@Preview(
name = "Light Mode",
uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
name = "Dark Mode",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Preview(
name = "Large Font",
fontScale = 1.5f
)
annotation class MultiPreview
// Now you can use your custom annotation
@MultiPreview
@Composable
fun ButtonPreview() {
MyButton(text = "Click Me", onClick = {})
}
@MultiPreview
@Composable
fun TextFieldPreview() {
MyTextField(value = "Input text", onValueChange = {})
}
This custom annotation approach, often referred to as @MultiPreview
, allows you to define a set of standard preview configurations once and apply them consistently across your codebase.
One of the most powerful features of Jetpack Compose Preview is the ability to interact with your previews directly in Android Studio using the "Interactive Mode":
@Preview(showSystemUi = true)
@Composable
fun InteractiveLoginScreenPreview() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
LoginScreen(
username = username,
password = password,
onUsernameChange = { username = it },
onPasswordChange = { password = it },
onLoginClick = {}
)
}
By enabling the interactive preview mode in Android Studio, you can interact with elements like text fields, buttons, and other interactive components directly in the preview pane. This feature allows you to test user interactions without deploying your app, further accelerating the development process.
Sometimes you need to see how your UI looks within the context of the system UI, including status bars and navigation bars:
@Preview(
name = "Full Screen Preview",
showSystemUi = true
)
@Composable
fun FullScreenPreview() {
MyApp()
}
The showSystemUi
parameter adds system UI elements to your preview, giving you a more complete picture of how your app will look on a real device.
Let's put everything together with a complete example of a user profile card component that demonstrates the power of Jetpack Compose Preview:
// File: UserProfileCard.kt
package com.example.myapp
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Phone
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
data class UserProfile(
val name: String,
val email: String,
val phone: String = "",
val profilePicUrl: String? = null,
val isVerified: Boolean = false
)
class UserProfileProvider : PreviewParameterProvider<UserProfile> {
override val values = sequenceOf(
UserProfile(
name = "John Doe",
email = "john.doe@example.com",
phone = "+1 (555) 123-4567"
),
UserProfile(
name = "Jane Smith",
email = "jane.smith@example.com",
phone = "+1 (555) 987-6543",
profilePicUrl = "https://example.com/jane.jpg",
isVerified = true
),
UserProfile(
name = "Alex Johnson",
email = "alex.johnson@example.com"
)
)
}
@Composable
fun UserProfileCard(
userProfile: UserProfile,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
// Profile picture
if (userProfile.profilePicUrl != null) {
Image(
painter = rememberImagePainter(userProfile.profilePicUrl),
contentDescription = "Profile picture of ${userProfile.name}",
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
)
} else {
Surface(
modifier = Modifier
.size(64.dp)
.clip(CircleShape),
color = MaterialTheme.colors.primary
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = userProfile.name.first().toString(),
style = MaterialTheme.typography.h5,
color = Color.White
)
}
}
}
Spacer(modifier = Modifier.width(16.dp))
Column {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = userProfile.name,
style = MaterialTheme.typography.h6,
fontWeight = FontWeight.Bold
)
if (userProfile.isVerified) {
Spacer(modifier = Modifier.width(8.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colors.primary,
modifier = Modifier.size(16.dp)
) {
Text(
text = "✓",
color = Color.White,
modifier = Modifier.padding(2.dp)
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = userProfile.email,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
if (userProfile.phone.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Phone,
contentDescription = "Phone number",
tint = MaterialTheme.colors.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = userProfile.phone,
style = MaterialTheme.typography.body2
)
}
Spacer(modifier = Modifier.height(8.dp))
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Email,
contentDescription = "Email address",
tint = MaterialTheme.colors.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = userProfile.email,
style = MaterialTheme.typography.body2
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* Contact action */ },
modifier = Modifier.align(Alignment.End)
) {
Text("Contact")
}
}
}
}
// Basic preview
@Preview(name = "User Profile Card")
@Composable
fun UserProfileCardPreview() {
MaterialTheme {
Surface {
UserProfileCard(
userProfile = UserProfile(
name = "John Doe",
email = "john.doe@example.com",
phone = "+1 (555) 123-4567"
)
)
}
}
}
// Preview with different user profiles
@Preview(name = "User Profile Card - Dynamic Data")
@Composable
fun UserProfileCardDynamicPreview(
@PreviewParameter(UserProfileProvider::class) userProfile: UserProfile
) {
MaterialTheme {
Surface {
UserProfileCard(userProfile = userProfile)
}
}
}
// Preview with different themes
@Preview(
name = "User Profile Card - Light Theme",
uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
name = "User Profile Card - Dark Theme",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun UserProfileCardThemePreview() {
MaterialTheme {
Surface {
UserProfileCard(
userProfile = UserProfile(
name = "Jane Smith",
email = "jane.smith@example.com",
phone = "+1 (555) 987-6543",
isVerified = true
)
)
}
}
}
// Preview with different font scales
@Preview(
name = "User Profile Card - Large Font",
fontScale = 1.5f
)
@Composable
fun UserProfileCardLargeFontPreview() {
MaterialTheme {
Surface {
UserProfileCard(
userProfile = UserProfile(
name = "Alex Johnson",
email = "alex.johnson@example.com"
)
)
}
}
}
// Full screen preview with system UI
@Preview(
name = "User Profile Card - Full Screen",
showSystemUi = true
)
@Composable
fun UserProfileCardFullScreenPreview() {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
UserProfileCard(
userProfile = UserProfile(
name = "John Doe",
email = "john.doe@example.com",
phone = "+1 (555) 123-4567"
),
modifier = Modifier.padding(16.dp)
)
}
}
}
To use this component in your project, you'll need to include the following dependencies in your build.gradle
file:
dependencies {
implementation "androidx.compose.ui:ui:1.5.4"
implementation "androidx.compose.material:material:1.5.4"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.4"
debugImplementation "androidx.compose.ui:ui-tooling:1.5.4"
implementation "io.coil-kt:coil-compose:2.5.0"
}