Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Jetpack Compose 最速導入フローチャート

こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回私が担当している「auサービスToday」で Jetpack Compose を導入したので、その経緯をお話しできたらと思います。

はじめに

Jetpack Compose とは

言わずと知れた Android ネイティブUIを構築するための 宣言型 UI ツールキットです。
2021年7月に1.0Stable版がリリースされ、さらにバージョンアップが進んでいます。
同じ宣言型 UI ツールキットである iOS の Swift UI とともにアプリ開発の主流になりつつあります。

developer.android.com

Jetpack Compose の導入メリット

公式では下記の導入メリットを挙げています

  • コードを削減:xml での記載がなくなることで、コードの可読性・保守性が向上
  • 直感的:宣言型 API であるため、直感的で簡単に記載できる
  • 開発の加速:AndroidStudio のサポートや Preview 機能が利用でき、時間の節約が可能
  • パワフル:Android プラットフォーム API から Material デザイン・ダークテーマ・アニメーションを組み込むことが可能

developer.android.com

他にも UI テストの記載が簡単になる等メリットがたくさん挙げられます。

…しかし、大幅な変更のため導入作業の時間がとれなかったり、
ユーザーに影響が出てしまうと考えると腰が重くなってしまう人も少なくはないと思います。

auサービスTodayでは 短時間影響が少ない 方法で Jetpack Compose を導入できたので、その方法をご紹介したいと思います。

Compose 導入の第一歩を私と一緒に踏み出しましょう!

導入方針

導入基本指針

公式から推奨されている移行戦略としては

  1. 新しい画面から Compose を使用する
  2. 既存の画面を基に、Compose と View を併用する
  3. 既存の画面を基に、完全移行する

の3パターンがあります。
developer.android.com
なぜ「Compose と View を併用する」をしなければならないかと言うと、一部 Compose では使用できない View( AdView 等)が存在するためだそうです。
そのため、止むを得ず併用するやり方を取る場合もあります。

今回の導入方針

今回は

  • 簡単な構成
  • 他の画面や処理に影響が出ないところ

の条件を満たした1画面に Compose を導入をしようと考えました。
そのため既存の View を併用することはせず、「既存の画面を基に、完全移行する」の方法を選択します。

次からその対応内容をフローチャートとして紹介します。

最速導入フローチャート

では Jetpack Compose の導入を行います。


(1) Compose を導入したい画面を決める

上述の条件に合致した、「アプリの外観の設定」画面を導入対象としました。

画面構成

  • アプリバー
  • 3つのラジオボタン
  • 一番下に Divider を表示

既存のファイル

  • Fragment:AppThemeSettingFragment.kt
  • ViewModel:AppThemeSettingViewModel.kt
  • xml:fragment_app_theme_setting.xml


(2) build.gradle に依存関係を記載する

Jetpack Compose の依存関係を build.gradle に記載します。
最新バージョンはこちらから参照してください。

build.gradle

buildscript {
    ext.compose_version = "x.x.x"    // ←バージョンはこちらに設定する
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
       kotlinCompilerExtensionVersion = "$compose_version"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

~~~~~

dependencies {
    ...
    // Compose
    implementation "androidx.compose.runtime:runtime:$composeVersion"
    implementation "androidx.compose.ui:ui:$composeVersion"
    implementation "androidx.compose.foundation:foundation:$composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
    implementation "androidx.compose.material:material:$composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$composeVersion"
    implementation "com.google.android.material:compose-theme-adapter:$composeVersion"
    ...
}


(3) Compose ファイルを作成する

kotlin ファイル AppThemeSetting.kt を作成しし、今回は Fragment と同じパッケージに配置しました。
そして下記のような @Compose アノテーションをつけたメソッドを作成します。

AppThemeSetting.kt

@Composable
fun AppThemeSetting() {
}


(4) Fragment の修正

Fragment の onCreateView で返却していた処理、ComposeViewを返却するように変更します。DataBinding を使用している場合は、DataBinding の削除 または コメントアウトを行なってください。
そして setContent の中に先ほど作成した AppThemeSetting を実装します。

AppThemeSettingFragment.kt
before

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.fragment_app_theme_setting, container, false)
    }

after

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setContent {
                AppThemeSetting()
            }
        }
    }


(5) 既存の xml ファイルを参考に、Compose ファイルに実装する

レイアウトの中身の実装を行います。
既存の xml ファイルの内容を基に、Compose ファイルに実装をしていきます。

今回対応をした段階での ver.1.1.1 では、RadioGroup が Compose で用意されていなかったため、RadioButton を組み合わせて選択状態を自分で切り替える必要がありました。
縦方向に要素を並べる Column で大枠を囲い、その中に RadioButton 3つと Divider(Composeで用意されているもの)を配置します。
RadioButtonのレイアウトは共通化させるために RadioButtonLayout のメソッドを作成し、横方向に要素を並べる Row の中に RadioButtonText を配置しました。

AppThemeSetting.kt

@Composable
fun AppThemeSetting() {
    val selectedType = AppThemeSettingType.LIGHT // TODO 後で切り替え処理をします
    Column {
        RadioButtonLayout(selected = selectedType == AppThemeSettingType.DEFAULT, textId = R.string.app_theme_setting_os) { }
        RadioButtonLayout(selected = selectedType == AppThemeSettingType.LIGHT, textId = R.string.app_theme_setting_light) { }
        RadioButtonLayout(selected = selectedType == AppThemeSettingType.DARK, textId = R.string.app_theme_setting_dark) { }
        Divider(color = Color(0xffcdcdcd))
    }
}

