Android Native - How to use Composables in the View system

dimitrilc 2 Tallied Votes 272 Views Share

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:

  1. How to add a Composable into a View system.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1.

Prerequisite Knowledge

  1. Intermediate Android.
  2. Basic Jetpack Compose.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the default “Hello World!” TextView from activity_main.xml.

  3. 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'
  4. Add the variable below to hold the compose version.

     def composeVersion = '1.0.5'
  5. Add these options to your android build options (the android{} block)

     buildFeatures {
         compose true
     }
    
     composeOptions {
         kotlinCompilerExtensionVersion = composeVersion
     }
  6. 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"
  7. Add Compose support for Material 3.

     //Material 3 Compose Support
     implementation 'androidx.compose.material3:material3:1.0.0-alpha04'
  8. 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.

  1. 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)
            }
        }
     }
  2. Next, we will obtain the reference to the <ComposeView>. Inside of onCreate(), after the setContent() call, add the line of code below.

     val composeView = findViewById<ComposeView>(R.id.compose_view)
  3. To make use of composeView, we have two options: call setContent() directly from the composeView variable or use the Kotlin scope function apply(). We will be using apply() in this case because it has the advantage of calling multiple functions on composeView rather than just a singular setContent(). Append the code snippet below into onCreate().

     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.

  1. ViewCompositionStrategy.DisposeOnDetachedFromWindow: disposes the composition whenever the view becomes detached from a window. This is the default behavior. This is a singleton object.
  2. 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.
  3. 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.

Compose_in_View.gif

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.