回答編集履歴

4

buttonにも同じ更新を

2025/04/04 05:50

投稿

utm.
utm.

スコア786

test CHANGED
@@ -53,10 +53,14 @@
53
53
  }
54
54
 
55
55
  var state: TextFieldState by mutableStateOf (initialState)
56
+ protected set
57
+
56
58
  var action: TextFieldAction by mutableStateOf(initialAction)
59
+ protected set
60
+
57
61
  var style: TextFieldStyle by mutableStateOf(initialStyle)
58
-
62
+ protected set
59
-
63
+
60
64
  fun updateInputValue(value: String) {
61
65
  state = state.copy(inputValue = value)
62
66
  }
@@ -132,17 +136,23 @@
132
136
 
133
137
  buttonのベース
134
138
  ```kt
135
- open class ButtonModel {
139
+ open class ButtonKit {
140
+
136
-
141
+ var initialAction: ButtonAction = ButtonAction(onClick = ::onClick)
142
+ set(value) {
143
+ field = value
144
+ action = value
145
+ }
146
+
147
+ var initialStyle: ButtonStyle = ButtonStyle()
148
+ set(value) {
149
+ field = value
150
+ style = value
151
+ }
152
+
137
- open var action: ButtonAction by mutableStateOf(
153
+ var action: ButtonAction by mutableStateOf(initialAction)
138
- ButtonAction(
139
- onClick = ::onClick)
140
- )
141
- protected set
154
+ protected set
142
-
143
- open var style: ButtonStyle by mutableStateOf(
155
+ var style: ButtonStyle by mutableStateOf(initialStyle)
144
- ButtonStyle()
145
- )
146
156
  protected set
147
157
 
148
158
  open fun onClick() {
@@ -208,16 +218,15 @@
208
218
 
209
219
  ```
210
220
 
211
- loginButtonModel
221
+ LoginButtonKit
212
- ```kt
222
+ ```kt
213
- class LoginButtonModel(
223
+ class LoginButtonKit(
214
- private val onDelegate: () -> Unit
224
+ onClick: () -> Unit
215
- ): ButtonModel() {
225
+ ): ButtonKit() {
226
+
227
+ init {
216
- override var action: ButtonAction by mutableStateOf(
228
+ initialAction = ButtonAction(onClick = onClick)
217
- ButtonAction(
218
- onClick = onDelegate)
219
- )
229
+ }
220
- protected set
221
230
 
222
231
  }
223
232
  ```

3

コードを一部修正し更新

2025/04/04 05:37

投稿

utm.
utm.

スコア786

test CHANGED
@@ -23,6 +23,7 @@
23
23
  ベースとなるクラスでインスタンスを持つ。
24
24
  また、関数ではベースとなるクラスを引数に取ることで、紐づけを行う
25
25
  // 構造体 + 関数のやり方みたい。やっぱりkotlinはこういう結論に至るのか
26
+ *この方法はデータモデルとバインド方式というみたいです。(おそらくなので出典ありません)
26
27
 
27
28
  表示関連
28
29
  (オブジェクトの型により表示状態を持つことで、LoginIdTextFieldなどのUI要素は不要になった)
@@ -33,22 +34,41 @@
33
34
 
34
35
  textFieldのベース
35
36
  ```kt
36
- open class TextFieldModel {
37
+ open class TextFieldKit() {
37
-
38
+ var initialState: TextFieldState = TextFieldState(inputValue = "")
39
+ set(value) {
40
+ field = value
41
+ state = value
42
+ }
43
+ var initialAction: TextFieldAction = TextFieldAction(onValueChange = ::updateInputValue)
44
+ set(value) {
45
+ field = value
46
+ action = value
47
+ }
48
+
49
+ var initialStyle: TextFieldStyle = TextFieldStyle()
50
+ set(value) {
51
+ field = value
52
+ style = value
53
+ }
54
+
38
- var state: TextFieldState by mutableStateOf(TextFieldState(""))
55
+ var state: TextFieldState by mutableStateOf (initialState)
39
- protected set
40
- open var action: TextFieldAction by mutableStateOf(
56
+ var action: TextFieldAction by mutableStateOf(initialAction)
41
- TextFieldAction(
42
- onValueChange = ::updateInputValue)
43
- )
44
- protected set
45
-
46
- open var style: TextFieldStyle by mutableStateOf(
57
+ var style: TextFieldStyle by mutableStateOf(initialStyle)
47
- TextFieldStyle()
58
+
48
- )
59
+
49
- protected set
50
60
  fun updateInputValue(value: String) {
51
61
  state = state.copy(inputValue = value)
62
+ }
63
+ fun clearValue(){
64
+ state = initialState
65
+ }
66
+ fun clearAction(){
67
+ action = initialAction
68
+ }
69
+
70
+ fun clearStyle(){
71
+ style = initialStyle
52
72
  }
53
73
 
54
74
  }
@@ -71,9 +91,7 @@
71
91
  val interactionSource: MutableInteractionSource? = null,
72
92
  val shape: @Composable () -> Shape = { TextFieldDefaults.TextFieldShape },
73
93
  val colors: @Composable () -> TextFieldColors = { TextFieldDefaults.textFieldColors() }
74
- ) {
94
+ )
75
-
76
- }
77
95
 
