Flutter／Dart開発におけるTips

現在makeshopのAPIを使ったモバイルアプリ開発をしています。単純なところですがバグがあったので対処していたのですが、どうもChatGPT o1にしても、Claude3.7 Sonnetにしても、コードは問題ないと言ってきます。可能性を指摘してもだめでした。

最終的に解決したのは、Flutterのスペシャリストに聞いたらたしかに、できるはずなのにですが、回避策を教えてもらってあっさり解決。解決したのをChatGPT o1に教えてあげても「通常起こりにくいです」と正しさをアピールされたけど、まあいいかｗ

ということで、発生した内容をまとめました。ご参考までに。

---

## 1. 発生した問題の概要

### 1.1 エラー内容

- Dart コード実行時に以下のようなエラーが発生  
  ```
  type 'List<String>' is not a subtype of type 'int' of 'value'
  ```
  もしくは実行時に「`Map` 内部へ型が異なるデータを入れようとしている」といった型不一致エラー。

### 1.2 原因仮説

1. **Dart が「あるキーに最初に設定された型」を元に推論し、後から別の型を入れようとして衝突**  
   - 具体的には `page` や `limit` に int をセットしたあと、同じ `Map` に `List<String>` を追加しようとした  
   - 解析段階または実行時に「int型用のMapと思ったらリストが来た」という矛盾を検知してエラーが出た可能性

2. **どこかで `const` / `final` の扱いが厳密に働いていた**  
   - Dart の `const` は**オブジェクトをイミュータブル化**する  
   - `final` は「変数自体を再代入しない」ものの、内部オブジェクトが可変かどうかは定義次第  
   - 場合によっては内部を変更できない状態になっているか、または静的解析で「型が合わない」と見なされた

3. **API スキーマやモデル定義の型と違うものを渡していた**  
   - GraphQL スキーマでは `[String!]` を期待しているが、Dart 上で `[int]` と解釈してしまったり、逆に `[String]` を入れようとしたのに Dart が int だと思っているフィールドに突っ込んだりして衝突

---

## 2. 実際に起きた状況と例

### 2.1 具体的なコード例

```dart
Future<List<Map<String, dynamic>>> fetchAllProductQuantities({List<String>? customCodes}) async {
  final int limit = 1000;
  int page = 1;

  // 初期定義
  final Map<String, dynamic> variables = {
    "input": {
      "page": page,
      "limit": limit,  // ここは int
    }
  };

  // 後から別のキーを追加
  if (customCodes != null && customCodes.isNotEmpty) {
    variables["input"]["customCodes"] = customCodes;  // ここは List<String>
  }

  ...
  // API呼び出し
}
```

このような形が一見正しそうに見えるものの、一部環境で  
```
type 'List<String>' is not a subtype of type 'int' of 'value'
```
というエラーが起きるケースがありました。  
表面的には `Map<String, dynamic>` のため、`int` と `List<String>` を混在しても大丈夫そうですが、**Dart の型推論・実行時チェック**が「ここは `int` 用では？」とみなし、衝突してしまうシナリオが発生しました。

### 2.2 どのように対処したか

```dart
final Map<String, dynamic> input = {
  "page": 1,       // int
  "limit": 10,     // int
};

final Map<String, dynamic> variables = {
  "input": input,  // input自体をマップとして入れる
};

// 後から customCodes を追加
if (customCodes != null && customCodes.isNotEmpty) {
  variables["input"]["customCodes"] = customCodes; // List<String>
}
```

このように**「最初から別変数 (`input`) で int フィールドをまとめつつ、再代入箇所を整理」**したところ、型不一致エラーが解消しました。  
推測される理由としては、**1つのマップに対して Dart が推論した「型情報の矛盾」** が回避されたからです。

---

## 3. なぜこうした型の衝突が起こったのか

### 3.1 Dart の型安全と Map<String, dynamic> の関係

- `Map<String, dynamic>` は一見「何でも入る」ように思えますが、Dart/Flutter では  
  **static analysis**（コンパイル時の型検査）が入り、必要に応じて**実行時にも型チェック**を行います。  
