個人アプリをJetpackComposeでリプレースしたときの作業まとめ

こんにちは、tkyです。


個人で作成しているアプリをJetpackComposeで書き直したものを先日リリースしました。
どんなことを考えて実際に移行したのか記録として残しておこうと思います。


対戦ゲームの勝敗記録アプリ
みんな使ってね！

https://play.google.com/store/apps/details?id=io.github.ticktakclock.worl


■できること
・勝/負　の戦績を記録する
・勝敗にメモを残す
・戦績をゲームタイトルやイベント毎に記録する
・日、週、月ごとの対戦数と勝率をグラフで確認する
・過去にさかのぼって戦績を記録する
・記録した戦績の勝敗を編集する

## 画面構成

全部で4つの画面を持つ小規模なアプリです。

- ゲーム一覧画面
- 戦績記録画面
- 分析画面（グラフのライブラリに依存するためリプレースはしませんでした）
- 設定画面


戦績をグラフで表示する分析画面はグラフ表示ライブラリに大きく依存するためリプレースはしませんでした。

## 元のアプリの構成

Single Activityの4 Fragment 構成です。
NavigationとFragmentも密接に関わっているのでこのあたりはAndroid Frameworkに任せてしまって
各View（<span style="color: #d32f2f">赤文字の部分</span>）をjetpack compose化することに決めました。

いきなり全部やると破綻するし、末端の細かいパーツやUIから作ったほうが楽だなと思いました。

![スクリーンショット 2022-02-17 14.21.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/70301de4-6a22-346e-ae58-2336782da4ef.png)


## 最初にどこから作る？

記録画面がメインとなる画面なのでそこから作り始めました。
一覧の画面からいきなり作っても良いですが、細かいパーツとか先に作っておいたほうがやりやすいかなという予測のもとメインの画面から着手しています。

ボタンから作りました

![スクリーンショット 2022-02-17 15.11.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/767b3469-34ac-ef55-b643-bfb2704f8e42.png)


コードとしてもかんたんでCircleButtonという元のボタンを作って勝ったときのボタンと負けたときのボタンをそれぞれ作っています。

```kotlin

@Composable
fun ResultWinButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
    CircleButton(text = "Win", modifier.size(96.dp), onClick = onClick)
}

@Composable
fun ResultLoseButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
    CircleButton(text = "Lose", modifier.size(96.dp), onClick = onClick)
}

@Composable
private fun CircleButton(
    modifier: Modifier,
    text: String,
    onClick: () -> Unit,
) {
    Button(
        onClick = {
            onClick()
        },
        modifier = modifier,
        shape = CircleShape,
    ) {
        Text(text = text)
    }
}

```

次にカードを作りました

![スクリーンショット 2022-02-17 15.12.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/100620a6-5925-5c58-3e12-a8ead5c89b29.png)


こちらについてはおおよそ以下の記事にまとめています。

Row/Column方式で作りました。

https://qiita.com/ticktakclock/items/57dd0aeb1f2e9ec019e3

## ダイアログをどうするか問題

ダイアログについてはjetpack composeで書くこともできるのですが、今のままだと状態管理がちょっと手間だなというのと
既存のDialogFragmentそのまま流用できたので画面単位のjetpack compose化に集中しました。

## ダークテーマについて

jetpack compose化する前から対応しています。
jetpack composeでもこのようなApplicationTheme上にUIを配置することでダークテーマに対応できます。

基本マテリアルデザインで提供されているデフォルトのカラーを使っています。

```kotlin
@Composable
fun ApplicationTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}
```

## CardViewがelevationでSurfaceカラーが変わる？

ダークテーマ上の動作確認をしていてわかりましたがelevationの値によって自動で色が変わるみたいです。

上が8dp,下が16dpのElevationを設定しています。

![スクリーンショット 2022-02-20 23.33.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193770/cb187204-4400-1d02-67f6-c22f924bc82f.png)

これはマテリアルデザインガイドラインのダークテーマ、Elevationの節で
高さによって白色透過色がオーバーレイされるという記述があります。

これを再現しているということになります。

https://material.io/design/color/dark-theme.html#properties

デザイン上「カードの色はこれ」というように決まるかもしれませんが、マテリアルデザインのダークテーマ上だと「カードのelevation対して白色のオーバーレイが背景色に加算された色」

ということになるので齟齬が生まれやすいポイントかなと思いました。

背景に対して要素のelevationがどれだけあるかで背景色が決まることを知識として持っておきたいところですね。

## style.xmlとJetpackComposeテーマの２つが共存しているということ

めちゃめちゃ混乱しました。
基本的にstyle.xmlの色を使ってくれますが、微妙に想定した色と違ったりして、どこの色を変更したら良いのかわからなくなってきたり。。。

途中でMaterial Degin Componentを使うようにスタイルの変更もしたため
ダイアログのボタンのテキストカラーがColorAccent->ColorPrimaryに変更されていたりでボタンの色が見えなくなっていたり

動作確認しているとポロポロと色が見えない部分があって大変でした。

jetpack composeを利用するにあたって、色定義周りをしっかりしておいたほうが良いなと痛感しました・・・


```diff
- Theme.AppCompat.DayNight.NoActionBar
+ Theme.MaterialComponents.DayNight.NoActionBar
```

```xml
    <style name="ThemeOverlay.MyApp.MaterialCalendar" parent="ThemeOverlay.MaterialComponents.MaterialCalendar">
        <item name="buttonBarButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog
        </item>
        <item name="buttonBarNegativeButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog
        </item>
    </style>

    <style name="Widget.MyApp.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
        <item name="android:textColor">@color/colorAccent</item>
    </style>
```

## ViewModelを変更することはなかった

ViewModelに関しては特に何も変更せずにfragment_gaems.xmlが GamesScreen()というコンポーザブル関数になるだけでほぼ完結しました。
しっかりとMVVMで作られているプレジェクトであれば結構すんなり置き換えできるかもしれないなと思いました。

## Experimental APIは使わなかった

アプリ自体がリスト表示、カード、ボタンとかの標準的な使い方のみだった為だと思います。
プロダクトに導入するにあたってはリスト表示するような簡単な画面からやり始めるのが良いのかなと思います。


## 今後の展望

- ダイアログ周りもjetpack compose化していきたい
- Navigationはそのまま残すかも
- jetpack compose関係ないけどデータ移行機能追加したい
- jetpack compose関係ないけどworlのweb版作りたい


## まとめ

- 個人アプリをjetpack composeで書き直してリリースした
- どこをjetpack composeにするか
  - navigationとFragmentはそのまま残して `fragment_{screen_name}.xml` を置き換えるようにした
- 簡単なパーツから徐々に画面を構成していった
- ViewModelはほとんど修正せずにxmlコードが激減した

- みんな使ってみてね！