既存のアプリにComposeを入れるにあたってのCompose学習ログ
前提
- 対象のアプリは、ComposeではなくViewでレイアウトが組まれている。
- Navigation Componentを使っている。
TODO: どんな画面に組み込みたいか
セットアップ
進め方
TODO:
チュートリアルではComposeTutorialThemeというComposableが自動で生成されているが、既存のアプリにComposeを入れる場合、どうする?
-
Compose のデザイン システム
-
Material Design 3 in Compose
- Material Design 3 Themeはcolor scheme, typography, shapeからなる。
Material theming
An M3 theme contains the following subsystems: color scheme, typography and shapes.
... Jetpack Compose implements these concepts with the M3 MaterialTheme composable:MaterialTheme( colorScheme = …, typography = …, shapes = …) { // M3 app content }
To theme your application content, define the color scheme, typography, and shapes specific to your app.
- 5つのキーカラーがある。おそらくPrimary, Secondary, Tertiary, Error, Backgroundのことかな。それぞれに対して、onPrimary, primaryContainer, onPrimaryContainerの色を用意する。
Color scheme
The foundation of a color scheme is the set of five key colors.- backgroundとsurfaceの違い
background
The background color that appears behind scrollable content.
...
surface
The surface color that affect surfaces of components, such as cards, sheets, and menus.
https://developer.android.com/reference/kotlin/androidx/compose/material3/ColorScheme
- backgroundとsurfaceの違い
- 以下のような感じでTheme.ktを用意する。
private val LightColorScheme = lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, primaryContainer = md_theme_light_primaryContainer, onPrimaryContainer = md_theme_light_onPrimaryContainer, // .. ) private val DarkColorScheme = darkColorScheme( primary = md_theme_dark_primary, onPrimary = md_theme_dark_onPrimary, primaryContainer = md_theme_dark_primaryContainer, onPrimaryContainer = md_theme_dark_onPrimaryContainer, // .. ) @Composable fun ReplyTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val colorScheme = if (!useDarkTheme) { LightColorScheme } else { DarkColorScheme } MaterialTheme( colorScheme = colorScheme, content = content ) }
- Material Design 3 Themeはcolor scheme, typography, shapeからなる。
- Compose のカスタム デザイン システム
- Compose 内のテーマの構造
-
Material Design 3 in Compose
ViewModel使い方
-
- これまでと似た感じで、viewModel()で取得できるみたい。
アーキテクチャ コンポーネントの ViewModel ライブラリを使用している場合、viewModel() 関数を呼び出すことで、任意のコンポーザブルから ViewModel にアクセスできます。
class MyViewModel : ViewModel() { /*...*/ } @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
- 依存関係の追加が必要。
注意: viewModel() 関数を使用するには、androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1 依存関係を build.gradle ファイルに追加してください。
- アクティビティ、フラグメントの近くで使う必要があるらしい。近くとは???直下が望ましいんだろうけど、Themeとかでラップする必要があるから直下は難しい。5~10個くらい深くても大丈夫なのかな。
注: ライフサイクルとスコープ設定のため、「スクリーン レベル」のコンポーザブルで ViewModel インスタンスにアクセスして呼び出す必要があります。スクリーン レベルとは、ナビゲーション グラフのアクティビティ、フラグメント、またはデスティネーションから呼び出されたルート コンポーザブルに近いものを指します。
- どういう仕組みになっているのか?
- たとえば、Activityの下のComposableで使っている場合、Activity終了したらViewModelも破棄される。
viewModel() は、既存の ViewModel を返すか、指定されたスコープで新しく作成します。ViewModel は、スコープが存続している限り保持されます。たとえば、コンポーザブルがアクティビティで使用されている場合、viewModel() は、アクティビティが終了するまで、またはプロセスが強制終了されるまで、同じインスタンスを返します。
- 何らかの仕組みで、Activityが生きている間は、ViewModelを覚えておいてくれているみたい。
- この仕組みは、Composableのrememberのような仕組みとは異なるみたい。何らかの方法で最も近いActivityやFragmentを探し出して、(ComposableではなくこれまでのViewでの実装の時と同じように)それをViewModelStoreOwnerとするっぽい。
ViewModel はコンポジションの一部として保存されません。フレームワークにより提供され、アクティビティ、フラグメント、ナビゲーション グラフ、あるいはナビゲーション グラフの宛先である ViewModelStoreOwner にスコープされます。ViewModel スコープの詳細については、ドキュメントをご覧ください。
状態をホイスティングする場所
- この仕組みは、Composableのrememberのような仕組みとは異なるみたい。何らかの方法で最も近いActivityやFragmentを探し出して、(ComposableではなくこれまでのViewでの実装の時と同じように)それをViewModelStoreOwnerとするっぽい。
- 何らかの仕組みで、Activityが生きている間は、ViewModelを覚えておいてくれているみたい。
- たとえば、Activityの下のComposableで使っている場合、Activity終了したらViewModelも破棄される。
- どういう仕組みになっているのか?
- 依存関係の追加が必要。
- これまでと似た感じで、viewModel()で取得できるみたい。
-
?: ViewModelのデータ取得のためのインターフェイスはどうするのがいいのか?前はよくLiveDataを使っていた。また、Composable側ではViewModelをどう使うのか。
- この例では、Listを公開している。
class WellnessViewModel : ViewModel() { private val _tasks = getWellnessTasks().toMutableStateList() val tasks: List<WellnessTask> get() = _tasks fun remove(item: WellnessTask) { _tasks.remove(item) } }
- 利用側ではそのまま使う。
import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun WellnessScreen( modifier: Modifier = Modifier, wellnessViewModel: WellnessViewModel = viewModel() ) { Column(modifier = modifier) { StatefulCounter() WellnessTasksList( list = wellnessViewModel.tasks, onCloseTask = { task -> wellnessViewModel.remove(task) }) } }
- ↑なんで、rememberしなくていいのか?→ViewModelが記憶してくれるから。
- 利用側ではそのまま使う。
- stateFlowも公開できる。
private val _uiState = MutableStateFlow(GameUiState()) val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
-
collectAsState()
でuiStateを利用する。@Composable fun GameScreen(/*略*/) { val gameUiState by gameViewModel.uiState.collectAsState() // ... }
-
- この例では、Listを公開している。
↑に関連して、一通り読んでみた。
-
Compose での ViewModel と状態
-
- Compose UIを設計する
- textfieldの部分Flutterではcontrollerで状態を保持するけど、viewModel等で保持する必要がある。あるいは、rememberSavable { mutableStateOf("") }かな。
GameLayout( currentScrambledWord = gameUiState.currentScrambledWord, userGuess = gameViewModel.userGuess, onUserGuessChanged = { gameViewModel.updateUserGuess(it) }, onKeyboardDone = { }, //... ) // GameLayout内 OutlinedTextField( value = userGuess, onChanged = onUserGuessChanged, // ... )
-
- ゲームの最終ラウンドを処理する
- Dialogの表示の仕方も宣言的。Flutterでは命令的であるとの対照的。
if (gameUiState.isGameOver) { FinalScoreDialog( score = gameUiState.score, onPlayAgain = { gameViewModel.resetGame() } ) }
- ?: これは、どこに記述しているのか? →
@Composable fun GameScreen(/*略*/)
の直下だった。Box(FlutterでいうStack)を使わなくても、手前に表示できるみたい。
- ?: これは、どこに記述しているのか? →
-
Jetpack ComposeでViewModelを使わずに、Composable関数を使って状態とロジックを切り出す!によると、宣言的UIにViewModelは適さないらしい。
- この記事の内容
- 宣言的UIにViewModelは適さないらしい
なぜViewModelを使わないのか?
宣言的UIには、ViewModelを使ったアーキテクチャは合わない、と感じているからです。- ViewModelにビジネスロジックを書くのは、よくない。DRYじゃないと言いたいっぽい。
Viewからロジックを切り出すために、ViewModelにロジックを切り出すことは、再利用性/変更容易性を妨げますし、UIロジックとドメインロジックの境界線が曖昧になります。
再利用されるビジネスロジックは、ドメインレイヤにカプセル化する必要がある と考えています。- これに関しては、ViewModelがダメというか、ViewModelのようなViewの状態を持つレイヤーでビジネスロジックを持つのがダメということだと思う。ViewModelが宣言的UIに適さないという根拠にはなっていない。
- そもそも、ViewModelって何?というところがある。→ Android Architecture ComponentのViewModelのことを言っている。
ViewModel は、アプリの回転時に破棄されない UI 関連のデータを保存します。
Android アーキテクチャ コンポーネント-
androidx.lifecycle.ViewModel
- ActivityやFragmentの状態を管理する。
ViewModel is a class that is responsible for preparing and managing the data for an Activity or a Fragment.
- ActivityやFragmentがfinishするまで生きる。
A ViewModel is always created in association with a scope (a fragment or an activity) and will be retained as long as the scope is alive. E.g. if it is an Activity, until it is finished.
In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a configuration change (e.g. rotation). The new owner instance just re-connects to the existing model.
- ActivityやFragmentの状態を管理する。
-
androidx.lifecycle.ViewModel
- ComposableがAndroidプラットフォームに依存することになるのは良くない。これはまあ確かに依存しないのが理想。
Having your composable functions reference the ViewModel unnecessarily ties your composables to your app/platform and is thus very smelly.
https://twitter.com/JimSproch/status/1397169679647444993?ref_src=twsrc^tfw|twcamp^tweetembed|twterm^1397169679647444993|twgr^0950479a50d3463865419f20f684c6405d2dddfa|twcon^s1_&ref_url=https%3A%2F%2Fqiita.com%2Fembed-contents%2Ftweetqiita-embed-content__ee10e26665104cb2adececfe729b494a- そもそもJetpack Composeって、Androidのものではないか。AndroidViewModelを使わないからといって、他のPlatform(iOS, Web, Desktopなど)に移植できるのか???
Jetpack Compose は、ネイティブ UI をビルドする際に推奨される Android の最新ツールキットです。
https://developer.android.com/jetpack/compose?gclid=CjwKCAjwkY2qBhBDEiwAoQXK5Q_s-o9J1_gJc2vLow5y0D_Nf7JJtthGjLJ7tfqNUefD_BEsluXwERoCMEMQAvD_BwE&gclsrc=aw.ds&hl=ja - Compose multiplatformは、Jetpack Composeをベースにしているらしい。
複数のプラットフォーム間で UI を共有できる宣言的フレームワーク。Kotlin と Jetpack Compose をベースにしています。
https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/ - Android ViewModelを使わない場合、AndroidのJetpack Composeを他のプラットフォームに移行するときに多少楽になる程度と認識。
- Compose multiplatformでは、ViewModelを利用せずどういうふうに設計するのか。その方法に従うというのはありかもしれない。→ このスクラップの別コメント参照。
- そもそもJetpack Composeって、Androidのものではないか。AndroidViewModelを使わないからといって、他のPlatform(iOS, Web, Desktopなど)に移植できるのか???
- 記事では触れられてないが、確かに、複雑な画面(例えば、LINEのホームのように友達リスト、ミニアプリ、ユーザーの投稿など相互に関連が少ない要素を複数表示するような画面)だと、1Activity 1ViewModelにすべきとされているAACのViewModelではきつい。(宣言的UIに適さない、という意見の根拠にはならないけど)
- しかしこれは、デザインの問題だし、仮にこのデザインでも複数のFragmentに相当するような複数のComposableを用意すればいけそうではある。Fragmentに相当する各Composableでby viewModel()でそれぞれのFragment相当のComposableに対応するViewModelを取得するような感じ。Activityの近くでby viewModel()を使えというルールで、このやり方がOKかは不明。
- ViewModelにビジネスロジックを書くのは、よくない。DRYじゃないと言いたいっぽい。
- 代案として、「オブジェクトを返すComposable関数(Factory Function)」を使う。
- これはいいと思う。
- DIどうするか?Hilt使える?
- Composeとその他のライブラリ > Hiltには、@HiltViewModelをViewModelにつける例が出てくるけど、ViewModel以外のクラスのインスタンスはComposableに注せないの?
- DIをどうするかという質問をした人がいたみたい(Tweetは消えている)。以下、回答。
A few good alternatives to consider, like Koin. Even better option would be Context Receivers which are a new feature we're previewing to the public in Kotlin 1.6.20. In general, avoiding DI like the plague would be advisable, especially in composables.
https://twitter.com/JimSproch/status/1502943763781484546- Koinというのがいいらしい。そもそもDIを使うべきでない???Context Receiversという方法があるみたい。Context Receiversとは?
- Context Receivers、ちょっとみてみた:Context Receiverを利用した簡易的なDI
- 以前どこかで見かけた、各依存を保持するstatic classに似ていると思った。Dependenciesクラスのstatic propertyで、
val xxxRepository: XxxRepository = XxxRepositoryImpl()
が定義されているというような。 - スコープ(インスタンスの生存期間)はどう制御するのか。
- 以前どこかで見かけた、各依存を保持するstatic classに似ていると思った。Dependenciesクラスのstatic propertyで、
- Context Receivers、ちょっとみてみた:Context Receiverを利用した簡易的なDI
- とりあえずViewModelでいい気がするな。ViewModelに依存するComposableは最小限にして。
- Koinも調べたい。
- 依存注入をどうしているかこのQiitaの筆者に聞いてみたいな。
- Koinというのがいいらしい。そもそもDIを使うべきでない???Context Receiversという方法があるみたい。Context Receiversとは?
- コードは以下。
data class CounterState( val count: Int, val increment: () -> Unit ) @Composable fun rememberCounter(): CounterState { var count by remember { mutableStateOf(0) } return remember(count) { CounterState( count = count, // 状態 increment = { count += 1 } // ロジック ) } }
- rememberとmuutableStateOf()ってなんだっけ?
- mutableStateOf()で取得できるMutableStateは、ComposableがそのStateを読み取ったことをCompopseが追跡するためのもの。Composeは、そのStateに変更があった時にそのComposableを再コンポースするためのもの。
Compose には、特定の状態を読み取るすべてのコンポーザブルの再コンポーズをスケジュール設定する、特別な状態トラッキング システムが用意されています。...これを行うには、状態の書き込み(「状態の変化」)だけでなく、状態の「読み取り」も追跡します。
Jetpack Composeの状態>コンポーズ可能な関数のメモリ - remember()は再コンポーズが発生しても、stateを記憶しておくためのもの。
このためには、コンポーズ可能なインライン関数 remember を使用できます。remember によって計算された値は、初回コンポーズ中にコンポジションに保存され、保存された値は再コンポーズ後も保持されます。
- mutableStateOf()で取得できるMutableStateは、ComposableがそのStateを読み取ったことをCompopseが追跡するためのもの。Composeは、そのStateに変更があった時にそのComposableを再コンポースするためのもの。
-
remember(count) {}
っていう書き方なんだ?このcountはkey。参考: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0) - 上の例で、CounterStateはMutableStateにしなくていいのかと思ったけど、その前にcountがmutableStateOf()で提供されているから、しなくても動くことは動く。
- だけど、なんか気持ち悪い。
- rememberとmuutableStateOf()ってなんだっけ?
- 以下の場合にViewModelを採用することがありうる
- Composeを組み込もうとしているアプリの既存のViewModelを使い続ける必要がある場合。
The docs must talk about them because many people have existing applications that already rely heavily on ViewModels, so the docs need to mention it for interoperability/completeness.
@JimSproch - 構成変更の前後で、同じインスタンスを維持したい時。
画面回転などによる再コンフィギュレーション時、rememberSaveableでの状態の「復元」ではなく、本当に状態をインスタンスレベルで維持しなければならないことがあります(センサAPI使ってるときとか)。
- rememberSaveableはsavedInstanceの仕組みを使って復元する。
rememberSaveable
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).
https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0) - ViewModelは構成変更があってもいきつづける。
ViewModel は、スコープの ViewModelStoreOwner が完全に消えるまでメモリ内に残ります。
- アクティビティの場合は終了時。
- フラグメントの場合はデタッチ時。
- Navigation エントリの場合はバックスタックからの削除時。
これにより、ViewModel は、構成の変更後に引き継ぐデータを保存するための優れたソリューションとなっています。
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja#lifecycle
- ViewModelは、onDestroyでは破棄されず、finish()で破棄される。
- rememberSaveableはsavedInstanceの仕組みを使って復元する。
- Composeを組み込もうとしているアプリの既存のViewModelを使い続ける必要がある場合。
- 宣言的UIにViewModelは適さないらしい
上の記事で触れられているCompositionLocalとは?
-
CompositionLocal でローカルにスコープ設定されたデータ
- ツリー内で利用できるデータの保存場所、と理解した。特定のノードで値を設定すると、その子孫で利用できる。FlutterのInhertedWidgetと似た感じかな。
ほとんどのコンポーザブルに明示的なパラメータ依存関係として色を渡す必要がなくなるよう、Compose には CompositionLocal が用意されています。これを使用して、ツリーをスコープとする名前付きオブジェクトを作成し、データを UI ツリーに流し込むための暗黙の方法としてそれを使用できます。
CompositionLocal 要素は通常、UI ツリーの特定のノードで値が設定されます。その値は、コンポーズ可能な関数のパラメータとして CompositionLocal を宣言しなくても、コンポーズ可能な子孫で使用できます。
- MaterialThemeでも利用されている。
CompositionLocal は、マテリアル テーマが内部で使用するものです。...具体的には、MaterialTheme colors、shapes、typography 属性からアクセスできる、LocalColors、LocalShapes、LocalTypography プロパティです。
- 設定するには、CompositionLocalProvider()をつかう。
CompositionLocal に新しい値を設定するには、CompositionLocalProvider と、CompositionLocal キーを value に関連付ける provides 中置関数を使用します。
...CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { DescendantExample() }
- ↑この記法何だ?
- providesはinfix(中置)関数らしい。Kotlinのinfixによれば、
to
とかもそう。infix fun provides(value: T): ProvidedValue<T>
Associates a CompositionLocal key to a value in a call to CompositionLocalProvider.
https://developer.android.com/reference/kotlin/androidx/compose/runtime/ProvidableCompositionLocal#provides(kotlin.Any)
- providesはinfix(中置)関数らしい。Kotlinのinfixによれば、
- キーの部分って、任意に指定できるのかな。
- ↑この記法何だ?
- 上の例では、Text内部でLocalContentAlpha.currentが使われている。
LocalContext.current.resources
のように、明示的に現在のContextを取得して、resourucesを取得することができる。CompositionLocal の現在の値にアクセスするには、その current プロパティを使用します。次の例では、Android アプリで一般的に使用されている LocalContext CompositionLocal の現在の Context 値を使ってテキストの書式設定を行っています。
- ツリー内で利用できるデータの保存場所、と理解した。特定のノードで値を設定すると、その子孫で利用できる。FlutterのInhertedWidgetと似た感じかな。
- Compose Multiplatformでは、Androidのandroidx.lifecycle.ViewModelは使えないと思うが、ViewModelの層はどう設計・実装するのがいいのか。
-
Kotlin Multiplatform samples
-
NYTimes KMP
- まとめ: このサンプルでは、手動で依存関係を解決している。具体的には、Composableで、ViewModel(AndroidViewModelではない、ほぼ単純なクラス)をnewしてrememberしている。ViewModelが汚れ役として、ViewModel内で、Domainをnewしつつ、Domainにstoreやserviceを注入している。このstoreやserviceは、staticだったり、ViewModelでその場でnewしていたりする。この方法もありかもしれない。
-
TopStoriesScreen.kt
- 以下の部分で、ViewModelを使っている。
@Composable fun TopStoriesScreen( onSelectArticle: (section: TopStorySection, uri: ArticleUri, title: String) -> Unit, ) { val viewModel: TopStoriesViewModel = rememberOnRoute(TopStoriesViewModel::class) { savedState -> TopStoriesViewModel(savedState) } // ... }
- ↑rememberしている。そして、普通にnewしちゃっている。
- このプロジェクトには、test codeないから、newしても困らないみたい。→ 一応以下の通りドメイン層はテストできるようになっていそう。
- ↑rememberしている。そして、普通にnewしちゃっている。
- 以下の部分で、ViewModelを使っている。
- ドメイン層をComposable Methodとして実装しているのが印象的: TopStoriesDomain.kt
- Composable Methodにしているのはなぜ?普通にclassでいいじゃないかと思う。
- 通常のクラスのViewModelで、Composable MethodであるTopStoriesDomain()を呼び出しているけど、これ可能なのか。
- moleculeFlowとは?→ Jetpack Composeを使って、StateFlowを作るやつらしい。だから、ViewModelでも、moleculeFlow {}内なのでComposable Methodを呼べる。
Build a StateFlow or Flow stream using Jetpack Compose
https://github.com/cashapp/molecule- やるとしたら、現時点では、この辺は使わないようにしたい。よくわかってないけど、今後結構変更が発生しそう。
- moleculeFlowを使うことでドメイン層をComposableで実現できるというのはなんとなくわかった。しかし、なぜ、ドメイン層をComposableで実装したいのかがよくわからない。さっきも書いたけど、普通にクラスでいいと思う。
- moleculeFlowとは?→ Jetpack Composeを使って、StateFlowを作るやつらしい。だから、ViewModelでも、moleculeFlow {}内なのでComposable Methodを呼べる。
- それで、このTopStoriesDomainには、呼び出し元のTopStoriesViewModelで依存を注入している。
-
NYTimes KMP
-
Jetpack Compose State Practices
-
まとめ:
- この記事を読んで感じた自分の理想は、UI(Composable) -> StateHolder(Composable) -> Data/Business Layer(DomainとRepository?)
- しかし、以下のデメリットが。
- 手動でのDIが必要(と認識している。少なくとも現時点では)。
- androidx.lifecycle.ViewModelと同じ寿命で管理する必要があるインスタンスを管理できない。(今回、今のところ、自分には影響はなさそう。)
- しかし、以下のデメリットが。
- 自分の場合、HiltによるDIが必要。そこで、androidx.lifecycle.ViewModelを使わざるをえない。
- UI(Composable) (-> StateHolder(Composable)) -> ViewModel -> Data/Business Layer とする。
- androidx.lifecycle.ViewModelを使う方法のデメリット
- この記事の例のように、ScaffoldStateのようなCompositionが管理している状態は、ViewModelに渡せない。この状態をComposableから抽出したい場合、StateHolderを用意する必要がある。
- 冗長。StateHolderとAndroidViewModelの役割が重複しているように見える。
- → なるべくStateHolderを作らなくて済むようにしたい。
- Multiplatformでも利用できるComposableから、Android platformへの依存が増えてしまう。将来Multiplatform対応する時に変更が増えるということだと思う。
- → 状態ホイスティングを意識しStatelessとStatefulのcomposableに分けることで、AndroidViewModelを使うComposableを限定して、将来の変更量を抑える。
- 結局、Jetpack Composeのドキュメント通りになった。
- この記事を読んで感じた自分の理想は、UI(Composable) -> StateHolder(Composable) -> Data/Business Layer(DomainとRepository?)
-
Practice2: Composableの中で作られたStateはrememberされなければならない
- remember {}すると、stateがcompositionの要素として保持される。
Composeは実行時にComposable関数を実行することによって、Compositionを構成します。
Compositionは木構造になっており...
...
このCompositionはUIの要素だけでなく、データも保持しておくことができ、それがremember{}による保存です。
- remember {}すると、stateがcompositionの要素として保持される。
-
Practice7: 必要な引数だけをComposable関数に渡そう
StatelessとStatefulのViewModelを分けて、必要なデータのみをstateless、つまり状態を持たないComposable関数に渡します。
// stateful @Composable fun SettingScreen( settingViewModel: SettingViewModel = viewModel() ) { val isDarkMode by settingViewModel.isDarkMode.collectAsState() SettingScreen(isDarkModeSetting = isDarkMode, onDarkModeSettingChanged = { settingViewModel.onDarkModeChange(it) }) } // stateless @Composable fun SettingScreen(isDarkModeSetting: Boolean, onDarkModeSettingChanged: (Boolean) -> Unit) { MySwitch(checked = isDarkModeSetting, onCheckChanged = { onDarkModeSettingChanged(it) }) }
- こんな感じでできれば、(下のStatelessの)SettingScreenは再利用可能になるので、上の記事でComposeからAndroid platformへの依存を増やすためよくないとされているAndroidViewModelを使ったとしても、将来Multiplatformにするときに変更の範囲をStatefulの方に抑え込むことができそう。⭐️
-
Practice8: Architecture ComponentのViewModelにCompositionで保持されている状態を渡さないようにしよう
- ScaffoldStateなどのCompositionが管理している状態は、Composableよりも寿命が長いAndroidViewModelには渡してはいけない。
-
Practice9: Composable関数にロジックが書かれないようにしよう
- Composableに状態やロジックが増えてきた場合、StateHolderに抽出する。
- ↑その状態やロジックは、ViewModelに持ってくればいいではないかと思った。
- だけど、この例だと、ScaffoldStateを使ってsnackbarを表示するロジックはViewModelに持って来れない(Practice8参照)。
- 公式では、Composable -> StateHolder -> ViewModel -> Data/Business Layerを推奨している。
Compose の状態ホルダー
シンプルな状態ホイスティングは、コンポーズ可能な関数自体で管理できます。ただし、トラッキングする状態の量が増加する場合や、コンポーズ可能な関数で実行するロジックが発生する場合は、ロジックと状態の役割を他のクラスの状態ホルダーにデリゲートすることをおすすめします。
https://developer.android.com/jetpack/compose/state?hl=ja#managing-state- 冗長すぎない?StateHolderとViewModelを分ける積極的な意味がわからない。この例のように、ScaffoldStateをViewModelに渡せないから仕方なくStateHolderを用意しているという感じ。
- ↑その状態やロジックは、ViewModelに持ってくればいいではないかと思った。
- Composableに状態やロジックが増えてきた場合、StateHolderに抽出する。
-
Compose感想
- @Composableが基本戻り値がなく、内部でComposeにUI要素を登録してくれるようになっているため、
if (isVisible) { Text("hoge") }
のような書き方ができていい。Flutterだと(そんなに変わらないかもしれないけど)isVisible ? Text("hoge") : const SizedBox()
みたいな書き方になる - androidx.lifecycle.ViewModelとStateHolderの役割が重なっている部分があり微妙。今回Hiltを使いつづけたいため他にいい方法が見つからずやむなくViewModelを使い続けている。
- modifierいい。FlutterではUI要素にpadding等を指定したい時Paddingで囲んだりしていたけど、それが必要ない。深くならなくていい。
エラーハンドリングについて
-
やりたいこと
- Crashlyticsに記録
- ダイアログの表示。ダイアログには必要に応じてエラーの種類によりメッセージを表示。
-
Crashlyticsへの記録をどこでやるかの案
- ViewModel
-
Composable← ここでやってはダメ。副作用がある処理は基本的にやらない。useEffect相当の機能を使えばできるかもしれないけど、わざわざここでやる必要もないかな。- onClicked = { viewModel.purchase(it) } のpurchaseの部分をtry-catchで囲んでハンドリングするのはありかも。
-
「ダイアログには必要に応じてエラーの種類によりメッセージを表示」の、エラー→メッセージの変換をどこでやるかの案
- ViewModel
- この変換ロジックは、ViewModelで持つのがいい。
- 変換ロジックで、想定しないエラーだったときに「想定しないエラーです」のようなメッセージに変換しつつ、Crashlyticsに記録するようにする。
Composable
- ViewModel