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 | 倍精度浮動小数点 |
| num | int, double の親型 |
| String | UTF-16文字列 |
| bool | true / 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'), ), ], ), );