Flutter: Dart言語速習
DartはFlutterの魂です — Dartをマスターすることは、交響曲を演奏する前に楽譜を読むことを学ぶようなものです。
📋 前提条件: 以下を先に完了している必要があります
- レッスン1:Flutter入門と環境構築
1. このレッスンで学ぶこと
- 変数と型システム:
int、double、String、bool;var/final/constの違い - コレクション操作:
List<E>、Map<K,V>、Set<E>;collection-if / collection-for - 関数の機能:オプショナルパラメータ、名前付きパラメータ、匿名関数とクロージャ
- 非同期プログラミング:
Future<T>、async/await、Stream<T> - クラスとミックスイン:コンストラクタ、ファクトリパターン、
abstract class、mixin
2. 開発者のリアルなストーリー
(1) 悩み:コールバック地獄と型の混乱
Bobはバックエンド開発者からFlutterに転向しました。JavaScriptの柔軟性に慣れていた彼は、Dartで何度も壁にぶつかりました:varとfinalを混同し、非同期コールバックを5レベル深くネストし、ジェネリックコレクションの型エラーに悩まされました。ショッピングカートの価格計算中、暗黙のintからdoubleへの変換バグにより99.99ドルが99ドルになり — リリース後にユーザーからの苦情が殺到しました。
(2) Dartのソリューション
Dartは強い型付け言語であり、コンパイル時に型エラーを検出します。async/awaitにより非同期コードが同期コードのように明確になり、final/constが意図しない変更を防ぎます。
DART
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
// Simplified class definition (for demonstration)
class Product {
final int id;
final String name;
final double price;
final String? imageUrl;
const Product({required this.id, required this.name, required this.price, this.imageUrl});
factory Product.fromJson(Map<String, dynamic> json) => Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
imageUrl: json['image_url'] as String?,
);
Product copyWith({String? name, double? price}) => Product(
id: id, name: name ?? this.name, price: price ?? this.price, imageUrl: imageUrl,
);
static List<Product> fromJsonList(List data) => data.map((j) => Product.fromJson(j as Map<String, dynamic>)).toList();
}
class CartItem {
final Product product;
final double price;
final int quantity;
CartItem({required this.product, required this.price, this.quantity = 1});
}
// Strong typing prevents price bugs
double calculateTotal(List<CartItem> items) {
return items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
}
// async/await eliminates callback hell
Future<List<Product>> fetchProducts() async {
final response = await http.get(Uri.parse('/api/v1/products'));
return Product.fromJsonList(jsonDecode(response.body));
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) 結果:型安全性 + 明確な非同期処理
Dartに切り替えた後、型エラーはコンパイル時に検出され、非同期コードはフラットになり、ショッピングカートの価格精度問題は二度と発生しませんでした。
3. 変数と型システム
Dartは型推論とSound Null Safetyを持つ強い型付け言語です。
sequenceDiagram
participant Bob
participant ShopAPI
Bob->>ShopAPI: fetchProducts() [async]
ShopAPI-->>Bob: Future<List<Product>>
Bob->>Bob: await parses JSON
Bob->>Bob: Renders product list
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(1) 基本型
| 型 | 説明 | 例 |
|---|---|---|
int |
64ビット整数 | int quantity = 3; |
double |
64ビット浮動小数点 | double price = 99.99; |
String |
UTF-16文字列 | String name = "ShopApp"; |
bool |
ブール値 | bool inStock = true; |
num |
int + doubleの親型 | num value = 42; |
(2) 変数宣言キーワード
| キーワード | 可変性 | 代入タイミング | 用途 |
|---|---|---|---|
var |
可変 | 実行時 | 型が推論可能なローカル変数 |
final |
不変 | 実行時 | 実行時定数(例:APIレスポンス) |
const |
不変 | コンパイル時 | コンパイル時定数(例:カラー値) |
late |
遅延初期化 | 実行時 | 使用前に必ず代入する必要がある |
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: 変数宣言の比較
DART
// var: type inferred, mutable
var productName = 'Flutter T-Shirt';
productName = 'Dart Hoodie'; // OK
// final: runtime constant, immutable
final double totalPrice = 29.99 * 3;
// totalPrice = 100.0; // Error: final variable
// const: compile-time constant
const double discountRate = 0.15;
const String storeName = 'ShopApp';
// late: deferred initialization
late String userToken;
void login() {
userToken = 'eyJhbGciOiJIUzI1NiIs...';
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
4. コレクション操作
(1) List、Map、Set
| コレクション | 特徴 | リテラル構文 |
|---|---|---|
List<E> |
順序あり、重複を許可 | [1, 2, 3] |
Map<K,V> |
キーと値のペア | {'key': 'value'} |
Set<E> |
順序なし、一意 | {1, 2, 3} |
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: 基本的なコレクション操作
DART
import 'package:flutter/material.dart';
// Product class definition shown above
// List: ordered collection
List<Product> products = [
Product(id: 1, name: 'Laptop', price: 1299.99),
Product(id: 2, name: 'Phone', price: 899.99),
];
// Map: key-value pairs
Map<String, double> prices = {
'Laptop': 1299.99,
'Phone': 899.99,
'Tablet': 599.99,
};
// Set: unique items
Set<String> categories = {'Electronics', 'Clothing', 'Books'};
// Access and modify
products.add(Product(id: 3, name: 'Tablet', price: 599.99));
prices['Headphones'] = 199.99;
categories.add('Electronics'); // Ignored, already exists
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) collection-if / collection-for
Dart独自のコレクション内条件/ループ構文は、UI構築を大幅に簡素化します。
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: collection-ifとcollection-for
DART
import 'package:flutter/material.dart';
// Product class definition shown above
// collection-if: conditional items
bool isAdmin = true;
var menuItems = [
'Home',
'Products',
'Cart',
if (isAdmin) 'Admin Panel',
];
// collection-for: expand items
var productNames = ['Laptop', 'Phone', 'Tablet'];
var descriptions = [
for (var name in productNames) '$name - Best Price'
];
// ['Laptop - Best Price', 'Phone - Best Price', 'Tablet - Best Price']
// Combined in Widget list
Widget buildProductList(List<Product> products) {
return Column(
children: [
for (var p in products)
ListTile(title: Text(p.name), trailing: Text('\$${p.price}')),
],
);
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
5. 関数の機能
(1) パラメータの種類
| 種類 | 構文 | 必須 | デフォルト値 |
|---|---|---|---|
| 位置指定 | (a, b) |
はい | なし |
| オプショナル位置指定 | ([a = 0]) |
いいえ | 対応 |
| 名前付き | ({required a}) |
required時は必須 |
対応 |
| オプショナル名前付き | ({a = 0}) |
いいえ | 対応 |
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: 関数パラメータのパターン
DART
import 'package:flutter/material.dart';
// Named parameters (recommended for Flutter)
Widget buildPriceTag({
required double price,
String currency = 'USD',
double fontSize = 16.0,
}) {
return Text(
'$currency \$${price.toStringAsFixed(2)}',
style: TextStyle(fontSize: fontSize),
);
}
// Call with named params
buildPriceTag(price: 99.99);
buildPriceTag(price: 49.99, currency: 'EUR', fontSize: 20.0);
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) 匿名関数とクロージャ
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: Flutterにおけるクロージャ
DART
import 'package:flutter/material.dart';
// Product class definition shown above
// Anonymous function (lambda)
var expensiveItems = products.where((p) => p.price > 500.0).toList();
// Closure: captures surrounding variables
double discountThreshold = 100.0;
var discountedItems = products.map((p) {
// Closure captures discountThreshold
if (p.price > discountThreshold) {
return p.copyWith(price: p.price * 0.9);
}
return p;
}).toList();
// Callback in Flutter widget
ElevatedButton(
onPressed: () {
// Closure captures context variables
addToCart(product);
},
child: const Text('Add to Cart'),
)
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. 非同期プログラミング
(1) Futureとasync/await
| 概念 | 説明 | 例え |
|---|---|---|
Future<T> |
非同期操作のプレースホルダー | レストランの注文番号 |
async |
非同期関数をマーク | システムに「待ちます」と伝える |
await |
Futureの完了を待つ | 食べる前に料理を待つ |
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: async/awaitによるネットワークリクエスト
DART
import 'dart:convert';
import 'package:http/http.dart' as http;
// Product class definition shown above
Future<List<Product>> fetchProducts({int page = 1}) async {
try {
final response = await http.get(
Uri.parse('https://api.shopapp.com/v1/products?page=$page'),
);
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Product.fromJson(json)).toList();
} else {
throw Exception('Failed to load products: ${response.statusCode}');
}
} catch (e) {
// Handle network errors
rethrow;
}
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) Stream:非同期データフロー
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: リアルタイムカート更新のためのStream
DART
import 'dart:async';
// Simplified class definition (for demonstration)
class Product {
final String name;
final double price;
const Product({required this.name, required this.price});
}
class CartItem {
final Product product;
int quantity;
CartItem({required this.product, this.quantity = 1});
}
// StreamController for cart updates
class CartBloc {
final _cartStream = StreamController<List<CartItem>>.broadcast();
Stream<List<CartItem>> get cartStream => _cartStream.stream;
List<CartItem> _items = [];
void addToCart(Product product) {
_items.add(CartItem(product: product, quantity: 1));
_cartStream.sink.add(_items);
}
void dispose() {
_cartStream.close();
}
}
// Listen to stream
cartBloc.cartStream.listen((items) {
print('Cart updated: ${items.length} items, total: \$${calculateTotal(items)}');
});
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. クラスとミックスイン
(1) クラスとコンストラクタ
| コンストラクタの種類 | 構文 | 目的 |
|---|---|---|
| デフォルト | ClassName() |
標準的なインスタンス化 |
| 名前付き | ClassName.named() |
複数の初期化方法 |
| ファクトリ | factory ClassName() |
インスタンス生成の制御(例:キャッシュ/シングルトン) |
| Const | const ClassName() |
コンパイル時定数オブジェクト |
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: 商品データモデル
DART
class Product {
final int id;
final String name;
final double price;
final String? imageUrl;
final double rating;
// Main constructor with named params
const Product({
required this.id,
required this.name,
required this.price,
this.imageUrl,
this.rating = 0.0,
});
// Named constructor from JSON
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
imageUrl: json['image_url'] as String?,
rating: (json['rating'] as num?)?.toDouble() ?? 0.0,
);
}
// Copy with pattern for immutable updates
Product copyWith({String? name, double? price}) {
return Product(
id: id,
name: name ?? this.name,
price: price ?? this.price,
imageUrl: imageUrl,
rating: rating,
);
}
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) ミックスイン:コードの再利用
▶ サンプル
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: 共通機能のためのMixin
DART
// Product class definition shown above
// Mixin: reusable behavior without inheritance
mixin PriceFormatter {
String formatPrice(double price, {String currency = 'USD'}) {
return '$currency \$${price.toStringAsFixed(2)}';
}
String formatDiscount(double original, double discounted) {
double percent = ((original - discounted) / original * 100);
return '${percent.toStringAsFixed(0)}% OFF';
}
}
// Apply mixin to class
class ProductCard with PriceFormatter {
final Product product;
ProductCard(this.product);
String get priceLabel => formatPrice(product.price);
String get discountLabel => formatDiscount(100.0, product.price);
}
// Abstract class: cannot be instantiated
abstract class Repository<T> {
Future<List<T>> getAll();
Future<T> getById(int id);
Future<T> create(T item);
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完成例:ShopAppデータレイヤー
⚙️ 依存パッケージのインストール:
flutter pub add http
DART
import 'dart:convert';
import 'package:http/http.dart' as http;
// Model
class Product {
final int id;
final String name;
final double price;
final String category;
const Product({
required this.id,
required this.name,
required this.price,
required this.category,
});
factory Product.fromJson(Map<String, dynamic> json) => Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
category: json['category'] as String,
);
}
// Repository with async/await
class ProductRepository {
final String baseUrl = 'https://api.shopapp.com/v1';
Future<List<Product>> fetchProducts({int page = 1, int limit = 20}) async {
final response = await http.get(
Uri.parse('$baseUrl/products?page=$page&limit=$limit'),
);
if (response.statusCode != 200) {
throw Exception('API error: ${response.statusCode}');
}
final List data = jsonDecode(response.body)['items'] as List;
return data.map((j) => Product.fromJson(j as Map<String, dynamic>)).toList();
}
Future<Product> fetchById(int id) async {
final response = await http.get(Uri.parse('$baseUrl/products/$id'));
return Product.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
}
// Usage
void main() async {
final repo = ProductRepository();
final products = await repo.fetchProducts(page: 1);
for (var p in products) {
print('${p.name}: \$${p.price.toStringAsFixed(2)}');
}
}
TEXT
> 出力: Flutter SDK(Flutter 3.x / Dart 3.x)でローカルに実行してください。PistonサーバーにはFlutterがインストールされていません。ローカルマシンで`flutter run`を使用して比較してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q finalとconstの本当の違いは?
A finalは実行時に一度だけ代入され、変更できません。constはコンパイル時に決定され、リテラルまたはconstコンストラクタでなければなりません。
Q Dartに三項演算子はありますか?
A はい、
condition ? value1 : value2のほか、??(null認識合体演算子)と?.(null安全アクセス)もあります。Q 非同期関数はFutureを返さなければならないのですか?
A いいえ、しかし推奨されます。非同期関数はvoidを返す場合でも非同期で実行されます。
Q StreamとFutureの違いは?
A Futureは一度きりの非同期結果です。Streamは継続的に生成される非同期データシーケンスで、リアルタイムの株価のようなものです。
Q ミックスインと抽象クラスのどちらを選ぶべきか?
A is-a関係なしに振る舞いを追加する(例:ログ、フォーマット)場合はミックスインを使用。is-a関係を必要とする型契約を定義する場合は抽象クラスを使用します。
Q Dartはnull安全性に対応していますか?
A Dart 2.12+ではSound Null Safetyが強制されます。
Stringはnullにできず、String?はnullにできます。コンパイラが実行時のnull参照エラーを保証します。📖 まとめ
- Dartはnull安全性を持つ強い型付け言語で、コンパイル時に型エラーを検出する
var/final/constで可変性と代入タイミングを区別- collection-if/collection-forがUIリスト構築を簡素化
async/awaitが非同期コードをフラットで明確にする- ミックスインは水平的なコード再利用を可能にし、抽象クラスは型契約を定義する
copyWithパターンにより不変オブジェクトの更新が可能
📝 練習問題
- 基本(難易度 ⭐): id(int)、name(String)、email(String?)を持つ
Userクラスを作成し、fromJsonファクトリコンストラクタを実装してください。 - 中級(難易度 ⭐⭐):
async/awaitを使ったシミュレーションネットワークリクエスト関数を書き、Future<List<Product>>を返すようにしてください。Future.delayedで2秒の遅延をシミュレートします。 - チャレンジ(難易度 ⭐⭐⭐):
StreamControllerを使ってカート変更をブロードキャストするCartManagerクラスを実装し、アイテムの追加/削除とクリア操作に対応してください。