Introduction
If you are working on a native Android app using the View system, then you might have come across a situation where you would need to add a Composable (androidx.compose.runtime.Composable
) into your View hierarchy. In this tutorial, we will learn how to add a Composable into an existing View system.
The Composables that we will use in this tutorial will come from the Material 3 library.
Goals
At the end of the tutorial, you would have learned:
- How to add a Composable into a View system.
Tools Required
- Android Studio. The version used in this tutorial is Bumblebee 2021.1.1.
Prerequisite Knowledge
- Intermediate Android.
- Basic Jetpack Compose.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Remove the default “Hello World!” TextView from activity_main.xml.
-
Inside of your module build.gradle, upgrade your module’s Material dependency to version
1.5.0
.implementation 'com.google.android.material:material:1.5.0'
-
Add the variable below to hold the compose version.
def composeVersion = '1.0.5'
-
Add these options to your android build options (the
android{}
block)buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = composeVersion }
-
Add the Compose dependencies below into your
dependencies{}
block.//Compose implementation "androidx.compose.runtime:runtime:$composeVersion" implementation "androidx.compose.ui:ui:$composeVersion" implementation "androidx.compose.ui:ui-tooling:$composeVersion" implementation "androidx.compose.foundation:foundation:$composeVersion" implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
-
Add Compose support for Material 3.
//Material 3 Compose Support implementation 'androidx.compose.material3:material3:1.0.0-alpha04'
-
For simplicity, in the project build.gradle file, downgrade your Android Kotlin plugin version to
1.5.31
.id 'org.jetbrains.kotlin.android' version '1.5.31' apply false
ComposeView
To bridge the gap between the Compose world and the View world, Android provides a couple of Interop APIs. ComposeView (androidx.compose.ui.platform.ComposeView
) is one of those APIs that we can use in scenarios where we need to insert a Composable into an existing View hierarchy.
ComposeView is actually a View itself, so we will be able to add it via XML. To add it to activity_main.xml, copy and paste the code below inside of ConstraintLayout.
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Insert Composable into View hierarchy
For this tutorial, we can just add a simple Composable that will print the String "Hello" 100 times.
-
In MainActivity.kt, add the top-level Composable below.
@Composable private fun Hellos(hellos: Array<String> = Array(100) {"Hello $it"} ) { LazyColumn { items(items = hellos) { hello -> Text(text = hello) } } }
-
Next, we will obtain the reference to the
<ComposeView>
. Inside ofonCreate()
, after thesetContent()
call, add the line of code below.val composeView = findViewById<ComposeView>(R.id.compose_view)
-
To make use of
composeView
, we have two options: callsetContent()
directly from thecomposeView
variable or use the Kotlin scope functionapply()
. We will be usingapply()
in this case because it has the advantage of calling multiple functions oncomposeView
rather than just a singularsetContent()
. Append the code snippet below intoonCreate()
.composeView.apply { setContent { Hellos() } }
ViewCompositionStrategy
We are almost done, but there is another important characteristic of ComposeView that needs discussion.
ComposeView also subclasses AbstractComposeView(androidx.compose.ui.platform.AbstractComposeView
). AbstractComposeView includes an interesting function that is setViewCompositionStrategy()
. Because we are mixing two different UI systems together, we will need to be aware of the Activity (or Fragment), View, and Composable lifecycles. With setViewCompositionStrategy()
, we can pass in a ViewCompositionStrategy object to configure how the composition should be destroyed. There are three premade ViewCompositionStrategy available for us to use.
- ViewCompositionStrategy.DisposeOnDetachedFromWindow: disposes the composition whenever the view becomes detached from a window. This is the default behavior. This is a singleton object.
- ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed: disposes the composition when the ViewTreeLifecycleOwner of the next window the view is attached to is destroyed. This is also a singleton object.
- ViewCompositionStrategy.DisposeOnLifecycleDestroyed: disposes the composition when the lifecycle is destroyed. Similar to
DisposeOnViewTreeLifecycleDestroyed
, but this is a class with constructors that allows you to specify a specific lifecycle.
To set the ViewCompositionStrategy, you can add it to the apply()
like below.
composeView.apply {
//Default option
//setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
//Explicit lifeCycle
//setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle))
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Hellos()
}
}
Run the App
It is now time to run our App to see if it works correctly.
We can see that it displays the Composable LazyList inside of our ConstraintLayout View successfully.
Solution Code
MainActivity.kt
package com.codelab.daniwebcomposeinviews
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val composeView = findViewById<ComposeView>(R.id.compose_view)
/* composeView.setContent {
Hellos()
}*/
composeView.apply {
//Default option
//setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
//Explicit lifeCycle
//setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle))
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Hellos()
}
}
}
}
@Composable
private fun Hellos(hellos: Array<String> = Array(100) {"Hello $it"} ) {
LazyColumn {
items(items = hellos) { hello ->
Text(text = hello)
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Project build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.0' apply false
id 'com.android.library' version '7.1.0' apply false
id 'org.jetbrains.kotlin.android' version '1.5.31' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
def composeVersion = '1.0.5'
android {
compileSdk 31
defaultConfig {
applicationId "com.codelab.daniwebcomposeinviews"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = composeVersion
}
}
dependencies {
//Compose
implementation "androidx.compose.runtime:runtime:$composeVersion"
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
//Material 3 Compose Support
implementation 'androidx.compose.material3:material3:1.0.0-alpha04'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary
We have learned how to add Composables into a View hierarchy using ComposeView. The full project code can be found at https://github.com/dmitrilc/DaniwebComposeInViews.