13.
Adding View Compatibility
Written by Denis Buketa
Congratulations on reaching the last chapter of this section!
So far, you’ve learned a lot about Jetpack Compose. In the book’s first section, you learned about basic composables. In the second, you saw how to use Compose when building a real app. In the third section, you learned how to build a more complex UI and how to make simple but beautiful animations.
In this chapter, you’ll finish your journey by learning the basic principles of combining Jetpack Compose and the old View framework, which can coexist in the same codebase. That knowledge will make it easier for you to gradually migrate your apps to Jetpack Compose.
Introducing the Chat Screen and the Trending View
To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.
Then, navigate to 13-adding-view-compatibility/projects and select the starter folder as the project root. Once the project opens, let it build and sync and you’re ready to go! You can see the completed project by skipping ahead to the final project.
Also make sure to clean the app’s storage, before running the project.
For this chapter, we’ve added a few things to the starter project.
These additions include a new Chat screen, which uses the old View framework. Access it by clicking the new Chat icon in the top bar of the Home screen, as you can see in the previous image.
If you tap that button, you’ll open the following screen:
In this chapter, you’ll replace the Start Chatting button with a button made up of composable functions. To see the final implementation, check out screens/ChatActivity.kt and res/layout/activity_chat.xml. You’ll also build a Trending Today component that will be the first item on the Home screen’s list.
You’ll build the entire component using composables, except for one piece of functionality: Trending topic. This will use the View framework in views/TrendingTopicView.kt and res/layout/view_trending_topic.xml.
Next, you’ll see how Jetpack Compose and the View framework work together.
Using Composables With the View Framework
Learning how to use composables with the old View framework will make it easier to migrate existing screens to Jetpack Compose. You’ll start with small components and gradually migrate the whole screen.
Furthermore, some components are easier to make using Jetpack Compose. There’s no reason not to use those components when the framework allows it. :]
You’ll start by implementing the Start Chatting button using Jetpack Compose.
Implementing the Start Chatting Button
Open ChatActivity.kt and add the following code below ChatActivity:
@Composable
private fun ComposeButton(onButtonClick: () -> Unit) {
val buttonColors = buttonColors(
backgroundColor = Color(0xFF006837),
contentColor = Color.White
)
Button(
onClick = onButtonClick,
elevation = null,
shape = RoundedCornerShape(corner = CornerSize(24.dp)),
contentPadding = PaddingValues(
start = 32.dp,
end = 32.dp
),
colors = buttonColors,
modifier = Modifier.height(48.dp)
) {
Text(
text = "Start chatting".toUpperCase(Locale.US),
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
}
}
@Preview
@Composable
private fun ComposeButtonPreview() {
ComposeButton { }
}
To break down the code, you added a root composable, ComposeButton(), for the button. You then exposed onButtonClick so it can react to clicks.
To emit the button UI, you used Button() from the material composables. You specified the background and content color with buttonColors() and passed in the backgroundColor and the contentColor. You also set the shape and styled the text to match the current implementation.
You also added @Preview to visualize how your button looks in Android Studio.
For this to work, you have to add the following imports as well:
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults.buttonColors
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.util.*
Build the project and check the preview panel. You’ll see something like this:
Adding ComposeButton to ChatActivity
Next, you have to replace the old implementation with the composable button. Open activity_chat.xml in the layout resource folder and replace the old AppCompatButton with the following:
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle" />
You also have to update ChatActivity. In ChatActivity.kt, replace onCreate() with the following code:
override fun onCreate(savedInstanceState: Bundle?) {
// Switch to AppTheme for displaying the activity
setTheme(R.style.Theme_JetReddit)
super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.backButton.setOnClickListener {
finish()
}
binding.composeButton.setContent {
MaterialTheme {
ComposeButton { showToast() }
}
}
}
Don’t forget to add the following import for MaterialTheme:
import androidx.compose.material.MaterialTheme
The old button’s implementation used AppCompatButton in activity_chat.xml. Here, you replaced that with ComposeView, which is a View that can host Jetpack Compose UI content. Compose needs a host Activity or Fragment to render UI.
setContent() supplies the content composable function for the view in ChatActivity’s onCreate().
ComposeView requires that the window it’s attached to contains a ViewTreeLifecycleOwner. This LifecycleOwner disposes the underlying composition when the host lifecycle is destroyed. That allows you to attach and detach the view repeatedly while preserving the composition.
Build and run the app. Open the Chat screen and check out your new button. The button looks and acts the same as the old one did.
Great job! That was easy, wasn’t it? With ComposeView, you can gradually migrate any screen that uses the old View framework. Once you migrate the layout, you can even remove the hosting Activity or Fragment and implement the whole screen using just Jetpack Compose.
But the great thing is that you can mix and match the two frameworks however you like it.
Using View With Jetpack Compose
Now, reverse the situation. Imagine that you decided to implement a screen or a component using Jetpack Compose, but for some reason — time restrictions, framework support, etc. — it would be easier to reuse a custom View you already implemented in that new screen. Well, Jetpack Compose allows you to do that! :]
In this section, you’ll implement the component for Trending Topics.
For this component, TrendingTopicView has been prepared for you. It represents one item in the scrollable list of topics. To see the implementation, check TrendingTopicView.kt and view_trending_topic.xml.
Before you implement that component, you need to make a few modifications to HomeScreen.kt.
Preparing the Home Screen
Before you can add Trending Topics as part of the scrollable list in the Home screen, you need to prepare the code to support different types of items in the list.
Open HomeScreen.kt and add the following code at the bottom:
private data class HomeScreenItem(
val type: HomeScreenItemType,
val post: PostModel? = null
)
private enum class HomeScreenItemType {
TRENDING,
POST
}
private data class TrendingTopicModel(
val text: String,
@DrawableRes val imageRes: Int = 0
)
You added HomeScreenItem, which represents one item in the list, then defined its type with HomeScreenItemType. If the item’s type is POST, the post parameter will contain data for the post. Otherwise, it will be null.
You also added TrendingTopicModel, which contains the data for one topic item that will be visible in the Trending Topics component.
To finish, add one additional import:
import androidx.annotation.DrawableRes
Adding TrendingTopic
Next, you’ll create a composable to represent one topic item. Add the following code below HomeScreen():
@Composable
private fun TrendingTopic(trendingTopic: TrendingTopicModel) {
AndroidView({ context ->
TrendingTopicView(context).apply {
text = trendingTopic.text
image = trendingTopic.imageRes
}
})
}
@Preview
@Composable
private fun TrendingTopicPreview() {
TrendingTopic(trendingTopic = TrendingTopicModel(
"Compose Animations",
R.drawable.jetpack_compose_animations)
)
}
To make Android Studio happy, add the following imports as well:
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import com.yourcompany.android.jetreddit.R
import com.yourcompany.android.jetreddit.views.TrendingTopicView
Here, you added a root composable called TrendingTopic for one topic item. It takes TrendingTopicModel as an argument.
The star of the show is AndroidView().
@Composable fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
): Unit
AndroidView() composes an Android View obtained from factory(). The factory block will be called exactly once to obtain the View you need to compose. It’s also guaranteed to be invoked on the UI thread. Therefore, in addition to creating the factory, the block can also perform one-off initializations and set View’s properties.
The app might run update() multiple times on the UI thread, as well, due to recomposition. It’s the right place to set View properties that depend on the state. When the state changes, the block will re-execute to set the new properties. The block will also run once, right after viewBlock() completes.
In your code, you passed trendingView as viewBlock(). In update(), you left it as a NoOpUpdate, which is an empty lambda function, that doesn’t update the IO.
You also added the preview composable so you can preview TrendingTopic() in the preview panel.
Build the project and check the preview panel and you’ll see:
Building a List of Trending Topics
Now that you have a composable that represents one trending topic, you’ll work on a composable to represent the whole component with multiple trending topics.
Add the following code below HomeScreen():
@Composable
private fun TrendingTopics(
trendingTopics: List<TrendingTopicModel>,
modifier: Modifier = Modifier
) {
Card(
shape = MaterialTheme.shapes.large,
modifier = modifier
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
// "Trending Today" heading
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(18.dp),
imageVector = Icons.Filled.Star,
tint = Color.Blue,
contentDescription = "Star Icon"
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Trending Today",
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
This is a larger piece of code, but the structure of the components is very simple. You add a card that will hold the entire trending topic section. You add Column() as the root of Card(), as you’ll have two elements ordered vertically. The first is Row() that holds the title and the star icon. Then second will be all the trending topic items.
Now add the last piece of code right after the last Spacer(), that represents the trending topic items:
LazyRow(
contentPadding = PaddingValues(
start = 16.dp,
top = 8.dp,
end = 16.dp
),
content = {
itemsIndexed(
items = trendingTopics,
itemContent = { index, trendingModel ->
TrendingTopic(trendingModel)
if (index != trendingTopics.lastIndex) {
Spacer(modifier = Modifier.width(8.dp))
}
}
)
}
)
This code is pretty straightforward. For the trending topics content, you added LazyRow(). Within it, you built TrendingTopic() for each item in the list. You also added some padding to each item using contentPadding.
For this to work, you also have to add the following imports:
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.filled.Star
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
Before adding the preview composable, first add the dummy data you’ll use as an argument. Add the following above HomeScreen():
private val trendingItems = listOf(
TrendingTopicModel(
"Compose Tutorial",
R.drawable.jetpack_composer
),
TrendingTopicModel(
"Compose Animations",
R.drawable.jetpack_compose_animations
),
TrendingTopicModel(
"Compose Migration",
R.drawable.compose_migration_crop
),
TrendingTopicModel(
"DataStore Tutorial",
R.drawable.data_storage
),
TrendingTopicModel(
"Android Animations",
R.drawable.android_animations
),
TrendingTopicModel(
"Deep Links in Android",
R.drawable.deeplinking
)
)
This is just dummy data that represents fake trending topics. The images you used here were already prepared for you.
Now that you have the dummy data, add the preview composable above TrendingTopicPreview():
@Preview
@Composable
private fun TrendingTopicsPreview() {
TrendingTopics(trendingTopics = trendingItems)
}
Build the project and check the preview panel. You’ll see this:
Adding TrendingTopics to the Home Screen
TrendingTopics() is now ready to use in the Home screen. Before integrating it into HomeScreen(), however, you have to add logic to map the trending items to HomeScreenItems.
In HomeScreen.kt, add the following code below HomeScreen():
private fun mapHomeScreenItems(
posts: List<PostModel>
): List<HomeScreenItem> {
val homeScreenItems = mutableListOf<HomeScreenItem>()
// Add Trending item
homeScreenItems.add(
HomeScreenItem(HomeScreenItemType.TRENDING)
)
// Add Post items
posts.forEach { post ->
homeScreenItems.add(
HomeScreenItem(HomeScreenItemType.POST, post)
)
}
return homeScreenItems
}
This function takes a list of PostModels and returns a list of HomeScreenItems, where the first item is of type HomeScreenItemType.TRENDING.
Now, add the code to invoke this method just above the Box() that defines HomeScreen()’s content:
fun HomeScreen(viewModel: MainViewModel) {
...
// Add this line
val homeScreenItems = mapHomeScreenItems(posts)
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(...)
...
}
}
With this, you mapped the list of PostModels to a list of HomeScreenItems.
Finally, update the LazyColumn() in HomeScreen(), like this:
LazyColumn(
modifier = Modifier
.background(color = MaterialTheme.colors.secondary),
content = {
items(
items = homeScreenItems,
itemContent = { item ->
if (item.type == HomeScreenItemType.TRENDING) {
TrendingTopics(
trendingTopics = trendingItems,
modifier = Modifier.padding(
top = 16.dp,
bottom = 6.dp
)
)
} else if (item.post != null) {
val post = item.post
if (post.type == PostType.TEXT) {
TextPost(
post = post,
onJoinButtonClick = onJoinClickAction
)
} else {
ImagePost(
post = post,
onJoinButtonClick = onJoinClickAction
)
}
Spacer(modifier = Modifier.height(6.dp))
}
})
}
)
Here, you added the logic that emits either TrendingTopics(), TextPost() or ImagePost(), depending on the item.type and item.post content.
Good job! :]
Build and run the app and check out your fancy trending topics component at the top of the Home screen.
Excellent work! You just learned the basic principles of combining Jetpack Compose and the old View framework. This will allow you to migrate any app to Jetpack Compose with no trouble! :]
Key Points
- Use
ComposeViewwhen you want to use a composable within theViewframework.ComposeViewis aViewthat can host Jetpack Compose UI content. - Use
setContent()to supply the content composable function for the view. -
AndroidView()lets you create a composable from the AndroidView. -
AndroidView()composes an AndroidViewobtained fromfactory().factory()will be called exactly once to obtain theViewto compose. It’s also guaranteed to be invoked on the UI thread. - The
update()block of theAndroidViewcan be run multiple times (on the UI thread) due to recomposition. It’s the right place to setViewproperties that depend on state.
Where to Go From Here?
Congratulations, you just completed the Adding View Compatibility chapter!
You are definitely ready to adopt Jetpack Compose even if you already have an app built with the old View framework. Just start bit by bit and in no time you’ll only write Compose! :]
In the next section, you will learn about how to write UI tests for your Compose code and how to handle accessibility. See you there!