Jetpack Compose is the new favourite of most Android developers, if not all. It has completely changed way we design our UI, shifting from traditional XML to new declarative approach of writing code for UI in Kotlin using Composable functions.
Previews
To give a very basic overview of previews, unlike XML, we cannot directly see how our UI looks based on the code we have written, in Jetpack Compose. We use something called Previews, which are Composable functions written specifically for the purpose of viewing(or even interacting with) our UI in the editor, without the need for running it on a real device. It is very simple to create a Preview in Compose, simply annotate a Composable function with “@Preview” annotation and call the Composable function that you want to render on your editor or screen. Eg.
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
PreviewParameterDemoTheme { | |
Greeting(name = "viewers") | |
} | |
} | |
} | |
} | |
@Composable | |
fun Greeting(name: String) { | |
Text(text = "Hello $name!") | |
} | |
@Preview(showBackground = true) | |
@Composable | |
fun GreetingPreview() { | |
Greeting("Android") | |
} |
Looks very clean and simple. But why did I mention PreviewParamters? What is it and is there any problem with using Previews the way it is used above, that is solves? I will answer all these questions in this article also show how you can implement the same in your project.
What is the problem with using Previews this way?
Huge Data classes
Before jumping to what is a PreviewParameters, I think it is important to address what exactly are the issues that can faced with using Previews this way. I know you’re wondering what “this way” means here. This way means providing the data directly to the previews inside the Composable functions.
The above example was a simple one and there is absolutely no harm in using Previews like this if the data need by the Composable functions inside is as less as a single string or even a couple of other parameters. But have you wondered what if the data/model to be provided to the Composable is too big?? It will simply add a bunch of unnecessary lines of code probably in the same file where our original Composable lies, just to show some random data and verify our UI is looking as expected. Look at this screenshot for reference as an example.
It is like 20 lines of code just to show some random data and verify that our UI looks as expected. Now you may think the solution to this is to simply convert the sample data model to a function and use that function in our Preview. It is valid argument and works for this particular use case, something like this, where fakeEmployeeData() is a function returning sample Employee Data.
@Preview | |
@Composable | |
fun PreviewEmployeeInformation(){ | |
EmployeeInformationUI( | |
fakeEmployeeData() | |
) | |
} |
But the problem does not end here. Let’s cover some other use cases.
Multiple Previews
There can be n number of reasons why your Composable functions may require multiple Preview functions, to reflect how the UI behaves based on different data provided to the Composable functions. For example in our EmployeeInformationUI Composable(check in screenshot above) , we may need to show a different text when the employee has no previous companies and he is a fresher. The logic in actual Composable function may go like -:
if (employeeData.previousCompanies.isNotEmpty()) { | |
append("\nPreview Companies-: ") | |
for (company in employeeData.previousCompanies) { | |
appendWithStyle(text = "$company ") | |
} | |
} else { | |
append("\nNo Previous Companies") | |
} |
This is just one use case, there can be many others like to see how our screen behaves in case the address is too long? Does it move to next line or behaves abnormally, is there enough padding at the end of the line? By any chance is there any overlapping between texts due to error in logic. So for these reasons it is good to have multiple Previews. Now let’s say we have a similar use case in our Composable(the logic written above) that we need to show “No Previous Companies” in case the employee has no previous companies. So we can create 2 Previews, one with an employee having multiple previous companies and one with none. So we need to write 2 different previews with different data. Something like this.
As you can see, it works and we have now 2 Previews rendered on our screen. But you may also have noticed that it has significantly increased the lines of code in our file, almost 45 lines, just to show previews in our screen. And imagine 10 more previews like these. This code is responsible just for showing the Previews and has nothing related to the actual implementation of the Composable. This for sure does not look clean. Moreover our screen now is doing 2 things, providing the code for actual implementation of the Composables and we are also providing fake data to our Previews in the same screen, which increases the count of lines of code a lot. The solution we came up with earlier “But we can write functions for the same” may not work very well in this case. What if we had 10 different data models? Will we create 10 different functions to load different data? It is also not good to create n number of functions just to load different random data. So is there any better way we can do it?
PreviewParameters to the rescue
What if I tell you there is a class which we can use to not only delegate the responsibility to provide different data models, but also to create multiple previews using a “single Preview function”. Yes you heard it right and such a class, we can easily create using PreviewParameter<T> Interface, where T is a generic, accepting any type. And we can easily use such a class in our Preview using @PreviewParameter annotation.
In short, you can use @PreviewParameter to provide sample data for your Composables, instead of doing it all in actual screen/file which has the Composable or using n amount of different functions returning random data.
Let’s quickly check how we can use PreviewParameter in our use case for providing EmployeeData.
class SampleEmployeeDataProvider : PreviewParameterProvider<EmployeeData> { | |
override val values: Sequence<EmployeeData> = sequenceOf( | |
EmployeeData( | |
name = "John Rick", | |
id = 34, | |
address = "767, abc colony, xyz city", | |
dob = "01-01-1999", | |
bankDetails = BankDetails( | |
accountNumber = 1234, | |
ifsc = "IFSC", | |
city = "New York", | |
pinCode = 142991, | |
accountName = "John Sample Rick" | |
), | |
previousCompanies = listOf( | |
"Company1", | |
"Company2", | |
"Company3", | |
"Company4", | |
) | |
), | |
EmployeeData( | |
name = "John Show", | |
id = 34, | |
address = "This is a long address intentionally " + | |
"written t check how it behvaes in UI", | |
dob = "Unable to find address", | |
bankDetails = BankDetails( | |
accountNumber = 1234, | |
ifsc = "IFSC", | |
city = "Paris", | |
pinCode = 32321, | |
accountName = "John" | |
), | |
previousCompanies = listOf() | |
) | |
) | |
} |
Job Offers
The class implements an interface PreviewParameterProvider<T>, where T is a generic type parameter, in our case EmployeeData. It overrides the “values” field which is a “[Sequence] of values of type [T] to be passed as @[Preview] parameter.”
Even if we have a single data model, we still needs to pass it as a sequence as the values field, which we are overriding is a sequence. Now let us check how to use this class in our Preview function.
@Preview(showBackground = true) | |
@Composable | |
fun PreviewEmployeeInformation( | |
@PreviewParameter(SampleEmployeeDataProvider::class) employeeData: EmployeeData | |
) { | |
EmployeeInformationUI( | |
employeeData | |
) | |
} |
Yes it is that simple to use this class, just annotate it with @PreviewParameter and provide an argument of the type you used in the class. The best part this small 9 lines(or less) of code will create n number of Previews automatically where n is the number of objects provided in the sequence of the SampleEmployeeDataProvider Class, value field. Look at this screenshot for reference.
On right side is our SampleEmployeeDataProvider class, which implements the PreviewParameterProvider interface. And on right side, is our Screen which contains the Composable as well as the Preview for the Composable, we have just used @PreviewParameter annotation, mentioning our class name and object data type, and it automatically created 2 Previews based on this data, even without actually writing 2 different Preview functions.
So PreviewParameterProvider has helped to drastically reduce the number of lines of code in EmployeeInformationScreen and this code looks much much cleaner than it was before. Here is the comparison again. The end result is same in both the cases.
When to use PreviewParemeters and when not?
Everything has its pros and cons. The only trade off I think of using PreviewParameterProvider to provide sample data to Previews is that we need to create a separate class each time for each different model in our app that is required by a Composable Preview. It can significantly increase the number of files or classes in our project, if we start to do it for every single Preview. So I think we should use PreviewParameter carefully and only when we have either large data models or multiple previews or both. If we have a short data model and we do not need to create multiple previews for a given Composable(which is true for most of the times we write Previews), we can simply provide data directly in our Previews, instead of creating a separate class for the same. Note this is only my personal opinion and preference.
Thanks for investing your time to read this article and I hope you gained some knowledge around Previews and PreviewParameterProvider from the article.
This article was originally published on proandroiddev.com on March 06, 2022