- 特に「同じキーに対して**異なる型**を入れた時」や「マップ全体を int だけのものと推定してしまった時」に矛盾を検知すると、実行時エラーが出る場合があります。

### 3.2 `final` / `const` の影響

- `final` は「変数の再代入」を禁止しますが、オブジェクトの内部書き換え自体はできるかもしれません。  
- しかし、もし `const` を使っていたり、あるいはコード生成などで**実行時にイミュータブルなマップ**が作られていると、「内部への値追加」ができなくなる → 型不一致や書き換え不可エラーにつながる。

### 3.3 API 定義との不整合

- もし API が「`customCodes` は `[Int]`」と定義していたら、Dart 側で `[String]` を渡すと衝突する。逆も然り。  
- スキーマドキュメントに `[String!]` と書いてあっても、Dart 側の自動生成コードで間違って `[int]` になっていると実行時にエラーが出る。

---

## 4. ロジックツリーで整理する

```
型不一致エラー: type 'List<String>' is not a subtype of type 'int'
├─ A. 実行時に、マップ全体の型推論が「int」とみなされている
│   ├─ 最初に page, limit(int) を入れているため
│   └─ 後から別型(List<String>) を追加しようとして衝突
├─ B. Dart の static analysis/実行時チェックの影響
│   ├─ IDE が constとして扱っている可能性
│   └─ code generation等で intを前提としているクラスがある
└─ C. サーバー/API定義とのギャップ
    └─ API側に送る際に intが期待される/実際にはList<String>を送ってエラー
```

このように複数の要因があり得ますが、**現在のケースでは A が最も該当**しやすいと思われます。

---

## 5. 今回の解決策と再発防止

### 5.1 解決策

1. **「最初に定義したマップに全部まとめておき、最小限の再代入にする」**  
   - `variables["input"] = {"page": ..., "limit": ..., "customCodes": ...}` のように最初からキーを確定  
   - あるいは「page, limit を持つ `input` マップを作成し、それを `variables["input"]` にセット。後からキーを追加しない or 追加するのを別マップにする」  
2. **「もし API 側の型が int / List<int> なら、型変換する」**  
   - 文字列が入っていれば `int.parse()` するなど

### 5.2 再発防止策

1. **型を明確にするためにクラスを定義**  
   - `class InputRequest { int page; int limit; List<String>? customCodes; ... }` のように設計し、JSON serialize/deserialize する  
   - コード生成ツール(例えば `json_serializable`)を使えば、型不一致があればコンパイル時点で検出しやすい  
2. **const, final の扱いやイミュータブル化を確認**  
   - UI などで const を多用しすぎると「再代入不可」や「内部書き換え不可」によるエラーが発生しがち  
3. **API スキーマとの整合**  
   - GraphQL や REST API スキーマが `[String]` か `[Int]` か、定義を事前に確認し、クライアントとサーバーで相違がないかテスト

---

## 6. まとめ

今回のトラブルは、**Dart が内部マップの型を矛盾なく扱えず、実行時に「int の位置に List<String> が来た」と判断してエラーを出した**のが原因です。  
- 一見 `Map<String, dynamic>` は自由に使えるように思えますが、最初に int フィールドを定義した後で List<String> を追加しようとすると衝突が起こるケースがあります。  
- **対処法**は「最初にきちんとマップ構造を定義する」「クラス化して型を厳格に管理する」「必要ならキャスト/コンバートで別型に変換する」などが挙げられます。

**本レポートの重要ポイント**:
1. **型推論と実行時チェック**の矛盾が起きると、`Map<String, dynamic>` でもエラーが出る  
2. **初期定義を明確化**し、あとから追加するキーを減らすことで回避できる  
3. **API スキーマ**や **自動生成コード**との整合性チェックが重要

これらを踏まえ、 **「型定義の一貫性」** を意識した実装と検証が、今後同様の問題を防ぐためには欠かせません。