@Composable
fun RadioButtonLayout(selected: Boolean, textId: Int, onSelected: () -> Unit) {
    Row(
            modifier = Modifier
                    .clickable(onClick = onSelected)
                    .padding(horizontal = 16.dp)
                    .fillMaxWidth()
                    .height(height = 48.dp),
            verticalAlignment = Alignment.CenterVertically
    ) {
        RadioButton(
                selected = selected,
                onClick = null,
        )
        Text(
                text = stringResource(id = textId),
                fontSize = 13.sp,
                modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 8.dp)
                        .wrapContentHeight(Alignment.CenterVertically)
        )
    }
}

ここまで実装した上で AppThemeSetting に @Preview アノテーションをつけることで、 AndroidStudio 上でレイアウトを描画して確認することができます。

@Preview
@Composable
fun AppThemeSetting() {

注)onPreview アノテーションは引数のないメソッドでしか指定することができません


(6) ViewModel の LiveData の監視設定

先ほどの RadioButton の選択状態切り替えを行う部分を対応します。
ViewModel に選択状態の LiveData があり、それを利用する必要があります。

Compose では State という概念があり、変化する可能性がある値は全て State として扱う必要があります。
LiveData を State として扱うには、LiveData.observeAsState() 関数を用いて、Compose が使用する State に変換します。
再利用性を考慮して、LiveData の使用とその値を使用する部分とで、メソッドを別々に分けます。
詳しくは下記を参照してください。
developer.android.com

AppThemeSetting.kt

@Composable
fun AppThemeSetting(viewModel: AppThemeSettingViewModel) {
    val selectedType by viewModel.selectedType.observeAsState()
    selectedType?.let { type ->
        AppThemeSettingContent(selectedType = type,  onSelected = { selectType ->
            viewModel.onSelected(selectType)
        })
    }
}

@Composable
private fun AppThemeSettingContent(selectedType: AppThemeSettingType, onSelected: (type: AppThemeSettingType) -> Unit) {
    Column {
        RadioButtonLayout(~~~) {
            onSelected.invoke(~~~)
        }
        ~~~
    }
}

AppThemeSettingFragment.kt

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        ~~~~
                AppThemeSetting(viewModel)

ここで、onPreview アノテーションは引数のないメソッドでしか指定することができないため、onPreview でエラーが発生してしまいます。
下記のような Preview 用のメソッドを用意して、Preview に描画することができるようになります。
AppThemeSetting.kt

@Preview
@Composable
fun AppThemeSettingContentPreview() {
    AppThemeSettingContent(
            selectedType = AppThemeSettingType.DEFAULT,
            onSelected = { }
    )
}


(7) Theme の設定

お気づきの方もいるかもしれませんが、RadioButton にアプリのテーマカラーが反映されていません。
テーマカラーは下記のように Theme.kt ファイルを作成し、 MaterialTheme API を用いてテーマの設定を行います。

Theme.kt

private val LightColors = lightColors(
        primary = Color(0xff969696),
        primaryVariant = Color(0xffe8e8e8),
        onPrimary = Colors.BLACK,
        secondary = Color(0xffed5505),
        secondaryVariant = Color(0xffed5505),
        onSecondary = Colors.BLACK,
)

@Composable
fun AppMaterialTheme(
        content: @Composable () -> Unit
) {
    MaterialTheme(
            colors = LightColors,
            content = content
    )
}

作成した Theme 設定を、先ほど作成した Compose レイアウトを囲うように指定します。

AppThemeSetting.kt

@Composable
private fun AppThemeSettingContent(~~~) {
    AppMaterialTheme {
        Column {
            RadioButtonLayout(~~~
            ~~~
        }
    }
}



(8) TopAppBar の設定

アプリバーToolbarと言った方が馴染みが深いかも)の設定になります。
Compose では Scaffold を使用することで、MaterialDesign のレイアウトを簡単に構築することができます。
TopAppBar を作成して topBar に指定するだけで、表示することが可能です。

AppThemeSettingFragment.kt

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setContent {
                AppMaterialTheme {
                    Scaffold(
                            topBar = { AppTopAppBar(R.string.app_theme_setting) },
                            content = { AppThemeSetting(viewModel = viewModel) }
                    )
                }
            }
        }
    }
~~~
@Composable
fun AppTopAppBar(stringId: Int) {
    TopAppBar(
            navigationIcon = {
                IconButton(onClick = {}) {
                    Icon(Icons.Filled.ArrowBack, "backIcon")
                }
            },
            title = {
                Text(text = stringResource(id = stringId))
            },
            backgroundColor = Color(0xffFAFAF8)
    )
}

※ Fragment に AppTopAppBar を設置していますが、アプリ共通で使用する Composable ファイルを作成すると良いと思います。


(9) xmlファイルの削除

最後に不要になった xml ファイルを削除します。

完成

これでひとまず基本的なレイアウトは完成しました。
View の時のレイアウトとほぼ同じであるため達成感はあまりありませんが、無事に Compose を導入することができました。

ここから

より Jetpack Compose の理解をしたい方は、Jetpack Compose の全てが学べる Jetpack Compose Pathways を一読しましょう。 developer.android.com
また Jetpack Compose Pathways を使ってComposeを学ぶと、期間限定でグッズがもらえるキャンペーンも行うようです。
プログラム期間
2022 年 7 月 19 日(火) ~ 8 月 19 日(金)
rsvp.withgoogle.com

これを機にみなさんも Jetpack Compose の世界に飛び込みましょう!