Stanby Tech Blog

求人検索エンジン「スタンバイ」を運営するスタンバイの開発組織やエンジニアリングについて発信するブログです。

Androidの既存xmlレイアウトに一部ComposeUIを適用させる

f:id:stanbyblog:20220204135745p:plain

概要

既存のプロジェクトで開発しているとComposeUIを導入したくてもすでにxmlでレイアウトを組んでいるので導入が難しいと思います。
そんな時に便利なのがxmlとComposeUIを共存させる方法です。
とりあえずComposeUIをやってみたいというプロジェクトには簡単に導入できるのでおすすめです。

ListのところのみComposeUIで実装するイメージを例とします。
f:id:stanbyblog:20220204135353p:plain

準備

build.gradleに依存関係を追加するだけです。
その他Composeに必要な依存関係は下記のbuild.gradleを参照してください。

implementation 'androidx.activity:activity-compose:1.4.0'

Activityで実装する場合

レイアウトファイルのComposeUIを実装したいところに<androidx.compose.ui.platform.ComposeViewを追加します。

<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">

    <TextView
        android:id="@+id/header"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="こんにちは"
        app:layout_constraintBottom_toTopOf="@+id/compose_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/footer"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/header" />


    <LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/purple_200"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/compose_view">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="26sp"
            android:gravity="center"
            android:text="Footer" />


    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

続いてActivityでは、ComposeViewをレイアウトファイルから取得し、setContent(content: @Composable () -> Unit) にComposeを実装していきます。これだけです。

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

        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                MainContent()
            }
        }
    }
    
    ・・・・

Fragmentで実装する場合

Fragmentの場合もActivityと同様でレイアウトファイルにComposeViewを追加し、 Fragment内でComposeViewを取得し同じように実装します。

class SampleFragment: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_sample, container, false).apply {
            findViewById<ComposeView>(R.id.compose_view).apply {
                // lifecycleに沿ってComposeViewを自動で破棄します(LifecycleOwnerが不明の場合DisposeOnViewTreeLifecycleDestroyed)
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
                    MainContent()
                }
            }
        }
        return view
    }
    
    ・・・

ちなみにDataBindingを使う場合

class SampleFragment2 : Fragment() {

    private var _binding: FragmentSample2Binding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSample2Binding.inflate(inflater, container, false)

        binding.composeView.apply {
            // lifecycleに沿ってComposeViewを自動で破棄します(LifecycleOwnerが不明の場合DisposeOnViewTreeLifecycleDestroyed)
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MainContent()
            }
        }

        return binding.root
    }

    ・・・

全ソース公開

MainActivity
package com.example.sampleapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp

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

        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                MainContent()
            }
        }
    }

    @Composable
    fun MainContent() {
        MaterialTheme {
            Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
                repeat(100) { i ->
                    ListItem(i) {
                        Toast.makeText(
                            applicationContext,
                            "Button$i Click!!",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }
        }
    }

    @Composable
    fun ListItem(index: Int, onClickListener: () -> Unit) {
        Button(
            onClick = {
                onClickListener()
            },
            modifier = Modifier.padding(10.dp)
        ) {
            Text(text = "サンプルボタン$index")
        }
    }
}
SampleFragment
package com.example.sampleapplication

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment

class SampleFragment: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_sample, container, false).apply {
            findViewById<ComposeView>(R.id.compose_view).apply {
                // lifecycleに沿ってComposeViewを自動で破棄します(LifecycleOwnerが不明の場合DisposeOnViewTreeLifecycleDestroyed)
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
                    MainContent()
                }
            }
        }
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        requireActivity().title = "SampleFragment"
    }

    @Composable
    fun MainContent() {
        MaterialTheme {
            Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
                repeat(100) { i ->
                    ListItem(i) {
                        Toast.makeText(
                            activity?.applicationContext,
                            "Button$i Click!!",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }
        }
    }

    @Composable
    fun ListItem(index: Int, onClickListener: () -> Unit) {
        Button(
            onClick = {
                onClickListener()
            },
            modifier = Modifier.padding(10.dp)
        ) {
            Text(text = "サンプルボタン$index")
        }
    }
}
SampleFragment2
package com.example.sampleapplication

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import com.example.sampleapplication.databinding.FragmentSample2Binding

class SampleFragment2 : Fragment() {

    private var _binding: FragmentSample2Binding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSample2Binding.inflate(inflater, container, false)

        binding.composeView.apply {
            // lifecycleに沿ってComposeViewを自動で破棄します(LifecycleOwnerが不明の場合DisposeOnViewTreeLifecycleDestroyed)
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MainContent()
            }
        }

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        requireActivity().title = "SampleFragment2"
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    @Composable
    fun MainContent() {
        MaterialTheme {
            Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
                repeat(100) { i ->
                    ListItem(i) {
                        Toast.makeText(
                            activity?.applicationContext,
                            "Button$i Click!!",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }
        }
    }

    @Composable
    fun ListItem(index: Int, onClickListener: () -> Unit) {
        Button(
            onClick = {
                onClickListener()
            },
            modifier = Modifier.padding(10.dp)
        ) {
            Text(text = "サンプルボタン$index")
        }
    }
}
activity_main
<?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">

    <TextView
        android:id="@+id/header"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="こんにちは"
        app:layout_constraintBottom_toTopOf="@+id/compose_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/footer"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/header" />


    <LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/purple_200"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/compose_view">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="26sp"
            android:gravity="center"
            android:text="Footer" />


    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
fragment_sample
<?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">

    <TextView
        android:id="@+id/header"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="こんにちは"
        app:layout_constraintBottom_toTopOf="@+id/compose_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/footer"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/header" />

    <LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/purple_200"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/compose_view">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="Footer"
            android:textSize="26sp" />


    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
fragment_sample2
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/header"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="こんにちは"
            app:layout_constraintBottom_toTopOf="@+id/compose_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/compose_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@+id/footer"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/header" />

        <LinearLayout
            android:id="@+id/footer"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/purple_200"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/compose_view">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textSize="26sp"
                android:gravity="center"
                android:text="Footer" />


        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.sampleapplication"
        minSdk 23
        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
    }

    dataBinding {
        enabled = true
    }

    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.1.0-rc02"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
    implementation 'androidx.compose.ui:ui:1.0.5'
    implementation 'androidx.activity:activity-compose:1.4.0'
    implementation 'androidx.compose.material:material:1.0.5'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。

www.wantedly.com