# Parky Flutter — コード生成移行 (freezed + riverpod_generator + json_serializable)

監査 P1 (C2 / C3 / 2026-04-26 / Phase 22-23): Mobile DTO を `@freezed` の sealed class
+ Provider を `@riverpod` の generator 駆動に段階移行する。

> **状態:** 配線完了 (pubspec / build.yaml / analysis_options)。実 migration は段階的 PR で。

## 全体像

| 領域 | 旧 | 新 (移行後) |
|---|---|---|
| **DTO** | 手書き class + `factory fromJson` + `toJson` + 手書き `==` / `hashCode` / `copyWith` | `@freezed` sealed class + `@JsonSerializable()` で全自動生成 |
| **Provider** | `final fooProvider = FutureProvider<...>((ref) async {...})` 直書き | `@riverpod Future<...> foo(FooRef ref) async {...}` で自動生成 |

## 環境セットアップ

```bash
cd mobileapp/prototype/flutter
flutter pub get
# 全 builder を 1 度走らせる
dart run build_runner build --delete-conflicting-outputs
# watch mode で開発中
dart run build_runner watch --delete-conflicting-outputs
# riverpod_lint を IDE で動かす
dart run custom_lint
```

`*.freezed.dart` / `*.g.dart` は **Git にコミット** する運用 (CI で `--exit-code` の
git diff で stale 検出する案あり)。

## freezed migration パターン

### 旧 (手書き class)

```dart
// lib/features/permissions/data/permissions_view_dto.dart
class PermissionsViewDto {
  PermissionsViewDto({
    required this.locationGranted,
    required this.notificationGranted,
    this.lastCheckedAt,
  });

  factory PermissionsViewDto.fromJson(Map<String, dynamic> json) {
    return PermissionsViewDto(
      locationGranted: json['location_granted'] as bool,
      notificationGranted: json['notification_granted'] as bool,
      lastCheckedAt: json['last_checked_at'] == null
          ? null
          : DateTime.parse(json['last_checked_at'] as String),
    );
  }

  final bool locationGranted;
  final bool notificationGranted;
  final DateTime? lastCheckedAt;

  Map<String, dynamic> toJson() => {
        'location_granted': locationGranted,
        'notification_granted': notificationGranted,
        if (lastCheckedAt != null) 'last_checked_at': lastCheckedAt!.toIso8601String(),
      };

  PermissionsViewDto copyWith({...}) { ... }

  @override
  bool operator ==(Object other) { ... }

  @override
  int get hashCode => Object.hash(...);
}
```

### 新 (freezed)

```dart
// lib/features/permissions/data/permissions_view_dto.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'permissions_view_dto.freezed.dart';
part 'permissions_view_dto.g.dart';

@freezed
class PermissionsViewDto with _$PermissionsViewDto {
  const factory PermissionsViewDto({
    required bool locationGranted,
    required bool notificationGranted,
    DateTime? lastCheckedAt,
  }) = _PermissionsViewDto;

  factory PermissionsViewDto.fromJson(Map<String, dynamic> json) =>
      _$PermissionsViewDtoFromJson(json);
}
```

build.yaml の `field_rename: snake` で `locationGranted` ↔ `"location_granted"` が
自動変換される。

### sealed class (union type) パターン

SDUI の View / Error / Loading 状態を表す sealed class:

```dart
@freezed
sealed class ViewState<T> with _$ViewState<T> {
  const factory ViewState.loading() = _ViewLoading<T>;
  const factory ViewState.data(T value) = _ViewData<T>;
  const factory ViewState.error(Object error, StackTrace stack) = _ViewError<T>;
}

// 使用側で型安全な pattern matching
final widget = switch (state) {
  _ViewLoading() => const Skeleton(),
  _ViewData(:final value) => ResultView(value),
  _ViewError(:final error) => ErrorView(error),
};
```

### 既存 Hand-rolled DTO の移行手順

1. 移行対象を選ぶ (data layer の DTO 1 つ)
2. クラスを `@freezed` 形式に書き換え
3. `part '<file>.freezed.dart';` `part '<file>.g.dart';` を import 直下に追加
4. `dart run build_runner build --delete-conflicting-outputs` を実行
5. 生成された `.freezed.dart` / `.g.dart` を Git に追加
6. 呼出し側 (constructor / copyWith / fromJson / toJson) で API は同じだが、
   `==` / `hashCode` が値ベースになる点だけ確認

### 移行優先順位

