SwiftUIとのUI描写の簡単な違い
SwiftUIは構造体がUI
struct 構造体の名前: View {
var 引数: Int
var body: some View {
ここから実際のUIを書く
}
}
Jetpack Composeは【@Composable】を付けた関数がUI(class MainActivity等からの呼び出し元)
@Composable
fun 関数名(
引数: String
) {
ここから実際のUIを書く
}
SwiftUIとの比較表
| SwiftUI | Jetpack Compose |
|---|---|
View | @Composable関数 |
@State | remember { mutableStateOf() } または mutableStateOf() |
@Binding | 状態を引数として受け渡す |
ObservableObject | ViewModel |
NavigationStack | NavHost |
VStack | Column |
HStack | Row |
ZStack | Box |
Spacer() | Spacer()(ほぼ同じ!) |
Text | Text |
Button | Button |
.padding() | Modifier.padding() |
.frame(maxWidth: .infinity) | Modifier.fillMaxWidth() |
.frame(maxWidth: .infinity, maxHeight: .infinity) | Modifier.fillMaxSize() |
Image("logo") | Image(painterResource(...)) |
Button {} | Button(onClick = {}) |
.frame(width:180, height:180) | Modifier.size(180.dp) |
.padding(.top, 32) | Spacer(height = 32.dp) または Modifier.padding(top = 32.dp) |
NavigationLink | navController.navigate() |
Toggle | Switch |
TextField | OutlinedTextField |
ObservableObject | ViewModel |
@Published | StateFlow |
@StateObject | viewModel() |
UserDefaults | DataStore |
Task {} | viewModelScope.launch {} |
アノテーション
| アノテーション | 役割 |
|---|---|
@HiltAndroidApp | アプリ全体でHiltを有効にする |
@AndroidEntryPoint | このActivityやFragmentでHiltを使う |
@Inject | このクラスやコンストラクタはHiltが生成できる |
@HiltViewModel | このViewModelはHiltが管理する |
@ApplicationContext | アプリ全体のContextを注入する |
ファイル分割するときの注意点
単純にファイルを追加して@Composable関数をコピペするとエラーが出ます。
対処を簡単に書くと引数に【modifier: Modifier = Modifier】が必要ということ。
ファイル分割の方法は関係なく、Composeのコンポーネント設計として「Modifierを受け取るようにしましょう」というルールに引っかかっています。
正しいファイル分割
例えば元が
MainScreen.kt
@Composable
fun MainScreen() {
Column {
Text("タイトル")
UserInfo()
}
}
@Composable
fun UserInfo() {
Column {
Text("名前")
Text("年齢")
}
}
だったとします。
これを
ui/
MainScreen.kt
UserInfo.kt
に分けるだけです。
MainScreen.kt
@Composable
fun MainScreen() {
Column {
Text("タイトル")
UserInfo()
}
}
UserInfo.kt
@Composable
fun UserInfo() {
Column {
Text("名前")
Text("年齢")
}
}
これだけでOKです。
同じpackageならimportすら不要です。
なぜ警告が出たのか
おそらく切り出したScreenが
@Composable
fun SettingScreen() {
Column {
...
}
}
のようになっているからです。
Composeの設計ガイドでは、
画面を表示するComposableはModifierを受け取れるようにしましょう
というルールがあります。
なので
@Composable
fun SettingScreen(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
...
}
}
とします。
呼び出し側は
SettingScreen()
でも
SettingScreen(
Modifier.fillMaxSize()
)
でも使えるようになります。
なぜModifierが必要?
例えば親画面で
Column {
SettingScreen(
Modifier.weight(1f)
)
}
のような使い方ができます。
Modifierを受け取れないと
@Composable
fun SettingScreen() {
親からレイアウトを制御できません。
Composeでは
Modifierは親が決める
という思想があります。
Googleのサンプルも同じ
Google公式ではほぼ全て
@Composable
fun ProfileCard(
modifier: Modifier = Modifier,
user: User
) {
Card(
modifier = modifier
) {
...
}
}
という形になっています。
ただし例外もある
例えば
@Composable
private fun NameText() {
Text(...)
}
のような
- private
- このファイルだけで使う
- 小さな部品
ならModifier無しでも問題ありません。
Lintもprivate関数には出ないことがあります。
コード分割のおすすめ構成
Composeでは一般的に
ui/
HomeScreen.kt
SettingScreen.kt
components/
UserCard.kt
TopBar.kt
Loading.kt
ErrorView.kt
のように
- Screen(画面)
- Components(部品)
を分けます。
さらに大きくなれば
ui/
home/
HomeScreen.kt
HomeViewModel.kt
HomeState.kt
HomeComponents.kt
setting/
SettingScreen.kt
SettingViewModel.kt
という構成もよく採用されます。
今回の警告への対処
もし切り出した関数が
@Composable
fun ○○Screen() {
なら、
@Composable
fun ○○Screen(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
...
}
}
プレビューのエラー
その警告もエラーではなくCompose Lintの推奨事項です。
原因
現在はおそらく
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
RoughlyCalculationTheme {
AppNavigation()
}
}
となっていますよね。
GreetingPreview()はプレビュー専用なので、他のファイルから呼ばれることはありません。
そのため、public(デフォルト)ではなく**privateにしてください**という警告です。
修正方法
@Preview(showBackground = true)
@Composable
private fun GreetingPreview() {
RoughlyCalculationTheme {
AppNavigation()
}
}
これだけです。
なぜprivateが推奨なの?
@Preview関数はAndroid Studioがプレビュー表示のためだけに使用します。
通常のアプリ実行中には呼ばれませんし、他のファイルから呼び出すこともありません。
そのため
- public(デフォルト)❌
- internal △
- private ○(推奨)
となります。
Composeでよくあるルール
今後もLintから次のような警告が出ることがあります。
| 対象 | 推奨 |
|---|---|
| 画面やUI部品 | modifier: Modifier = Modifier を受け取る |
| Preview関数 | private にする |
| ファイル内だけで使うComposable | private にする |
| 外部から使うComposable | public(デフォルト)でOK |
@Composable
fun AppNavigation(
modifier: Modifier = Modifier
) {
// ...
}
@Preview(showBackground = true)
@Composable
private fun AppNavigationPreview() {
RoughlyCalculationTheme {
AppNavigation()
}
}
○○Preview()という名前にしておくと、画面ごとにプレビューを管理しやすくなります。
Unit
Jetpack Composeにおける「unit(ユニット)」は、文脈によって主に2つの意味に分かれます。
最も一般的な文脈では、UI画面を構成する最小の組み立て単位である「コンポーズ可能な関数(Composable関数)」を指します。もう一つは、Kotlinの言語仕様である戻り値がないことを示すUnit型です
1. UIの構成単位(ビルディングブロック)としてのunit
Androidの公式ドキュメントでは、「コンポーズ可能な関数(Composable関数)とは、UIの一部を記述するUnit(単位)を出力する関数のこと」と定義されています。
Jetpack Composeは、ボタンやテキストといった小さなUIの「単位(unit)」をレゴブロックのように組み合わせることで、1つの大きな画面(UIツリー)を構築していきます
特徴: 細かい単位に分割して作成(コンポーネント化)するため、同じUIの見た目や機能を別の画面で簡単に再利用できます。
具体例:
|
1 2 3 4 5 |
@Composable fun UserCard(name: String) { // これ自体が1つのUIユニット(単位)になる Text(text = "こんにちは、${name}さん") } |
2. Kotlinの「Unit型」としてのunit
Kotlinプログラムとしての文脈では、「値を返さない関数」の戻り値の型である Unit を指します。Javaでいう void に相当するものです。
Jetpack Composeのコードでは、主に以下の2つの場所で Unit という記述を頻繁に目にします。
① ボタンのクリックイベント(コールバック関数)
ボタンが押されたときの処理を親コンポーネントに伝える際、引数も戻り値もないラムダ式として () -> Unit が使われます。
|
1 2 3 4 5 6 |
@Composable fun MyButton(onClick: () -> Unit) { // 戻り値がないためUnitを指定 Button(onClick = onClick) { Text("クリック") } } |
② レイアウトの中にさらにUIを配置する枠(スロット)
Column や Row、Box など、他のUI要素を内側に挟み込めるコンポーネントは、引数として content: @Composable () -> Unit を受け取ります
|
1 2 |
// コンテンツを中に入れるための枠の定義 content: @Composable () -> Unit |
引数の順番
Jetpack Composeにおける自作のComposable(コンポーズ可能)関数を定義する際の引数の順番には、公式ガイドラインで推奨されている明確なルール(ベストプラクティス)があります。
ルールに従うことで、他のエンジニアが読みやすく、名前付き引数を省略した際にもバグの起きにくいコードになります。
📌 推奨される引数の順番
引数は以下の4つのグループの順に並べます。
@Composable
fun MyButton(
// 1. 必須パラメーター(デフォルト値なし)
text: String,
onValueChange: (String) -> Unit,
// 2. Modifier(最初のオプショナル引数)
modifier: Modifier = Modifier,
// 3. オプショナルパラメーター(デフォルト値あり)
enabled: Boolean = true,
style: TextStyle = TextStyle.Default,
// 4. Composableスロット(末尾のラムダ式)
content: @Composable () -> Unit
) {
// 処理
}
1. 必須パラメーター
- デフォルト値を持たない、そのコンポーネントの表示や動作に絶対必要な引数です。
- テキスト、状態(State)、イベントのコールバック(
onClickなど)が該当します。
2. Modifier(最重要)
- オプショナル(デフォルト値あり)パラメーターの先頭に、必ず
modifier: Modifier = Modifierを配置します。 - これにより、呼び出し側が名前付き引数を省略して
MyButton("Click Me", Modifier.padding(8.dp))のように直感的に Modifier を渡せるようになります。
3. オプショナルパラメーター
- デフォルト値が設定されている引数です。
- 色、スタイル、有効/無効フラグ(
enabled)など、基本はデフォルトのままで、必要に応じてカスタマイズしたい項目を並べます。
4. 組み込みの縦・横サイズや子要素(スロット)
- 関数の最後の引数には、
content: @Composable () -> Unitのような Composable ラムダ式を配置します。 - Kotlin の「末尾のラムダ式(Trailing Lambda)」の構文が使えるため、呼び出し側がカッコの外にスッキリと子要素を記述できるようになります。
// 末尾ラムダにより、カッコの外にコンテンツを書ける MyButton(text = "送信") { Text("中身のテキスト") }
💡 【おまけ】Modifier の中の「チェーンの順番」について
引数ではなく Modifier.padding().clickable() のようにメソッドを繋げる順番の質問である場合、Compose は左から右(上から下)に順番に適用されます。
Modifier.padding(16.dp).clickable { }➔ 余白を含めない内側だけがタップ可能Modifier.clickable { }.padding(16.dp)➔ 余白を含めた全体がタップ可能
順番によって見た目や挙動が変わるため注意してください。
compose-lints の導入の仕方
Slackが開発している compose-lints は、Android標準のLint(Android Lint)を拡張する仕組みになっているため、Gradleファイルに依存関係を1行追加するだけで極めて簡単に導入できます。
具体的な導入手順と実行方法は以下の通りです。
1. 依存関係の追加
アプリモジュール、またはJetpack Composeを使用している機能モジュールの build.gradle.kts を開き、dependencies ブロック内に以下の記述を追加します。
// app/build.gradle.kts (または各機能モジュールの build.gradle.kts)
dependencies {
// 依存関係を追加(バージョンは最新のものを指定してください)
lintChecks("com.slack.lint.compose:compose-lint-checks:1.4.2")
}
※ マルチモジュール構成のプロジェクトの場合は、各モジュールの build.gradle.kts に記述するか、共通の共通設定スクリプトに記述します。
※ 記述したら、Android Studio右上に出現する 「Sync Now」 ボタンをクリックして同期させてください。
2. Lintの実行方法
導入が完了すると、Android Studioのエディタ上、およびコマンドラインから自動的にチェックが機能するようになります。
💻 コマンドラインから実行(CI環境など)
プロジェクトのルートディレクトリで以下のコマンドを実行します。
- Mac / Linux:
./gradlew lint - Windows:
gradlew lint
これにより、Slackのカスタムルール(例: ComposeModifierOrder, ComposeNaming など)も含めた解析結果のHTMLレポートが生成されます。
🛠️ Android Studio 上で手動実行
- 上部メニューから [Code] > [Inspect Code…] を選択します。
- スコープ(プロジェクト全体、または特定のファイル)を指定して [OK] を押します。
- 画面下の
Inspection Resultsウィンドウに結果が表示されます。
💡 知っておくと便利な補足設定
Slackのルールの一部はデフォルトで「無効(Disabled)」になっており、オプトイン(明示的な有効化)が必要なものもあります。例えば「Material 2のAPI利用を禁止するルール(ComposeM2Api)」などを有効化したい場合は、build.gradle.kts に以下のように設定を追加します。
android {
lint {
// 特定のルールを有効化、またはエラー扱いにしたい場合
enable += "ComposeM2Api"
error += "ComposeM2Api"
// 逆に特定のルールを無視したい場合(例: 既存コードが多くて一旦スキップする場合)
// disable += "ComposeNaming"
}
}
導入作業中に Gradleの同期エラー が発生したり、特定のルールだけを チーム開発用にカスタマイズ(エラーではなく警告に変更するなど) したい場合はありますか?必要であれば、プロジェクトの運用に合わせた調整方法もご案内できます。
it
Jetpack Composeにおける it は、Kotlinの言語仕様に基づき、「ラムダ式(関数に渡す匿名ブロック)に渡された『唯一の引数』を省略して表現したキーワード」の役割を持っています。
Compose特有の機能ではなくKotlinの標準機能ですが、Composeのコード内ではUIの配置やイベント処理で頻繁に登場します。
主に使われる3つの具体的な役割とシーンは以下の通りです。
1. リスト表示(LazyColumnなど)のデータ参照
大量のデータをリスト表示する LazyColumn の中では、現在処理している「要素そのもの」を指す役割になります。
val itemList = listOf("リンゴ", "バナナ", "ミカン")
LazyColumn {
items(itemList) { // ← 引数が1つ(各フルーツの文字列)なので省略可能
// 「it」は itemList から1つずつ取り出された文字列(例:"リンゴ")を指す
Text(text = it)
}
}
2. 入力フォーム(TextField)の値の更新
テキスト入力欄の TextField では、ユーザーがキーボードで「新しく入力した文字列」を指す役割になります。
var textState by remember { mutableStateOf("") }
TextField(
value = textState,
onValueChange = { // ← 新しい入力文字列が1つだけ渡される
// 「it」はユーザーが今入力した最新の文字列を指す
textState = it
}
)
3. レイアウトの余白(PaddingValues)の適用
画面の基本構造を作る Scaffold コンポーネントなどでは、ツールバーやボトムバーと重ならないように自動計算された「余白データ(PaddingValues)」を指す役割になります。
Scaffold(
topBar = { TopAppBar(title = { Text("画面タイトル") }) }
) { it -> // ← 計算された余白データが1つ渡される(明示的に書くことが多い)
// 「it」を使って、中身のコンテンツに適切な余白(インセット)を設定する
Box(modifier = Modifier.padding(it)) {
Text("メインコンテンツ")
}
}
💡 さらに読みやすくするためのコツ
it はコードを短く書けて便利ですが、コードが複雑になると「it が何を指しているか」が分かりにくくなります。その場合は、以下のように明示的な名前をつけて書き換えるのがベストプラクティスです。
// it を使わず「fruit」と名前をつけることで、中身がハッキリわかる
LazyColumn {
items(itemList) { fruit ->
Text(text = fruit)
}
}
by
Jetpack Composeにおける by キーワードの主な役割は、状態(State)の値を読み書きする際に .value の記述を省略し、通常の変数と同じように扱えるようにすることです。
これはKotlinのプロパティ委譲(Property Delegation)という機能を利用しています。
💻 コードで見る違い
by を使う場合と使わない( = を使う)場合で、コードの書き方が以下のように変わります。
1. by を使用する場合(推奨)
by を使うと、変数は自動的に中身の型(この場合は String)として扱われます。
var text by remember { mutableStateOf("こんにちは") }
// 読み込み:.value は不要
Text(text = text)
// 書き込み:直接代入できる
Button(onClick = { text = "さようなら" }) { ... }
2. = を使用する場合
= を使うと、変数の型は MutableState<String> のままになります。そのため、値を操作するたびに毎回 .valueを付ける必要があります。
val textState = remember { mutableStateOf("こんにちは") }
// 読み込み:.value が必要
Text(text = textState.value)
// 書き込み:.value への代入が必要
Button(onClick = { textState.value = "さようなら" }) { ... }
🛠️ by を使うための準備(注意点)
by キーワードを使用するには、拡張関数である getValue と setValue をインポートする必要があります。
Android Studioで by の部分に赤線(エラー)が出た場合は、ファイルの先頭に以下の import 文を追加してください。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
(※通常は、エラー部分で Alt + Enter(Macは Option + Return)を押すことで自動インポートできます)
💡 メリットのまとめ
- コードがスッキリする: 画面上に何度も登場する
.valueを排除できます。 - 直感的に書ける: 一般的なKotlinの変数(
var)と全く同じ感覚で、状態の更新や参照が行えます。
Jetpack Composeで画面の状態(State)を定義する際は、特別な理由がない限り by remember { mutableStateOf(...) } の構文を使うのが標準的なベストプラクティスとなっています。
remember(rememberSaveable)
画面回転で消えないようにするには【rememberSaveable】を代わりに使用します。
もっと有用なデータ保持クラス【ViewModel】
Jetpack Composeにおける remember の効果は、UIの再描画(再コンポーズ)が発生しても、内部のデータや状態を破棄せずに保持し続けることです。
通常、Composeの関数(@Composable)はデータが変わるたびに何度も最初から実行し直されるため、通常の変数は毎回リセットされてしまいます。remember を使うことで、初回実行時の計算結果をメモリにキャッシュし、2回目以降の実行時にもその値を再利用できます。
🧱 基本的な使い方と効果
主に mutableStateOf(画面を書き換えるための状態管理)とセットで使われます。
// 🔴 remember がない場合:ボタンを押して再コンポーズが走るたびに、countが0にリセットされる
var count = 0
// 🟢 remember がある場合:再コンポーズが発生しても、前回の数値を保持できる
var count by remember { mutableStateOf(0) }
⚡ 3つの主なメリット(効果)
- 画面のチラつきやリセットを防ぐ: ユーザーが入力した文字(
TextField)や、ボタンのタップ回数などの状態を維持します。 - 重い処理の最適化(無駄な計算のスキップ): 初期化時に時間のかかる計算や、重いオブジェクト(例:
Bitmapのデコードなど)の生成をrememberで囲むと、2回目以降はキャッシュが返されるためアプリの動作が軽くなります。 - 無駄なインスタンス生成の防止: 描画のたびに新しいオブジェクトが作られるのを防ぎ、メモリ(GC)への負荷を減らします。
🔑 キー(引数)によるキャッシュの破棄
remember に引数(キー)を渡すと、「その値が変わったときだけ、中身を再計算する」という効果を持たせることができます。
// userIdが変わったときだけ、重いユーザー情報の取得処理を再実行する
val userInfo = remember(userId) { fetchUserInfo(userId) }
userId が同じである限り、どれだけ画面が再描画されても fetchUserInfo は実行されず、キャッシュが使い回されます。
⚠️ 注意すべき限界(落とし穴)
remember の保持期間は、そのUI要素が画面に表示されている間(コンポジションの寿命内)だけです。以下のケースでは値が消えてリセットされるため注意が必要です。
- 画面の回転(Activityの再生成)
- 画面の縦横切り替えやダークモード切り替え
- リスト(LazyColumn)をスクロールして、項目が画面外に見えなくなったとき
💡 解決策: 画面回転などに対応したい場合は、公式ドキュメントでも推奨されている rememberSaveable を使用すると、データを一時的に保存(Bundle)して状態を復元してくれます。
mutableStateOf(false)
Jetpack Compose における mutableStateOf(false) は、「初期値が false の、Compose が変更を監視できるデータ(状態)」を作る効果があります。
この関数を使用することで、値が true や false に切り替わった瞬間に、その値を読み取っている UI 部分だけが自動的に再描画(再コンポーズ)されます。
💡 主な効果と仕組み
1. 状態の変更を検知して UI を自動更新する
通常の var isActive = false のような変数では、値を書き換えても画面は変わりません。mutableStateOf(false) でラップすることで、Compose が値の変更(false ⇄ true)をリアルタイムに監視し、影響のある UI コンポーネントだけをピンポイントで更新します。 [1, 3]
2. ダイアログの表示やボタンの有効化(Boolean 制御)に最適
初期値が false(偽)であるため、以下のような「最初はオフ、トリガーでオンになる」機能の実装によく使われます。
- ダイアログの表示・非表示(最初は
falseで非表示、クリックでtrueにして表示) - ローディング中フラグ(処理が始まったら
true、終わったらfalse) - ボタンの有効・無効化(入力チェックが通ったら
trueに変更)
🛠️ 基本的なコード例
実際にコンポーザブル関数内で使う場合は、画面が再描画されるたびに値が false にリセットされないよう、通常は remember と組み合わせて宣言します。 [1]
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun ToggleButtonExample() {
// 1. 初期値 false の状態を作成(rememberで画面再描画時も値を保持)
var isEnabled by remember { mutableStateOf(false) }
// 2. 状態(isEnabled)の値に応じて UI が自動で切り替わる
Button(onClick = {
// ボタンクリックで true / false を反転
isEnabled = !isEnabled
}) {
Text(if (isEnabled) "ON の状態です" else "OFF の状態です")
}
}
⚠️ 注意すべきポイント
rememberを忘れない
単にval state = mutableStateOf(false)と書くだけだと、他の要因で画面が再描画(再コンポーズ)された際に、再びfalseに初期化されてしまいます。値を維持するために必ずremember { mutableStateOf(false) }の形で使いましょう。 [1]- 画面回転対策には
rememberSaveable
スマホの画面を回転させたり、アプリがバックグラウンドに回ったりした際にもtrue/falseの状態を維持したい場合は、rememberの代わりにrememberSaveableを使用します。var isEnabled by rememberSaveable { mutableStateOf(false) }
CoroutineScope(Dispatchers.IO)
Jetpack Compose で CoroutineScope(Dispatchers.IO) を使用する最大の効果は、重い通信やデータ処理を UI スレッドから完全に切り離し、画面のカクつき(ブロック)を防ぐことです。
しかし、Compose 内でこの記述を直接生で使うのは メモリリークや意図しないバグの原因になるため推奨されません。 Android 開発のベストプラクティスに基づいた、効果的な仕組みと正しい代替案を解説します。
💡 3つの主な効果
- UI のフリーズ防止
ネットワーク通信やデータベース(Room)処理、ファイルの読み書きなどの重い処理をバックグラウンドスレッドで実行します。これにより、毎秒 60〜120 フレームで動作する UI 描画スレッド(Main スレッド)を絶対に止めません。 - 並行処理の最適化
Dispatchers.IOは、Qiita の解説にもある通り、I/O 待ち(ブロック状態)が発生する処理用に設計されています。必要に応じてスレッド数が自動で柔軟に増減するため、アプリ全体の処理効率が向上します。 - UI ライフサイクルとの分離
Compose の関数外で独立して動き続けるスコープを作れるため、画面が再描画(リコンポジション)されても途中で処理がリセットされません。
⚠️ 直呼びが NG な理由(危険性)
もし Composable 関数の中で以下のように記述した場合、深刻な問題が発生します。
// ❌ やってはいけないアンチパターン
@Composable
fun MyComponent() {
Button(onClick = {
// ボタンを押すたびに古い処理がキャンセルされず、新しいスコープが乱立してメモリがリークする
CoroutineScope(Dispatchers.IO).launch {
fetchData()
}
}) { Text("データ取得") }
}
- 理由:
CoroutineScope()を手動で生成すると、Compose のライフサイクル(画面が消える、コンポーネントが破棄されるなど)と連動しません。 画面が閉じてもバックグラウンドで処理が走り続け、メモリリークやクラッシュの原因になります。 [1]
🛠️ 正しい実装パターン(推奨)
Compose で安全に Dispatchers.IO の効果を得るには、以下のいずれかの方法を使ってください。
1. ViewModel を使う(最も推奨)
ビジネスロジックやデータ取得は ViewModel に任せます。これにより、画面の回転や破棄が起きても適切に処理が管理されます。 [2]
class MyViewModel : ViewModel() {
fun loadData() {
// viewModelScope は ViewModel 破棄時に自動キャンセルされる
viewModelScope.launch {
// 処理自体を IO ディスパッチャに切り替える
withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
}
}
}
2. Compose 内なら rememberCoroutineScope と withContext を組み合わせる
UI 側のイベント(ボタンタップなど)をきっかけに、Compose のライフサイクルに紐づいたコルーチンを作りたい場合に使用します。 [3]
@Composable
fun MyComponent() {
// Compose のライフサイクルに連動した安全なスコープ(Mainスレッド起点)
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
// スコープ自体は Main だが、重い処理の箇所だけ IO に切り替える
val result = withContext(Dispatchers.IO) {
fetchDataFromNetwork() // 安全にバックグラウンド実行
}
// ここに戻ってきたときは Main スレッドなので、そのまま UI に値をセットできる
}
}) { Text("データ取得") }
}
Icons.Default.ArrowBack Iconsでエラー
|
1 2 3 4 5 6 7 8 9 |
dependencies { // 拡張マテリアルアイコンライブラリを追加 implementation("androidx.compose.material:material-icons-extended") } import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack // AutoMirroredの場合 // または import androidx.compose.material.icons.filled.ArrowBack |
TopAppBar()エラーが出る場合
赤字でオンマウスしてオプトインを実行
自作する場合
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 例: シンプルなトップバーの自作 @Composable fun SimpleCustomTopAppBar(title: String) { Surface(color = MaterialTheme.colorScheme.primaryContainer) { Row( modifier = Modifier.fillMaxWidth().height(64.dp).padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { Text(text = title, style = MaterialTheme.typography.titleLarge) } } } |
AlertDialog
従来のXML開発(DialogFragment)とは異なり、Boolean 型の「状態(State)」によって表示・非表示を管理するのが最大の特徴です。
1. 基本的な実装例
以下は、ボタンを押すとダイアログが開き、「確認」または「キャンセル」を選択できる最も標準的なコード例です。 [1, 4]
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun AlertDialogExample() {
// 1. ダイアログの表示状態を管理するStateを定義
var showDialog by remember { mutableStateOf(false) }
// ダイアログを表示するトリガーとなるボタン
Button(onClick = { showDialog = true }) {
Text("ダイアログを表示")
}
// 2. Stateがtrueの場合のみAlertDialogを呼び出す
if (showDialog) {
AlertDialog(
// ダイアログの外側や戻るボタンが押された時の処理
onDismissRequest = { showDialog = false },
// タイトル領域
title = { Text(text = "確認") },
// 本文領域
text = { Text(text = "この操作を実行してもよろしいですか?") },
// 確定ボタン(右側に配置されます)
confirmButton = {
TextButton(onClick = {
// 確定時の処理をここに記述
showDialog = false
}) {
Text("確認")
}
},
// 閉じる・キャンセルボタン(左側に配置されます)
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("キャンセル")
}
}
)
}
}
2. 主要なパラメータ解説
AlertDialog に用意されている主な引数は以下の通りです。
| パラメータ名 [1, 5] | 役割 |
|---|---|
onDismissRequest | ユーザーがダイアログの外側をタップしたり、戻るボタンを押した際に呼ばれるラムダ式。通常は状態を false にします。 |
title | ダイアログ最上部に表示するタイトル(Text など)。 |
text | ダイアログ中央に表示するメッセージ本文。 |
confirmButton | ユーザーに承認を求めるポジティブなボタン(通常は右側)。 |
dismissButton | 操作を中断するネガティブなボタン(通常は左側、省略可能)。 |
icon | タイトルの上に配置できるアイコン(グラフィック要素、省略可能)。 |
3. よくあるカスタマイズ・注意点
外側をタップしても閉じないようにする(モーダル化)
デフォルトでは、ダイアログ外の背景領域をタップすると onDismissRequest が走り、ダイアログが閉じます。
これを禁止し、ボタンを押すまで閉じない強力な警告にしたい場合は、onDismissRequest の中身を空にします。
AlertDialog(
onDismissRequest = { /* 何も記述しないことで外側タップを無効化 */ },
title = { Text("重要な警告") },
// ...
)
アイコンを追加して視覚的に分かりやすくする
Material 3のデザインガイドラインに沿って、タイトルの上にアイコンを表示させることができます。
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Icon
AlertDialog(
onDismissRequest = { showDialog = false },
icon = { Icon(Icons.Filled.Warning, contentDescription = "警告アイコン") },
title = { Text(text = "削除の確認") },
// ...
)
さらに自由なレイアウト(テキスト入力欄や独自のリストなどを配置したい場合)を作りたいときは、AlertDialog ではなく、制約のないオープンなコンテナである Dialog コンポーザブル を使用するのが便利です。
正確な小数点を扱う型
Jetpack Composeで金額や高精度な10進数を扱うために BigDecimal を使用する場合、Compose特有の「状態管理(State)」や「再描画(Recomposition)」の仕組みを考慮する必要があります。
具体的には、不変性(Immutable)の扱いや、TextField で文字列と相互変換する際の入力制御に注意が必要です。
1. BigDecimal は自動的に「安定(Stable)」とみなされる
Jetpack Composeのコンパイラは、UIの不要な再描画(スキップ)を最適化するために、クラスが「Stable(安定)」か「Unstable(不安定)」かを判定します。
BigDecimalは不変(Immutable)オブジェクトです。- 一度生成されたインスタンスの内部値は変更されず、計算のたびに新しいインスタンスを返します。
- そのため、Composeのコンパイラは
BigDecimalを自動的にStableな型として扱い、Composable関数の引数に渡しても不要な再描画を引き起こしません。 [2, 3]
2. 基本的な状態管理(State)の書き方
画面内の計算結果や入力値を保持する場合、mutableStateOf と組み合わせます。
import androidx.compose.runtime.*
import java.math.BigDecimal
@Composable
fun PriceCalculator() {
// 初期値 0 の BigDecimal を保持
var amount by remember { mutableStateOf(BigDecimal.ZERO) }
Button(onClick = {
// BigDecimalは不変なので、新しいインスタンスを代入することでStateが検知される
amount = amount.add(BigDecimal("10.50"))
}) {
Text(text = "合計: ¥${amount.toPlainString()}")
}
}
3. TextField(入力フォーム)で扱う際の注意点と実装パターン
最も開発者が直面しやすい問題は、TextField の値(String)を BigDecimal に変換して保持しようとするときです。
ユーザーが「12.」や「0.」と入力した段階で即座に BigDecimal に変換しようとすると、末尾のドットが消えてしまったり、変換エラーが発生して入力ができなくなります。
💡 推奨される解決策:Stateは String で保持し、表示や計算時に BigDecimal に変換する
入力中の文字列はそのまま String として保持し、バリデーション(数値チェック)を行いながら、必要な場所でのみ BigDecimal を使うのが最も安全なパターンです。
import androidx.compose.runtime.*
import androidx.compose.material3.TextField
import androidx.compose.material3.Text
import java.math.BigDecimal
@Composable
fun CurrencyInput() {
var inputStr by remember { mutableStateOf("") }
// 入力文字列から安全に BigDecimal をパース(失敗したら null)
val bigDecimalValue = remember(inputStr) {
inputStr.toBigDecimalOrNull()
}
TextField(
value = inputStr,
onValueChange = { newValue ->
// 数字、または1つだけのドット/カンマのみ入力を許可する簡易バリデーション
if (newValue.isEmpty() || newValue.matches(Regex("""^\d*\.?\d*$"""))) {
inputStr = newValue
}
},
label = { Text("金額を入力") }
)
// 計算や表示部分
if (bigDecimalValue != null) {
// 例: 消費税10%を計算
val tax = bigDecimalValue.multiply(BigDecimal("0.10"))
Text("消費税: ¥${tax.toPlainString()}")
}
}
4. ViewModelとの連携(ベストプラクティス)
ビジネスロジックや計算ロジックはUI(Composable)に書かず、ViewModel 内で StateFlow などを使って管理するのが一般的です。 [4]
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.math.BigDecimal
class InvoiceViewModel : ViewModel() {
private val _priceStr = MutableStateFlow("")
val priceStr = _priceStr.asStateFlow()
// 画面側で collectAsState するための、計算済み状態のStateFlow
private val _totalPrice = MutableStateFlow(BigDecimal.ZERO)
val totalPrice = _totalPrice.asStateFlow()
fun onPriceChanged(newStr: String) {
if (newStr.isEmpty() || newStr.matches(Regex("""^\d*\.?\d*$"""))) {
_priceStr.value = newStr
// 有効な数値であれば計算処理を行う
val decimal = newStr.toBigDecimalOrNull() ?: BigDecimal.ZERO
_totalPrice.value = decimal.multiply(BigDecimal("1.10")) // 税込計算
}
}
}
まとめ
BigDecimalは不変オブジェクト(Immutable)なのでComposeのState管理と相性が良い。TextFieldで扱う際は、State自体をBigDecimalにするのではなく、Stringのまま保持して必要な時にtoBigDecimalOrNull()で安全に変換する。
三項演算子を使った変換
val wage = hourlyWage.toBigDecimalOrNull() ?: BigDecimal.ZERO
隙間自動調整
Spacer(Modifier.weight(1f))
画面遷移について
Androidの場合、画面遷移した後popBackStack()で戻る時に何秒か遷移前の参照が切れないのでボタンを連打するとpopBackStack()が複数呼ばれてしまい、結果バグるので対処が必要。
対処例(遷移先が一つで戻る場所が一つの場合)
if (navController.previousBackStackEntry == null) return
数字のみのキーボード
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
例:
|
1 2 3 4 5 6 7 8 9 |
OutlinedTextField( value = hourlyWage, onValueChange = { hourlyWage = it }, label = { Text("時給") }, singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number ) ) |
コメント