Are you building a modern Native Android App with Jetpack Compose but struggling to integrate existing views, plugins, or features built with traditional Android Views? In this blog post, we’ll show you a solution to this problem by demonstrating how to convert your existing views into reusable composable functions (AKA composables), making it easier to integrate them seamlessly into your Jetpack Compose app.
Note: Throughout this blog post, we’ll utilize the Android “TextView” as a demonstration of integrating an Android View into a Jetpack Compose-based project. Though TextView is a basic component, the methods and techniques outlined can be applied to more complex views as well.
Let’s say we want to use Android’s TextView widget in our Compose-based project. We plan on using the TextView in numerous places. Since it’s not a composable, it would be nice to convert it to one and use that composable throughout our project instead. This allows us to use a simple Compose API instead of having to interface with the Android Views library every time we use the view, which would reduce a lot of boilerplate and duplicate code. Let’s build out this composable!
Building the composable
The first thing we’ll need to do is figure out how to convert a traditional Android View into a composable. Luckily this is quite simple with the use of the “AndroidView” composable from Android’s interoperability API.
This is a very simple implementation that shows the basic usage of AndroidView. The factory block expects a View (android.view.View) to be returned, so here we’re simply returning an empty TextView. But an empty TextView is pretty useless. How can we make it display some text? (Let’s also add some color to the text too).
Here, the TextView will display the text and color provided by the parameters of the composable. We hold a reference of the TextView in the factory block so that we can access its attributes and methods needed to alter the view before returning it. Pretty simple!
But wait… What if the text or color is changed in the parent composable? The example above will only show the text and color that were initially used when creating the composable, so when the values are changed in the parent the view will remain the same. How can we fix this?
Two things have been done here to allow for the TextView to be updated every time the inputs (text and color) are changed:
- Two new variables “currentText” and “currentColor have been introduced and are used to give this composable a state. The function “rememberUpdatedState()” is used to update these variables whenever their related parameter changes. “rememberUpdatedState()” also makes use of Compose’s “remember” API which allows for these variables’ values to persist through future recompositions, i.e., it gives the composable a state.
- The code to set/update the TextView attributes has been moved to the “update” callback so that it is called during every recomposition. This way, when the inputs (text and color) are changed, the TextView will be updated accordingly. We can leave certain things in the factory block that we only want called initially (e.g., we have left the text size being set there if that’s something we want to remain the same).
We now have a simple composable to use in our project whenever we want to use Android’s TextView. As more features from TextView are needed, the composable can be changed to accommodate them (e.g., allowing the textSize to be customized/altered).
Adding some complexity
As you can imagine, not all traditional views you’d want converted into a composable will be this simple. All we’ve done so far is provide the ability to set values in the TextView and have them change when the related composable parameter also changes. What if the view you are trying to implement has events that you want to listen to and expose? What if the view has functions that you want to call? Surely, we’d need a way to listen to these events and call these functions from the parent composable, but how can it be done? We’ll pretend the TextView has the following features that we want to be able to control from a parent of TextViewComposable:
Let’s say the TextView has an event called “abracadabra” which changes the TextView’s text to a random word whenever it is clicked on. Additionally, there’s a callback available at “TextView.abracadabraListener” that is called every time the abracadabra event is initiated, passing the new text value.
There’ll also be the ability to rotate the text using the following functions:
- TextView.rotateX(): Rotates text on the X axis
- TextView.rotateY(): Rotates text on Y axis
- TextView.reset(): Resets rotated text to the original state
How can we enable the TextViewComposable to expose the TextView “abracadabra” event? Fortunately, it’s quite simple:
Firstly, we need to create a parameter in “TextViewComposable” that will be used as a callback for when the event is made. This will allow us to transmit the event and value to the parent composable.
Next, we must listen for the abracadabra event and call the “onAbracadabra” callback within the listener.
To trigger the rotate functionality, it’s not as straightforward. One way we could do this is by creating a class that contains functions that represent the controls we’d like to expose. This class will also contain a list of listeners that can be added and removed from an instance of it. The idea here is that the “control” functions (rotateX(), rotateY(), reset()) will call the listeners’ associated function. Therefore, we can initialize the controller in the parent composable and call the control functions from there. The TextView will take this instance of the controller as a parameter and attach a listener to it to translate each controller function call to the associated TextView function call.
The controller class would look like this:
Next, in the “TextViewComposable” we add the controller as a parameter so that the parent composable can provide it. We also want to maintain its state by using the “rememberUpdatedState()” function.
Lastly, we’ll create a listener in the AndroidView factory block and add it to the controller. The listener will call the associated TextView functions.
The full composable with both the “Abracadabra” and “Rotate functionality” features (and a few new customizable attributes) will look like this:
So now, we have a decent composable built-out that allows us full control over the TextView! But there’s still something we can do to improve the code.
Making it maintainable and testable
As we add more functionality and features to our composable, or if we’re converting a much more complex view, the approach above may not be entirely feasible. It can lead to our composable being hundreds of lines long which would not make it very maintainable or easy to test.
One thing that can be done is to delegate the responsibility of updating the TextView to another class and call that class’s functions from the composable when we want to update the view. This way, we can break out the functionality that was once in the composable body into smaller functions that are easier to maintain and test.
Here’s an example of a class that can be used this way:
The class above is still quite simple, but it allows us to separate out the functionalities for controlling the TextView into separate functions. For a more complex view with more business logic required for certain operations, this can be important since this added modularity allows for easier testing.
The class above can be used in the TextViewComposable:
Since we’ve delegated the responsibility of updating the TextView to the TextViewUpdater class, the composable now just calls the functions within that class. Notice how we’ve reduced the length and complexity of the composable, making it more maintainable and readable.
In conclusion, we’ve demonstrated a method for converting traditional Android views into reusable composables using Jetpack Compose. While there may be other ways to accomplish this, we hope that this serves as a useful starting point for your own projects. If you’d like to test this out yourself, a complete Android project including the example code used in this blog post is available on GitHub at the following link: https://github.com/skyler-de/AndroidViewsInComposeExample
Indellient is a Software Development Company that specializes in Data Analytics, Cloud Services, and DevOps Services.
We’re dedicated to creating a fruitful, inclusive, and memorable work environment for all of our team members. Check out open opportunities on our Careers page.