78
96
  data class TextFieldState(
79
97
  val inputValue: String ,
@@ -164,6 +182,31 @@
164
182
  )
165
183
  }
166
184
  ```
185
+ 入力要素の具象クラス
186
+ ```kt
187
+ // バリデーションが必要であれば継承とインタフェースを用いて拡張してください
188
+ class LoginIdStateKit : TextFieldKit() {
189
+
190
+ init {
191
+ initialStyle = TextFieldStyle(
192
+ label = { Text("Login ID") }
193
+ )
194
+ }
195
+
196
+ }
197
+
198
+ class LoginPasswordStateKit : TextFieldKit() {
199
+
200
+ init {
201
+ initialStyle = TextFieldStyle(
202
+ label = { Text("Password") },
203
+ visualTransformation = PasswordVisualTransformation(), // パスワードを隠す
204
+ )
205
+ }
206
+
207
+ }
208
+
209
+ ```
167
210
 
168
211
  loginButtonModel
169
212
  ```kt
@@ -179,30 +222,39 @@
179
222
  }
180
223
  ```
181
224
 
182
- Loginに際するビジネスロジックの集約(これはサービスなのか?ドメインなのか?ViewModelなのか?アプリケーション的にはViewModelのスコープがないとスレッド的に問題ありそうだし、ドメインが状態を持っていいのかもわからない)
225
+ Loginに際するビジネスロジックの集約
226
+ . stateの状態変化をすべて集約
227
+ . actionはviewModel層に預ける
183
- ```kt
228
+ ```kt
229
+ // 値の保持、更新に専念しアクションはviewModelに任せましょう
184
- class LoginFormModel(){
230
+ class LoginFormComponent(
231
+ val onLoginButtonClick: () -> Unit
232
+ ): FormComponent() {
185
- val loginIdModel = LoginIdStateModel()
233
+ val loginIdKit = LoginIdStateKit()
186
- val loginPasswordModel = LoginPasswordStateModel()
234
+ val loginPasswordKit = LoginPasswordStateKit()
187
-
235
+
188
- val loginButtonModel = LoginButtonModel(::submitLogin)
236
+ val loginButtonKit = LoginButtonKit(::submitLogin)
189
237
 
190
238
  private fun submitLogin(){
191
- // 入力をすべてリセット
239
+ onLoginButtonClick()
192
- clearForm()
193
-
194
- }
240
+ }
195
-
241
+
196
- private fun clearForm(){
242
+ fun clearForm(){
197
- loginIdModel.action.onValueChange("")
243
+ loginIdKit.clearValue()
198
- loginPasswordModel.action.onValueChange("")
244
+ loginPasswordKit.clearValue()
199
245
  }
200
246
  }
201
247
  ```
202
248
  当然インスタンスはviewModelが持つ
