遅くなりましたがあけましておめでとうございます!!今年もTimers Tech Blogをよろしくお願いします✌('ω') ✌
さて今日はFlutterアプリ開発からAndroidアプリ開発に戻った @akatsuki174 から、既存のAndroidアプリ「Famm」にJetpack Composeを導入した話をしたいと思います。
※以下、Jetpack ComposeをComposeと略します。
導入した画面のスクショ
まずは完成形を見てみましょう。今回は開発版アプリのときだけ表示できるデバッグメニュー画面にComposeを入れてみました。いくつかのCardビューが並んでいますが、上4つがCompose、下1つ(「リッチポップアップ」の部分)が従来のViewです。(1つの画面内にComposeと従来のViewが混在できるってすごいですよね!)
  
ではここから具体的な話をしていきたいと思います。
基本的な設定
build.gradleファイルに以下を追記します。
android {
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }
}
さらに、ライブラリ群を入れます。何を入れるかはプロジェクトによって異なってきます。例えばFammはMaterial Design 2, 3を使っていないので、それらを呼ぶ宣言をしてません。公式のクイックスタートを見ながら選別すると良いと思います。
def composeBom = platform("androidx.compose:compose-bom:2022.10.00")
implementation composeBom
androidTestImplementation composeBom
implementation "androidx.compose.material:material"
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:1.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
implementation("androidx.compose.runtime:runtime-livedata")
implementation "com.google.accompanist:accompanist-appcompat-theme:0.25.1"
よく使うコンポーネント群の作成
「タイトルは24px、色は#404040、Bold」というようによく使うテキストやボタンはルールが決まっているので、まとめて定義しておきました。一例を書いておきます。
object DesignGuideText { @Composable fun Title(text: String) { DesignGuideText( text = text, fontSize = dimensionResource(id = R.dimen.design_guide_text_title).value.sp, fontWeight = FontWeight.Bold, ) } @Composable fun SubTitle(text: String) { DesignGuideText( ..., ) } @Composable private fun BaseDesignGuideText( text: String, modifier: Modifier = Modifier, color: Color = colorResource(id = R.color.design_guide_main_text_color), fontSize: TextUnit, fontWeight: FontWeight = FontWeight.Normal, ) { Text( text = text, color = color, fontSize = fontSize, fontWeight = fontWeight, modifier = modifier, fontFamily = FontFamily(Font(R.font.fonts)), style = TextStyle( lineHeight = 1.2.em, ) ) } }
使うときはこのように使えます。
DesignGuideText.Title(
    text = "titleだよ",
)
既存Viewの書き換え
ではいよいよ既存のxmlのレイアウトにComposeを入れ込んでいきます。まずはCardView1つをComposeに置き換えてみます。CardViewをComposeViewに差し替えます。
<!--<androidx.cardview.widget.CardView--> <!-- android:layout_width="match_parent"--> <!-- android:layout_height="wrap_content"--> <!-- app:cardUseCompatPadding="true">--> ....... <!--</androidx.cardview.widget.CardView>--> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeView" android:layout_width="match_parent" android:layout_height="match_parent" />
一番上のCardView、「APIリクエスト・レスポンス」をComposeで再現してみます。
@Composable fun Chucker(onClick: () -> Unit) { Card { Column( modifier = Modifier.padding(dimensionResource(id = R.dimen.space_normal)), verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.space_small)), ) { DesignGuideTextComponents.SectionTitle( text = "APIリクエスト・レスポンス", ) DesignGuideButtonComponents.PrimaryButton( text = "Open Chucker", onClick = onClick, ) } } }
Previewで確認するとこんなかんじです。
@Preview @Composable private fun DebugMenuContentPreview() { AppCompatTheme { Column( verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.space_normal)), ) { Chucker(onClick = {}) } } }

ではActivityの onCreate から呼び出してみます。
binding.composeView.apply { --- ①
    setViewCompositionStrategy( --- ②
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
    )
    setContent { --- ③
        AppCompatTheme { --- ④
            Column(
                    verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.space_normal)),
            ) {
                Chucker {
                    startActivity(Chucker.getLaunchIntent(this@DebugMenuActivity))
                }
            }
        }
    }
}
①:今まで通りの呼び出し方でActivity側から呼び出すことができます。このファイルではViewBindingを使っているので上記のような呼び出し方になってます。
②:Composition が不要になったときに自動的に破棄するための設定です。
③: setContent 内にCompose関数を並べていきます。
④:FammではAppCompatを使っているので AppCompatTheme で囲っています。プロジェクトによっては MaterialTheme を使ったり独自のThemeを使ったりすることになります。
Tips
既存リソースの使用
既存のリソースを活用するには以下のように書きます。
string: stringResource(id = R.string.string_id) color: colorResource(id = R.color.color_id) dimen: dimensionResource(id = R.dimen.dimen_id)
fontSizeにdimenを使う場合は、型をTextUnitにするために .value.sp をつける必要があります。
dimensionResource(id = R.dimen.dimen_id).value.sp
textの更新
既存のLiveDataを活用しつつ、データの更新があったときにComposeのtextを変更したい場面に遭遇しました。この場合はLiveDataを observeAsState でState型に変換すれば実現できました。
setContent {
    val userInfo = viewModel.userInfoLiveData.observeAsState()
    AppCompatTheme {
        userInfo.value?.let {
            Account(userInfo = it ) {
                onClick()
            }
        }
    }
}
もちろんFlowでも同様のことができます。
setContent {
    val userName = viewModel.userNameFlow.collectAsState()
    AppCompatTheme {
        Text("${userName.value}")
    }
}
これからの展望
今回は年末のタスクデー(直接業務に関係のないことでも、個人の判断で任意の業務を行える日)を使ってちょっと手を付けただけなので画面の一部しか置き換えられませんでした。ので、今後は
- デバッグメニュー画面のすべてをCompose化
 - 既存の単純な画面のCompose化
 - 新規で作る画面をComposeで作る
 
などをどんどんしていきたいと思っています。
参考にしたリンク集
- Compose をアプリに導入する  |  Jetpack Compose  |  Android Developers
- 既存アプリにComposeを導入する上で必要になってくる設定や、併用ならではのいろいろについて書かれてるので必読です
 
 - Quick start  |  Jetpack Compose  |  Android Developers
- gradleファイルの書き方などが書かれています
 
 - Compose と Kotlin の互換性マップ  |  Android デベロッパー  |  Android Developers
- ComposeとKotlinのバージョンの相性確認に必要です
 
 
おまけ1
登壇しましたブログを書き忘れてたんですよねぇ…。
去年の12/16に行われたDevFest tokyo 2022にFlutterの話で登壇してきました。以前同僚が書いたFlutter初心者たちが3ヶ月で新規アプリをリリースした話をベースにしつつ、ここに書いてない話も話してます。資料はこちらなのでパラパラと見てみてください。そのうち動画も公開されると思います。
おまけ2
恐ろしいことに去年9月にあったiOSDCの「登壇しましたブログ」も書いてなかったんですよねぇ…。書かなきゃと思ってたんですがずるずる先延ばしちゃって、気づいたら「いまさら?」感ある時期になってしまい…。一応ここに載せておきますね。
PR
子育て家族アプリFammを運営するTimers inc.では現在エンジニアを積極採用中! オンラインでの面談やカジュアルランチなどもやってますので是非お気軽にご連絡ください!