teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

4

buttonにも同じ更新を

2025/04/04 05:50

投稿

utm.
utm.

スコア860

answer 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,18 +136,24 @@
132
136
 
133
137
  buttonのベース
134
138
  ```kt
135
- open class ButtonModel {
139
+ open class ButtonKit {
136
140
 
137
- open var action: ButtonAction by mutableStateOf(
141
+ var initialAction: ButtonAction = ButtonAction(onClick = ::onClick)
142
+ set(value) {
143
+ field = value
138
- ButtonAction(
144
+ action = value
139
- onClick = ::onClick)
140
- )
145
+ }
141
- protected set
142
146
 
143
- open var style: ButtonStyle by mutableStateOf(
147
+ var initialStyle: ButtonStyle = ButtonStyle()
144
- ButtonStyle()
148
+ set(value) {
149
+ field = value
150
+ style = value
145
- )
151
+ }
152
+
153
+ var action: ButtonAction by mutableStateOf(initialAction)
146
154
  protected set
155
+ var style: ButtonStyle by mutableStateOf(initialStyle)
156
+ protected set
147
157
 
148
158
  open fun onClick() {
149
159
  println("on click")
@@ -208,17 +218,16 @@
208
218
 
209
219
  ```
210
220
 
211
- loginButtonModel
221
+ LoginButtonKit
212
222
  ```kt
213
- class LoginButtonModel(
223
+ class LoginButtonKit(
214
- private val onDelegate: () -> Unit
224
+ onClick: () -> Unit
215
- ): ButtonModel() {
225
+ ): ButtonKit() {
216
- override var action: ButtonAction by mutableStateOf(
217
- ButtonAction(
218
- onClick = onDelegate)
219
- )
220
- protected set
221
226
 
227
+ init {
228
+ initialAction = ButtonAction(onClick = onClick)
229
+ }
230
+
222
231
  }
223
232
  ```
224
233
 

3

コードを一部修正し更新

2025/04/04 05:37

投稿

utm.
utm.

スコア860

answer CHANGED
@@ -23,6 +23,7 @@
23
23
  ベースとなるクラスでインスタンスを持つ。
24
24
  また、関数ではベースとなるクラスを引数に取ることで、紐づけを行う
25
25
  // 構造体 + 関数のやり方みたい。やっぱりkotlinはこういう結論に至るのか
26
+ *この方法はデータモデルとバインド方式というみたいです。(おそらくなので出典ありません)
26
27
 
27
28
  表示関連
28
29
  (オブジェクトの型により表示状態を持つことで、LoginIdTextFieldなどのUI要素は不要になった)
@@ -33,24 +34,43 @@
33
34
 
34
35
  textFieldのベース
35
36
  ```kt
36
- open class TextFieldModel {
37
+ open class TextFieldKit() {
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
+ }
37
48
 
38
- var state: TextFieldState by mutableStateOf(TextFieldState(""))
39
- protected set
40
- open var action: TextFieldAction by mutableStateOf(
49
+ var initialStyle: TextFieldStyle = TextFieldStyle()
50
+ set(value) {
41
- TextFieldAction(
51
+ field = value
42
- onValueChange = ::updateInputValue)
43
- )
44
- protected set
52
+ style = value
53
+ }
45
54
 
55
+ var state: TextFieldState by mutableStateOf (initialState)
56
+ var action: TextFieldAction by mutableStateOf(initialAction)
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)
52
62
  }
63
+ fun clearValue(){
64
+ state = initialState
65
+ }
66
+ fun clearAction(){
67
+ action = initialAction
68
+ }
53
69
 
70
+ fun clearStyle(){
71
+ style = initialStyle
72
+ }
73
+
54
74
  }
55
75
  data class TextFieldStyle(
56
76
  val modifier: Modifier = Modifier,
@@ -71,10 +91,8 @@
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
95
 
76
- }
77
-
78
96
  data class TextFieldState(
79
97
  val inputValue: String ,
80
98
  )
@@ -164,7 +182,32 @@
164
182
  )
165
183
  }
166
184
  ```
185
+ 入力要素の具象クラス
186
+ ```kt
187
+ // バリデーションが必要であれば継承とインタフェースを用いて拡張してください
188
+ class LoginIdStateKit : TextFieldKit() {
167
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
+ ```
210
+
168
211
  loginButtonModel
169
212
  ```kt
170
213
  class LoginButtonModel(
@@ -179,30 +222,39 @@
179
222
  }
180
223
  ```
181
224
 
182
- Loginに際するビジネスロジックの集約(これはサービスなのか?ドメインなのか?ViewModelなのか?アプリケーション的にはViewModelのスコープがないとスレッド的に問題ありそうだし、ドメインが状態を持っていいのかもわからない)
225
+ Loginに際するビジネスロジックの集約
226
+ . stateの状態変化をすべて集約
227
+ . actionはviewModel層に預ける
183
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.

スコア860

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

1

考えを追記

2025/03/28 08:31

投稿

utm.
utm.

スコア860

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