CheatSheet
日本語 icon日本語English iconEnglish
チートシートとはカンニングペーパーのことです。それが転じて、本来覚えることをまとめておいたものです。
要点をすぐに参照できるようにまとめてみました。

Flutter / Dart

エンジニアのためのWebチートシート

Flutter は Google が開発したクロスプラットフォームUIフレームワークで、Dart 言語を使用します。 1つのコードベースで iOS、Android、Web、デスクトップアプリを開発できます。 Dart の基本構文、Null Safety、クラス、非同期処理、Widget、レイアウト、ナビゲーション、状態管理などをチートシートにまとめました。

Dart 基本構文

変数と定数

  • var, final, const, 型推論, Null Safetyの基本です。

    var name = 'Dart';          // 型推論
    String lang = 'Flutter';    // 明示的な型
    int age = 10;
    double pi = 3.14;
    bool isActive = true;
    
    final createdAt = DateTime.now(); // 実行時定数
    const maxRetry = 3;               // コンパイル時定数
    var greeting = 'Hello, $name!';
    var calc = '合計: ${1 + 2}';
    var multi = '''複数行文字列''';
    var raw = r'改行しない\n';
    
    'hello'.toUpperCase();        // 'HELLO'
    '  hello  '.trim();           // 'hello'
    'hello world'.split(' ');     // ['hello', 'world']
    'hello'.contains('ell');      // true
    'hello'.substring(1, 3);     // 'el'

Null Safety & 型操作

  • Null許容型、null合体演算子、型チェック・キャストです。

    // Null Safety (Dart 3.x)
    int a = 1;           // non-nullable
    int? b;              // nullable (デフォルト null)
    int c = b ?? 0;      // null合体演算子
    b ??= 5;             // bがnullの場合のみ代入
    
    // nullアクセス防止
    String? s = null;
    int len = s?.length ?? 0;
    
    // null assertion (確実にnullでない場合)
    int value = b!;
    
    // 型チェック・キャスト
    if (value is String) { /* ... */ }
    if (value is! int) { /* ... */ }
    var str = value as String;  // キャスト

基本型一覧

説明
int整数(64bit)
double倍精度浮動小数点
numint, double の親型
StringUTF-16文字列
booltrue / false
List<T>順序付きコレクション(配列)
Set<T>重複なしコレクション
Map<K,V>キーバリュー
dynamic動的型(型チェック無し)
Object全オブジェクトの基底型(null以外)
void値を返さない
Never正常に完了しない関数の戻り値型
Recordレコード型(Dart 3)

制御フロー & 関数

制御フロー & パターンマッチ

  • if/else, switch式, if-case, for/for-in です。Dart 3のパターンマッチを含みます。

    var result = x > 0 ? 'positive' : 'negative';
    
    var message = switch (status) {
      200 => 'OK',
      404 => 'Not Found',
      500 => 'Server Error',
      _ => 'Unknown',
    };
    switch (value) {
      case int n when n > 0:
        print('正の整数: $n');
      case String s:
        print('文字列: $s');
      case (int x, int y):
        print('座標: ($x, $y)');
      default:
        print('その他');
    }
    
    if (json case {'name': String n, 'age': int a}) {
      print('$n ($a)');
    }
    for (var i = 0; i < 5; i++) { /* ... */ }
    for (var item in list) { /* ... */ }

関数定義

  • アロー構文, 名前付き/位置パラメータ, クロージャ, typedefです。

    int add(int a, int b) => a + b;
    
    String greet(String name,
        {required int age, String? title}) {
      return '${title ?? "Mr."} $name ($age)';
    }
    greet('Taro', age: 25);
    
    String say(String msg,
        [String? from, String device = 'phone']) {
      return '$msg from ${from ?? "unknown"}';
    }
    var double = (int n) => n * 2;
    [1, 2, 3].map((e) => e * 2).toList();
    
    typedef IntOp = int Function(int, int);
    IntOp multiply = (a, b) => a * b;

Records(Dart 3)

  • 軽量なデータ構造と分割代入です。

    // Records (Dart 3.x)
    (String, int) userInfo() => ('Alice', 30);
    var (name, age) = userInfo(); // 分割代入
    
    // 名前付きフィールド
    ({String name, int age}) getUser() =>
        (name: 'Bob', age: 25);
    final (:name, :age) = getUser();
    
    // パターンマッチと組み合わせ
    var json = {'name': 'Alice', 'age': 30};
    if (json case {'name': String n, 'age': int a}) {
      print('$n is $a years old');
    }

