Android development has come a long way since it emerged. New technologies and approaches are being developed, and old proven solutions are being improved.
Recently, Jetpack Compose has been gaining popularity among developers. This is a modern UI-building tool utilizing a declarative approach. In this context, the advantage of a declarative approach is that it requires less code compared to XML. This increases development speed and reduces the number of errors.
The framework itself works fine. But as usual, problems arise at the intersection of codebases. Suppose you already have a functioning application that has been around for many years, it is debugged and tested.
As a rule, in such cases, new technologies are tested in production as minor features to collect statistics and evaluate their value for the product. In such situations, the interoperability capabilities of the framework come to the rescue. Interoperability makes it possible to use multiple approaches to UI development within a single application.
In this article, I will cover two examples of working with the interoperability feature using the example of a side menu — in both the optimal and not-so-optimal ways. From the non-optimal scenario, we learn what to avoid and identify red flags that alert you if you are heading in the wrong direction. In the optimal scenario, we will take any drawbacks into account and correct them by analyzing prior experience.
I chose the application side menu as an example because it is a complex task. To solve it, you will need, inter alia, to work with gestures, monitor the state of the menu, and tackle other tasks. As a result, we want to get a side menu that can be pulled out from the left edge or opened with a button from the toolbar. At the same time, the gesture of opening the menu should not interfere with scrolling the horizontal meme feed and should be as intuitive as possible for the user. Inside the menu, there should be a list of items.
When you start exploring the subject, you will come across a description of the interop mechanism https://developer.android.com/jetpack/compose/migrate/interoperability-apis and find that a ready-made side menu is already in the standard components. Great, let’s try to integrate it into our codebase.
Our app already has a basic activity that came with legacy code, so let’s start with that.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragmentFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/adFrame" />
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
layout="@layout/ad_frame" />
</RelativeLayout>
The activity above is the base we already have, we need to keep it, and rewriting it is not an option, as anyone who has worked with legacy code knows. This task will require lots of development resources, and it is almost impossible to persuade managers that this should be done. Inside the fragmentFrame, we have a feed with content, and ad_frame is reserved for a banner.
To add the menu, let’s use interoperability and add our view to the activity.
class SimpleMenuViewHolder(activity: Activity) {
private val menuView: View = ComposeView(activity).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { composeMenu() }
}
private var rootView: ViewGroup? = null
fun attach(rootView: ViewGroup) {
this.rootView = rootView
rootView.addView(menuView)
}
fun detach() {
rootView?.removeView(menuView)
}
@Composable
fun composeMenu() {
ModalDrawer(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding(), drawerContent = { DrawerContent() }, content = {})
}
@Composable
fun DrawerContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
.padding(8.dp)
.background(Color.DarkGray), contentAlignment = Center
) {
Text(text = "hello world", color = Color.White)
}
}
}
Launching the application, we will see the following result.
At first glance, everything seems to work, the gestures for opening the menu work, the menu is displayed, and everything looks good, but the feed scroll does not work for some reason. Let’s get to the bottom of this.
The Layout Inspector immediately highlights the problem — our root view composeMenu overlays the content.
Let’s think about how we can get out of this situation. Hiding the composeMenu when the side menu is closed seems like a reasonable solution. If we try this trick, the feed will work properly after closing the menu, but the menu won’t open anymore. This happens because composeMenu is an important element — it catches and handles swipes. Okay, then we don’t have to hide this element completely, but leave about 20dp on the left side. Unfortunately, this won’t work either because the menu will only slide out by 20dp.
It is clear that we are heading in the wrong direction, and there are too many unnecessary issues with non-obvious ways to solve them.
Let’s try to approach this from a different angle.
Looking for a workaround, it will be useful to remember how similar functionality was implemented before Jetpack Compose was introduced.
Navigation Drawer is a component that appeared back in Android 4.0. According to the documentation, the recommended way to create a menu in Android is by using an XML file with the root element <menu>. However, there is an alternative approach. As navigation inherits from ViewGroup, it has the addView method. And we will use it to connect the Compose layout.
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawerLayoutMenu"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragmentFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/adFrame" />
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
layout="@layout/ad_frame" />
</RelativeLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationViewMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"/>
</androidx.drawerlayout.widget.DrawerLayout>
Now let’s create and connect a Compose View to the Navigation View, but this time, we will pass drawerLayoutMenu to the rootView.
fun attach(rootView: ViewGroup) {
this.rootView = rootView
navigationView = rootView.findViewById<ViewGroup?>(R.id.navigationViewMenu)
navigationView?.addView(menuView)
}
The good thing is that we do not need the modalDriver now — its function will be performed by the DrawerLayout.
@Composable
fun composeMenu() {
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
DrawerContent()
}
}
@Composable
fun DrawerContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
.padding(8.dp)
.background(Color.DarkGray), contentAlignment = Center
) {
Text(text = "hello world", color = Color.White)
}
}
After launching the application, let’s make sure that everything works as it should. The menu opens now, and you can view the feed.
A similar approach, mixing new and old technologies, can be used in any situation when you need to link existing XML code and Compose components. For example, in BottomSheet or in dialogs. The idea is the same: we take an xml component and extend it with Compose code.
In conclusion, interoperability between Jetpack Compose and XML gives a powerful tool to developers. You can us it to adjust the balance between different approaches and achieve the best results. It seems that there should be more and more of such tasks over time, as the popularity of the new framework grows, and the old code still needs support.
Certainly, if it is possible, and you have time to rewrite everything from scratch or extract the functionality into a separate module, this should be done, but unfortunately, this is not always possible.
We plan to continue our growth and development by entering new markets and finding new business niches. Take a look at open positions. Perhaps there is one that is right for you!
If you know a passionate Software Developer who's looking for job opportunities, e-mail us at job@fun.co. In case of successful recommendation you will get a $3000 reference fee.