@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,199 +0,0 @@
1
- ---
2
- docmeta:
3
- id: riverpod-pattern
4
- title: Riverpod 상태관리 패턴 (Page + VM)
5
- type: output
6
- createdAt: 2026-04-09T00:00:00Z
7
- updatedAt: 2026-04-09T00:00:00Z
8
- source:
9
- producer: agent
10
- skillId: harness-generator-frontend-flutter
11
- inputs:
12
- - documentId: clue-fe-flutter-riverpod
13
- uri: ../../../../../moon_web/clue-fe-flutter.skill
14
- relation: output-from
15
- sections:
16
- - sourceRange:
17
- startLine: 1
18
- endLine: 149
19
- targetRange:
20
- startLine: 32
21
- endLine: 180
22
- tags:
23
- - flutter
24
- - riverpod
25
- - state-management
26
- - notifier-provider
27
- ---
28
-
29
- # Riverpod 상태관리 패턴 (Page + VM)
30
-
31
- ## 기본 구조: Page + VM 쌍
32
-
33
- 모든 화면은 `xxx_page.dart` + `xxx_page_vm.dart` 쌍으로 구성.
34
- 이는 테스트 가능성과 UI/로직 분리를 위한 강제 규약.
35
-
36
- ### VM (ViewModel)
37
-
38
- ```dart
39
- // example_page_vm.dart
40
- import 'package:equatable/equatable.dart';
41
- import 'package:flutter_riverpod/flutter_riverpod.dart';
42
- import 'package:integrated_data_layer/data_layer.dart';
43
-
44
- final pExampleProvider =
45
- NotifierProvider<ExampleNotifier, ExampleState>(ExampleNotifier.new);
46
-
47
- class ExampleNotifier extends Notifier<ExampleState> {
48
- @override
49
- ExampleState build() {
50
- return const ExampleState();
51
- }
52
-
53
- Future<void> loadData() async {
54
- state = state.copyWith(isLoading: true);
55
- try {
56
- final result = await ref.read(dataLayer).ac.getExample(exampleId: 1);
57
- if (result.code == 200) {
58
- state = state.copyWith(
59
- isLoading: false,
60
- data: result.data,
61
- );
62
- } else {
63
- state = state.copyWith(
64
- isLoading: false,
65
- error: 'code=${result.code}',
66
- );
67
- }
68
- } catch (e) {
69
- state = state.copyWith(isLoading: false, error: e.toString());
70
- }
71
- }
72
- }
73
-
74
- class ExampleState extends Equatable {
75
- final bool isLoading;
76
- final dynamic data;
77
- final String? error;
78
-
79
- const ExampleState({
80
- this.isLoading = false,
81
- this.data,
82
- this.error,
83
- });
84
-
85
- ExampleState copyWith({
86
- bool? isLoading,
87
- dynamic data,
88
- String? error,
89
- }) {
90
- return ExampleState(
91
- isLoading: isLoading ?? this.isLoading,
92
- data: data ?? this.data,
93
- error: error ?? this.error,
94
- );
95
- }
96
-
97
- @override
98
- List<Object?> get props => [isLoading, data, error];
99
- }
100
- ```
101
-
102
- ### Page
103
-
104
- ```dart
105
- // example_page.dart
106
- import 'package:flutter/material.dart';
107
- import 'package:flutter_riverpod/flutter_riverpod.dart';
108
- import 'example_page_vm.dart';
109
-
110
- class ExamplePage extends ConsumerStatefulWidget {
111
- const ExamplePage({super.key});
112
-
113
- @override
114
- ConsumerState<ExamplePage> createState() => _ExamplePageState();
115
- }
116
-
117
- class _ExamplePageState extends ConsumerState<ExamplePage> {
118
- @override
119
- void initState() {
120
- super.initState();
121
- // 초기 데이터 로드는 addPostFrameCallback 으로
122
- WidgetsBinding.instance.addPostFrameCallback((_) {
123
- ref.read(pExampleProvider.notifier).loadData();
124
- });
125
- }
126
-
127
- @override
128
- Widget build(BuildContext context) {
129
- final state = ref.watch(pExampleProvider); // watch로 상태 구독
130
-
131
- if (state.isLoading) {
132
- return const Center(child: CircularProgressIndicator());
133
- }
134
- if (state.error != null) {
135
- return Center(child: Text(state.error!));
136
- }
137
- // ... 정상 상태 렌더링
138
- return const SizedBox.shrink();
139
- }
140
- }
141
- ```
142
-
143
- ## Family 패턴 (다중 자식 컴포넌트)
144
-
145
- 리스트 아이템처럼 동일 타입 인스턴스가 여러 개 필요할 때:
146
-
147
- ```dart
148
- // relay_tile_vm.dart
149
- final pRelayItemProvider = NotifierProvider.family<
150
- RelayItemNotifier,
151
- RelayItemState,
152
- ({int ioId, String topic})>(
153
- () => RelayItemNotifier(),
154
- );
155
-
156
- class RelayItemNotifier
157
- extends FamilyNotifier<RelayItemState, ({int ioId, String topic})> {
158
- @override
159
- RelayItemState build(({int ioId, String topic}) arg) {
160
- ref.onDispose(() {
161
- // 정리 로직 (스트림 구독 해제 등)
162
- });
163
- return RelayItemState(ioId: arg.ioId, topic: arg.topic);
164
- }
165
-
166
- Future<void> doAction() async {
167
- final result = await ref.read(dataLayer).ac.someMethod(id: state.ioId);
168
- state = state.copyWith(/* ... */);
169
- }
170
- }
171
- ```
172
-
173
- 사용:
174
- ```dart
175
- // Page/Widget에서
176
- final itemState = ref.watch(
177
- pRelayItemProvider((ioId: item.ioId, topic: item.topic)),
178
- );
179
- ```
180
-
181
- ## 핵심 규칙
182
-
183
- | 규칙 | 설명 |
184
- |------|------|
185
- | `ref.watch` | UI rebuild이 필요한 곳 (build 메서드 내) |
186
- | `ref.read` | 일회성 호출 (이벤트 핸들러, initState 콜백) |
187
- | `dataLayer` 접근 | VM 내에서 `ref.read(dataLayer).repository.method()` |
188
- | State 불변성 | Equatable 상속 + copyWith 패턴 |
189
- | Provider 네이밍 | `p` 접두사 + PascalCase + Provider (예: `pHomePageProvider`) |
190
- | 초기 로드 | `addPostFrameCallback` 으로 첫 프레임 이후 호출 |
191
- | 에러/로딩 | State에 `isLoading`, `error` 필수 포함 (UI 3가지 상태 처리) |
192
-
193
- ## 금지
194
-
195
- - `bridges/` 사용 금지 — BLOC 기반 레거시
196
- - StatefulWidget 내 직접 API 호출 — 반드시 VM 경유
197
- - `setState` 로 API 응답 상태 관리 — Riverpod 상태로 대체
198
- - `ref.read`를 build 메서드 내에서 사용 (→ `ref.watch`)
199
- - VM 내 UI 의존 코드 (Navigator, ScaffoldMessenger 등) — UI 콜백으로 분리