Jetpack Compose is designed with View interoperability in mind. This means that existing View-based libraries can readily be used in Compose. However, considering how View-based libraries are used in Compose can improve support for Compose. For library authors, this can inform how you design your APIs, and further, how you may want to provide explicit support for Compose through additional libraries.
In this blog post, I’ll cover how to think about View APIs from the Compose perspective, and how you can wrap existing View-based libraries in Compose.
The levels of support for Compose define the different options available for View-based library authors to add support for Compose.
Levels of support for Compose include the following:
No Support: Your View-based library can be consumed in Compose but developers have to think in both Compose and Views at the same time. Developers need to use available interoperability APIs to use Views in your library in their Compose app.
Compose Support with Wrapper: Your library can be consumed in Compose using composables provided in a Compose library wrapping your View-based library. Your wrapper Compose library handles interoperability APIs so that developers can use composables directly.
Full Compose Support: Your library can be consumed in Compose using composables provided in an entirely Compose-written library. Any shared code by the View and Compose libraries can be extracted as another dependency that both libraries depend on. As an author, feature development and maintenance of your library takes advantage of the benefits of Compose.
Say you are the author of a library that provides a custom View called DropDownCheckboxMenuView, which is a UI component that can display a dropdown list of items that can be toggled when clicked.
The API for DropDownCheckboxMenuView looks something like the following:
a setter called setItems to set the list of items to display on the menu
a getter called getSelectedItems to get the list of selected items on the menu
a setter called setOnItemToggled to set a lambda to be invoked when an item is toggled between selected and unselected states due to user clicks
Without explicitly adding support for Compose, your consumers would have to use the AndroidView interop API (or AndroidViewBinding if view binding is enabled) to use your View in Compose, like so:
On the other hand, by explicitly adding support via wrapper or through full Compose support, consumers can use a composable directly — no need to interact with View or interop code. Much simpler!
Below are some of the benefits of adding explicit support for Compose for your library.
The rest of this post covers how you can improve your library’s support for Compose by rethinking how you design View APIs and how to build a composable wrapper for your View. It will also cover the state management differences between Views and Compose, and how adopting the Compose way of thinking (state as the source of truth) can help both your View and Compose users. Beyond creating a wrapper, it would be desirable to eventually add full support for Compose so that authors can take advantage of the benefits of Compose while continuing to develop their library.
Using View-based libraries in Compose is not any different from using Views in Compose — through the interoperability APIs.
Using the same DropDownCheckboxMenuView mentioned above, to be able to use this component in Compose, you would use the AndroidView (or AndroidViewBinding if you are using view binding) to render this View in Compose. Additionally, to improve the usability of this View throughout your Compose codebase, you can write a wrapper composable called DropDownCheckboxMenu so that you only have to write interoperability code once — within the implementation of the DropDownCheckboxMenu composable.
Generally, configurable parameters for the underlying View should be exposed as parameters to the composable wrapper so that those properties can be modified in the underlying View. When parameter values change, you can then update the corresponding View’s properties in the update lambda of the AndroidView.
So far, this all looks good. But how can callers retrieve the set of selected items on the DropDownCheckboxMenuView? That is, how should getSelectedItems be exposed through the wrapper?
One technique is to use the state holder pattern and expose a read-only state called selectedItems which updates as items are selected by the user. You can maintain this state within the wrapper and set a different onItemToggled listener to the View, which will pass through item toggles to the composable-provided onItemToggled. But before going down this path, let’s cover how state ownership differs between Views and Compose.
Traditional Views are stateful. They maintain and manage internal states and perform mutations to those states based on interactions. For example, the CheckBox View widget toggles between check and unchecked states when a user click occurs. Events emitted to listeners from Views correspond to internal state already having changed (i.e. OnCheckedChangeListener for CheckBox). In other words, the source of truth of a View’s state can be internal to the component.
In contrast, Compose toolkit-provided composables are stateless. This means that the state provided into composables is the source of truth and entirely controls them. For example, the Checkbox provided in the Material3 library accepts a checked boolean parameter. By default, user clicks will not toggle the checkbox — this must be programmatically handled by modifying the state directly by listening to events, and modifying state when an event occurs, like so:
Given these different state ownership models, how can View authors think about designing APIs to be supported effectively in Compose?
The answer: design View APIs the Compose way!
Just because traditional Views were designed this way, it doesn’t mean Views need to maintain internal state. The Compose way of thinking can also be applied to designing View APIs.
The Compose way of thinking can also be applied to designing View APIs.
Revisiting the CheckBox View widget, if we were to design this from scratch it could very well have been written as a stateless component. For the existing CheckBox View widget, that would mean preventing the check state from toggling when clicked. Implementation-wise, this would mean that CheckBox should be refactored such that toggleCheck is not invoked when clicked.
Refactoring Views to be stateless allows composable wrappers to be written idiomatically — the state passed into them entirely controls the various states components can be in.
Going back to the hypothetical DropDownCheckboxMenuView, following this guidance would mean refactoring the API such that selected items in the menu would have to be explicitly provided. Additionally, the semantics of the onItemToggled listener changes. The listener would be invoked when an item is clicked, but the View would not internally mutate the set of selected items — that should be up to the caller.
With this change, we can now update the DropDownCheckboxMenu composable’s API to match how Compose consumers would expect to interact with this component. Another added benefit to this is that a stateless View becomes easier to test as you can provide inputs to it directly and write assertions to verify the expected output.
While refactoring Views should be your first option for improved Compose support, refactoring existing Views may introduce breaking changes and so this strategy needs to be taken into careful consideration. If refactoring is not an option for you, consider using the state holder pattern to synchronize on state changes. With this approach, you can maintain an internal copy of the View’s internal state, and expose an external property for it.
View APIs designed with Compose in mind ease consumption in Compose. But, if you choose to release your own wrapper library accompanying your View library, it’s important to design your composable wrapper in an idiomatic way. Designing your API idiomatically also ensures that should you change your implementation from a wrapper to a full Compose implementation, you can elect to keep the same API surface while just replacing the implementation.
For example, your composable should:
Accept and respect a Modifier parameter so that it can be customized with built-in Modifier functions
Provide defaults for optional parameters
Offer a content slot if the component can have a customizable hierarchy
This list of design considerations is not comprehensive. To learn more about designing APIs, see Compose API guidelines.
To be able to see your Views and corresponding wrapper with composable previews via @Preview, ensure that your View at minimum accepts a Context and an AttributeSet. Doing so allows Android Studio to create an instance of your View to be used in the design view to preview your composable.
If for any reason your library cannot meet this requirement, or fails to render in preview mode due to known preview limitations, you can use LocalInspectionMode.current in your composable wrapper, or View.isInEditMode in your View code. With both APIs, you can skip executing failing code paths and instead perform the necessary fallback actions; for example, by displaying a placeholder image instead of a network-fetched image.
Once you have sufficient coverage of composables for your View-based libraries, you can release it as a separate artifact from the View library. A common naming convention is to suffice the Compose library with -compose (e.g. my.lib.library-compose) although that’s not a necessary requirement. Releasing a separate artifact is preferable so that you are not introducing Compose as a transitive dependency to View consumers who might not have migrated to Compose just yet.
If you are a View-based library author, consider improving the usability of your library in Compose by first evaluating your View API. If possible, modify your Views to be stateless so that they translate well when used via interoperability APIs in Compose.
To further improve your support of Compose so that consumers don’t have to write interop code, consider providing a Compose wrapper around Views in your library. Doing so allows you to continue supporting View consumers by keeping the source of truth in Views, while also providing a much better developer experience for Compose consumers. Down the road, you can transition to providing full Compose support for your library so that you too can take advantage of the benefits of Compose.
Got any questions or feedback? Leave a comment below!
The following post was written based on learnings from developing Maps Compose, a library that provides composables for the Maps SDK for Android. Thanks to Adam Powell, Florina Muntenescu, Jolanda Verhoef, Rebecca Franks, and Lauren Ward for their reviews.