Compose for Android Integration: Seamless Guide

Jetpack Compose, Google’s modern UI toolkit for building native Android apps, simplifies and accelerates UI development. However, many existing Android projects rely on the traditional View-based system. Seamlessly integrating Compose into such projects allows developers to adopt modern practices incrementally. This article will guide you through integrating Compose for Android within Jetpack Compose, offering strategies, code samples, and best practices for a smooth transition.

Why Integrate Compose with Views?

  1. Incremental Adoption: Gradually migrate from Views to Compose without rewriting the entire app at once.
  2. Leverage Existing Code: Reuse custom views and existing logic within a Compose-based UI.
  3. Feature Parity: Implement new features using Compose while maintaining legacy features in Views.

Key Components for Integration

  1. ComposeView: Allows you to host Compose UI within a View hierarchy.
  2. AbstractComposeView: Base class of ComposeView allows you to build custom compose-integrated Views.
  3. ViewComposeFactory: Utility for converting regular Android XML Layout View elements to Compose Views on-the-fly.
  4. AndroidView: Enables you to use traditional Views within Compose layouts.

Integrating Compose into Existing XML Layouts

This section explains how to host Compose UI inside traditional Android XML layouts.

Step 1: Add Dependencies

Ensure you have the necessary dependencies in your build.gradle file:

dependencies {
    implementation "androidx.compose.ui:ui:1.6.0" // or newer
    implementation "androidx.compose.ui:ui-tooling-preview:1.6.0"
    debugImplementation "androidx.compose.ui:ui-tooling:1.6.0"
    implementation "androidx.compose.material:material:1.6.0"
    implementation "androidx.activity:activity-compose:1.8.2"
    implementation "androidx.fragment:fragment-ktx:1.6.2" // For Fragment integration

    // Optional: Compose lifecycle integration
    implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
}

Step 2: Add ComposeView to XML Layout

In your XML layout file, add a ComposeView:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Traditional View"
        android:padding="16dp"/>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

Step 3: Set Content in ComposeView

In your Activity or Fragment, find the ComposeView and set its content:

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Text
import androidx.compose.ui.platform.ComposeView

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView: TextView = findViewById(R.id.text_view)

        val composeView: ComposeView = findViewById(R.id.compose_view)
        composeView.setContent {
            Text("Hello from Compose!")
        }
    }
}

Integrating Views into Compose Layouts

This section explains how to incorporate traditional Android Views inside Compose layouts.

Step 1: Use AndroidView

Use the AndroidView composable to embed a traditional View in your Compose UI:

import android.widget.TextView
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.Modifier

@Composable
fun ViewInCompose() {
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                text = "View inside Compose"
                textSize = 20f
            }
        },
        update = { view ->
            // Update the view
        },
        modifier = Modifier
    )
}

Communicating Between Compose and Views

Data exchange between Compose and traditional Views can be achieved using ViewModels, shared state holders, and callbacks.

Using ViewModel

Using a shared ViewModel can centralize and manage the data required by both Compose and View components.

ViewModel Implementation

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class SharedViewModel : ViewModel() {
    val textData = MutableLiveData("Initial Text")

    fun updateText(newText: String) {
        textData.value = newText
    }
}
Activity/Fragment Code

import android.os.Bundle
import android.widget.EditText
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Observer

class IntegrationActivity : AppCompatActivity() {

    private val sharedViewModel: SharedViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_integration)

        val editText: EditText = findViewById(R.id.edit_text)

        sharedViewModel.textData.observe(this, Observer { text ->
            editText.setText(text)
        })


        val composeView: ComposeView = findViewById(R.id.compose_view)
        composeView.setContent {
            ComposeContent(sharedViewModel)
        }
    }
}


@Composable
fun ComposeContent(sharedViewModel: SharedViewModel) {
    val textValue by sharedViewModel.textData.observeAsState("Compose Initial Text")

    Button(onClick = { sharedViewModel.updateText("Text updated from Compose") }) {
        Text("Update Text from Compose, Current text: $textValue")
    }
}

Advanced Scenarios

Using AbstractComposeView for Custom Views

For a custom View, AbstractComposeView can be extended. This simplifies managing Compose content within a custom View component:


import android.content.Context
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.material.Text

class CustomComposeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {

    @Composable
    override fun Content() {
        Text("Custom Compose View")
    }
}

Best Practices

  • Start Small: Begin by integrating Compose in isolated areas of your app.
  • Maintain Consistency: Ensure the visual style and behavior are consistent between Compose and View-based UIs.
  • Use ViewModel: Employ ViewModels for state management to facilitate data sharing between Compose and Views.
  • Test Thoroughly: Test both the Compose and View parts of your UI to ensure seamless integration.
  • Leverage Preview: Utilize Compose Previews extensively during development to visualize and test UI components quickly.

Conclusion

Integrating Compose for Android into existing projects provides a practical approach to adopting modern UI development practices. By leveraging ComposeView and AndroidView, along with employing strategies like ViewModels for data sharing, developers can seamlessly blend Compose UI with traditional Android Views, leading to a smoother transition and more maintainable codebase.