クラス & OOP

クラス定義 & コンストラクタ

  • コンストラクタ省略記法, ファクトリ, ゲッター/セッターです。

    class User {
      final String name;
      int _age; // _ でプライベート
    
      User(this.name, this._age);
      User.guest() : name = 'Guest', _age = 0;
    
      factory User.fromJson(Map<String, dynamic> json) {
        return User(json['name'] as String,
            json['age'] as int);
      }
      int get age => _age;
      set age(int value) {
        if (value >= 0) _age = value;
      }
    
      String greet() => 'Hi, I\'m $name';
      @override
      String toString() => 'User($name, $_age)';
    }

継承 & Mixin

  • extends, implements, with(Mixin), Extension Methodsです。

    class Admin extends User {
      final String role;
      Admin(super.name, super.age, this.role);
      @override
      String greet() => '${super.greet()} [Admin]';
    }
    
    abstract class Shape { double area(); }
    
    class Circle implements Shape {
      final double radius;
      Circle(this.radius);
      @override
      double area() => 3.14 * radius * radius;
    }
    mixin Flyable {
      void fly() => print('Flying!');
    }
    class Duck extends Animal with Flyable {}
    
    extension StringX on String {
      String get reversed =>
          split('').reversed.join('');
      bool get isEmail => contains('@');
    }
    print('hello'.reversed); // 'olleh'

クラス修飾子(Dart 3)& Enum

  • sealed, final, base, interface, Enhanced Enumです。

    sealed class Result {}    // 網羅チェック可
    final class Config {}     // extend/implement禁止
    base class BaseService {} // implement禁止
    interface class Printable {} // extend禁止
    mixin class Validator {}  // mixin+class両用
    enum Priority implements Comparable<Priority> {
      low(1, 'Low'),
      medium(2, 'Medium'),
      high(3, 'High');
    
      final int value;
      final String label;
      const Priority(this.value, this.label);
      @override
      int compareTo(Priority other) =>
          value - other.value;
    }

コレクション

List(配列)

  • 配列の操作、スプレッド演算子、コレクションif/forです。

    var list = [1, 2, 3];
    var typed = <String>['a', 'b', 'c'];
    
    list.add(4);       list.addAll([5, 6]);
    list.remove(1);    list.removeAt(0);
    list.insert(0, 10);
    list.contains(3);  list.indexOf(2);
    list.sublist(1, 3);
    list.sort();       list.reversed.toList();
    list.first;        list.last;
    var extra = [0, ...list];
    var safe = [0, ...?nullableList];
    
    var items = [
      'always',
      if (isLoggedIn) 'profile',
      for (var i in list) 'item_$i',
    ];

Map & Set

  • キーバリュー、重複なしコレクション、主要メソッドです。

    var set = {1, 2, 3};
    set.add(4);
    set.contains(2);          // true
    set.union({3, 4, 5});     // {1,2,3,4,5}
    set.intersection({2, 3}); // {2, 3}
    set.difference({2, 3});   // {1}
    var map = {'name': 'Dart', 'version': 3};
    map['name'];             // 'Dart'
    map['newKey'] = 'value';
    map.containsKey('name'); // true
    map.remove('version');
    map.keys.toList();
    map.values.toList();
    map.putIfAbsent('key', () => 'default');
    map.update('name', (v) => v.toUpperCase());

コレクション操作

  • メソッドチェーン、ジェネリクスの使い方です。

    var nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    nums.where((n) => n.isEven).toList(); // [2,4,6,8,10]
    nums.map((n) => n * n).toList();
    nums.fold(0, (sum, n) => sum + n);    // 55
    nums.reduce((a, b) => a + b);        // 55
    nums.any((n) => n > 5);              // true
    nums.every((n) => n > 0);            // true
    nums.firstWhere((n) => n > 3);       // 4
    nums.take(3).toList();                // [1,2,3]
    nums.skip(7).toList();                // [8,9,10]
    class Box<T> {
      final T value;
      Box(this.value);
    }
    var intBox = Box<int>(42);
    
    class NumBox<T extends num> {
      final T value;
      NumBox(this.value);
    }
    T first<T>(List<T> items) => items[0];

非同期プログラミング

Future & async/await

  • 非同期処理の基本とFutureメソッドです。

    Future<String> fetchData() async {
      await Future.delayed(Duration(seconds: 2));
      return 'Data loaded';
    }
    
    void main() async {
      var data = await fetchData();
      print(data);
    }
    Future.value('instant');
    Future.error('oops');
    Future.delayed(Duration(seconds: 1), () => 'delayed');
    
    var results = await Future.wait([
      fetchUser(), fetchPosts(), fetchComments(),
    ]);
    
    var data = await fetchData()
        .timeout(Duration(seconds: 5));

Stream

  • async*, yield, StreamControllerによるデータストリームです。

    Stream<int> countStream(int max) async* {
      for (var i = 0; i < max; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i;
      }
    }
    
    await for (var count in countStream(5)) {
      print(count);
    }
    stream
        .where((e) => e > 2)
        .map((e) => e * 10)
        .listen((data) => print(data));
    
    var ctrl = StreamController<String>();
    ctrl.sink.add('data1');
    ctrl.stream.listen((d) => print(d));
    ctrl.close();
    
    var result = await Isolate.run(() {
      return heavyComputation();
    });

エラーハンドリング

  • try/catch/finally, on句, thenチェーンです。

    try {
      var result = await riskyOperation();
    } on FormatException catch (e) {
      print('Format error: $e');
    } on HttpException catch (e, stackTrace) {
      print('HTTP error: $e');
    } catch (e) {
      print('Unknown: $e');
    } finally {
      cleanup();
    }
    fetchData()
        .then((data) => processData(data))
        .then((result) => saveResult(result))
        .catchError((e) => handleError(e))
        .whenComplete(() => cleanup());

Widget基礎

最小アプリ & StatelessWidget

  • MaterialApp, Scaffold, StatelessWidgetの基本構造です。

    import 'package:flutter/material.dart';
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'My App',
          theme: ThemeData(useMaterial3: true),
          home: const HomePage(),
        );
      }
    }
    class GreetingCard extends StatelessWidget {
      final String name;
      const GreetingCard({
        super.key, required this.name,
      });
      @override
      Widget build(BuildContext context) {
        return Text('Hello, $name!');
      }
    }

StatefulWidget

  • setState, ライフサイクル, initState/disposeです。

    class Counter extends StatefulWidget {
      const Counter({super.key});
      @override
      State<Counter> createState() =>
          _CounterState();
    }
    class _CounterState extends State<Counter> {
      int _count = 0;
    
      @override void initState() { super.initState(); }
      @override void dispose() { super.dispose(); }
    
      @override
      Widget build(BuildContext context) {
        return Column(children: [
          Text('Count: $_count'),
          ElevatedButton(
            onPressed: () => setState(() => _count++),
            child: const Text('Increment'),
          ),
        ]);
      }
    }

ライフサイクル

メソッドタイミング
createState()StatefulWidget生成時
initState()State生成直後(1回)
didChangeDependencies()依存Widget変更時
build()UIの構築(setState毎)
didUpdateWidget()親からの設定変更時
deactivate()ツリーから除去時
dispose()State完全破棄時

レイアウトWidget

Row & Column

  • 横並び・縦並び, MainAxisAlignment, Expanded/Spacerです。

    Row(
      mainAxisAlignment:
          MainAxisAlignment.spaceBetween,
      crossAxisAlignment:
          CrossAxisAlignment.center,
      children: [
        const Text('Left'),
        Expanded(child: const Text('Fill')),
        const Text('Right'),
      ],
    )
    Column(
      mainAxisAlignment:
          MainAxisAlignment.start,
      crossAxisAlignment:
          CrossAxisAlignment.stretch,
      children: [
        const Text('Top'),
        const Spacer(),
        const Text('Bottom'),
      ],
    )
    Padding(padding: EdgeInsets.all(8), child: w)
    Center(child: widget)
    SizedBox(width: 100, height: 50)
    Flexible(flex: 2, child: widget)
    Wrap(spacing: 8, children: [...])

Alignment

MainAxisAlignment動作
.start先頭に寄せる
.end末尾に寄せる
.center中央に配置
.spaceBetween均等配置(両端余白なし)
.spaceEvenly均等配置(両端余白あり)
.spaceAround均等配置(両端は半分の余白)

Container & Stack

  • 装飾・余白・重ね合わせレイアウトです。

    Container(
      width: 200, height: 100,
      margin: const EdgeInsets.all(16),
      padding: const EdgeInsets.symmetric(
        horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [BoxShadow(
          color: Colors.black26, blurRadius: 10)],
      ),
      child: const Text('Container'),
    )
    Stack(children: [
      Image.network('https://...'),
      Positioned(
        bottom: 16, left: 16,
        child: const Text('Overlay'),
      ),
    ])

ListView & GridView

  • スクロール可能なリスト・グリッドレイアウトです。

    ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) => ListTile(
        leading: const Icon(Icons.star),
        title: Text(items[index].title),
        subtitle: Text(items[index].subtitle),
        trailing: const Icon(Icons.chevron_right),
        onTap: () {},
      ),
    )
    GridView.count(
      crossAxisCount: 2,
      crossAxisSpacing: 8,
      mainAxisSpacing: 8,
      children: List.generate(6,
        (i) => Card(
          child: Center(child: Text('$i')),
        ),
      ),
    )
    Scaffold(
      appBar: AppBar(title: Text('Title')),
      body: Center(child: Text('Content')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    )

ナビゲーション

Navigator(基本)

  • push, pop, pushReplacement, 値の受け渡しです。

    Navigator.push(context,
      MaterialPageRoute(
        builder: (_) => const DetailPage()),
    );
    Navigator.pop(context);
    Navigator.pop(context, 'result_data');
    final result = await Navigator.push<String>(
      context,
      MaterialPageRoute(
        builder: (_) => const SelectPage()),
    );
    
    Navigator.pushReplacement(context,
      MaterialPageRoute(
        builder: (_) => const HomePage()),
    );
    
    Navigator.pushAndRemoveUntil(context,
      MaterialPageRoute(
        builder: (_) => const HomePage()),
      (route) => false,
    );

GoRouter(宣言的ルーティング)

  • パスパラメータ, ShellRoute, リダイレクトです。

    final router = GoRouter(
      initialLocation: '/',
      routes: [
        GoRoute(
          path: '/',
          builder: (_, state) => const HomePage(),
          routes: [
            GoRoute(
              path: 'detail/:id',
              builder: (_, state) => DetailPage(
                id: state.pathParameters['id']!),
            ),
          ],
        ),
      ],
    );
    ShellRoute(
      builder: (_, state, child) =>
          AppShell(child: child),
      routes: [
        GoRoute(path: '/home',
          builder: (_, __) => HomePage()),
        GoRoute(path: '/profile',
          builder: (_, __) => ProfilePage()),
      ],
    )
    MaterialApp.router(routerConfig: router)
    
    context.go('/detail/123');
    context.push('/detail/123');
    context.pop();

状態管理 & パターン

Provider / Riverpod

  • ChangeNotifier, Consumer, ref.watch/readです。

    class CounterModel extends ChangeNotifier {
      int _count = 0;
      int get count => _count;
      void increment() {
        _count++;
        notifyListeners();
      }
    }
    
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const MyApp(),
    )
    Consumer<CounterModel>(
      builder: (context, counter, child) {
        return Text('${counter.count}');
      },
    )
    context.watch<CounterModel>(); // rebuild
    context.read<CounterModel>().increment();
    // Riverpod
    final counterProvider =
        StateNotifierProvider<CounterNotifier, int>(
      (ref) => CounterNotifier(),
    );
    
    class CounterNotifier extends StateNotifier<int> {
      CounterNotifier() : super(0);
      void increment() => state++;
    }
    class Page extends ConsumerWidget {
      @override
      Widget build(BuildContext context,
          WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Text('$count');
      }
    }
    
    ref.watch(p);   // リアクティブ監視
    ref.read(p);    // 1回読み取り
    ref.listen(p, (prev, next) { /* 副作用 */ });

よく使うパターン

  • FutureBuilder, MediaQuery, ダイアログ表示です。

    FutureBuilder<String>(
      future: fetchData(),
      builder: (context, snapshot) {
        if (snapshot.connectionState ==
            ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        return Text(snapshot.data!);
      },
    )
    StreamBuilder<int>(
      stream: countStream,
      builder: (context, snapshot) {
        return Text('${snapshot.data ?? 0}');
      },
    )
    
    final size = MediaQuery.of(context).size;
    final isTablet = size.width > 600;
    final theme = Theme.of(context);
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Confirm'),
        content: const Text('Are you sure?'),
        actions: [
          TextButton(
            onPressed: () =>
                Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () { /* 処理 */ },
            child: const Text('OK'),
          ),
        ],
      ),
    );