203
249
  ```kt
250
+ // Actionにおける動作はこちらにかいて、値の更新処理はFormに任せましょう
204
251
  class LoginViewModel () : ViewModel() {
205
- val loginFormModel = LoginFormModel()
252
+ val loginFormComponent = LoginFormComponent(
253
+ onLoginButtonClick = ::login
254
+ )
255
+ private fun login(){
256
+ loginFormComponent.clearForm()
257
+ }
206
258
  }
207
259
  ```
208
260
 
@@ -213,18 +265,16 @@
213
265
  viewModel: LoginViewModel = viewModel<LoginViewModel>()
214
266
  ) {
215
267
 
216
- val loginFormModel = viewModel.loginFormModel
268
+ val loginFormModel = viewModel.loginFormComponent
217
269
  Column(modifier = Modifier.padding(16.dp)) {
218
- AppTextFiled(loginFormModel.loginIdModel)
270
+ AppTextFiled(loginFormModel.loginIdKit)
219
- AppTextFiled(loginFormModel.loginPasswordModel)
271
+ AppTextFiled(loginFormModel.loginPasswordKit)
220
-
272
+
221
- AppButton(loginFormModel.loginButtonModel){
273
+ AppButton(loginFormModel.loginButtonKit){
222
-
274
+
223
- }
275
+ }
224
- }
276
+ }
225
- }
277
+ }
226
-
227
-
228
278
  ```
229
279
 
230
280
  UIの状態は依存注入できるようにすべきなのか疑問

2

回答の内容を編集

2025/03/29 13:38

投稿

utm.
utm.

スコア786

test CHANGED
@@ -2,13 +2,57 @@
2
2
  考え方としては
3
3
  xmlで記述していて見えない何かがインフレートやレンダリングを担っていたのを、コンポーザブル関数を使って明示的に行うと考えることですっきりしました。
4
4
  htmlやxmlのような別言語で書かれたプロパティではなくてオブジェクトとして一度定義しそれを使って関数に何かを任せるという発想で問題が解決できそうです。
5
+ やはりxmlのような別言語で書いていた部分がオブジェクトとなるのは若干違和感がありますが、そういうものだという認識で問題なさそうです。
6
+
7
+ そうすることで、
8
+ 1の問題は若干解決しました。
9
+ コンポーザブルな関数がhtmlの役割ではなく、クラスがhtmlの役割を担っている。
10
+
11
+ 2の問題についても、
5
- 若干違和感がありまが、classがxmlの代替で、コンポズ関数は今まで見えないころでしていたインフレートであるととらえると、フレームワークチックになりが、以下のようなコードが出来上がりました。
12
+ 関数にデータを渡だけよくって入力要素をパとしてえる必要がくな、見やさを担保できるようなりました。(time pickerなどの実装はまた一考必要かもしれない)
13
+
14
+ 3についても完全に解決したました。
15
+
6
- (和感も慣れ+フレムワークしてそいうものだと解釈すること特に引ところはなさそう)
16
+ 4については若干コンテキストがうが、下記のコドでは従来同じよな動作を担保できてい
17
+ また、stateについてはリストなどはまた違ってくるだろうが、その時その時で適切なstateを選ぶ方向でよいのではないかという結論に至りました。
7
18
 
8
19
  data classを用いて、
9
20
  TextFieldに必要な引数をすべて定義しておく
21
+
22
+
23
+ ベースとなるクラスでインスタンスを持つ。
24
+ また、関数ではベースとなるクラスを引数に取ることで、紐づけを行う
25
+ // 構造体 + 関数のやり方みたい。やっぱりkotlinはこういう結論に至るのか
26
+
27
+ 表示関連
28
+ (オブジェクトの型により表示状態を持つことで、LoginIdTextFieldなどのUI要素は不要になった)
29
+
30
+ LoginFormModelはインスタンスをdata classと保持することでログの出力や状態の比較が簡単になりそう(未検証)。
31
+ 考えをブラッシュアップしてソースコードに反映し以下ような構成になりました。
32
+ フレームワークとしての考えが提唱されていないのでModelだらけになっていますが。。。
33
+
34
+ textFieldのベース
10
- ```kt
35
+ ```kt
36
+ open class TextFieldModel {
37
+
38
+ var state: TextFieldState by mutableStateOf(TextFieldState(""))
39
+ protected set
40
+ open var action: TextFieldAction by mutableStateOf(
41
+ TextFieldAction(
42
+ onValueChange = ::updateInputValue)
43
+ )
44
+ protected set
45
+
46
+ open var style: TextFieldStyle by mutableStateOf(
47
+ TextFieldStyle()
48
+ )
49
+ protected set
50
+ fun updateInputValue(value: String) {
51
+ state = state.copy(inputValue = value)
52
+ }
53
+
54
+ }
11
- data class TextFieldParams(
55
+ data class TextFieldStyle(
12
56
  val modifier: Modifier = Modifier,
13
57
  val enabled: Boolean = true,
14
58
  val readOnly: Boolean = false,
@@ -31,126 +75,161 @@
31
75
 
32
76
  }
33
77
 
34
- ```
35
-
36
- ベースとなるクラスでインスタンスを持つ。
37
- また、関数ではベースとなるクラスを引数に取ることで、紐づけを行う
38
- // 構造体 + 関数のやり方みたい。やっぱりkotlinはこういう結論に至るのか
39
-
40
- ```kt
41
- open class InputStateBase {
78
+ data class TextFieldState(
42
-
43
- open protected var _inputValue = mutableStateOf("")
44
- val inputValue: State<String> = _inputValue
45
- open fun updateInputValue(value: String) {
46
- _inputValue.value = value
79
+ val inputValue: String ,
47
- }
48
-
49
- open var textFieldParams: TextFieldParams by mutableStateOf(
50
- TextFieldParams()
51
- )
80
+ )
52
- protected set
81
+ data class TextFieldAction(
53
-
82
+ val onValueChange: (String) -> Unit ,
54
- }
83
+ )
55
84
 
56
85
  @Composable
57
86
  fun AppTextFiled(
58
- state: InputStateBase
87
+ textFieldModel: TextFieldModel
59
88
  ) {
60
89
 
61
90
  TextField(
62
- value = state.inputValue.value,
63
- onValueChange = {state.updateInputValue(it)},
64
- modifier = state.textFieldParams.modifier,
65
- enabled = state.textFieldParams.enabled,
66
- readOnly = state.textFieldParams.readOnly,
67
- textStyle = state.textFieldParams.textStyle(),
68
- label = state.textFieldParams.label,
69
- placeholder = state.textFieldParams.placeholder,
70
- leadingIcon = state.textFieldParams.leadingIcon,
71
- trailingIcon = state.textFieldParams.trailingIcon,
72
- isError = state.textFieldParams.isError,
73
- visualTransformation = state.textFieldParams.visualTransformation,
74
- keyboardOptions = state.textFieldParams.keyboardOptions,
75
- keyboardActions = state.textFieldParams.keyboardActions,
76
- singleLine = state.textFieldParams.singleLine,
77
- maxLines = state.textFieldParams.maxLines,
78
- minLines = state.textFieldParams.minLines,
79
- interactionSource = state.textFieldParams.interactionSource,
80
- shape = state.textFieldParams.shape(),
81
- colors = state.textFieldParams.colors()
82
- )
83
- }
84
-
85
- ```
86
-
87
- こうすることで、
88
- 1の問題は若干解決しました。
89
- コンポーザブルな関数がhtmlの役割ではなく、クラスがhtmlの役割を担っている。
90
-
91
- 2の問題についても、
92
- 関数にデータを渡すだけでよくって、入力要素をパーツとして考える必要がなくなり、見やすさを担保できるようになった。(time pickerなどの実装はまた一考必要かもしれない)
93
-
94
- 3についても完全に解決した。
95
-
96
- 4については若干コンテキストが違う。またこの変更によりかくつきが発生するようになった気がするため少し調整が必要か。
97
-
98
-
99
- そのほかの構成要素の修正
100
- state関連
101
- ```kt
102
- // バリデーションが必要であれば継承とインタフェースを用いて拡張してください
103
- class LoginIdState : InputStateBase() {
104
-
105
- override var textFieldParams: TextFieldParams by mutableStateOf(
106
- TextFieldParams(
107
- label = { Text("Login ID") }
108
- )
109
- )
110
- }
111
-
112
- class LoginPasswordState : InputStateBase() {
113
-
114
- override var textFieldParams: TextFieldParams by mutableStateOf(
115
- TextFieldParams(
116
- label = { Text("Password") },
117
- visualTransformation = PasswordVisualTransformation(), // パスワードを隠す
118
- )
119
- )
120
- }
121
- ```
122
-
123
-
124
- 表示関連
125
- (オブジェクトの型により表示状態を持つことで、LoginIdTextFieldなどのUI要素は不要になった)
91
+ value = textFieldModel.state.inputValue,
92
+ onValueChange = {textFieldModel.action.onValueChange(it)},
93
+ modifier = textFieldModel.style.modifier,
94
+ enabled = textFieldModel.style.enabled,
95
+ readOnly = textFieldModel.style.readOnly,
96
+ textStyle = textFieldModel.style.textStyle(),
97
+ label = textFieldModel.style.label,
98
+ placeholder = textFieldModel.style.placeholder,
99
+ leadingIcon = textFieldModel.style.leadingIcon,
100
+ trailingIcon = textFieldModel.style.trailingIcon,
101
+ isError = textFieldModel.style.isError,
102
+ visualTransformation = textFieldModel.style.visualTransformation,
103
+ keyboardOptions = textFieldModel.style.keyboardOptions,
104
+ keyboardActions = textFieldModel.style.keyboardActions,
105
+ singleLine = textFieldModel.style.singleLine,
106
+ maxLines = textFieldModel.style.maxLines,
107
+ minLines = textFieldModel.style.minLines,
108
+ interactionSource = textFieldModel.style.interactionSource,
109
+ shape = textFieldModel.style.shape(),
110
+ colors = textFieldModel.style.colors()
111
+ )
112
+ }
113
+ ```
114
+
115
+ buttonのベース
116
+ ```kt
117
+ open class ButtonModel {
118
+
119
+ open var action: ButtonAction by mutableStateOf(
120
+ ButtonAction(
121
+ onClick = ::onClick)
122
+ )
123
+ protected set
124
+
125
+ open var style: ButtonStyle by mutableStateOf(
126
+ ButtonStyle()
127
+ )
128
+ protected set
129
+
130
+ open fun onClick() {
131
+ println("on click")
132
+ }
133
+
134
+ }
135
+ data class ButtonStyle(
136
+ val modifier: Modifier = Modifier,
137
+ val enabled: Boolean = true,
138
+ val interactionSource: MutableInteractionSource? = null,
139
+ val elevation: @Composable () -> ButtonElevation? = { ButtonDefaults.elevation() },
140
+ val shape: @Composable () -> Shape = { MaterialTheme.shapes.small },
141
+ val border: BorderStroke? = null,
142
+ val colors:@Composable () -> ButtonColors = { ButtonDefaults.buttonColors() },
143
+ val contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
144
+ )
145
+ data class ButtonAction(
146
+ val onClick: () -> Unit,
147
+ )
148
+ @Composable
149
+ fun AppButton(
150
+ buttonModel: ButtonModel,
151
+ content:@Composable RowScope.() -> Unit
152
+ ) {
153
+ Button(
154
+ onClick = buttonModel.action.onClick,
155
+ modifier = buttonModel.style.modifier,
156
+ enabled = buttonModel.style.enabled,
157
+ interactionSource = buttonModel.style.interactionSource,
158
+ elevation = buttonModel.style.elevation(),
159
+ shape = buttonModel.style.shape(),
160
+ border = buttonModel.style.border,
161
+ colors = buttonModel.style.colors(),
162
+ contentPadding = buttonModel.style.contentPadding,
163
+ content = content
164
+ )
165
+ }
166
+ ```
167
+
168
+ loginButtonModel
169
+ ```kt
170
+ class LoginButtonModel(
171
+ private val onDelegate: () -> Unit
172
+ ): ButtonModel() {
173
+ override var action: ButtonAction by mutableStateOf(
174
+ ButtonAction(
175
+ onClick = onDelegate)
176
+ )
177
+ protected set
178
+
179
+ }
180
+ ```
181
+
182
+ Loginに際するビジネスロジックの集約(これはサービスなのか?ドメインなのか?ViewModelなのか?アプリケーション的にはViewModelのスコープがないとスレッド的に問題ありそうだし、ドメインが状態を持っていいのかもわからない)
183
+ ```kt
184
+ class LoginFormModel(){
185
+ val loginIdModel = LoginIdStateModel()
186
+ val loginPasswordModel = LoginPasswordStateModel()
187
+
188
+ val loginButtonModel = LoginButtonModel(::submitLogin)
189
+
190
+ private fun submitLogin(){
191
+ // 入力をすべてリセット
192
+ clearForm()
193
+
194
+ }
195
+
196
+ private fun clearForm(){
197
+ loginIdModel.action.onValueChange("")
198
+ loginPasswordModel.action.onValueChange("")
199
+ }
200
+ }
201
+ ```
202
+ 当然インスタンスはviewModelが持つ
203
+ ```kt
204
+ class LoginViewModel () : ViewModel() {
205
+ val loginFormModel = LoginFormModel()
206
+ }
207
+ ```
208
+
209
+ UI
126
210
  ```kt
