0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter/Dart開発におけるTips

Posted at

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

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

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


1. 発生した問題の概要

1.1 エラー内容

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

1.2 原因仮説

  1. Dart が「あるキーに最初に設定された型」を元に推論し、後から別の型を入れようとして衝突

    • 具体的には pagelimit に int をセットしたあと、同じ MapList<String> を追加しようとした
    • 解析段階または実行時に「int型用のMapと思ったらリストが来た」という矛盾を検知してエラーが出た可能性
  2. どこかで const / final の扱いが厳密に働いていた

    • Dart の constオブジェクトをイミュータブル化する
    • final は「変数自体を再代入しない」ものの、内部オブジェクトが可変かどうかは定義次第
    • 場合によっては内部を変更できない状態になっているか、または静的解析で「型が合わない」と見なされた
  3. API スキーマやモデル定義の型と違うものを渡していた

    • GraphQL スキーマでは [String!] を期待しているが、Dart 上で [int] と解釈してしまったり、逆に [String] を入れようとしたのに Dart が int だと思っているフィールドに突っ込んだりして衝突

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

2.1 具体的なコード例

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> のため、intList<String> を混在しても大丈夫そうですが、Dart の型推論・実行時チェックが「ここは int 用では?」とみなし、衝突してしまうシナリオが発生しました。

2.2 どのように対処したか

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 の関係

  • 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.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 が来た」と判断してエラーを出したのが原因です。

  • 一見 Map<String, dynamic> は自由に使えるように思えますが、最初に int フィールドを定義した後で List を追加しようとすると衝突が起こるケースがあります。
  • 対処法は「最初にきちんとマップ構造を定義する」「クラス化して型を厳格に管理する」「必要ならキャスト/コンバートで別型に変換する」などが挙げられます。

本レポートの重要ポイント:

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?