Jetpack Compose Preview are great, I’ve blogged about them before and also delivered talks on the subject. But sometimes you can’t, or don’t want to, use them for specific UI components. That does not mean you have to miss out on the usefulness of Previews!
One of the reasons why you might not be able to create a Preview for a composable is when you have an image that is downloaded from the internet. If you haven’t set up your image error handling correctly you’ll just see a blank space (or perhaps even a missing space) when your Preview renders.
Another occasion might be when you are including an API that requires initialisation somewhere in the rest of the app (which doesn’t happen when Previews are rendered). One of the times I have experienced this is when my Composable includes Firebase.analytics.logEvent
and you will get a Render problem when displaying your Preview:
If you check what the problem is, you may see something like:
Failed to instantiate a Composition Local
This preview was unable to find a CompositionLocal. You might need to define it so it can render correctly.
Failed to instantiate a Composition Local
This preview was unable to find a CompositionLocal.
You might need to define it so it can render correctly.
If you check the exception you will see:
java.lang.IllegalStateException: Default FirebaseApp is not initialized
in this process null. Make sure to call FirebaseApp.initializeApp(Context)
first.
at com.google.firebase.FirebaseApp.getInstance(FirebaseApp.java:179)
Removing the Firebase call will fix the issue and the Preview will display.
Ideally, our Composables should be stateless and have all the information passed into them so Previews will not require internet or API access but often, this is just not practical especially if you want to initiate analytics calls.
And sometimes, you just want to show something different in your Preview than what you want to show in the live version because it makes more sense (although, this shouldn’t take the place of using well-crafted PreviewParameters and may cause bloat in your composables — so only use it when you have a good reason).
Enter LocalInspectionMode.
LocalInspectionMode
provides a CompositionLocal
(part of the data that is passed into the composable) which you can query to determine if this composable instance is locally inspectable — which is true if this composable is in a Preview.
Then, to check if your composable is in a preview you can use:
LocalInspectionMode.current
You can then use this to exclude code from being run if the composable is being run inside a Preview or add extra code if needed.
A simple example of conditionally including some text:
Job Offers
@Composable | |
fun TextComponent() { | |
Column( | |
verticalArrangement = Arrangement.spacedBy(16.dp), | |
modifier = Modifier.padding(16.dp) | |
) { | |
Text( | |
if (LocalInspectionMode.current) { | |
"This is only displayed in a preview" | |
} else { | |
"This is only displayed in live code" | |
} | |
) | |
Text("This should always be displayed") | |
} | |
} | |
@Preview(showBackground = true) | |
@Composable | |
fun TextComponentPreview() { | |
TextComponent() | |
} |
When we look at the Preview versus the emulator:
We can see that text is different based on where it is displayed.
Some other examples:
@Composable | |
fun ImageComponent() { | |
Column( | |
verticalArrangement = Arrangement.spacedBy(16.dp), | |
modifier = Modifier.padding(16.dp) | |
) { | |
val imageUrl = "https://img.goodfon.com/original/800x600/e/12/voda-more-aysberg-nebo.jpg" | |
if (LocalInspectionMode.current) { | |
// Show this image from the resources rather than loading an image from the internet | |
Image( | |
painter = painterResource(id = R.drawable.iceberg_preview), | |
contentDescription = null, | |
contentScale = ContentScale.FillWidth, | |
modifier = Modifier.fillMaxWidth() | |
) | |
} else { | |
// Show this image in the live version | |
AsyncImage( | |
model = imageUrl, | |
contentDescription = null, | |
contentScale = ContentScale.FillWidth, | |
modifier = Modifier.fillMaxWidth() | |
) | |
} | |
} | |
} | |
@Composable | |
fun AnalyticsComponent() { | |
if (!LocalInspectionMode.current) { | |
// Firebase is not initialised in Previews and will cause an error | |
Firebase.analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, Bundle()) | |
} | |
Column(modifier = Modifier.padding(16.dp)) { | |
Text("This is a component that contains an analytics call that runs on display") | |
} | |
} | |
@Composable | |
fun ViewModelComponent() { | |
val viewModel: MainViewModel = hiltViewModel() | |
val list: List<String> = if (!LocalInspectionMode.current) { | |
viewModel.someViewModelFlow.collectAsState(initial = emptyList()).value | |
} else { | |
// For Previews, we want to show the flow in a different state | |
// In this case, it would be better to create a composable from the actual content | |
// (the Row below) and pass in a list | |
listOf("d", "e", "f") | |
} | |
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { | |
Text("From the view model flow: ") | |
list.forEach { | |
Text(it) | |
} | |
} | |
} |
@HiltViewModel | |
class MainViewModel @Inject constructor() : ViewModel() { | |
val someViewModelFlow = MutableStateFlow(listOf("a", "b", "c")) | |
} |
As we can see, the previews are showing the alternate content:
Pretty easy!
You can find all the code I have shown above on my Github here:
This article is previously published on proandroiddev.com