127
211
  @Composable
128
212
  fun LoginScreen(
129
213
  viewModel: LoginViewModel = viewModel<LoginViewModel>()
130
214
  ) {
131
215
 
132
- val screenState = viewModel.loginFormState
216
+ val loginFormModel = viewModel.loginFormModel
133
217
  Column(modifier = Modifier.padding(16.dp)) {
134
- AppTextFiled(screenState.loginIdState)
218
+ AppTextFiled(loginFormModel.loginIdModel)
135
- AppTextFiled(screenState.loginPasswordState)
219
+ AppTextFiled(loginFormModel.loginPasswordModel)
220
+
221
+ AppButton(loginFormModel.loginButtonModel){
222
+
136
- }
223
+ }
137
- }
224
+ }
138
-
225
+ }
226
+
227
+
139
- ```
228
+ ```
140
-
229
+
141
- ほかの構成変更
230
+ UI状態依存注入できるようにすべきのか疑問
142
-
143
-
144
- ---
145
- 追記
146
- そのほの構成要素変化しと書ましたが以下のようなクラはdata classと定義して状態比較や取得を簡単に出来るようにした方がいいかもなど思考していたのが抜ました
231
+ ログをだして、テストをけるようすべきら注入できたほういいが、そもそも予期できないような外部リソースの取得がのでセットする処理を淡々い気もする。。
147
- まだ、実験できていません。
148
-
149
- ```kt
150
- class Screen1State(){
151
- val loginIdState = LoginIdState()
232
+ TextFieldStateなどは処理ではなくて状態のラッパーでしかないので。。。
152
- val loginPasswordTextField = LoginPasswordState()
233
+ また、MVVMやMVCの観点からいってModelがアプリケーション内での状態を持つのはなんとなく不自然かもしれないので、用語を整理できればしたい。
153
- }
234
+
154
-
155
- ```
235
+ *回答を編集しました。文字数の関係から一部コードを削除しました。
156
-

1

考えを追記

2025/03/28 08:31

投稿

utm.
utm.

スコア786

test CHANGED
@@ -141,4 +141,16 @@
141
141
  そのほかの構成は変更なし
142
142
 
143
143
 
144
+ ---
145
+ 追記
146
+ そのほかの構成要素に変化なしと書きましたが以下のようなクラスはdata classと定義して状態の比較や取得を簡単に出来るようにした方がいいかも、などと思考していたのが抜けていました。
147
+ まだ、実験できていません。
144
148
 
149
+ ```kt
150
+ class Screen1State(){
151
+ val loginIdState = LoginIdState()
152
+ val loginPasswordTextField = LoginPasswordState()
153
+ }
154
+
155
+ ```
156
+