Jetpack Compose の @Preview を簡素化するためのモジュール分割アプローチ
この記事は、GENDA Advent Calendar 2024 9日目の記事です。
はじめに
Jetpack Composeの@Preview機能は、UIのデザインや動作を素早く確認するための便利なツールです。
しかし、階層構造が複雑なアプリケーションでは、@Previewで使用するデータを準備する負担が増え、開発者にとって課題となることがあります。
特に以下のような悩みを抱える方も多いのではないでしょうか。
• 階層構造を持つComposable関数ごとに、テスト用データを準備する手間がかかる
• データクラスに変更が入るたびに、@Preview関数の修正が必要になる
• データクラスを作成するために、本来不要なimport文が増える
こうした課題を解決するため、本記事では@Previewをスリムにする工夫として、パッケージ分割のアプローチを試してみました。
Jetpack Composeを活用した効率的なUI開発を目指している方に、少しでも参考になれば幸いです。
なぜマルチモジュール化だと考えたのか
表示用のデータを準備するだけであれば、単一のモジュールで十分です。
ただ、データクラスが増えてくると、肥大化してしまい、コードの見通しが悪くなりそうですし、
プロダクトコードとも関連がないデータクラスが増えてしまうため、なるべく分離したいと考えました。
そこで、データクラスや関数を別のモジュールに分割することで、以下のようなメリットがあります。
• コードの見通しが良くなる
• データクラスや関数の再利用性が高まる
• @Preview関数の修正がしやすくなる
具体例の紹介
説明のために下記のような画面を作成しました。
また、この画面を作成するために、以下のようなデータクラスを用意しました。
data class Monster(
val id: Int,
val name: String,
val types: List<MonsterType>,
val imageUrl: String,
val hp: Int,
val attack: Int,
val defense: Int,
val specialAttack: Int,
val specialDefense: Int,
val speed: Int,
val moveList: List<String>,
val abilityList: List<String>
)
画面を構成するComposable関数は以下の通りです。
※冗長な部分は省略しています。
@Composable
fun MonsterDetails(monster: Monster) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(16.dp)
.verticalScroll(
rememberScrollState(0)
)
) {
// モンスターの画像
MonsterImage(monster, modifier = Modifier.padding(bottom = 16.dp))
// モンスターの番号と名前
NumberAndName(monster.id, monster.name, modifier = Modifier.padding(bottom = 8.dp))
// タイプ
TypeRow(monster.types, Modifier.padding(bottom = 16.dp))
// ステータス
Stats(monster, modifier = Modifier.padding(bottom = 16.dp))
// とくせい
Ability(monster.abilityList, modifier = Modifier.padding(bottom = 16.dp))
// おぼえるわざ
MoveList(monster.moveList)
}
}
そして、本題である@Preview関数は以下のようになります。
@Composable
@Preview
private fun MonsterDetailsPreview() {
MonsterDetails(
monster = Monster(
id = 1,
name = "イシノツブテ",
types = listOf(MonsterType.ROCK, MonsterType.GROUND),
imageUrl = "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdW2bw5XFelBMMNMSx4_PST670AxRputfVYU_24fkGYmNeXerrd48CYIgE70vrT711Lwa5Xlgk1DqRSGjxfTZgpq26ky_TV5BazG_rhuMGufQgFFTBJE30qt04EHrTMRe2FqemO0Bk-bM/s800/monster11.png",
hp = 40,
attack = 80,
defense = 100,
specialAttack = 30,
specialDefense = 30,
speed = 20,
moveList = listOf(
"たいあたる",
"まるまる",
"どろをかける",
"ころころ",
"いわおとす",
"ステルスブロック",
"ロッククラッシュ",
"じばくする",
"すなまきこむ",
"いわふり",
"だいちのゆれ",
"ストーンスラッシュ",
"ばくはつする"
),
abilityList = listOf("いしのあたま", "けんろう", "すなひそみ")
)
)
}
実際のアプリでもより複雑なデータを扱う場合は、このようにデータを準備するのは大変です。
(今回はmoveListで行数を稼いでいてちょっとずるい気もしますが、より実際のアプリの表示を再現するためには、このようなデータが必要になることもあるかと思います。)
パッケージの構成
今回は3つのパッケージに分割しました。
-
app
- 画面の表示に関するコードを格納
- domain-modelとpreview-dummyに依存
-
preview-dummy
- @Preview関数で使用するテスト用データを格納
- domain-modelに依存
-
domain-model
- データクラスを格納
- 依存関係なし
実装後のコード
ダミークラスの実装は以下の通りです。
Companion objectを使用して、dummyプロパティを定義しています。
package com.oukoda.preview_dummy
import com.oukoda.domain_model.Monster
import com.oukoda.domain_model.MonsterType
object MonsterDummy {
val Monster.Companion.dummy: Monster
get() = Monster(
id = 1,
name = "イシノツブテ",
types = listOf(MonsterType.ROCK, MonsterType.GROUND),
imageUrl = "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdW2bw5XFelBMMNMSx4_PST670AxRputfVYU_24fkGYmNeXerrd48CYIgE70vrT711Lwa5Xlgk1DqRSGjxfTZgpq26ky_TV5BazG_rhuMGufQgFFTBJE30qt04EHrTMRe2FqemO0Bk-bM/s800/monster11.png",
hp = 40,
attack = 80,
defense = 100,
specialAttack = 30,
specialDefense = 30,
speed = 20,
moveList = listOf(
"たいあたる",
"まるまる",
"どろをかける",
"ころころ",
"いわおとす",
"ステルスブロック",
"ロッククラッシュ",
"じばくする",
"ありじごく",
"いわふらし",
"だいちのゆれ",
"ストーンスラッシュ",
"ばくはつする"
),
abilityList =
listOf("いしのあたま", "けんろう", "すなひそみ")
)
}
プレビュー関数の実装は以下の通りです。
import com.oukoda.preview_dummy.MonsterDummy.dummy
@Composable
@Preview
private fun MonsterDetailsPreview() {
MonsterDetails(monster = Monster.dummy)
}
結論
「はじめに」であげた課題は解消できました。
@Preview関数の中身がスッキリし、データの準備にかかる手間を減らせました。
しかし、このままだとプロダクトコードとの分離ができていません。
preview-dummyモジュールをdebugImplementationで依存させること考えていましたが、
Releaseビルドができなくなってしまうため、切り離せませんでした。
今後は、この問題を解決する方法を模索していきたいと思います。
最後まで読んでいただき、ありがとうございました!
Discussion