1. **小さい DTO** (フィールド 5 個以下) — 簡単、慣れの足場
2. **API レスポンス DTO** (lib/features/*/data/*_dto.dart) — fromJson / toJson の自動化が大きい
3. **SDUI ViewEnvelope の Domain 型** — sealed class で状態遷移を型安全に
4. **Riverpod state class** — Notifier の state も freezed で

## riverpod_generator migration パターン

### 旧 (FutureProvider 直書き)

```dart
// lib/features/profile/data/profile_repository.dart
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
  return ProfileRepository(ref.watch(bffClientProvider));
});

final profileViewProvider = FutureProvider<ProfileView>((ref) async {
  final repo = ref.watch(profileRepositoryProvider);
  return repo.fetchView();
});
```

### 新 (@riverpod)

```dart
// lib/features/profile/data/profile_providers.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'profile_providers.g.dart';

@riverpod
ProfileRepository profileRepository(ProfileRepositoryRef ref) {
  return ProfileRepository(ref.watch(bffClientProvider));
}

@riverpod
Future<ProfileView> profileView(ProfileViewRef ref) async {
  final repo = ref.watch(profileRepositoryProvider);
  return repo.fetchView();
}
```

`profileViewProvider` / `profileRepositoryProvider` の名前は generator が
`<関数名>Provider` で生成する。`Ref` 型は `<関数名>Ref` 型で。

### Notifier (state あり) パターン

```dart
@riverpod
class CounterNotifier extends _$CounterNotifier {
  @override
  int build() => 0;

  void increment() => state = state + 1;
  void decrement() => state = state - 1;
}
// 呼出: ref.watch(counterNotifierProvider) / ref.read(counterNotifierProvider.notifier).increment()
```

### family / autoDispose

annotation で型安全に表現:

```dart
@riverpod
Future<ParkingLot> parkingLot(ParkingLotRef ref, {required String lotCode}) async {
  return ref.watch(bffClientProvider).fetchLot(lotCode);
}
// 呼出: ref.watch(parkingLotProvider(lotCode: 'XYZ'))
```

`@Riverpod(keepAlive: true)` で disposable を抑止可能 (デフォルト autoDispose)。

### 既存 Provider の段階移行

1. feature ディレクトリ単位で 1 つずつ移行
2. data 層から先に (Repository → FetchView の順)
3. 移行中は新旧を並走させる (provider 名を変えなければ呼出し側変更不要)
4. 全 feature 完了後に旧 Provider 直書きを削除

### riverpod_lint の strict 化

migration が進んだら analysis_options.yaml で:

```yaml
custom_lint:
  rules:
    - provider_dependencies: error  # warning → error に昇格
    - avoid_public_notifier_properties: error
```

## CI 配線 (将来)

GitHub Actions に build_runner の drift check を追加 (api/openapi.json の drift と同じ運用):

```yaml
- name: Generate Flutter codegen
  run: dart run build_runner build --delete-conflicting-outputs

- name: Detect generated drift
  run: |
    if ! git diff --exit-code lib/; then
      echo "::error::Flutter codegen output is stale. Run build_runner and commit."
      exit 1
    fi
```

## トラブルシューティング

### `*.freezed.dart not found`

→ `dart run build_runner build` 未実行。`--delete-conflicting-outputs` 付きで再実行。

### `Conflicting outputs` エラー

→ 既存の手書き `.g.dart` (旧 openapi_generator 残骸) と衝突。
→ `dart run build_runner build --delete-conflicting-outputs` で強制上書き。

### analyzer 衝突 (再発)

→ riverpod_generator + freezed は analyzer ^7 対応済み (今回の構成)。
→ openapi_generator (gibahjoe) は退避済み (Docker CLI に移行済み)。
→ 新規 dev_dep を入れる際は `flutter pub deps` で analyzer 制約を確認。

## 関連

- [parky/mobileapp/prototype/flutter/pubspec.yaml](../../mobileapp/prototype/flutter/pubspec.yaml)
- [parky/mobileapp/prototype/flutter/build.yaml](../../mobileapp/prototype/flutter/build.yaml)
- [parky/mobileapp/prototype/flutter/analysis_options.yaml](../../mobileapp/prototype/flutter/analysis_options.yaml)
- [parky/docs/mobile-app/openapi-codegen.md](./openapi-codegen.md) — Docker CLI 経由の OpenAPI 生成
- [memory: project_parky_riverpod_3_migration_2026_04_26](../../.memory/project_parky_riverpod_3_migration_2026_04_26.md)
