Jetpack Compose has transformed Android UI development with its declarative approach, making UI code simpler to write. One of its standout features is Compose Previews, which allows developers to instantly view how Composable functions will look on a device, without the need to run the app on a device or emulator.
This means they showcase not only a static layout but also the UI state derived from the Composable’s underlying UI logic and can even play animations if included, among other features.
In contrast, the old Android XML-based Views allow previewing their XML layouts. But these are limited to static layouts with placeholder content, lacking any real logic or data: they are as basic as it gets.
So if parts of your app still rely on XML-based Views, you might be missing out on the full power of Compose Previews… or maybe not?
Because Compose Previews can actually be used with XML-based Views!
Why XML Layout Previews Fall Short
Consider we have the following RecyclerView’s ViewHolder and its corresponding XML layout Preview.
The difference between both is very noticeable. This is because the ViewHolder includes UI logic that cannot be represented in a static XML preview. For instance:
Dynamic View Injection: The ViewHolder injects programmatically additional Views, like the radio buttons (i.e. language flags).
Use of Custom Views: It includes a custom view to represent the amount of “words to train” composed of two TextViews whose visibility changes at runtime to animate the counting up and down of numbers. Although one could use tools attribute reference i.e.
tools:
to preview sample data, it cannot simulate the runtime behaviour of this Custom View.
When such dynamic behaviour is involved, XML layout previews become nearly useless.
Enabling Compose Previews for your XML Layouts
Rendering a View consists of two main parts:
Inflating: The View created by inflating the associated XML layout. For Viewholders, that View is passed as argument to the constructor.
Binding: The method that passes the UI state to be rendered by the View or ViewHolder.
// 1. Inflating
val container = LayoutInflater
.from(context)
.inflate(R.layout.mylayout, parentViewGroup)
// Create the ViewHolder with the container
val viewHolder = MyViewHolder(
container = container,
// other arguments, e.g. listeners, here...
)
// 2. Binding
viewHolder.bind(uiState)
When it comes to previewing a ViewHolder in a Compose Preview, we first need to access its underlying View. We can access it via ViewHolder’s itemView
property.
But how do we render this View in a Compose Preview? The solution lies in Jetpack Compose’s AndroidView
Composable. If you’ve integrated Compose into an existing app using XML-based Views, you may have already used AndroidView
to wrap existing custom View
instances and integrate them into a Compose hierarchy.
This is exactly the trick we’ll use to render ViewHolders — and any other XML-based View — into Compose Previews.
Let’s put this into action with the previous ViewHolder!
@Preview
@Composable
fun TrainingViewHolderPreview() {
AndroidView(
// You might prefer fillMaxheight or fillMaxSize
// depending on the container's layout constraints
modifier = Modifier.fillMaxWidth(),
factory = { context ->
// 1. inflating
val containerParent = FrameLayout(contextWrapper)
val container = LayoutInflater
.from(context)
.inflate(R.layout.training_row, containerParent)
val viewHolder = TrainingViewHolder(container = layout)
// 2. Binding
viewHolder.bind(
item = initialTrainingItem,
languageClickedListener = null // or a meaningful listener
)
.itemView
},
)
}
And this is how that Preview renders compared to what we see on a device
As we can see, the Preview does not fully match what we see on the device. That’s because the implementation of our viewHolder.bind()
runs some animations, and it seems the Preview reflects its state before they are fully executed.
That’s where Preview Interactive Mode comes in handy.
In most cases, Compose Previews render very accurately also when used with XML-based Views. This is an exceptional case due to the animations running upon the ViewHolder’s creation
Preview Interactive Mode
Starting the Preview Interaction mode allows us to quickly observe the full animation executed in viewHolder.bind()
Nevertheless, it is still not identical to what we see on an actual device: the British flag should be disabled to match the “6 words to train” (5 Russian, 1 German). This is because it runs within Android Studio, which uses Layoutlib to render the Composable.
Layoutlib is a custom version of the Android framework specifically designed to function outside of Android devices. Its purpose is to provide a layout preview in Android Studio that closely resembles the rendering on a device, though it may not be an exact match.
When running a screenshot test of the Preview using different libraries, what you can do with ComposablePreviewScanner, the results vary:
Paparazzi generates a screenshot that reflects what we see in the Preview, which is inaccurate.
Compose Preview Screenshot Testing tool crashes while inflating the ViewHolder, as I reported in this issue. However, it is expected to display the same output we see in the Preview under normal circumstances. This is because it is also based on Layoutlib, the same as Paparazzi.
Roborazzi, which is JVM-based, as well as all instrumentation-based screenshot testing libraries, like Dropshots, Shot, or Android-Testify generate accurate screenshots identical to what renders on a device. None of them uses Layoutlib.
You can check it on your own in the following Github repository
Android Screenshot Testing Playground
under the module :recyclerviewscreen-previews
Therefore, this seems to be a Layoutlib related bug.
Since all instrumentation-based libraries render it properly, we can assume that if we run the Preview on a device, the result will be accurate.
And that’s where the Run Preview option comes to rescue
Run Preview
This option runs the Preview on an emulator or physical device, and it is very quick as well if the emulator is already running.
Since the Preview runs on an emulator or device, the real Android Framework is used to render the Preview, instead of Layoutlib. As a consequence, this is the most accurate option to check how the code inside your Previews is rendered.
In running the Preview on the device, the British flag is displayed disabled, as expected.
It comes with a drawback though: it renders using the configuration (i.e. locale, UI mode, font size, etc…) of the device instead of that of the Preview.
However, it is still more efficient than installing the app and navigating to the Screen where the previewed UI component is located.
Conclusion
We’ve explored how Compose Previews can render XML-based layouts, showcasing several advantages over traditional XML previews, including:
Verifying the binding logic for a View’s UI state
Previewing animations with interactive mode
Quickly running previews on a device to ensure their accuracy
We’ve also looked at how to efficiently use Compose Preview Interactive mode and Run preview options, their drawbacks, how rendering in Previews relates to the screenshot files generated by the most popular screenshot testing libraries, depending on whether they are Layoutlib based or not.
Using Compose Previews with XML-based Views opens new possibilities. Thanks to libraries like ComposablePreviewScanner, we can auto-generate Screenshot tests from Previews and run them with the screenshot testing library of our choice. This empowers us to:
Smoothly migrate our UI from XML-Based layouts to Jetpack Compose
Bring Composable Preview Driven Development to XML-based Views
These tools not only save time but also help us adopt better UI development practices.
You can find the runnable code examples from this blog post in the Android Screenshot Testing Playground repository.
Links
Repo with executable screenshot test examples for Compose Previews of XML-Based layouts:
Android Screenshot Testing Playground under:recyclerviewscreen-previews
Library to help auto-generate screenshot tests from previews with any screenshot testing library:
Composable Preview Driven development (Droidcon Lisbon 2024)
Special thanks to Pablo Gallego for reviewing this blog post!
If you are interested in the content I publish, follow me on Bluesky, Twitter or LinkedIn!