Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

既存プロジェクトにJetpack Composeをちょっと導入しました #JetpackCompose

遅くなりましたがあけましておめでとうございます!!今年も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で作る

などをどんどんしていきたいと思っています。

参考にしたリンク集

おまけ1

登壇しましたブログを書き忘れてたんですよねぇ…。

去年の12/16に行われたDevFest tokyo 2022にFlutterの話で登壇してきました。以前同僚が書いたFlutter初心者たちが3ヶ月で新規アプリをリリースした話をベースにしつつ、ここに書いてない話も話してます。資料はこちらなのでパラパラと見てみてください。そのうち動画も公開されると思います。

docs.google.com

おまけ2

恐ろしいことに去年9月にあったiOSDCの「登壇しましたブログ」も書いてなかったんですよねぇ…。書かなきゃと思ってたんですがずるずる先延ばしちゃって、気づいたら「いまさら?」感ある時期になってしまい…。一応ここに載せておきますね。

speakerdeck.com

PR

子育て家族アプリFammを運営するTimers inc.では現在エンジニアを積極採用中! オンラインでの面談やカジュアルランチなどもやってますので是非お気軽にご連絡ください!

採用HP: https://timers-inc.com/recruit/engineering

timers-inc.com

timers-inc.com

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる