
AIのおかげで実装が早くなりましたが、速さの裏で「本当に理解してるのか?」と感じる場面も増えました。 今回は Flutter / Dart の基礎を AI と一緒に学び直した話を書きます。
はじめに
AI がコードを書いてくれる時代になりました。 Copilot、Cursor、Claude Code。もう「書けない」で困ることは減りました。
でも、こんな経験はないでしょうか?
- AIが生成したコードの
finalとconstの使い分けが合ってるか、自信がない case final String nameをレビューで指摘されたけど、なぜそう書くべきか説明できないdynamicとObjectの違いを聞かれて、言葉に詰まる
わかってるつもりだけど、手が止まる。
これは「知識としては知ってるけど、体得されてない」状態です。 AIが書いてくれるからこそ、基礎が曖昧なまま進んでしまいます。
だから今回、AIと対話しながら言語の基礎を1つずつ叩き直すことにしました。 私は Flutter エンジニアなので、Dart にしました。
なぜ AI と勉強するのか?
実際にやってみて感じたメリットが3つあります。
1. 遠慮なく、すぐに深掘りできる
「こんな基礎的なこと聞いていいかな…」がありません。
気になったらその場で聞けて、そのまま深掘りできます。
ずっと曖昧だった const の理解も、その場で確認して修正できました。
2. 自分の開発に合わせた説明をしてくれる
比喩や図解を理解度に合わせて出してくれるだけでなく、今書いているコードの文脈で教えてもらえます。 「あ、だからこう書くのか」と、実務と繋がった瞬間にスッと理解できます。
3. 公式ドキュメントを一緒に読める
公式は正確ですが、そのまま読むと難しいです。AIに噛み砕いてもらいながら読むと理解が速くなります。 さらに、ドキュメントの読み方自体を教えてもらえます。
今回AIと一緒にやったこと
Topic 1: final vs const vs var vs 型宣言
🧐 クイズ:この4つの違いを説明できますか?
final name = 'Middle'; const name = 'Middle'; var name = 'Middle'; String name = 'Middle';
💭 私の最初の理解
final→ 一回だけ入るconst→ 最初から fix、メモリ使わない ← ここが間違いvar→ あまり使わないと思ってた ← 使う時があるString name→ 型を定義した var
✅ 正しい理解
| 再代入 | いつ決まる | |
|---|---|---|
var |
できる | 実行時 |
String name |
できる | 実行時(型明示) |
final |
できない | 実行時でもOK |
const |
できない | コンパイル時のみ |
const の本当の意味:コンパイル時に値が確定することです。
final now = DateTime.now(); // OK。実行時に決まる const now = DateTime.now(); // エラー!コンパイル時に決まらない
const は「メモリを使わない」のではなく、同じ値なら使い回される(正規化される) のでメモリ効率が良い、が正しいです。
const a = 'hello'; const b = 'hello'; identical(a, b); // true。同じオブジェクトを指してる
var は普通に使うものです。「あまり使わない」と思っていましたが、for ループなどで普通に書いていました。動くからOK、で深く考えていませんでした。これがまさに、なんとなく書けてしまう → 理由がわからない、の原因だと思います。
Dart 公式の lint ルール(omit_local_variable_types)でも、ローカル変数で型が推論できる場合は省略を推奨しています。理由は、ローカル変数はスコープが小さいので、型よりも変数名と値に集中してもらう方が読みやすいからです。
// 型明示(型が長いと読みづらい) List<List<Ingredient>> desserts = <List<Ingredient>>[]; // 型省略(変数名と値に集中できる) var desserts = <List<Ingredient>>[];
ただし、後で別の型を代入する場合は型明示が推奨されます。
Widget result = Text('hello');
result = Padding(padding: EdgeInsets.all(8.0), child: result);
ドキュメントだけ読んでも、実際のコードとどう繋がるのかがわかりづらいです。でも AI なら「このルール、うちのコードだとどこに当てはまる?」とすぐ聞けます。ドキュメントと実装を素早く連携できるのが、学習の効率化に繋がっています。
Topic 2: Null Safety と3つの演算子
🧐 クイズ:この3つはそれぞれいつ使う?
name?.length // 1 name!.length // 2 name ?? 'default' // 3
💭 私の最初の理解
?.→ null だったら??でデフォルトが必要 ←?.自体の説明としては不十分!→ 強制、絶対入ってるよ!でもなかったら crash ← 合ってた??→ name が null ならデフォルトを決める ← 合ってた
✅ 正しい理解
| 演算子 | 意味 | null のとき |
|---|---|---|
?. |
安全アクセス | null を返す(crash しない) |
! |
強制アンラップ | crash する |
?? |
フォールバック | 右側の値を使う |
String? name = null; name?.length; // → null(length を呼ばない) name!.length; // → runtime crash 💥 name ?? 'default'; // → 'default'
よく使うコンボ:
final len = name?.length ?? 0; // name が null → null → 0 // name が 'hello' → 5 → 5
Topic 3: Pattern Matching(case final)
🧐 クイズ:この2つの書き方、何が違う?
// 書き方A if (user.name != null) { print(user.name!); } // 書き方B if (user.name case final String name) { print(name); }
💭 私の理解
- A →
user.nameが null じゃないとき - B →
caseでパターンマッチして、final Stringだったらnameに変数に入れる
理解はできていましたが、なぜ B が良いのかは曖昧でした。
💡 なぜ B が良いのか
A の問題:! が必要になる
user.name が getter の場合、2回目の呼び出しで値が変わる可能性があります。
だから Dart コンパイラは「さっき null チェックしたよね」を信用してくれません。
結果、! を書かされます。
B の利点:! が不要
case final String name は「その瞬間の値」をローカル変数に変数に入れる。
!不要(crash リスクゼロ)- 型が
String(String?じゃない) - 変数名を短くできる(
user.name→name)
A (!= null) |
B (case final) |
|
|---|---|---|
! |
必要 | 不要 |
| 安全性 | getter だと危険 | 常に安全 |
| 型 | String? のまま |
String に確定 |
似ていますが、別のパターン構文もあります:
// typed variable pattern(型が String か確認して変数に入れる) if (user.name case final String name) { ... } // null-check pattern(null じゃないか確認して変数に入れる) if (user.name case final name?) { ... }
結果は似てるけど、チェックしていることが違います。final String name は型を見て、final name? は null かどうかを見ています。
最初は省略形だと思っていましたが、社内レビューで「これは別のパターン構文だよ」と指摘してもらって気づきました。
- typed variable pattern - 型をチェックする
- null-check pattern - null をチェックする
実はこの記事を書く過程で、AI も「省略形」と説明していました。AI は学習を加速してくれますが、たまに間違えます。だからこそ公式ドキュメントでの裏取りや、人からのレビューが大事です。
Topic 4: dynamic vs Object
🧐 クイズ:この3つの違いは?
List<String> names = ['a', 'b']; List<dynamic> items = ['a', 1, true]; List<Object> things = ['a', 1, true];
💭 私の最初の理解
List<String>→ String のみ ← 合ってたList<dynamic>→ なんでもいい ← 合ってたけど不十分List<Object>→ Object とは? ← わかってなかった
✅ 正しい理解
dynamic も Object も「なんでも入る」。でも安全性が全然違います。
// dynamic: コンパイラが何もチェックしない dynamic x = 'hello'; x.foo(); // コンパイル通る → runtime crash 💥 x.length; // コンパイル通る → たまたま動く // Object: Dart の全ての型の親 Object x = 'hello'; x.foo(); // コンパイルエラー ← 守ってくれる x.length; // コンパイルエラー ← Object に length はない x.toString(); // OK ← 全ての Object が持つメソッド
| 何が入る | コンパイラ | 危険度 | |
|---|---|---|---|
dynamic |
なんでも | チェックしない | 危ない |
Object |
なんでも | チェックする | 安全 |
🔍 深掘り:なぜ dynamic は JSON でだけ使うのか
JSON の中身は String, int, bool, List, null が混在してる。
Dart のコンパイラはコンパイル時に「このキーの値は何型?」を知りようがありません。
そして dart:convert の jsonDecode が dynamic を返す。
json_serializable / freezed もこれに合わせて Map<String, dynamic> を前提にしています。

