実現したいこと
マルチプラットフォームアプリを作成する際に関数型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
最後に質問を整理します
-
今までのXMLやHTML、ネイティブアプリではnameのような一つの要素に紐づいたキーワードを使って、値の取得やセットを行っていました。ボタンであればクリックイベントの処理などもそこで行っていました。そのような感覚に近いやり方として、皆さまはどのように開発しているのでしょうか。
-
各入力要素は変数との紐づけをする処理と、レイアウトに埋め込む処理の二つのことをしなければなりません。これらを分離する方法はあるのでしょうか。mapを使うのであれば、直接レイアウトに埋め込むほうがマシだと思っています。
-
UIの各要素をパーツ化すると、TextFieldのようなコンポーザブル関数が自由に引数をとれるとしてもそこにアクセスする手段がありません。これはカリー化などで状態を変更できるようにすべきなのでしょうか。(オブジェクトの考えに引っ張られているのでよくなさそう)
-
入力の管理に使う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/

回答3件
あなたの回答
tips
プレビュー