質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.32%
Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

UI Design

UI Design(UIデザイン)は、ユーザインターフェースをデザインすることです。ユーザーとシステムがスムーズにコミュニケーションを取るために、OSやアプリ画面などを使いやすくデザインすることを指します。

Q&A

解決済

3回答

712閲覧

宣言的UIの入力要素の考え方について知りたい

utm.

総合スコア676

Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

UI Design

UI Design(UIデザイン)は、ユーザインターフェースをデザインすることです。ユーザーとシステムがスムーズにコミュニケーションを取るために、OSやアプリ画面などを使いやすくデザインすることを指します。

0グッド

1クリップ

投稿2025/03/27 10:04

編集2025/03/28 09:32

実現したいこと

マルチプラットフォームアプリを作成する際に関数型UIというXMLとは違う方法でUIを作ると思いますが、従来の方法と異なっており良いやり方がわからずにいます。
自分はkotlinのjetpackを使用していますが、flutter/dartに影響を受けているようなので、一般的な考え方で大丈夫なのでよい方法があれば教えていただきたいと思っています。
Reactなどもバインディングを扱うと思いますが、それらの開発経験がないので助言をいただけると幸いです。

考え方について

オブジェクト指向と関数型プログラミング双方とも完璧にできるわけではないのと、
従来のネイティブアプリやHTMLではnameのようなidが各入力要素に紐づけられておりそこから値をとるという方式をとっていて、私はこれにもメリットを感じていましたが、関数型UIは完全に入力要素とレイアウト要素が埋め込まれており更新ロジックをUI側に書かなければならない状態にある種困惑しています。(入力要素って関数じゃなくてオブジェクトじゃないの。。。と)
ただ、関数型のメリットとしてもわかっているつもりで、引数に取った値から常に同じ状況を再現できるということでこの長所を生かした設計にしたいです。(例えば状態をログに書き込み再現しやすくするなど)
また、レイアウトと入力要素を分離するのが当面の目標になっています。

前提

まず、初めに入力要素を定義し従来のnameのようなアクセス方法に近いやり方でやりたいと思いまして、下記のようなコードを作りました。

ベースクラス

kt

1// 共通のベースクラス 2open class InputState { 3 4 private var _inputValue = mutableStateOf("") 5 val inputValue: State<String> = _inputValue 6 7 // 値を更新する共通のメソッド 8 open fun updateInputValue(value: String) { 9 _inputValue.value = value 10 } 11}

入力要素の紐づけ

kt

1// バリデーションが必要であれば継承とインタフェースを用いて拡張してください 2class LoginIdState : InputState() { 3 // ここに特別なロジックが必要な場合は追加できますが、現状はそのままです 4} 5 6// 引数の型を強要する 7@Composable 8fun LoginIdTextField(state: LoginIdState ) { 9 Column(modifier = Modifier.padding(16.dp)) { 10 TextField( 11 value = state.inputValue.value, 12 onValueChange = { state.updateInputValue(it) }, 13 label = { Text("Login ID") } 14 ) 15 } 16} 17 18class LoginPasswordState : InputState() { 19} 20 21@Composable 22fun PasswordTextField(state: LoginPasswordState) { 23 Column(modifier = Modifier.padding(16.dp)) { 24 TextField( 25 value = state.inputValue.value, 26 onValueChange = { state.updateInputValue(it) }, 27 label = { Text("Password") }, 28 visualTransformation = PasswordVisualTransformation() // パスワードを隠す 29 ) 30 } 31}

uiのパーツとしては一旦としてログイン画面として、ログインIDとログインパスワードがあると仮定しています。
また質問にさしあたりコメントにて拡張方法を補足しています。

それぞれのパーツは様々な画面で様々な方法で扱われると思うので、画面事にグループ化し管理しやすくしたいと思っており、最終的には以下のような構成要素を定義しています。

そのほかの構成要素

グループ化

kt

1// TODO: この画面特有の処理を実装する 2// info: あるパーツが別の画面で意味としては同じでも機能としては違うケースを考慮してください 3class Screen1State(){ 4 val loginIdState = LoginIdState() 5 val loginPasswordTextField = LoginPasswordState() 6}

UI

kt

1@Composable 2fun TestScreen1( 3 screenState: Screen1State = viewModel<MyViewModel>().screen1State 4) { 5 Column(modifier = Modifier.padding(16.dp)) { 6 LoginIdTextField(screenState.loginIdState) 7 PasswordTextField(screenState.loginPasswordTextField) 8 } 9}

ViewModel

kt

1class MyViewModel : ViewModel() { 2 val screen1State = Screen1State() 3}

発生している問題

単刀直入に言って、皆さまはこの関数型UIという制約の中でどのように開発を進めているのでしょうか?
また、私のやり方だと致命的な弱点があったりしますか?
パフォーマンスの影響や拡張性の観点から言って、どのようにコーディングしていくのが正しいのでしょうか。

また、具体的な質問としてもう一つ
これらのUI要素は
TestScreen1とあるように実際にはUIとクラスの紐づけだけをして
別のスクリーンで定義されているであろうレイアウトに単に埋め込む方法として使いたいです。
その場合、コンポーザブルな要素(TestScreen1)はmapなどを返すようにするしかないのでしょうか?

また、各要素は
UIの拡張性などを考えるとそれぞれの具体的な実装は書かずにカリー化すべきなのでしょうか?
(オブジェクト指向の考えに強く引っ張られている。。。)

カリー化の例

kt

1// 引数の型を強要する 2@Composable 3fun LoginIdTextField( 4 state: LoginIdState 5): @Composable (callback: @Composable (LoginIdState) -> Unit) -> Unit { 6 7 return { callback -> 8 callback(state) 9 } 10} 11 12@Composable 13fun TestScreen1( 14 screenState: Screen1State = viewModel<MyViewModel>().screen1State 15) { 16 17 val loginIdTextField = LoginIdTextField(screenState.loginIdState) 18 19 Column(modifier = Modifier.padding(16.dp)) { 20 loginIdTextField{ state -> 21 // 分けたことで柔軟にパディング調整などをできる 22 Column(modifier = Modifier.padding(16.dp)) { 23 TextField( 24 value = state.inputValue.value, 25 onValueChange = { state.updateInputValue(it) }, 26 label = { Text("Login ID") }, 27 ) 28 } 29 } 30 } 31} 32

すみません。それともう一つなのですが、
Stateの公開方法についても皆様がどうしているのかを伺いたいです。
現状のソース例であれば先に挙げたようにしてあり、カプセル化の観点からmutableStateofが別のstateに指し変わっても公開可能なようにStatenにしていますが、正直に言うとmutableState一択で問題ないのではないかと思っています。

kt

1// 共通のベースクラス 2open class InputState { 3 4 private var inputValue by mutableStateOf("") 5 private set 6 7 // 値を更新する共通のメソッド 8 open fun updateInputValue(value: String) { 9 inputValue = value 10 } 11}

最終的にはInputState以外にもCheckBoxStateやButtonState(Command?Action?)も適宜追加する必要はあると思っています。

