Jetpack Compose Previews are a great tool for rapidly iterating over UI design. Understanding their underlying mechanics is optional for everyday tasks but invaluable when facing uncommon problems.
In this article, we’ll explore how Jetpack Compose Previews work. We’ll also cover how to run them using adb
, the nuances in a multi-modular project, things we can customize, and even how we can preview a non @Preview
annotated composable.
At the end of this article, you’ll find a complete sample project and a few practical use cases where this knowledge proves useful.
Composable preview modes
We’ll categorize preview inspection into two modes:
- Design inspection through the Android Studio design window.
- Runtime inspection by running previews into an emulator or device.
Note: This article focuses exclusively on runtime inspection mode.
How do runtime previews work?
The core component and heart of runtime previews is the androidx.compose.ui.tooling.PreviewActivity
. This activity is responsible for hosting the composable annotated with the @Preview
annotation, passing its @PreviewParameter
data, and setting up the preview appearance according to the properties specified in the @Preview
annotation.
PreviewActivity
lives in the androidx.compose.ui:ui-tooling
artifact, which we usually add as the Gradle dependencies:
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
These contain the preview annotations and the required classes that support the Jetpack Compose Preview tooling.
Runtime previews are instrumented APKs
Whenever we run a preview through the run button on the left, it triggers a compilation for the androidTest
source set of the module where the preview is defined:
You can verify this by checking the executed Gradle tasks in the build output tab of Android Studio:
The output of the :assembleDebugAndroidTest
task is an APK generated for instrumentation. Anecdotally, this APK is used to run the preview; however, there is an exception when the preview is run through the :app
module, in which case the APK won’t be used. Since this exception does not significantly impact the overall process, we will assume that the APK is used consistently.
Lastly, Android Studio will install the instrumented APK and launch the PreviewActivity
, passing information about which composable preview to render.
Launching ModuleComposablePreview on 'Pixel 7 API 34'.
Starting: Intent {
act=android.intent.action.MAIN
cat=[android.intent.category.LAUNCHER]
cmp=me.jansv.previewinspect.module.test/androidx.compose.ui.tooling.PreviewActivity (has extras)
}
Open logcat panel for emulator Pixel 7 API 34
Connected to process 7329 on device 'Pixel_7_API_34 [emulator-5554]'.
Inspecting the AndroidManifest.xml of the preview
Running the ModuleComposablePreview
results in the generation of the APK module/build/intermediates/apk/androidTest/debug/module-debug-androidTest.apk
.
The
ModuleComposablePreview
is located in the:module
of the sample project.
By inspecting the AndroidManifest.xml
of this APK, we can uncover the following details:
- The
instrumentation
tag is present. Even though previews are compiled into an instrumented APK, they run as normal applications, meaning that any instrumentation customization won’t be used:
<instrumentation
android:label="Tests for me.jansv.previewinspect.module.test"
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="me.jansv.previewinspect.module.test"
android:handleProfiling="false"
android:functionalTest="false" />
- By placing an
AndroidManifest.xml
under themodule/src/androidTest
source set, you can customize certain aspects of the preview, such as theApplication
instance or the app’s label:
<application
android:label="Module Label"
android:name="me.jansv.previewinspect.module.ModuleInstrumentedApplication"
android:debuggable="true"
android:extractNativeLibs="false">
...
</application>
- The APK does not include an activity with the
android.intent.action.MAIN
intent action, meaning it won’t appear on the device’s home screen.
These insightful hints suggest we can perform a preview run entirely manually, so let’s find out how!
Job Offers
Running previews without Android Studio
Using the adb
command-line tool, you can run a composable preview without Android Studio.
The following steps use the sample project as a playground
Step 1: Generate the preview instrumented APK
./gradlew :module:assembleDebugAndroidTest
Once complete, the task will generate the APK at: module/build/outputs/apk/androidTest/debug/module-debug-androidTest.apk
, which we will install as the next step.
Note: This APK is located in a different path than the one generated by Android Studio.
Step 2: Install the APK in a device or emulator.
adb install -r -t \
module/build/outputs/apk/androidTest/debug/module-debug-androidTest.apk
Since this APK does not have an activity with the android.intent.action.MAIN
intent action, it will not appear on the device’s home screen.
Step 3: Launch the preview
Until now, we haven’t discussed how PreviewActivity
determines which Jetpack Compose preview to render. The activity requires two intent extra parameters to specify the composable to preview:
- composable: A fully qualified path to the
@Preview
composable function to render. - parameterProviderClassName: A fully qualified class name of the
PreviewParameterProvider
subclass used to populate the preview parameter annotated with@PreviewParameter
.
PreviewActivity
uses these intent extras to bring these entities to life through reflection.
The preview we want to launch is defined as follows:
// ModuleComposable.kt
package me.jansv.previewinspect.module
@Composable
fun ModuleComposable(text: String? = null) { ... }
@Preview
@Composable
fun ModuleComposablePreview() = ModuleComposable()
@Preview
@Composable
fun ModuleComposablePreview(
@PreviewParameter(TextParameterProvider::class) text: String,
) = ModuleComposable(text = text)
class TextParameterProvider : PreviewParameterProvider<String> {
override val values = (1..5).map { "Param #$it" }.asSequence()
}
The fully qualified path for the ModuleComposablePreview
is: me.jansv.previewinspect.module.ModuleComposableKt.ModuleComposablePreview
.
With this, we can finally launch the preview:
adb shell am start \
-n me.jansv.previewinspect.module.test/androidx.compose.ui.tooling.PreviewActivity \
-e composable "me.jansv.previewinspect.module.ModuleComposableKt.ModuleComposablePreview"
With this command, we have started the PreviewActivity
as a component of the installed APK. The package name of this APK is me.jansv.previewinspect.module.test
which combines the module’s namespace with the .test
suffix typically added to instrumented APKs.
This is good 🎉 but let’s push it a bit more…
Additionally, you can run the ModuleComposablePreview
with a parameter provider class passing in the parameterProviderClassName
:
adb shell am start \
-n me.jansv.previewinspect.module.test/androidx.compose.ui.tooling.PreviewActivity \
-e composable "me.jansv.previewinspect.module.ModuleComposableKt.ModuleComposablePreview" \
-e parameterProviderClassName "me.jansv.previewinspect.module.TextParameterProvider"
And yet a bit more…
How about non @Preview composables?
It turns out you can leverage this method to preview a composable that is not annotated with @Preview
. For example, you can use the same command to preview ModuleComposable
.
adb shell am start \
-n me.jansv.previewinspect.module.test/androidx.compose.ui.tooling.PreviewActivity \
-e composable "me.jansv.previewinspect.module.ModuleComposableKt.ModuleComposable"
Previews in a multi-module setup
Some considerations to keep in mind regarding the differences between running previews in the :app
module versus a separate module are:
- The instrumented APK is relevant only for modules that don’t produce an APK, such as those applying the Android library plugin. Since this is not the case for the
:app
module, the default APK is used instead. - Because the instrumented APK generated for the
:app
module is not used for previews, no customizations made in theapp/src/androidTest
source set will be applied. - Previews for library modules can be customized through
<module>/src/androidTest
. For example, you can modify theApplication
instance, thePreviewActivity
theme, the label, and more. - If the module you run the preview from introduces
AndroidManifest.xml
placeholders that are only provided through the:app
module, the module will need to supply them for the instrumented APK as well for the compilation to work. You can leverage Gradle to supply them conditionally:
// module's build.gradle.kts
androidComponents {
onVariants(selector().withBuildType("debug")) {
it.androidTest?.manifestPlaceholders?.put("placeholder", "value")
}
}
Conclusion
Jetpack Compose Previews are powerful for inspecting and refining your composables. This deep dive covered running previews with adb
, understanding their compilation, and previewing composables without the @Preview
annotation.
Insights into handling multi-module setups and customizations provide a comprehensive understanding to maximize their utility. With this knowledge, you can streamline your development process and tackle complex preview scenarios confidently.
Thanks for reading and I hope you found this guide useful 🙌🏼
Sample app
https://github.com/janselv/JetpackPreviewInspectSample?source=post_page—–57601ea32ebc——————————–
This article is previously published on proandroiddev.com