dynamic= 「型の世界」と「型がない世界」の国境ゲート
国境の中(アプリ内)では使いません。国境(JSON パース)でだけ使います。
ずっとぼんやりしてた dynamic の使い所が、この一言でスッと理解できました。
まとめ
| 学び直したこと | Before | After |
|---|---|---|
const |
メモリ使わない | コンパイル時確定、同じ値は共有される |
?. と ?? |
セットで考えてた | ?. 単体で null を返す。?? はフォールバック。別の役割 |
case final |
書き方は知ってた | getter の型昇格問題を解決する手段 |
dynamic vs Object |
違いがわからなかった | コンパイラチェックの有無。dynamic は JSON 境界だけ |
AI 時代だからこそ、基礎を固め直す価値がある。
AI が生成したコードを読んで「これで合ってる」と判断するのも、 レビューで「なぜこう書くべきか」を説明するのも、基礎がないとできません。
そして AI は、基礎を固め直す最高のパートナーでした。 遠慮なく聞けて、すぐ深掘りできて、自分に合った説明をしてくれます。
今回はこの4つをやった。でもこれはまだ Dart の基礎の一部でしかありません。 ここから Flutter の描画の仕組み、パッケージの設計など、さらにいろんなものをしっかり重ねていきたいです。
「わかってるつもり」を「本当にわかってる」に変える。 AIがあればいつでもできる。
基礎を固めることで実装の速度が一気に上がる。 速度と加速度こそ、AI時代に必要なもの。 まず言語の基礎から始めてみてはいかがでしょうか!