試したこと

時間をかけて解決することもありますので、かれこれずっと考えています。(調べてもピンとくるものが正直ない気がします。。。)
その中でファイルごとにパーツを管理してクラス定義とUI要素をその中に固めて書いて、バリデーションように継承したクラスなどをそこに記述していけば整理がつくのでは?という発想が出てきたのでこれが問題なさそうか知識の共有をしたいと思い至りました。

補足情報(FW/ツールのバージョンなど)

Kotlin
jetpack compose for multplatform

最後に質問を整理します

  1. 今までのXMLやHTML、ネイティブアプリではnameのような一つの要素に紐づいたキーワードを使って、値の取得やセットを行っていました。ボタンであればクリックイベントの処理などもそこで行っていました。そのような感覚に近いやり方として、皆さまはどのように開発しているのでしょうか。

  2. 各入力要素は変数との紐づけをする処理と、レイアウトに埋め込む処理の二つのことをしなければなりません。これらを分離する方法はあるのでしょうか。mapを使うのであれば、直接レイアウトに埋め込むほうがマシだと思っています。

  3. UIの各要素をパーツ化すると、TextFieldのようなコンポーザブル関数が自由に引数をとれるとしてもそこにアクセスする手段がありません。これはカリー化などで状態を変更できるようにすべきなのでしょうか。(オブジェクトの考えに引っ張られているのでよくなさそう)

  4. 入力の管理に使うStateに関して、StateやMutableStateの基本挙動とベストな設計が正直にいうとよくわかりません。いったんはStateとして公開しておいたほうが安全そうではありますが、これは本当に必要なのでしょうか。皆さんはどうしているのでしょうか。

質問が複雑になっており大変申し訳ございませんが、情報交換お力添えいただけると幸いです。
内容が不足していましたら、追記等致しますのでよろしくお願いいたします。


情報の追記

junerさんよりコメントをいただきました。
以下、ツールの詳細に記載した内容の補足及びやり取りとなります

関数型UI はどこ由来の用語でしょうか……?

すみません。コメント意図が今分かりました。用語の由来というか、要は私の開発体制が何から発せられているのかという話を聞きたいという意図ですね。
以下にルーツとなるリンクを貼ります。

Androidデベロッパー
(最も役に立たない、今回コメント返信に差し当たり参照)
https://developer.android.com/kotlin/multiplatform?hl=ja

Jet Brains ブログ
(次に役に立たない、今回コメント返信に差し当たり参照)
https://blog.jetbrains.com/ja/kotlin/2023/11/kotlin-multiplatform-stable/

Compose For Multiplatform
(最も有益だが、Web上のコミュニティの動向(ナレッジシェア)が追えない)
https://www.jetbrains.com/ja-jp/compose-multiplatform/

もっと詳細
https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-explore-composables.html

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

juner

2025/03/28 02:14

関数型UI はどこ由来の用語でしょうか……?
utm.

2025/03/28 03:17

junerさん コメントありがとうございます。 質問の意図が私としては不明確でどのように言えばいいのか分かりませんが、 単に宣言的UIやコンポーズ関数などの用語がちらほら出る中でごちゃまぜになったのかもしれません。 https://developer.android.com/codelabs/jetpack-compose-basics?hl=ja#0 意図を明確にしてくだされば、もう少し適切な補足ができるかもしれません。 // 単に自分がこのUIを宣言的というのに違和感があるのも関係あるかもしれません 正しくは宣言型UIでした。 https://developer.android.com/develop/ui/compose/mental-model?hl=ja Functional UIや関数型UIについてAIに聞いてみるとそれなりの答えを返してきますので、何か参考になるかもしれません。 文意が伝われば幸いです。
dodox86

2025/03/28 04:37

横からすみません。 @utm. さん > 質問の意図が私としては不明確でどのように言えばいいのか分かりませんが、 回答できるほどの知見は私には無いので静観していましたが、「関数型UI」と言う単語については私もjunerさんと同じような疑問を抱きました。 閲覧、回答者側の心理としては知らない用語が出てきた時、その用語がその界隈で一般的に使われているかどうか確認するものだと思います。「関数型UI」と言う日本語での単語、あるいは言い方は少なくともBingやGoogleで検索した限りはヒットしない単語であり、「関数型プログラミングにおける宣言的UI...」みたいに違ったかんじに誘導されます。その点で少し「関数型UI?? ハテ???」と疑問に思われるかもしれません(私は思いました)。ただ、このコメントを投稿する前に試しに英語で「functional UI」で仮定してAI(Copilot)に訊ねてみたところ、一応ヒットはしました。 https://www.infoq.com/articles/functional-UI-introduction-no-framework/ しかしながらこれは当該サイトの当時の記事における造語のようにも思えます。 まぁ、今回のご質問に限って言えば、「宣言型UI(declarative UI)」に落ち着いたようですが。 https://developer.android.com/develop/ui/compose/mental-model?hl=en
juner

2025/03/28 04:47 編集

