airbnb / Showkase

πŸ”¦ Showkase is an annotation-processor based Android library that helps you organize, discover, search and visualize Jetpack Compose UI elements
https://medium.com/airbnb-engineering/introducing-showkase-a-library-to-organize-discover-and-visualize-your-jetpack-compose-elements-d5c34ef01095
Apache License 2.0
2.1k stars 107 forks source link

Add support for CustomPreviewAnnotations in Showkase 🚿 🌈 #303

Closed oas004 closed 1 year ago

oas004 commented 1 year ago

This was moved from #263 to avoid a lot of the heat after rebasing. Also fixed some of the stuff for the compiler args so that it works for KAPT as well.

Add support for CustomMultiPreviewAnnotations in Showkase.

This is the third PR in a series to add support for Custom MultiPreview Annotations. Referencing as a followup on #259 and #255.

By completing this should solve #233

resolves #233

What do I mean by Custom Preview Annotations?

This is an annotation in the form

@Preview(name = "Custom Preview One First", group = "Custom Previews")
@Preview(name = "Custom Preview One Second", group = "Custom Previews")
annotation class CustomPreview1
@Preview(name = "Custom Preview One First", group = "Custom Previews")
annotation class CustomPreview2

this can be used to annotate a composable function like you would do any other preview function in Compose like this:

@CustomPreview1
@Composable
fun CustomAnnotationPreview() {
}

This can also be combined like this:

@Preview(name = "Custom Preview One First", group = "Custom Previews")
@Preview(name = "Custom Preview One Second", group = "Custom Previews")
annotation class CustomPreviewOne

@Preview(name = "Custom Preview Two First", group = "Custom Previews")
annotation class CustomPreviewTwo

@CustomPreviewOne
@CustomPreviewTwo
@Composable
fun CustomAnnotationPreviewCombined() {
}

They should be able to have the custom annotations and the use of them in different modules in a project.

For more information, please see docs on tooling in compose here

Goal

The goal is to add support for this in Showkase. We would like showkase to act like the tooling in Android Studio does. That means that when Android Studio Preview would have generated a Preview, showkase should also generate a ShowkaseMetadata function.

KAPT vs KSP

Because of https://youtrack.jetbrains.com/issue/KT-49682/Support-JVM-IR-in-KAPT-stub-generation KAPT does not quite support repeatable annotations from Kotlin. This means that if you have an annotation class like:

@Preview(name = "Shape 100 by 100", group = "Shapes", widthDp = 100, heightDp = 100)
@Preview(name = "Shape 150 by 150", group = "Shapes", widthDp = 150, heightDp = 150)
annotation class CustomShape

This will be skipped by KAPT, but KSP will pick it up. However, if you have an annotation like:

@Preview(name = "Shape 100 by 100", group = "Shapes", widthDp = 100, heightDp = 100)
annotation class CustomShape

It will be picked up by both KSP and KAPT.

Important for KAPT users

You will need to provide a compiler arg in you module for the custom preview annotations that you are using and expecting to be picked up by Showkase. This can be done with the following code:

kapt  {
        arguments {
            arg("multiPreviewType","com.airbnb.android.submodule.showkasesample.LocalePreview")
        }
    }

It is important to remember to use the whole qualified name of the annotation, and not just the name.

Testing

Sample App

In the sample app I have introduced CustomShape from the same module

@Preview(name = "Shape 100 by 100", group = "Shapes", widthDp = 100, heightDp = 100)
@Preview(name = "Shape 150 by 150", group = "Shapes", widthDp = 150, heightDp = 150)
annotation class CustomShape

And FontPreview from the same module

@Preview(name = "Normal font size", group = "FontPreview", fontScale = 1f)
@Preview(name = "High font size", group = "FontPreview", fontScale = 1.5f)
@Preview(name = "Super High font size", group = "FontPreview", fontScale = 2f)
annotation class FontPreview

You can check this in the sample app if you build it with KSP.

Browser Test

In the browser tests I have introduced CustomTextPreview


@Preview(
    name = "Custom Text Dark",
    group = "Custom Text",
    uiMode = Configuration.UI_MODE_NIGHT_YES,
)
annotation class CustomTextPreview

There was already a composable to verify compilation from a pervious PR, so used that as well. Since the CustomTextPreview only had one Preview annotation, this was tested to be visible with KAPT as well.

I have done the same with the submodule in the browser test. However, here I introduced CustomSizePreview and CustomFontSizePreview and verified that it works with both KAPT and KSP.


@Preview(name = "CustomSize 100 * 100", widthDp = 100, heightDp = 100, group = "CustomSubmodulePreview")
@Preview(name = "CustomSize 200 * 200", widthDp = 200, heightDp = 200, group = "CustomSubmodulePreview")
annotation class CustomSizePreview

@Preview(name = "Custom Font Size 1.2f", fontScale = 1.2f, group = "Custom Size Submodule")
annotation class CustomFontSizePreview

Processor Test

In the processor tests, I have made tests that make sure all the correct metadata objects are created and that we are creating the files to store the custom annotations that the processor has registered. You can check these tests in showcase-processor-testing module.

anhanh11001 commented 1 year ago

Thanks a lot for this work @oas004 πŸ™ πŸ™ πŸŽ† 🍾

oas004 commented 1 year ago

Seems like the ui tests are crashing on sdk 26 default. For me it looks like the emulator is crashing before it starts the tests. Do u know how I can fix this @vinaygaba ? :)

anhanh11001 commented 1 year ago

@vinaygaba When this is merged, can you please also make a new release? This would help me with my work at work a lot! Thanks guys for this work πŸ™

oas004 commented 1 year ago

Fantastic job on this PR πŸ‘πŸ» This has been a long time coming but I'm so glad you were able to be persistent and took care of all the cases that we could think of.

Wow this is awesome! πŸ₯³ Thanks a lot for all the help and mentorship through the process! Learned a lot from this!

anhanh11001 commented 1 year ago

Hi @oas004, I've tested this and it works really well. One question related to stacked custom preview, would something like this work?


@Preview(details1)
annotation class CustomPreview1

@Preview(details2)
annotation class CustomPreview2

@CustomPreview1
@CustomPreview2
annotation class CentralizedCustomPreview
oas004 commented 1 year ago

Hi @oas004, I've tested this and it works really well. One question related to stacked custom preview, would something like this work?


@Preview(details1)
annotation class CustomPreview1

@Preview(details2)
annotation class CustomPreview2

@CustomPreview1
@CustomPreview2
annotation class CentralizedCustomPreview

Hi @anhanh11001 :) So glad to hear that it is working! The case that you are describing should work for KSP. I think we have a test case for this here:https://github.com/airbnb/Showkase/blob/master/showkase-processor-testing/src/test/resources/ShowkaseProcessorTest/composable_function_with_multiple_repeatable_custom_preview_annotation_generates_output/input/Composables.kt that is very similar :) Is it not working in your project?

anhanh11001 commented 1 year ago

I see, I also use ksp. Then it could be something related to my setup. I'll double check. Thanks for confirming

oas004 commented 1 year ago

I think this should work out of the box if you are using KSP. I will try to check this out as well!

takahirom commented 1 year ago

I had the same problem and I found that we should apply ksp to the module we have the custom preview annotation. https://github.com/DroidKaigi/conference-app-2023/pull/605/files#diff-deb2ae6119caad892229b16120b0d33fe23ff75d1430dfb235c463482f10c386R10