• トップ
  • ブログ一覧
  • JetpackCompose日記(Composable関数にViewModelを渡したいが…)
  • JetpackCompose日記(Composable関数にViewModelを渡したいが…)

    Composable関数にViewModelを渡したいが…

    JetpackComposeで実装していると、
    Composable関数の引数がだんだん多くなり辛くなる経験をした人は多いと思います。
    それを解消するために、Composable関数にViewModelを渡したくなりました。

    が、しかし
    Composable関数にViewModelを渡して良いのでしょうか?

    そもそも何に困っていたのか

    ↓この程度の引数なら、まあ気にならない。

    1// Hoge.kt
    2@Composable
    3fun Hoge(
    4  hogeUiState: HogeUiState,
    5  onClickFuge: (String) -> Unit,
    6  onChangeHage: (Int) -> Unit
    7) {
    8  // ...省略...
    9}

    ↓このあたりから、引数が多くて辛くなってくる。

    1// Hoge.kt
    2@Composable
    3fun Hoge(
    4  hogeUiState: HogeUiState,
    5  onClickAge: (Int) -> Unit,
    6  onClickFuge: (String) -> Unit,
    7  onClickSage: (Int) -> Unit,
    8  onClickUndo:  (Int) -> Unit,
    9  onChangeFuge:  (Float) -> Unit,
    10  onChangeHage: (Int) -> Unit,
    11  onChangeBar:  (Int) -> Unit
    12) {
    13  // ...省略...
    14}

    ↓呼び元のComposable関数の引数はもっと多くて辛くなる。

    1// ParentContent.kt
    2@Composable
    3fun ParentContent(
    4  parentUiState: ParentUiState,
    5  onClickAge: (Int) -> Unit,
    6  onClickFuge: (String) -> Unit,
    7  onClickSage: (Int) -> Unit,
    8  onClickUndo:  (Int) -> Unit,
    9  onChangeFuge:  (Float) -> Unit,
    10  onChangeHage: (Int) -> Unit,
    11  onChangeBar:  (Int) -> Unit,
    12  // 呼び出し元の引数は特に多い...Hoge以外の引数も必要になる
    13) {
    14  Hoge(
    15    hogeUiState = parentUiState.hogeUiState,
    16    onClickAge = onClickAge,
    17    onClickFuge = onClickFuge,
    18    onClickSage = onClickSage,
    19    onClickUndo = onClickUndo,
    20    onChangeFuge = onChangeFuge,
    21    onChangeHage = onChangeHage,
    22    onChangeBar = onChangeBar
    23  )
    24  // ...省略...
    25}

    UiStateはまとまっているが、イベントリスナー(onXxx系)の引数が多いのが辛い。

    どう解消したのか?

    ということで
    ↓のようなintefaceを作りイベントリスナーをまとめる。

    1interface HogeEventsInterface {
    2  fun onClickAge: (Int) -> Unit
    3  fun onClickFuge: (String) -> Unit
    4  fun onClickSage: (Int) -> Unit
    5  fun onClickUndo:  (Int) -> Unit
    6  fun onChangeFuge:  (Float) -> Unit
    7  fun onChangeHage: (Int) -> Unit
    8  fun onChangeBar:  (Int) -> Unit
    9}

    ↓HogeEventsInterfaceを引数にすることで、引数を大幅に減らすことに成功。

    1// ParentContent.kt
    2@Composable
    3fun ParentContent(
    4  parentUiState: ParentUiState,
    5  hogeEventsInterface: HogeEventsInterface,
    6  // ...省略...
    7) {
    8  Hoge(
    9    hogeUiState = parentUiState.hogeUiState,
    10    hogeEventsInterface = hogeEventsInterface
    11  )
    12  // ...省略...
    13}

    なぜComposable関数にViewModelを渡したいのか?

    HogeEventsInterfaceをViewModelに実装する

    1// MainPageViewModel.kt
    2class MainPageViewModel(): HogeEventsInterface {
    3  // ...省略...
    4}

    ルートのComposable関数(今回はMainPage)でViewModelを保持。

    この時に
    ParentContentの引数にHogeEventsInterfaceを実装したViewModelを渡したくなる!!!

    1// MainPage.kt
    2@Composable
    3fun MainPage(
    4  viewModel: MainPageViewModel = hiltViewModel()
    5) {
    6  val parentUiState by viewModel.parentUiState.collectAsStateWithLifecycle()
    7  ParentContent(
    8    parentUiState = parentUiState,
    9    hogeEventsInterface = viewModel, // ←ViewModelをComposable関数に渡したい!
    10    // ...省略...
    11  )
    12}

    Composable関数にViewModelを渡して良いのか?

    なぜViewModelを渡したくなるのか述べてきたが、
    やって良いことなのだろうか?

    公式ドキュメントには

    注: ViewModel インスタンスは他のコンポーザブルに渡さないでください。詳細については、アーキテクチャ状態ホルダーのドキュメントをご覧ください。

    引用元: 画面 UI 状態 | DevelopersAndroid

    と書かれている。

    うーん・・・
    これはどういうこと?
    アーキテクチャ状態ホルダーのドキュメントのリンクを見てみる。

    警告: ViewModel インスタンスを他のコンポーズ可能な関数に渡さないでください。そのようにすると、コンポーズ可能な関数と ViewModel 型が結合されるため、再利用性が低くなり、テストとプレビューが難しくなります。また、ViewModel インスタンスを管理する明確な SSOT(信頼できる単一の情報源)がなくなります。ViewModel を渡すと、複数のコンポーザブルが ViewModel 関数を呼び出して状態を変更できるようになり、バグのデバッグが難しくなります。代わりに、UDF ベスト プラクティスに沿って、必要な状態のみを渡します。同様に、ViewModel のコンポーザブルの SSOT に達するまで、伝播イベントを渡します。これは、イベントを処理し、対応する ViewModel メソッドを呼び出す SSOT です。

    引用元: ビジネス ロジックとその状態ホルダー | DevelopersAndroid

    なるほど
    要するに公式見解としては

    • ViewModelのインスタンスを他のComposable関数に渡さないでください
    • なぜなら、テストやプレビューがしづらくなるから(再利用性の低下)
    • ViewModelが複数の箇所から呼ばれて編集されるので、SSOT(信頼できる単一の情報源)の観点から良くないよ
    • ViewModelを保持しているComposable関数まで、イベントを伝播させてから、ViewModelを呼び出してね

    ということで
    「Composable関数にViewModelは渡しちゃだめだよ!」
    ということになっている。

    つまり、Composable関数の引数が莫大に増えることを正としているということなのでしょうか?

    うーん、納得できない。私の解釈に間違いがある?

    たしかに
    ViewModelの具象クラスをそのままComposable関数の引数に渡すのは良くないと思います。
    テストもプレビューもしづらくなるので。

    ただ
    今回はinterfaceを通してComposable関数に渡しているので、テストやプレビューは問題なくできます。
    ViewModelが複数の箇所から呼び出されると言っても、interfaceを通しているのでSSOT(信頼できる単一の情報源)の観点としても問題ないように感じます。

    ということで、正直
    「Composable関数の引数が莫大に増えることを正としている公式見解」には納得できていません。
    「私の解釈に誤りがある」 or 「私のSSOTへの理解が間違っている」
    ということなのかな?

    ViewModelのインスタンスをComposable関数に渡さない方法

    ViewModelに直接HogeEventsInterfaceを実装するのではなく、
    HogeEventsInterfaceのインスタンスをViewModelを取り扱うComposable関数(MainPage)にて作成し
    それ経由でViewModelにイベントを送る。

    1// MainPage.kt
    2@Composable
    3fun MainPage(
    4  viewModel: MainPageViewModel = hiltViewModel()
    5) {
    6  val parentUiState by viewModel.parentUiState.collectAsStateWithLifecycle()
    7  val hogeEvents = object : HogeEventsInterface {
    8    fun onClickAge(value: Int) {
    9      viewModel.onClickAge(value)
    10    }
    11
    12    fun onClickFuge(value: String) {
    13      viewModel.onClickFuge(value)
    14    }
    15    // ...省略...
    16  }
    17  ParentContent(
    18    parentUiState = parentUiState,
    19    hogeEventsInterface = hogeEvents, // ←ViewModelの代わりに、インスタンス化したHogeEventsInterfaceのobjectを渡す
    20    // ...省略...
    21  )
    22}

    これであれば

    • Composable関数の引数を減らす
    • ViewModelのインスタンスをComposable関数に渡さない

    を両立できそう。

    まあ・・・
    MainPageの文量が冗長に見えてしまうのが気になるところではありますね。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background