「正しくは宣言型UIでした。」であるならば質問文 も合わせて修正した方がいいかな感はあります。 (これはあとで質問のコメント欄を見なくてもよくする為に必要です。 (一般的でなく、どこかに記載のある用語であれば質問文にURLというのもありますが、今回はちゃんと正しい用語がある様なのでそっちに揃えたらいいという話。
utm.

2025/03/28 05:21

dodox86さん、junerさん コメントありがとうございます。宣言的UIという言葉は一般的ですか? 皆さんはなんと呼ばれているのでしょうか? > 閲覧、回答者側の心理としては知らない用語が出てきた時、その用語がその界隈で一般的に使われているかどうか確認するものだと思います。 一応その観点もあるかと思い、宣言型UIというのが正しいのかも?というニュアンスも含む形で返信させて頂きました。 由来についてのご質問かと思ったので、 前置きが好意的でなかったかもしれません。失礼しました。 > 一般的でなく、どこかに記載のある用語であれば質問文にURLというのもありますが、今回はちゃんと正しい用語がある様なのでそっちに揃えたらいいという話。 確かにおっしゃる通りかと思うので、関数型UIではなく宣言的UIと書き直したいと思います。 ただ、宣言的か命令的かと言うよりは、 XMLを使うか関数を使うのかオブジェクトを使うのかという組み立ての話なので、厳密に正しいのか分かりませんし、 Jetpack Composeでのレイアウトの定義とXMLでのレイアウトの定義をわけて、それぞれをなんと呼ぶのか?は定かではありません。 Composableという名称もあるようなのですが、FlutterやReactでの用語の扱われ方を存じ上げないため、そのような語義が一般的に正しいのかも不明です。 従って、呼び方の正解はなさそうにも思えます。
utm.

2025/03/28 05:30

AIにもお伺いを立ててみました。エビデンス0ですが以下のようにも解釈できるようです。 ↓ Declarative UI は広い概念で、「UIを状態ベースで宣言的に定義する」方式全般を指します。 Function UI はその中でも特に、「関数を使ってUIを定義する」スタイルを指します。 Function UI(Jetpack Compose) 関数としてUIを定義し、状態変化時に再コンポーズされる。 Declarative UI(SwiftUI) SwiftUIは関数ではなく構造体(struct)で定義するが、宣言的UIの一例。 つまり、Function UIはDeclarative UIの中の一形態 という関係です。
melian

2025/03/28 06:32

参考までに、Google の検索エンジンで "関数型UI" を検索すると7件ヒットしました。 https://www.google.com/search?q=%22%E9%96%A2%E6%95%B0%E5%9E%8BUI%22&complete=0 関数型UIプログラミング https://ympbyc.hatenablog.com/entry/2015/02/15/%E9%96%A2%E6%95%B0%E5%9E%8BUI%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0 > では関数型UIに対して非常に素朴なアプローチを取ったとしたらどうなるでしょう。たとえばある状態からなにかしらのUIを生成する関数を作成します... 翻訳:Rich Harris「形而上学とJavaScript」に関する見解(ReactによるDOMの抽象化の不完全性について) https://yuheiy.hatenablog.com/entry/2020/01/25/230154 > 2020年代:Rustの台頭、関数型UI、ストリームデータ処理. 他の分野でも関数型が求められる(当たり前になってきた)時代. プロダクションでの10万行以上のElmコード: 楽天が学んだ教訓を共有 https://www.infoq.com/jp/news/2021/10/elm-lessons-learnt-production/ > 楽天は、Elm の関数型 UI アプローチ、その型システム ... テレビのUIは最強 https://b.hatena.ne.jp/entry/s/anond.hatelabo.jp/20170910184746 > 関数型UIの時代も来るかもしれない。状態を持たず、同じ操作をすれば必ず同じ結果が得られるような。
dodox86

2025/03/28 07:44

@melian さん フォローの情報、どうもありがとうございます。本当ですね。Googleへは「関数型UI」で検索していましたが、"で括って「"関数型UI"」だとヒットしました。私の検索方法も浅かったようです。用語も検索方法も認識を新たにしました。
utm.

2025/03/28 08:26

@melianさん、@dodox86さん コメントありがとうございます。 全てのリンクは読めていませんが、 https://ympbyc.hatenablog.com/entry/2015/02/15/%E9%96%A2%E6%95%B0%E5%9E%8BUI%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0 のリンクの考え方は自分の疑問とマッチしており、大変興味深く読めました。 (設計と疑問の順序が私とは逆ですが) dodox86さんが仰っていましたが > 回答できるほどの知見は私には無いので静観していましたが、「関数型UI」と言う単語については私もjunerさんと同じような疑問を抱きました。 ということで疑問や情報を持っているがコメントを控えているという方がいらっしゃれば、それも勉強/参考になりますのでお気軽にコメントいただければと思います。 なにぶん、黎明期のようで情報が少ないため、皆様のお力を貸していただけるとありがたいです。 よろしくお願いいたします。
utm.

2025/03/28 09:28

@junerさん > 関数型UI はどこ由来の用語でしょうか……? すみません。コメント意図が今分かりました。用語の由来というか、要は私の開発体制が何から発せられているのかという話を聞きたいという意図ですね。 以下にルーツとなるリンクを貼ります。 Androidデベロッパー (最も役に立たない、今回コメント返信に差し当たり参照) https://developer.android.com/kotlin/multiplatform?hl=ja Jet Brains ブログ (次に役に立たない、今回コメント返信に差し当たり参照) https://blog.jetbrains.com/ja/kotlin/2023/11/kotlin-multiplatform-stable/ Compose For Multiplatform (最も有益だが、Web上のコミュニティの動向(ナレッジシェア)が追えない) https://www.jetbrains.com/ja-jp/compose-multiplatform/ もっと詳細 https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-explore-composables.html
guest

回答3

0

ベストアンサー

こんにちは。
質問で挙げている UI フレームワークなどはどれも使ったことがなく、質問のコード例などをちゃんと理解できていないので、回答にするべきか悩みましたが、
マイナーですがとある純粋関数言語の UI フレームワークをよく知っていて、考え方としては似ているものだと思うので、質問への回答にはなっていないかもしれませんが、参考までにここに書いておきます。


まず、今回説明に用いるフレームワークでは UI の構成要素を「コンポーネント」という合成可能単位と見なして扱えるようにしています。本質問で言う Composable と同様の概念だと考えていますが、確証はありません。

コンポーネントというのは「input となる任意の値を引数に取る関数」の別名であり、以下の3つの要素から構成されます。

  • InitialState: input -> State の型を持つ関数。コンポーネント初期化時の input を元に内部状態の初期状態を作成する純粋関数
  • Render: State -> View の型を持つ関数。コンポーネントの内部状態を元にレイアウトを出力する純粋関数
  • Eval: Action -> StateReaderWriter -> Effect の型を持つ関数。コンポーネントに定義されたアクションを元に、内部状態を書き換えたり何らかの作用を作る純粋関数

実際のものとは少し異なり、またかなり単純化/抽象化してはいますが、これらを組み合わせたものをコンポーネントと呼び、単体で完結した UI の要素を表現しています。(実際はこれに加えて、コンポーネント外からの問いかけに対して振る舞いを起こしたり値を返したりする機能が付きますが、今回は省略しています。)
この定義から読み取れるものとして、Render が純粋関数でかつ引数が State のみなので、「View (UI の要素) はコンポーネントの State によって一意に定まる」 という点があります。
また、その「State は Eval によってのみ更新される」ということも分かります。
これらの要素が後に「宣言的」と呼ばれるものになります。

コンポーネントの例として、インクリメントするカウンターの例を挙げると、
State を現在の数値を表す数値型、Action は Increment のみとして、

  • InitialState: 初期数値を常に 0 とするのであれば、「input は無視して 0 を返す」関数
  • Render: 現在の数値を元に、「その数値を表示する箱」と「Increment のアクションに紐付いたボタン」の2つを持つレイアウトを出力する関数
  • Eval: 「Increment がトリガーされたとき、State の数値を +1 する」という作用を定義した関数

とすることで作成できます。

このようにコンポーネントは単体で完結したライフサイクルを持っているので、最小の UI 要素でありながら、そのコンポーネントを入れ子に組み合わせて大きい塊を作ったものもまたコンポーネントとなります。これが質問で言う合成可能 (Composable) の名前の由来だと想像しています。

UI をこのようにコンポーネントで考えることで得られるメリットは、

  • 画面表示の内容がその内部状態によって一意に定まるので、問題などが起きても再現しやすい
  • コンポーネント単位で状態が閉じているので、状態が外から変更されて意図しない UI の崩壊などを起こさない (起こせない)
  • コンポーネントを組み合わせてコンポーネントを作れるので、パーツの再利用性が高い

あたりが考えられます。
作ったコンポーネントは他のコンポーネントの好きなところに埋め込めるので、再利用性が非常に高いです。
それもコンポーネントが単体で完結していることで得られるメリットの一つです。

逆に少し面倒だと思うところは、

  • コンポーネントの持つ状態はコンポーネント内に閉じているので、親コンポーネントから子コンポーネントの状態を直接見ることはできず、仮に子の状態を知りたければ、子コンポーネントに「問いかけをされたら状態を返す」という振る舞いを明示的に定義する必要がある

という点です。
それでも、コンポーネントの組み合わせが巨大になったときに全てのコンポーネントの状態を正確に管理するのは困難なため、状態の取得を明確にインターフェースにしなければならないというのは逆にメリットでもあると考えています。

このコンポーネントの実装では、UI の定義としてその「振る舞いに関するルール」だけを記述しているのが分かるかと思います。
このように、ルールの宣言のみを元に UI が定義されることを指して「宣言的」と表現しているわけですね。
それらの性質は上記の例のように純粋関数だけでも表現可能で、そのように実装しているものを「関数型 UI」と呼び分けているのだと推測しています。
実際にコンポーネントを走らせるには、その内部状態を管理したり、UI を実際に描画する仕組みが必要なのですが、それらはフレームワークの評価器に丸投げすることでよしなにやってくれるという仕組みなので、利用者の立場では「ルールを定めていくだけで UI になる」という世界に住んでいられるのです。
このクリーンな世界 (ディストピアかも?) に生きられるのが、宣言的 UI を使っていて一番うれしいところかなと思います。

かなり抽象的というかぼんやりした説明になってしまった気がしますが、何か質問がありましたらコメントにお願いします。


あと話は全く関係ないですが、
質問本文中で「カリー化」を本来の意味とは全く異なる意味で使っているように見えて気になりました。
実際のところ、どのような概念をイメージして言っていたのか、少し興味があります。

投稿2025/03/29 17:50

tamoto

総合スコア4275

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

utm.

2025/03/29 20:40

回答ありがとうございます。 非常に筋が通った説明だと思いました。 関数型プログラミングでそのようなシステムを作成する際の手続き的な流れとして自分の知識では(従来の一般的な発想ではかもしれません) 1. アプリケーションを表現する巨大なmapを定義する 2. そのmapを引数に取りUIを描画する関数を呼び出す。 3. ユーザー操作があれば、mapを引数に取り操作部分を更新したmapのコピーを返す関数を呼び出す。 という知識があります。 おそらく純粋関数言語というやつは2のような副作用を許さないので、その関数はmapをuiとして返すようにしているという話で、ご回答に出てくる要素を引用すると 2を動作させる、Render(InitialState())という処理と、 3を動作させるEval()という処理があるのではないかと読解しました。 (関数型のあいまいな知識で文法を間違えると、大変なため記法が独特であれば申し訳ありません) 明確な疑似言語があれば意思疎通しやすいのですが、疑似言語の知識がないというか、完璧な疑似言語を知らないので、申し訳ないです。 > このように、ルールの宣言のみを元に UI が定義されることを指して「宣言的」と表現しているわけですね。 に関してなのですが、私の感性からいってUIは基本的に宣言的で手続き的な部分のほうが例外に思えてなりません。特にその思想はCSSの文法に影響されている気は自分の中でもするのですけども。 そのため、これをあえて宣言的ということに違和感を覚えます。 > InitialState: 初期数値を常に 0 とするのであれば、「input は無視して 0 を返す」関数 というのが自分のイメージとあっているかわかりませんが、InitialStateは別に初期値という意味ではなくそういう関数だと解釈して、結局UI要素はactionを定義しないと入力が無視される(入力不可能)だけのUIになってしまうので、Actionを定義するのが逆に手続き的ですし、本来一番やりたい部分というか根幹の部分なのでこれを宣言的というのはやはり違和感をぬぐえません。 表示の全体像として、値が決まればその通りに描画するというイメージで語れば確実に宣言的で、 手続き的であったページ遷移が分岐を示す部分の更新だけで、表示が変われるのが宣言的といえばそうですが。(プログラムのロジックが宣言的なのではなくViewが宣言的) まあ、用語に文句を言っていても仕方ないとも思いますが。 > あと話は全く関係ないですが、 質問本文中で「カリー化」を本来の意味とは全く異なる意味で使っているように見えて気になりました。 実際のところ、どのような概念をイメージして言っていたのか、少し興味があります。 今となっては関数自体をパーツと考えていた時の発想なので考えるのも面倒ではありますが、 もともとはスマホアプリではなくて業務アプリをスマホに移植しようと考えた際に入力フォームが多すぎてどこがどこと対応しているのかよくわからなくなるという問題がおきていたのを解決したいという発想から生まれました。(画面回転などでUI部分のライフタイムが破棄されるなどの説明は割愛) 自分がカリー化といっているのはおそらくどこかでは一般的ではあると思うのですが、 Aという引数をとって、Aという引数をとる関数を返すことを指していました。 Anyという型があると仮定してvoidは戻り値無しとします(printのような副作用は含んでいいのでこれがUIを描画します) 引数の型 -> 戻り値の型 とし、()は関数そのものであると定義します 以下はanyを引数に取り戻り値がvoidの関数 (any -> void) 私の記載したコードでは (any -> ((any -> void) -> void)) というのを定義しています 要は 1. anyを引数に取り 関数を返します 2. 1で返された関数は 関数を引数に取り voidを返します 引数の関数はanyを引数に取り、voidを返します という関数です 値: 型の形として、 fun first( value1 : any ) { return fun second( // 本来は無名関数 third: fun(value2: any) -> void // 引数に関数を受け取る 関数はanyを引数に取り、voidを返します ) { return third(value1) // secondにvalueを渡して実行する } } 本来のカリー化のイメージではないかもしれませんが、これの呼び方を知りません。 本来のイメージ fun first(value1: int) { return fun second(value2: int) { return value1 + value2 } } --- 返信前に色々調べてみたのですが、jetpack composeのUI更新はかなり独特かもしれません。 Reactではstateを更新する関数を定義し、どういったアクションがあるかはその関数に定義し、引数で受け取るようです。 逆にjetpack composeではUIは値のオブザーブに専念し、入力やクリックなどのイベントはviewModelに処理を書きます。 自己回答しましたが、これらを踏襲すると考え直したほうがよさそうです。 発想的にはデータが先かコードが先かというのではなく、「データはどう更新されるのかを知っている」(それがいつかはしらない)ということなのか。 宣言的ではあるな。 でもやっぱりフレームワークがまだないと考えるのが妥当なのか。
xebme

2025/03/29 22:39

関数コンビネータを調べてみると良いかもしれません。たとえば、Kコンビネータは、 (a -> *) -> a -> a 動作としてa -> a 引数をそのまま返すのですが、ついでにa -> *をやってしまう。 a->voidを実行してさらに次の関数に引数aをそのまま渡したいときに使います。 (関数コンビネータは制御ロジックとして使います)
tamoto

2025/03/30 01:42

> 1. アプリケーションを表現する巨大なmapを定義する -> 2. そのmapを引数に取りUIを描画する関数を呼び出す。 すみません。おそらくこの部分は IO の実行コンテキスト的なものをイメージしているように見られますが、申し訳ないですが具体的にイメージできていません。 > 2を動作させる、Render(InitialState())という処理と、3を動作させるEval()という処理があるのではないかと読解しました。 これは明確に違いますね。 この回答で示しているのは「InitialState, Render, Eval という3つの関数を定義しているが、それをコード上では誰も実行をしていない」という点が重要になります。 状態の構成やレイアウトの構成などは全て隠蔽されたランタイム側が行うので、プログラマの視点では UI が「どのようにあるべきか」だけを記述しており、それをもって「宣言的」である、と表現しています。 どの部分が手続き的だと感じているのかが分からず申し訳ないですが、UI を自分で描画するコードがどこにも存在しない点が重要になります。 例として、Eval が実行されると、State に影響を与えることができますが、View を直接いじることはできません。そして、View は State によってのみ定まるので、State が変更されるとき View がそれに合わせて変化する、という考え方になります。 この表現で伝わると良いのですが。 > Aという引数をとって、Aという引数をとる関数を返すことを指していました。 それはカリー化ではない別の概念を考えていそうです。 カリー化はもっとシンプルで、`(a, b) -> c` という2引数の関数を `a -> (b -> c)` という1引数の関数を返す関数に変換する処理で、これらは相互に変換しても一切の情報の欠落がないため、実質同じもの同士の読み替えに過ぎません。 > (any -> ((any -> void) -> void)) 単なる `any -> void` の処理に異なる `any -> void` を埋め込んで処理するという想定でしょうか。たしかにそういう処理は考えられますが、カリー化ではないですね。 `any` と `any -> void` の型は異なるので、先に述べていた「Aという引数をとって、Aという引数をとる関数」にも相当していません。 ただし、1引数の関数であり、1引数の関数を返しているというの点では、単純に「既にカリー化が既に行われている関数」でもあります。 これがカリー化される前を書くと `(any, (any -> void)) -> void` になります。 ところで、実際に関数プログラミングで用いるこれに似た感じのある変換というと、CPS 変換が思いつきます。 CPS 変換は、`a -> b` という関数を `a -> (b -> result) -> result` という関数に変換するもので、「続きの処理」を関数自体に任せるということを表現しています。 これも相互の変換で情報欠落が起きない実質同じものです。 > 発想的にはデータが先かコードが先かというのではなく、「データはどう更新されるのかを知っている」(それがいつかはしらない)ということなのか。 > 宣言的ではあるな。 これはイメージとしては正しそうです。 「データはどう更新されるのかを知っている」(それがいつかはしらない) は、それこそがコンポーネントの Eval の考え方です。Eval は要するにコンポーネントに属したイベントハンドラなので、まさに「データをどのように更新するか」を記述した仕様書なのです。 コンポーネント的な考え方ではその「データ」を自分の利用する部分のみに狭くしたものを State とし、「View のアクションが呼ばれたとき、State はこうなる」「State のあらゆる状態に対して、UI はこうなる」という関係性を定義しているイメージが持てるとより正しいかと思います。 jetpack compose の考え方は分かっていませんが、コードをチラ見した限りでは、コンポーネントで言う State と Eval を切り出したものを ViewModel と呼び、Composable はその ViewModel と Render を持っているもの、と考えられそうです。 ViewModel が一段挟まっていますが、最終的には Composable とコンポーネントは同じ概念を表しているように思えます。
utm.

2025/03/30 03:05

@xebmeさん、@tamotoさん コメントありがとうございます。 > 関数コンビネータを調べてみると良いかもしれません。 確かにこの概念が近そうです。ただ、自分が発想したものはわかりづらいですし、使うことはないと思います。 > すみません。おそらくこの部分は IO の実行コンテキスト的なものをイメージしているように見られますが、申し訳ないですが具体的にイメージできていません。 自分もIOの実行コンテキストというのが分かりませんが、関数型プログラミングがどのような分野で使われているのかも知らないので、単にオブジェクト指向で書かないなら、巨大なmapを用意して何かが起きたらmapの一部分だけを変えたコピーを返すという設計が自然になるのかなという程度の話でした(あんまり深堀しなくていいかもしれません) > この回答で示しているのは「InitialState, Render, Eval という3つの関数を定義しているが、それをコード上では誰も実行をしていない」という点が重要になります。 んーなるほど?確かにそういう話を前提に置くと趣旨がかなり変わりそうです。 ただイメージとしてそれらを紐づける何かがあるのではないか?と感じました。 ちょっとReactのコードを調べてみたのですが、以下のような定義と実装になるようです。 https://ja.react.dev/reference/react/useReducer#adding-a-reducer-to-a-component Kotlinのjetpackだとどうなるのだろうと若干疑問に思っていて、回答欄に書いたTextFieldModelに機能を追加していくのが妥当なのかなぁという感じに思っています。 私的に一番疑問というか、問題が発生しているのは単にTextFieldというコンポーザブルな関数を提供しているだけでその値の管理はViewModelでやってくださいと言っているだけなのです。そういう決まりがあるわけではないのですがViewModelは基本一画面に一つですし、そこに変数を持たないとボタン押下時に値を集めるなどができなくなってきます。 公式の例 https://developer.android.com/develop/ui/compose/text/user-input?hl=ja onValueChange = { text = it }, の部分が重要で入力(it)がtextを更新し、textがコンポーザブルな関数を再描画しなければこのテキストフィールドにユーザーが入力することはできません、 これを愚直にやっていくと非常に手続き的にも思えます。 > 例として、Eval が実行されると、State に影響を与えることができますが、View を直接いじることはできません。そして、View は State によってのみ定まるので、State が変更されるとき View がそれに合わせて変化する、という考え方になります。 > この表現で伝わると良いのですが。 おそらく疑似言語がないと厳しい。。。 実際に実行される手順が分からないとどういう順序で何が制約的に定められているのかがわからない。。。 > これがカリー化される前を書くと `(any, (any -> void)) -> void` になります。 カリー化される前、というのが便宜上使っている言葉ではないのであればカリー化で正しいでは?単に(any -> (any -> void))の戻り値がほしいときには実際には実行したくなくて、後から実行したいので後から引数に関数を受け取りたいという形でした (まあこのコード自体もう使わないのでどっちでも良い話なのですが。) `(any, (any -> void)) -> void`でも実際にはいいというか必要な部分は(any -> void)を実行することだけなのですが、これが非常に多いためレイアウトに埋め込むとどこに何があるのか、そもそもここにあるのかが全くわからないのです。カリー化というかやりたいことはむしろ関数の引数を先に定義したいだけなのでメタプログラミングとかのほうがニュアンスが近く違和感があるのかもしれません。 これは単に正しい用語を自分が知らないだけだったので、コメントにある関数コンビネータと解釈すればいいのではないでしょうか。あるいはほかに適切な用語はありそうでしょうか。 > CPS 変換は、`a -> b` という関数を `a -> (b -> result) -> result` という関数に変換するもので、「続きの処理」を関数自体に任せるということを表現しています。 > これも相互の変換で情報欠落が起きない実質同じものです。 私がやりたかったのはこれです。カリー化ではなくCPS変換というのですね。今回副作用が目的なのでresultは具体的な戻り値ではありません 私的にはカリー化はめちゃくちゃ語釈が広いものだと思っていました *aはbに渡されます *引数を取って戻り値を返す関数であることを明示するために私の場合カッコですべて囲っています > 「View のアクションが呼ばれたとき、State はこうなる」「State のあらゆる状態に対して、UI はこうなる」という関係性を定義しているイメージが持てるとより正しいかと思います。 あーでもやっぱりそうなのですね。 jetpackの場合、StateはViewModelのプロパティで、コンポーザブル関数がそれを監視する(更新されたら再描画する) アクションはViewModelのメソッドでコンポーザブル関数がイベントを実行する際にメソッドを呼びだす。 いや、勝手にReactの例を想像しているから根本的に違うのか StateとEvalとRenderのイメージが全くついていなくてどこがどこに対応しているのか分かりませんが、要は 構造体のようなあるデータがある (初期値と考える) ある関数がありそれは構造体と操作を引数に取り、構造体の値の更新などを担う、戻り値は更新された構造体 (値の関係性を定義する) 構造体を見ることと操作をすることを提供するオブジェクトのインスタンスとして値の監視と操作によるアクションの通知を行う (Viewに埋め込む) ReactではViewを表示する関数Counter、値を管理する{ age: 42 }があり、値の更新とアクションを定義するreducerがある 関数Counterは `const [state, dispatch] = useReducer(reducer, { age: 42 })` を初めに呼び出す 後はreducerの定義にのっとりdispatchを介してアクションを実行したり、stateを監視したりする これはこれですっきり行くがおそらくtamotoさんが伝えたいこととは別物だろうというのが釈然としなくはあります。 > 「View のアクションが呼ばれたとき、State はこうなる」「State のあらゆる状態に対して、UI はこうなる」 ああ、でもなるほど。サークルの様に関係性が回っているのがイメージつきます。
tamoto

2025/03/30 06:09

なるほど、その React ドキュメントにある reducer の例が正しく理解できているなら、その考え方がかなり適切だと思いました。 React はちゃんと触っていなかったのですが、自分の例で言ってる Eval がまさに reducer そのものだったので、自分の例はいわゆる「reducer が暗黙的に適用されている React」と想像していただければ間違いなさそうに思います。 自分の Render は「dispatch が既にある前提で、state を元にレイアウトを出力する関数」という扱いになるので、即ち React で言う「useReducer の後のコード」に相当するわけですね。 useReducer はその場で reducer を実行することが目的ではなくて、「このコンポーネントでは reducer を使う」という宣言であり、その reducer と紐付いた state と dispatch 関数を得て、後の View のレンダリング処理で使えるようにする、というのが目的です。 なので、React の reducer も「状態遷移がどのようにあるべきか」の宣言に他なりません。 React の例をちゃんと読んだのは初めてですが、おそらく同じ概念であるという想定は正しかったと確信できました。 Jetpack Compose の例を見た限りでは、 `onValueChange = { text = it },` の右辺は `(String) -> Unit` の型を持つ関数なので、即ち値の変更が発生したら呼び出されるイベントハンドラです。 React や自分の例ではこれを reducer として独立して管理していましたが、Compose では単一の関数で直接状態の更新を指示できるのですね。 どちらにしても、「値の変更が発生したら、状態にそれを書き込む」という「振る舞いの宣言」しかしていないことに気が付いたでしょうか。 状態に書き込むことで連鎖的に画面の更新が走るだけであり、コード上では「画面を更新する手続き」などは一切行っていないのです。 それが宣言的 UI であるということです。 これらのどのあたりに手続き的要素を感じていたのかがいまいち推測できておらず申し訳ないですが、他に気になる点がありましたら分かる範囲で答えられればと思います。
utm.

2025/03/30 07:24

@tamotoさん ご返信ありがとうございます、この解釈で問題なさそうであれば一旦は理解したという感じで落ち着こうと思います。 手続き的か宣言的かというところで、議論の余地があるのか分かりませんが自分的に釈然としない点としては、 端的に表した語釈がWikipediaに載ってないので 宣言的 何をしたいかを書く(方法は書かない) 手続的 どうやって実行するかを書く とすると、 UIの描画は宣言的です(これが来たらこう表示する) ただ、テキストフィールドの入力の処理の仕方はやはり手続き的に思えます(onValueChangeが呼び出される、入力値は`(String) -> Unit`で処理するという手続きが発生する、この関数型の引数でstateを更新し、入力が反映される) 実際、わざわざ入力されたら更新するなどと書かなくても、HTMLであれば、<input type="text">とすれば必要な機能は満たせました(宣言的であり、わざわざonValueChangeの処理を書かなくても入力が保証される) 見方を変えれば宣言的と言い張ることもできるが、 いや、やはり入力バリデーションでmax="8"のようにかけていたのを、入力を受け取って切り取るといぅロジックが必要にはなる。。。 TextFieldを宣言的に使いたい場合、そりゃもちろん入力の処理は当然のごとく着いてきて欲しいので、それを書かなければいけないのは手続的で、これを宣言的というのは違和感があります。 当然、UIの描画自体はStateに基づき宣言的に描画されるという点は同意出来ます。 もしかしたら、宣言的と手続的の私の語釈が誤っているのかも知れません。 そしてもうひとつ、 > React や自分の例ではこれを reducer として独立して管理していましたが、Compose では単一の関数で直接状態の更新を指示できるのですね。 詳しい方ならこれをメリットに感じるのでしょうか? 私としてはUIに処理フローが書かれていると今回のように混乱(関心の分離/単一責任の原則できているのか?)しますし、要素が増えるとかなり管理が煩雑になっていきます。 (HTMLやJavaScriptを学び始めた頃はHTMLからonclickを呼び出している方がどういう動作をするのかがわかりやすいと思っていたし、その当時はReact/vue.jsも盛んでそのような方法が流行っていましたが) 最小の例であれば、いいかもしれませんが一貫性がないとかなり煩雑になる気もします。 コンポーズ可能関数がコンポーネントという発想が自分は弱いのかも知れませんが、 補足 Kotlinの文法が独特ですが以下は onValueChange = { text = it }, 擬似言語で表わすなら onValueChange = fun(it: String){ text = it }, です。(無名関数を渡していることを示したつもり) 蛇足 自己回答をしてますが、 初期値の保持の要素が足りないことが分かっているので、そこら辺を修正したいと思います。 (Stateの初期値を親要素、子要素、外部要素(DBとの通信で初期化可能にするため)で変更できるようにしなければならない、resetableにして、内部状態をクラス内(reducerに該当)で管理できるようにするためにも)
tamoto

2025/03/30 08:38

なるほど。おっしゃりたいことがようやく理解できた気がします。 > ただ、テキストフィールドの入力の処理の仕方はやはり手続き的に思えます これは「入力された時に状態を更新する」という部分のコードは、ミクロの視点において実装は手続き的ではないかという話ですね。 これには同意します。入力の処理を行う、というその部分のコードに関しては、一つの手続き的な処理であると言えます。 一方で、それを UI の構成要素の一つとして見たときには、「そのような手続きを処理する」という宣言しかしていない、という点で宣言的であると言っているわけです。 確かに個別のハンドラには手続き的な処理を書いていますが、UI を構成する際には「誰もその手続きを実行しておらず」、「記述したルールに則って手続きが走る」という「仕様の定義」のみで UI が形作られることを指して「宣言的 UI」と呼んでいるのです。 これは視点の違いによるものなので、個別の処理について手続き的と感じたことは間違いではないです。 > <input type="text">とすれば必要な機能は満たせました(宣言的であり、わざわざonValueChangeの処理を書かなくても入力が保証される) これについては、コンポーネントはコンポーネントとして振る舞うための内部状態を持つのに対し、内部状態に一切反映しないテキストエリアだけを確保しているのと同義なので、いわゆる「onValueChange などを一切定義していない TextField の Composable」に相当するので、それは宣言的に見えるのは仕方ないと思いました。 入力は保証されますが、入力の活用ができません。 > 詳しい方ならこれをメリットに感じるのでしょうか? > (関心の分離/単一責任の原則できているのか?) Compose の「イベントにハンドラ関数を直接渡す」というのはコンポーネントの構成が「簡単」(easy) になるとは思いますが、個人的には、イベントハンドラをコンポーネントの「処理系」として独立させている Eval/reducer の構成の方が圧倒的に好みです。 Compose の設計理念については分かりませんが、Eval の概念を Render の要素に直接混ぜていることになるので、本来役割が分かれているものを一つにしている点はかなり気に入らないですね。
utm.

2025/03/30 09:10

概ね同意見で安心しました。 宣言的UIに関しては、従来のXMLの方法ではパーツ化して別のXMLに埋め込むという手続きでいくらかの問題が発生していた点を解決したという意味で宣言的UIというパラダイムシフトが起こしたのだと考えています。 また、そのようにした際にパーツとして捉えるとそれぞれのコンポーネントの任意のvalueを取りたい際に、例えばname="loginId"としたら同じコンポーネントを同じところに埋め込むとnameが競合してしまうとでも考えたのだろうと勝手に解釈します(xmlのパーツ化で起きる問題を解決したいだけならUIはstateによって宣言的だが、値はnameでアクセスできるとすることも出来なくはない気がします)。 いや、違うか。入力を制御したい(8文字しか入れられないみたいな)時に入力を制御することと値をセットすることを定義しなければならないから、二度手間を無くしたのか。なるほど。ここが一貫性を保ちたい時に設計概念がややこしくなっていますね。 ほとんどのアプリがそうであるという訳では無いのですが、やはりアプリケーションソフトウェアの根幹は入力をどう扱うか?だと思うのでTextFieldがその機能を記述しないと行けないというのは如何せん不思議ではありました。 また、これを宣言的と言ってしまうと手続き型言語も外部からモジュールとして捉えれば宣言的ということになり、手続き型との境界が無くなる気もします(笑) すくなくとも、xmlをどこかに埋め込むという操作をしないのであれば宣言的なので、(これも仮に手続的だとしても外部から見たら宣言的という話もできるな…) 得てしてXMLとの比較で宣言的という用語がでてきたのは納得感はないところではあります。 (Stateの状態でUIが変わるので、画面1Flagみたいなのが、Trueになったら別の画面が描画されるという宣言的なUIはスムーズでGOODではあると思いますが)
guest

0

フロントエンド開発のフレームワークはまったく知りません。質問に答えられるほどの知識はありませんが「関数型UI」が気になったので調べてみました。

The Functional UI Model

The Functional UI Model で定義する関数は入力と出力の2種類。

  • Event Function
  • View Function

以下、UIモデルの特徴が書かれています。ComposableとPlatform agnosticがあるので概念は質問の趣旨に合う。この後を読むとclojureのコードが出てきます。

Elm

2つの関数を手がかりにThe Elm Architectureを発見。こちらはMVCモデルに近くてModelが登場します。

  • Model アプリケーションの状態
  • Event 状態を HTML に変換する方法
  • Update メッセージを使って状態を更新する方法

ここに掲載された例は動かすことができます。実行環境はEllieがお薦めのようです。

概念理解の参考になれば。(ならないか ... )

投稿2025/03/28 23:50

編集2025/03/28 23:52
xebme

総合スコア1109

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

utm.

2025/03/29 05:18

ありがとうございます。 > The Functional UI Model これはボタンの例だと思いますがテキストフィールドにおいても入力と入力値の管理と見た目は別物だよねといえ話をしているのかなと解釈しました。 おっしゃる通りで、質問にあるTextFieldの引数の定義とマッチしています。 フレームワーク的にはなりますが、クラスの設計として、分離して考える方向に活かしたいと思います。 > Elm これは恐らく、基本的にオブザーバるな変数を監視しview(言い換えればユーザインタフェース)に伝える基本的なアーキテクチャ構成の話をしているのだと読解しました。 kotlinのJetpackではコンポーザブルとステートが内部的にこれらをやってくれますが、他言語ではどのように明示するのか知らないので重要な概念だと思います。 (XMLではfragmentで変数をオブザーブするので、関数型UIより幾分か使いやすかった。関数型UIへの遷移はおそらくはXMLのような別言語を仲介しないことで、どのプラットフォームでも動作するUIを定義することが出来る、というのを目指しているものと思われる。)
guest

0

[自己回答]
考え方としては
xmlで記述していて見えない何かがインフレートやレンダリングを担っていたのを、コンポーザブル関数を使って明示的に行うと考えることですっきりしました。
htmlやxmlのような別言語で書かれたプロパティではなくてオブジェクトとして一度定義しそれを使って関数に何かを任せるという発想で問題が解決できそうです。
やはりxmlのような別言語で書いていた部分がオブジェクトとなるのは若干違和感がありますが、そういうものだという認識で問題なさそうです。

そうすることで、
1の問題は若干解決しました。
コンポーザブルな関数がhtmlの役割ではなく、クラスがhtmlの役割を担っている。

2の問題についても、
関数にデータを渡すだけでよくって、入力要素をパーツとして考える必要がなくなり、見やすさを担保できるようになりました。(time pickerなどの実装はまた一考必要かもしれない)

3についても完全に解決したました。

4については若干コンテキストが違うが、下記のコードでは従来と同じような動作を担保できている。
また、stateについてはリストなどはまた違ってくるだろうが、その時その時で適切なstateを選ぶ方向でよいのではないかという結論に至りました。

data classを用いて、
TextFieldに必要な引数をすべて定義しておく

ベースとなるクラスでインスタンスを持つ。
また、関数ではベースとなるクラスを引数に取ることで、紐づけを行う
// 構造体 + 関数のやり方みたい。やっぱりkotlinはこういう結論に至るのか

表示関連
(オブジェクトの型により表示状態を持つことで、LoginIdTextFieldなどのUI要素は不要になった)

LoginFormModelはインスタンスをdata classと保持することでログの出力や状態の比較が簡単になりそう(未検証)。
考えをブラッシュアップしてソースコードに反映し以下ような構成になりました。
フレームワークとしての考えが提唱されていないのでModelだらけになっていますが。。。

textFieldのベース

kt

1open class TextFieldModel { 2 3 var state: TextFieldState by mutableStateOf(TextFieldState("")) 4 protected set 5 open var action: TextFieldAction by mutableStateOf( 6 TextFieldAction( 7 onValueChange = ::updateInputValue) 8 ) 9 protected set 10 11 open var style: TextFieldStyle by mutableStateOf( 12 TextFieldStyle() 13 ) 14 protected set 15 fun updateInputValue(value: String) { 16 state = state.copy(inputValue = value) 17 } 18 19} 20data class TextFieldStyle( 21 val modifier: Modifier = Modifier, 22 val enabled: Boolean = true, 23 val readOnly: Boolean = false, 24 val textStyle: @Composable () -> TextStyle = { LocalTextStyle.current }, 25 val label: @Composable (() -> Unit)? = null, 26 val placeholder: @Composable (() -> Unit)? = null, 27 val leadingIcon: @Composable (() -> Unit)? = null, 28 val trailingIcon: @Composable (() -> Unit)? = null, 29 val isError: Boolean = false, 30 val visualTransformation: VisualTransformation = VisualTransformation.None, 31 val keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 32 val keyboardActions: KeyboardActions = KeyboardActions(), 33 val singleLine: Boolean = false, 34 val maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 35 val minLines: Int = 1, 36 val interactionSource: MutableInteractionSource? = null, 37 val shape: @Composable () -> Shape = { TextFieldDefaults.TextFieldShape }, 38 val colors: @Composable () -> TextFieldColors = { TextFieldDefaults.textFieldColors() } 39) { 40 41} 42 43data class TextFieldState( 44 val inputValue: String , 45) 46data class TextFieldAction( 47 val onValueChange: (String) -> Unit , 48) 49 50@Composable 51fun AppTextFiled( 52 textFieldModel: TextFieldModel 53) { 54 55 TextField( 56 value = textFieldModel.state.inputValue, 57 onValueChange = {textFieldModel.action.onValueChange(it)}, 58 modifier = textFieldModel.style.modifier, 59 enabled = textFieldModel.style.enabled, 60 readOnly = textFieldModel.style.readOnly, 61 textStyle = textFieldModel.style.textStyle(), 62 label = textFieldModel.style.label, 63 placeholder = textFieldModel.style.placeholder, 64 leadingIcon = textFieldModel.style.leadingIcon, 65 trailingIcon = textFieldModel.style.trailingIcon, 66 isError = textFieldModel.style.isError, 67 visualTransformation = textFieldModel.style.visualTransformation, 68 keyboardOptions = textFieldModel.style.keyboardOptions, 69 keyboardActions = textFieldModel.style.keyboardActions, 70 singleLine = textFieldModel.style.singleLine, 71 maxLines = textFieldModel.style.maxLines, 72 minLines = textFieldModel.style.minLines, 73 interactionSource = textFieldModel.style.interactionSource, 74 shape = textFieldModel.style.shape(), 75 colors = textFieldModel.style.colors() 76 ) 77}

buttonのベース

kt

1open class ButtonModel { 2 3 open var action: ButtonAction by mutableStateOf( 4 ButtonAction( 5 onClick = ::onClick) 6 ) 7 protected set 8 9 open var style: ButtonStyle by mutableStateOf( 10 ButtonStyle() 11 ) 12 protected set 13 14 open fun onClick() { 15 println("on click") 16 } 17 18} 19data class ButtonStyle( 20 val modifier: Modifier = Modifier, 21 val enabled: Boolean = true, 22 val interactionSource: MutableInteractionSource? = null, 23 val elevation: @Composable () -> ButtonElevation? = { ButtonDefaults.elevation() }, 24 val shape: @Composable () -> Shape = { MaterialTheme.shapes.small }, 25 val border: BorderStroke? = null, 26 val colors:@Composable () -> ButtonColors = { ButtonDefaults.buttonColors() }, 27 val contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 28) 29data class ButtonAction( 30 val onClick: () -> Unit, 31) 32@Composable 33fun AppButton( 34 buttonModel: ButtonModel, 35 content:@Composable RowScope.() -> Unit 36) { 37 Button( 38 onClick = buttonModel.action.onClick, 39 modifier = buttonModel.style.modifier, 40 enabled = buttonModel.style.enabled, 41 interactionSource = buttonModel.style.interactionSource, 42 elevation = buttonModel.style.elevation(), 43 shape = buttonModel.style.shape(), 44 border = buttonModel.style.border, 45 colors = buttonModel.style.colors(), 46 contentPadding = buttonModel.style.contentPadding, 47 content = content 48 ) 49}

loginButtonModel

kt

1class LoginButtonModel( 2 private val onDelegate: () -> Unit 3): ButtonModel() { 4 override var action: ButtonAction by mutableStateOf( 5 ButtonAction( 6 onClick = onDelegate) 7 ) 8 protected set 9 10}

Loginに際するビジネスロジックの集約(これはサービスなのか?ドメインなのか?ViewModelなのか?アプリケーション的にはViewModelのスコープがないとスレッド的に問題ありそうだし、ドメインが状態を持っていいのかもわからない)

kt

1class LoginFormModel(){ 2 val loginIdModel = LoginIdStateModel() 3 val loginPasswordModel = LoginPasswordStateModel() 4 5 val loginButtonModel = LoginButtonModel(::submitLogin) 6 7 private fun submitLogin(){ 8 // 入力をすべてリセット 9 clearForm() 10 11 } 12 13 private fun clearForm(){ 14 loginIdModel.action.onValueChange("") 15 loginPasswordModel.action.onValueChange("") 16 } 17}

当然インスタンスはviewModelが持つ

kt

1class LoginViewModel () : ViewModel() { 2 val loginFormModel = LoginFormModel() 3}

UI

kt

1@Composable 2fun LoginScreen( 3 viewModel: LoginViewModel = viewModel<LoginViewModel>() 4) { 5 6 val loginFormModel = viewModel.loginFormModel 7 Column(modifier = Modifier.padding(16.dp)) { 8 AppTextFiled(loginFormModel.loginIdModel) 9 AppTextFiled(loginFormModel.loginPasswordModel) 10 11 AppButton(loginFormModel.loginButtonModel){ 12 13 } 14 } 15} 16 17

UIの状態は依存注入できるようにすべきなのか疑問
ログをだして、テストをかけるようにすべきなら注入できたほうがいいが、そもそも予期できないような外部リソースの取得がないので、セットする処理を淡々と書けばいい気もする。。。
TextFieldStateなどは処理ではなくて状態のラッパーでしかないので。。。
また、MVVMやMVCの観点からいってModelがアプリケーション内での状態を持つのはなんとなく不自然かもしれないので、用語を整理できればしたい。

*回答を編集しました。文字数の関係から一部コードを削除しました。

投稿2025/03/28 03:33

編集2025/03/29 13:38
utm.

総合スコア676

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.32%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問