Flutter チートシート

cheatsheet

レイアウト構築 🧱

UIの骨組みを作るためのウィジェット

基本レイアウト

  • Row: 子ウィジェットを水平方向に並べる。
  • Column: 子ウィジェットを垂直方向に並べる。
  • Stack: 子ウィジェットを重ねて表示する。最後に配置したものが一番上に来る。
  • IndexedStack: Stackと似ているが、一度に表示する子ウィジェットは1つだけ(indexで指定)。状態保持に便利。
  • Flow: より複雑なカスタムレイアウトを実現。子ウィジェットの配置をデリゲートで制御。

// Rowの使用例
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 主軸方向の配置
  crossAxisAlignment: CrossAxisAlignment.center, // 交差軸方向の配置
  children: <Widget>[
    Icon(Icons.star, color: Colors.yellow),
    Text('アイテム 1'),
    Text('アイテム 2'),
  ],
)

// Columnの使用例
Column(
  mainAxisAlignment: MainAxisAlignment.start,
  crossAxisAlignment: CrossAxisAlignment.stretch, // 子を幅いっぱいに広げる
  children: <Widget>[
    Text('タイトル', style: TextStyle(fontSize: 24)),
    Padding(
      padding: EdgeInsets.all(8.0),
      child: Text('コンテンツ'),
    ),
  ],
)

// Stackの使用例
Stack(
  alignment: AlignmentDirectional.center, // 子ウィジェットの重ね合わせ基準
  children: <Widget>[
    Container(width: 100, height: 100, color: Colors.blue),
    Container(width: 60, height: 60, color: Colors.red),
    Positioned( // 特定の位置に配置
      bottom: 10,
      right: 10,
      child: Text('P'),
    ),
  ],
)
            

コンテナと装飾

  • Container: 単一の子ウィジェットを持つ多機能ウィジェット。パディング、マージン、ボーダー、背景色、形状などを設定可能。
  • Padding: 子ウィジェットの周囲に余白を追加する。
  • DecoratedBox: 子ウィジェットの描画前後に装飾 (Decoration) を適用する。Containerdecoration プロパティの内部で使用されている。
  • SizedBox: 指定したサイズのボックスを作成。子を持たない場合はスペーサーとして、子を持つ場合は子のサイズを制約するために使用。
  • AspectRatio: 子ウィジェットのアスペクト比を強制する。
  • ConstrainedBox: 子ウィジェットのサイズに制約(最小/最大幅・高さ)を追加する。

Container(
  padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
  margin: EdgeInsets.all(10.0),
  decoration: BoxDecoration(
    color: Colors.lightBlueAccent,
    borderRadius: BorderRadius.circular(8.0),
    border: Border.all(color: Colors.blue, width: 2.0),
    boxShadow: [
      BoxShadow(
        color: Colors.grey.withOpacity(0.5),
        spreadRadius: 5,
        blurRadius: 7,
        offset: Offset(0, 3), // 影の位置
      ),
    ],
  ),
  child: Text('装飾されたコンテナ'),
)

Padding(
  padding: EdgeInsets.only(left: 20.0),
  child: Text('左にパディング'),
)

SizedBox(
  width: 150.0,
  height: 50.0,
  child: ElevatedButton(onPressed: () {}, child: Text('サイズ指定')),
)
            

配置とサイズ調整

  • Center: 子ウィジェットを中央に配置する。
  • Align: 親ウィジェット内で子ウィジェットの配置を指定する (例: Alignment.topLeft)。
  • Expanded: Row, Column, Flex の子ウィジェットが利用可能なスペースを埋めるように拡張する。flex プロパティで拡張比率を指定可能。
  • Flexible: Expandedと似ているが、必ずしもスペースを埋めるとは限らない (fit プロパティで挙動を制御: FlexFit.tight または FlexFit.loose)。
  • FittedBox: 子ウィジェットを指定されたフィットタイプ (BoxFit) に従って親ウィジェット内にスケールおよび配置する。
  • FractionallySizedBox: 親ウィジェットの利用可能なスペースに対する割合で子のサイズを指定する。

Row(
  children: <Widget>[
    Container(color: Colors.red, height: 50, width: 50),
    Expanded(
      flex: 2, // 他のExpandedの2倍のスペースを占有
      child: Container(color: Colors.green, height: 50),
    ),
    Expanded(
      flex: 1,
      child: Container(color: Colors.blue, height: 50),
    ),
  ],
)

Center(
  child: Text('中央寄せテキスト'),
)

Align(
  alignment: Alignment.bottomRight,
  child: Text('右下寄せテキスト'),
)
            

リストとグリッド 📜

  • ListView: スクロール可能な線形リスト。
    • ListView(): 子ウィジェットのリストを直接指定(要素数が少ない場合)。
    • ListView.builder(): 必要に応じてリスト項目を構築(要素数が多い、動的な場合)。パフォーマンスが良い。
    • ListView.separated(): 各項目の間に区切り線(セパレーター)を挿入。
  • GridView: スクロール可能な2次元グリッドリスト。
    • GridView.count(): 交差軸方向のアイテム数を指定。
    • GridView.extent(): 交差軸方向の各アイテムの最大サイズを指定。
    • GridView.builder(): 必要に応じてグリッド項目を構築。SliverGridDelegate でレイアウトを制御。
  • SingleChildScrollView: 単一の子ウィジェットをスクロール可能にする。
  • ListTile: マテリアルデザインの標準的なリスト項目スタイルを提供。アイコン、テキスト、サブタイトルなどを配置しやすい。
  • Card: マテリアルデザインのカードコンポーネント。情報をグループ化して表示するのに適している。

ListView.builder(
  itemCount: 100, // リストの項目数
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      leading: Icon(Icons.person),
      title: Text('アイテム ${index + 1}'),
      subtitle: Text('詳細情報...'),
      trailing: Icon(Icons.arrow_forward_ios),
      onTap: () {
        print('アイテム ${index + 1} がタップされました');
      },
    );
  },
)

GridView.count(
  crossAxisCount: 3, // 1行に表示するアイテム数
  crossAxisSpacing: 10.0, // アイテム間の水平方向のスペース
  mainAxisSpacing: 10.0, // アイテム間の垂直方向のスペース
  padding: EdgeInsets.all(10.0),
  children: List.generate(20, (index) {
    return Container(
      color: Colors.teal[100 * (index % 9)],
      child: Center(child: Text('アイテム $index')),
    );
  }),
)
            

レスポンシブデザイン 📱💻

  • MediaQuery: 現在のメディア(画面サイズ、向き、プラットフォームの明るさ設定など)に関する情報を取得する。
  • LayoutBuilder: 親ウィジェットが子ウィジェットに提供する制約 (constraints) に基づいてウィジェットツリーを構築する。
  • OrientationBuilder: デバイスの向き(縦向き/横向き)に基づいて異なるウィジェットツリーを構築する。
  • Wrap: RowやColumnのように子を並べるが、スペースが足りなくなると次の行/列に折り返す。

// MediaQueryの使用例
Widget build(BuildContext context) {
  final screenSize = MediaQuery.of(context).size;
  final orientation = MediaQuery.of(context).orientation;

  return Scaffold(
    appBar: AppBar(title: Text('レスポンシブ')),
    body: Center(
      child: Text(
        '画面幅: ${screenSize.width.toStringAsFixed(2)}\n'
        '画面高さ: ${screenSize.height.toStringAsFixed(2)}\n'
        '向き: $orientation',
      ),
    ),
  );
}

// LayoutBuilderの使用例
LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    if (constraints.maxWidth > 600) {
      return Text('広い画面用のレイアウト');
    } else {
      return Text('狭い画面用のレイアウト');
    }
  },
)

// Wrapの使用例
Wrap(
  spacing: 8.0, // 主軸方向のスペース
  runSpacing: 4.0, // 交差軸方向のスペース
  children: List.generate(10, (index) => Chip(
    label: Text('タグ $index'),
    avatar: CircleAvatar(child: Text('${index+1}')),
  )),
)
            

状態管理 🔄

アプリケーションの状態を効率的に管理する手法

状態管理は Flutter アプリ開発の核心部分です。アプリの規模や複雑さに応じて適切な手法を選択することが重要です。

手法 概要 主な特徴 適しているケース
setState StatefulWidget 内で状態を更新し、UIを再描画する最も基本的な方法。
  • シンプルで学習コストが低い。
  • ウィジェットローカルな状態管理に適している。
  • コードがウィジェット内に閉じている。
  • 小規模なアプリ。
  • 特定のウィジェット内でのみ完結する状態。
  • 一時的なUIの状態(アニメーションなど)。
Provider 依存性注入(DI)と状態管理のためのラッパー。InheritedWidget を使いやすくしたもの。
  • 比較的シンプル。
  • 状態の提供と消費が分離される。
  • 複数の種類のプロバイダー (ChangeNotifierProvider, FutureProvider, StreamProvider など)。
  • 公式ドキュメントでも推奨されている。
  • 中規模程度のアプリ。
  • 状態を複数のウィジェットで共有したい場合。
  • 依存関係の注入を行いたい場合。
Riverpod Providerの作者による、より安全でテストしやすい状態管理ライブラリ。コンパイル時安全性を提供。
  • Providerの問題点を解消 (BuildContext への依存排除など)。
  • コンパイル時の安全性。
  • 状態がイミュータブルであることを奨励。
  • テストがしやすい。
  • 柔軟なプロバイダーの種類。
  • 中規模〜大規模アプリ。
  • より堅牢でテスト可能な状態管理を求める場合。
  • Providerからの移行。
Bloc / Cubit ビジネスロジックとUIを明確に分離するパターン。イベント駆動(Bloc)またはメソッド呼び出し(Cubit)で状態を更新。
  • 状態遷移が明確。
  • UIとロジックの分離が徹底される。
  • テスト容易性が高い。
  • Blocはイベントベース、Cubitはよりシンプル。
  • 状態変化の追跡が容易 (BlocObserver)。
  • 大規模で複雑なアプリ。
  • 厳密なアーキテクチャ分離が必要な場合。
  • 状態遷移のテストを重視する場合。
GetX 状態管理、依存性注入、ルート管理などを統合したフレームワーク。少ないコード量を目指す。
  • 多機能でオールインワン。
  • コード記述量が少ない。
  • パフォーマンスが高いと主張されている。
  • 独自の構文が多い。
  • BuildContext への依存が少ない。
  • 素早く開発を進めたい場合。
  • 多くの機能を単一のライブラリで管理したい場合。
  • GetXのエコシステムに慣れている開発者。
InheritedWidget Flutterフレームワーク組み込みの、ウィジェットツリーを通じて効率的にデータを渡すための仕組み。Providerなどの基礎技術。
  • Flutterのコア機能。
  • 効率的なデータ伝搬。
  • 直接使うのはやや複雑。
  • カスタムの状態管理ソリューションを構築する場合。
  • テーマやロケールなど、アプリ全体で共有される静的なデータ。

Provider の基本的な使い方


// 1. 状態クラス (ChangeNotifierを継承)
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // 状態の変更を通知
  }
}

// 2. main.dart または上位ウィジェットでProviderを提供
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

// 3. UIウィジェットで状態を消費
class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Provider.ofで状態を取得 (変更をリッスン)
    // final counter = Provider.of<CounterModel>(context);
    // Consumerウィジェットで状態を取得 (特定の部分のみ再描画)
    return Consumer<CounterModel>(
      builder: (context, counter, child) {
        return Text('Count: ${counter.count}');
      },
    );
  }
}

class IncrementButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // listen: false で状態変更をリッスンせず、メソッド呼び出しのみ行う
    final counter = Provider.of<CounterModel>(context, listen: false);
    return ElevatedButton(
      onPressed: () => counter.increment(),
      child: Text('Increment'),
    );
  }
}
            

Riverpod の基本的な使い方


import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. プロバイダーを定義 (グローバルに定義可能)
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// 2. 状態クラス (StateNotifierを継承)
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0); // 初期状態

  void increment() {
    state++; // 状態を直接変更 (イミュータブル)
  }
}

// 3. main.dart で ProviderScope を設定
void main() {
  runApp(
    ProviderScope( // アプリ全体をラップ
      child: MyApp(),
    ),
  );
}

// 4. UIウィジェットで状態を消費 (ConsumerWidgetを使用)
class CounterApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch で状態を監視し、変更時に再描画
    final count = ref.watch(counterProvider);
    // ref.read で状態を一度だけ読み取る (メソッド呼び出しなど)
    final counterNotifier = ref.read(counterProvider.notifier);

    return Scaffold(
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counterNotifier.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
            

ナビゲーション 🧭

画面間の遷移を管理する

Navigator 1.0 (Imperative API)

スタックベースのシンプルなナビゲーション。push で画面を追加、pop で画面を削除。

  • Navigator.push: 新しい画面 (Route) をスタックに追加する。
  • Navigator.pop: 現在の画面をスタックから削除し、前の画面に戻る。値を返すことも可能。
  • Navigator.pushNamed: 事前に定義された名前付きルートに遷移する。MaterialApproutes で定義。
  • Navigator.pushReplacement: 現在の画面を新しい画面に置き換える(ログイン画面からホーム画面への遷移など)。
  • Navigator.pushAndRemoveUntil: 新しい画面を追加し、指定した条件を満たすまですべての前の画面を削除する。
  • MaterialPageRoute / CupertinoPageRoute: プラットフォームに応じた画面遷移アニメーションを提供するRoute。

// 基本的な画面遷移
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);

// 前の画面に戻る (値を渡す例)
Navigator.pop(context, '戻り値データ');

// 名前付きルートへの遷移
Navigator.pushNamed(context, '/details');

// MaterialAppでのルート定義
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/details': (context) => DetailScreen(),
    '/settings': (context) => SettingsScreen(),
  },
)

// 画面遷移時に引数を渡す
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(itemId: '123'),
  ),
);

// 前の画面から値を受け取る
void _navigateToSecondScreen(BuildContext context) async {
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondScreen()),
  );

  if (result != null) {
    ScaffoldMessenger.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text('受け取った値: $result')));
  }
}
            

Navigator 2.0 (Declarative API / Router API)

より複雑なナビゲーションシナリオ(WebのURL同期、ネストされたナビゲーションなど)に対応するための宣言的なAPI。学習コストは高い。

  • Router: アプリケーションの状態に基づいてナビゲーションスタックを構築するウィジェット。
  • RouterDelegate: アプリの状態が変化したときに Router に新しいルートスタックを構築するように指示する。OSからのイベント(戻るボタンなど)も処理。
  • RouteInformationParser: OSからのルート情報(URLなど)をアプリが理解できるデータ型に変換する。
  • RouteInformationProvider: OSからの新しいルート情報をアプリに通知する。
  • Pages API: ナビゲーションスタックを Page オブジェクトのリストとして表現する。

Navigator 2.0 は強力ですが複雑です。多くの場合、go_routerのようなパッケージを利用することで、宣言的なナビゲーションをより簡単に実装できます。

GoRouter パッケージ 🛣️

Navigator 2.0 をベースにした、宣言的で使いやすいルーティングパッケージ。

  • URLベースのルーティング。
  • 型安全なルートパラメータ。
  • リダイレクト、ネストされたナビゲーション、画面遷移アニメーションなどをサポート。
  • Navigator 1.0 のようなシンプルなAPIも提供。

// main.dart で GoRouter を設定
import 'package:go_router/go_router.dart';

final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) => HomeScreen(),
    ),
    GoRoute(
      path: '/details/:itemId', // パスパラメータを含むルート
      builder: (BuildContext context, GoRouterState state) {
        final itemId = state.pathParameters['itemId'];
        return DetailScreen(itemId: itemId!);
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (BuildContext context, GoRouterState state) => SettingsScreen(),
      // ネストされたルートの例
      routes: <GoRoute>[
        GoRoute(
          path: 'profile', // 親ルートからの相対パス: /settings/profile
          builder: (BuildContext context, GoRouterState state) => ProfileScreen(),
        ),
      ],
    ),
  ],
  // エラーページ
  errorBuilder: (context, state) => ErrorScreen(state.error),
  // リダイレクト
  redirect: (BuildContext context, GoRouterState state) {
    // 例: ログインしていない場合、ログインページにリダイレクト
    final loggedIn = checkUserLoggedIn(); // ログイン状態を確認する関数
    final loggingIn = state.matchedLocation == '/login';
    if (!loggedIn && !loggingIn) return '/login';
    if (loggedIn && loggingIn) return '/';
    return null; // リダイレクトしない場合は null を返す
  },
);

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router, // ここで GoRouter を設定
      title: 'GoRouter Example',
    );
  }
}

// 画面遷移 (GoRouter)
// context.go('/details/abc'); // 絶対パスで遷移
// context.push('/settings'); // スタックにプッシュして遷移
// context.goNamed('routeName', pathParameters: {'id': '123'}); // 名前付きルート (要設定)

// 戻る
// context.pop();
            

非同期処理 ⏳

時間のかかる処理(ネットワーク通信、ファイルI/Oなど)を扱う

  • Future: 非同期操作の結果(成功時の値またはエラー)を表すオブジェクト。
  • async / await: 非同期コードを同期コードのように書くための構文糖衣。async キーワードを関数に付け、Future が完了するのを await キーワードで待つ。
  • Stream: 非同期イベントのシーケンス(複数回発生する可能性がある)。ネットワークのストリーミングデータ、ユーザー入力イベントなどに使用。
  • FutureBuilder: Future に基づいてUIを構築するウィジェット。Future の状態(未完了、完了、エラー)に応じて異なるUIを表示。
  • StreamBuilder: Stream からの最新のデータに基づいてUIを構築するウィジェット。新しいデータがストリームから来るたびに再描画される。
  • Isolate: メインのUIスレッドとは別のスレッドでDartコードを実行する仕組み。重い計算処理をバックグラウンドで行い、UIのフリーズを防ぐ。compute 関数を使うと簡単に利用できる。

// Future と async/await の例
Future<String> fetchData() async {
  // ネットワークリクエストなどの非同期処理をシミュレート
  await Future.delayed(Duration(seconds: 2));
  // 成功した場合
  return 'データ取得完了!';
  // エラーの場合
  // throw Exception('データの取得に失敗しました');
}

void loadData() async {
  print('データ取得開始...');
  try {
    String result = await fetchData();
    print(result);
  } catch (e) {
    print('エラーが発生しました: $e');
  } finally {
    print('データ取得処理終了');
  }
}

// FutureBuilder の例
FutureBuilder<String>(
  future: fetchData(), // 非同期処理を実行するFutureを指定
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      // 処理中の表示
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      // エラー発生時の表示
      return Text('エラー: ${snapshot.error}');
    } else if (snapshot.hasData) {
      // 成功時の表示
      return Text('取得データ: ${snapshot.data}');
    } else {
      // データがない場合 (通常は発生しないはず)
      return Text('データがありません');
    }
  },
)

// StreamBuilder の例
Stream<int> countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // ストリームにデータを流す
  }
}

StreamBuilder<int>(
  stream: countStream(), // イベントストリームを指定
  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Text('待機中...');
    } else if (snapshot.hasError) {
      return Text('エラー: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text('現在のカウント: ${snapshot.data}');
    } else {
      return Text('ストリームが完了しました');
    }
  },
)

// Isolate (compute関数) の例
import 'package:flutter/foundation.dart';

int heavyComputation(int input) {
  // 時間のかかる計算処理をシミュレート
  int result = 0;
  for (int i = 0; i < input * 100000000; i++) {
    result += i;
    result -= i ~/ 2;
  }
  return result;
}

void runHeavyTask() async {
  print('重い処理を開始します...');
  // compute 関数で別Isolateで実行
  int result = await compute(heavyComputation, 10);
  print('重い処理が完了しました: $result');
}
            

データ永続化 💾

アプリのデータをデバイスに保存する

手法 概要 データ形式 適しているケース
shared_preferences キーバリュー形式で少量の単純なデータを永続化する。 キー(String)と値(bool, int, double, String, List<String>)
  • 設定値(ダークモードのオン/オフなど)。
  • ユーザーの簡単な選択。
  • 少量のキャッシュデータ。
sqflite SQLiteデータベースをFlutterアプリで使用するためのプラグイン。構造化されたデータを扱う。 リレーショナルデータベース (テーブル、行、列)
  • 複雑なクエリが必要な構造化データ。
  • 大量のデータを効率的に管理したい場合。
  • オフラインでのデータ操作。
Hive Dartネイティブの軽量・高速なキーバリューストア。オブジェクトの保存も容易。 キーバリューストア (ほぼ全てのDartオブジェクトを保存可能)
  • shared_preferencesより高速なキーバリューアクセスが必要な場合。
  • Dartオブジェクトを簡単に永続化したい場合。
  • 中程度の量のデータ。
Drift (Moor) sqfliteやWeb Storage APIの上に構築された、リアクティブな永続化ライブラリ。SQLクエリをDartで記述可能。 リレーショナルデータベース (型安全なDart API経由)
  • sqfliteをより型安全かつリアクティブに使いたい場合。
  • SQLをDartコード内で書きたい場合。
  • 複雑なデータモデルやリレーション。
ファイルシステム (path_provider) デバイスのファイルシステムに直接ファイルを読み書きする。path_providerで適切なディレクトリパスを取得。 任意のファイル形式 (JSON, テキスト, バイナリなど)
  • ユーザーが生成したファイル(画像、ドキュメント)。
  • 設定ファイル。
  • キャッシュファイル。
Firebase Firestore / Realtime Database GoogleのクラウドベースNoSQLデータベース。リアルタイム同期機能を持つ。 JSONライクなドキュメント (Firestore) / JSONツリー (Realtime DB)
  • リアルタイムでのデータ同期が必要なアプリ。
  • 複数デバイス間でのデータ共有。
  • サーバーレスアーキテクチャ。
  • オフラインサポートが必要なクラウドデータ。

shared_preferences の例


import 'package:shared_preferences/shared_preferences.dart';

// 値の保存
Future<void> saveSettings(bool isDarkMode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('darkMode', isDarkMode);
  await prefs.setInt('counter', 10);
  await prefs.setString('username', 'FlutterUser');
}

// 値の読み込み
Future<void> loadSettings() async {
  final prefs = await SharedPreferences.getInstance();
  bool isDarkMode = prefs.getBool('darkMode') ?? false; // デフォルト値を指定
  int counter = prefs.getInt('counter') ?? 0;
  String username = prefs.getString('username') ?? 'Guest';
  print('Dark Mode: $isDarkMode, Counter: $counter, Username: $username');
}

// 値の削除
Future<void> removeSetting(String key) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove(key);
}
            

Hive の例


import 'package:hive_flutter/hive_flutter.dart'; // hive_flutter を使うと初期化が簡単

// main関数で初期化
Future<void> main() async {
  await Hive.initFlutter(); // Flutterアプリ用の初期化
  // Box を開く (なければ作成される)
  await Hive.openBox('mySettingsBox');
  runApp(MyApp());
}

// Boxインスタンスを取得して使用
void saveData() {
  final box = Hive.box('mySettingsBox');
  box.put('name', 'Hive User');
  box.put('age', 30);
  box.put('isSubscribed', true);
  box.put('items', ['apple', 'banana', 'orange']);
}

void loadData() {
  final box = Hive.box('mySettingsBox');
  String name = box.get('name', defaultValue: 'Unknown');
  int age = box.get('age', defaultValue: 0);
  bool isSubscribed = box.get('isSubscribed') ?? false; // ?? 演算子でも可
  List<dynamic> items = box.get('items', defaultValue: []);

  print('Name: $name, Age: $age, Subscribed: $isSubscribed, Items: $items');
}

void deleteData(String key) {
   final box = Hive.box('mySettingsBox');
   box.delete(key);
}

// アプリ終了時にBoxを閉じる (任意だが推奨)
void dispose() {
  Hive.close(); // すべての開いているBoxを閉じる
  // Hive.box('mySettingsBox').close(); // 特定のBoxだけ閉じる
  // super.dispose();
}
            

ネットワーク通信 🌐

APIとのデータ送受信

パッケージ 概要 主な機能
http Flutterチームによってメンテナンスされている、基本的なHTTPリクエストを行うためのパッケージ。
  • GET, POST, PUT, DELETEなどの基本的なメソッド。
  • ヘッダー、ボディの設定。
  • シンプルなAPI。
dio 多機能なHTTPクライアントライブラリ。httpよりも高度な機能を提供。
  • インターセプター (リクエスト/レスポンスの加工、ログ記録、認証など)。
  • グローバル設定 (ベースURL、タイムアウトなど)。
  • フォームデータ、ファイルアップロード/ダウンロード。
  • リクエストのキャンセル。
  • Cookie管理。
  • エラーハンドリングの改善。
Chopper DioやHttpをベースにした、コード生成を利用するHTTPクライアントライブラリ。Retrofit (Android) にインスパイアされている。
  • 抽象クラスとアノテーションでAPIを定義。
  • コード生成により、型安全なクライアント実装を自動生成。
  • コンバーター (JSONなど) の統合。
  • インターセプター。
Firebase (Firestore, Realtime DB, Functions) Firebase SDKを通じてバックエンドサービスと通信。
  • Firestore/Realtime DB: リアルタイムデータ同期。
  • Cloud Functions: サーバーレス関数呼び出し。
  • オフラインサポート。
  • 認証連携。

http パッケージの例 (GETリクエスト)


import 'package:http/http.dart' as http;
import 'dart:convert'; // jsonDecode を使うため

Future<void> fetchDataWithHttp() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
  try {
    final response = await http.get(url);

    if (response.statusCode == 200) {
      // 成功
      final Map<String, dynamic> data = jsonDecode(response.body);
      print('取得したデータ: $data');
      print('タイトル: ${data['title']}');
    } else {
      // エラー
      print('リクエスト失敗: ${response.statusCode}');
      print('エラー内容: ${response.body}');
    }
  } catch (e) {
    print('通信エラー: $e');
  }
}
            

dio パッケージの例 (POSTリクエストとインターセプター)


import 'package:dio/dio.dart';

// Dioインスタンスを作成 (グローバル設定も可能)
final dio = Dio(BaseOptions(
  baseUrl: 'https://jsonplaceholder.typicode.com',
  connectTimeout: Duration(seconds: 5),
  receiveTimeout: Duration(seconds: 3),
  headers: {
    'User-Agent': 'MyFlutterApp/1.0',
    'Accept': 'application/json',
  },
));

// インターセプターの追加 (例: ログ出力)
void setupDioInterceptors() {
  dio.interceptors.add(LogInterceptor(
    requestBody: true,
    responseBody: true,
    logPrint: (obj) => print(obj.toString()), // ログの出力先をカスタマイズ
  ));
  // 他のインターセプター (認証トークン追加など) も追加可能
  // dio.interceptors.add(InterceptorsWrapper(
  //   onRequest:(options, handler){
  //     // リクエスト前にトークンを追加するなど
  //     options.headers["Authorization"] = "Bearer YOUR_TOKEN";
  //     return handler.next(options); //continue
  //   },
  // ));
}

Future<void> postDataWithDio() async {
  try {
    final response = await dio.post(
      '/posts',
      data: {
        'title': 'foo',
        'body': 'bar',
        'userId': 1,
      },
      options: Options(
        contentType: Headers.jsonContentType, // Content-Type を指定
      ),
    );

    if (response.statusCode == 201) { // POST成功時のステータスコードは通常201
      print('データ送信成功: ${response.data}');
    } else {
      print('リクエスト失敗: ${response.statusCode}');
    }
  } on DioException catch (e) { // Dio専用のエラーハンドリング
    if (e.response != null) {
      print('Dioエラー レスポンスあり: ${e.response?.statusCode} ${e.response?.data}');
    } else {
      print('Dioエラー レスポンスなし: ${e.requestOptions.path} ${e.message}');
    }
  } catch (e) {
    print('予期せぬエラー: $e');
  }
}

// main関数などでインターセプターを設定
// void main() {
//   setupDioInterceptors();
//   runApp(MyApp());
// }
            

フォーム 📝

ユーザーからの入力を受け取り、検証する

  • Form: 複数のフォームフィールド (FormField) をグループ化し、状態 (保存、リセット、検証) を管理するウィジェット。
  • GlobalKey<FormState>: Form ウィジェットの状態にアクセスするためのキー。formKey.currentState?.validate() のように使う。
  • TextFormField: TextField をラップし、Form との連携機能(検証、保存など)を追加したウィジェット。
  • TextEditingController: TextFieldTextFormField のテキスト内容をプログラムから制御(読み取り、変更、リスン)するためのオブジェクト。
  • validator: TextFormField のプロパティ。入力値を受け取り、問題があればエラーメッセージ (文字列)、なければ null を返す関数。FormState.validate() で実行される。
  • onSaved: TextFormField のプロパティ。FormState.save() が呼び出されたときに実行される関数。入力値を引数として受け取る。
  • InputDecoration: TextFormField の見た目をカスタマイズするためのクラス (ラベル、ヒントテキスト、アイコン、ボーダーなど)。
  • その他のフォームフィールド: CheckboxFormField, DropdownButtonFormField, RadioListTile など、他の入力タイプにも対応する FormField がある (または自作可能)。

class MyCustomForm extends StatefulWidget {
  @override
  MyCustomFormState createState() {
    return MyCustomFormState();
  }
}

class MyCustomFormState extends State<MyCustomForm> {
  // Formの状態を管理するためのGlobalKeyを作成
  final _formKey = GlobalKey<FormState>();
  // 各TextFormFieldを制御するためのController
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  String? _selectedOption; // Dropdown用

  @override
  void dispose() {
    // ウィジェットが破棄されるときにControllerも破棄する
    _nameController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, // Formにキーを設定
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            TextFormField(
              controller: _nameController,
              decoration: InputDecoration(
                labelText: '名前',
                hintText: 'お名前を入力してください',
                icon: Icon(Icons.person),
                border: OutlineInputBorder(),
              ),
              // バリデーションロジック
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '名前を入力してください';
                }
                return null; // 問題なければnullを返す
              },
              onSaved: (value) {
                // 保存時の処理 (例: 状態変数に保存)
                print('名前が保存されました: $value');
              },
            ),
            SizedBox(height: 16),
            TextFormField(
              controller: _emailController,
              decoration: InputDecoration(
                labelText: 'メールアドレス',
                icon: Icon(Icons.email),
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'メールアドレスを入力してください';
                }
                // 簡単なメール形式チェック
                if (!RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) {
                  return '有効なメールアドレスを入力してください';
                }
                return null;
              },
            ),
            SizedBox(height: 16),
            DropdownButtonFormField<String>(
              value: _selectedOption,
              decoration: InputDecoration(
                labelText: 'オプション選択',
                border: OutlineInputBorder(),
                icon: Icon(Icons.arrow_drop_down_circle),
              ),
              items: <String>['Option A', 'Option B', 'Option C']
                  .map<DropdownMenuItem<String>>((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value),
                );
              }).toList(),
              onChanged: (String? newValue) {
                setState(() {
                  _selectedOption = newValue;
                });
              },
              validator: (value) => value == null ? 'オプションを選択してください' : null,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  // バリデーションを実行
                  if (_formKey.currentState!.validate()) {
                    // バリデーションが成功した場合
                    _formKey.currentState!.save(); // 各 TextFormField の onSaved を実行
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('フォームを送信しました')),
                    );
                    print('送信データ: Name=${_nameController.text}, Email=${_emailController.text}, Option=$_selectedOption');
                    // ここで実際の送信処理などを行う
                  } else {
                     ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('入力内容を確認してください')),
                    );
                  }
                },
                child: Text('送信'),
              ),
            ),
            TextButton(
              onPressed: () {
                 _formKey.currentState!.reset(); // フォームをリセット
                 _nameController.clear();
                 _emailController.clear();
                 setState(() {
                   _selectedOption = null;
                 });
              },
              child: Text('リセット'),
            ),
          ],
        ),
      ),
    );
  }
}
            

テスト 🧪

コードの品質と信頼性を確保する

Flutter は包括的なテスト機能を提供しています。テストの種類を理解し、適切に活用しましょう。

テストの種類 目的 主な対象 実行環境 主要パッケージ
ユニットテスト (Unit Test) 単一の関数、メソッド、またはクラスのロジックを検証する。外部依存関係はモック化する。
  • ビジネスロジック
  • モデルクラス
  • ユーティリティ関数
  • 状態管理クラス (Bloc, Notifier など)
Dart VM (高速) test / flutter_test, mockito / mocktail
ウィジェットテスト (Widget Test) 単一のウィジェットが期待通りにレンダリングされ、インタラクションに反応するかを検証する。
  • StatelessWidget / StatefulWidget
  • UIの見た目(テキスト、アイコンなど)
  • ユーザー操作への反応(タップ、入力など)
  • 状態変化に伴うUIの更新
Flutterテスト環境 (高速) flutter_test
インテグレーションテスト (Integration Test) アプリ全体または主要な機能が、複数のウィジェットやサービスと連携して期待通りに動作するかを検証する。
  • 画面遷移
  • フォーム送信
  • API連携を含むユーザーフロー
  • パフォーマンス測定
実機またはエミュレータ/シミュレータ (低速) integration_test, flutter_driver (古い)

ユニットテストの例 (test パッケージ)


// counter.dart (テスト対象のクラス)
class Counter {
  int _value = 0;
  int get value => _value;

  void increment() => _value++;
  void decrement() => _value--;
  void reset() => _value = 0;
}

// counter_test.dart (テストファイル)
import 'package:test/test.dart';
// import 'package:your_app/counter.dart'; // 実際のパスに置き換える

void main() {
  // テストのグループ化
  group('Counterクラスのテスト', () {
    late Counter counter; // 各テストの前に初期化

    setUp(() {
      // 各テストの前に実行されるセットアップコード
      counter = Counter();
    });

    // 個別のテストケース
    test('初期値は0であるべき', () {
      expect(counter.value, 0); // 期待値と実際の値を比較
    });

    test('incrementメソッドを呼ぶと値が1増えるべき', () {
      counter.increment();
      expect(counter.value, 1);
    });

    test('decrementメソッドを呼ぶと値が1減るべき', () {
      counter.decrement();
      expect(counter.value, -1);
    });

     test('複数回のインクリメントとデクリメント', () {
      counter.increment();
      counter.increment();
      counter.decrement();
      expect(counter.value, 1);
    });

    test('resetメソッドで値が0に戻るべき', () {
      counter.increment();
      counter.increment();
      counter.reset();
      expect(counter.value, 0);
    });

    tearDown(() {
      // 各テストの後に実行されるクリーンアップコード (必要な場合)
    });
  });
}

// 実行コマンド: flutter test test/counter_test.dart
            

ウィジェットテストの例 (flutter_test パッケージ)


// my_widget.dart (テスト対象のウィジェット)
import 'package:flutter/material.dart';

class MyWidget extends StatelessWidget {
  final String title;
  final String message;

  const MyWidget({Key? key, required this.title, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp( // テスト対象ウィジェットをラップする必要がある場合が多い
      home: Scaffold(
        appBar: AppBar(title: Text(title)),
        body: Center(child: Text(message)),
      ),
    );
  }
}

// my_widget_test.dart (テストファイル)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// import 'package:your_app/my_widget.dart'; // 実際のパスに置き換える

void main() {
  testWidgets('MyWidgetがタイトルとメッセージを表示するかのテスト', (WidgetTester tester) async {
    // テスト対象ウィジェットをビルドしてフレームをトリガー
    await tester.pumpWidget(MyWidget(title: 'テストタイトル', message: 'テストメッセージ'));

    // findを使ってウィジェットを検索
    final titleFinder = find.text('テストタイトル');
    final messageFinder = find.text('テストメッセージ');

    // マッチャーを使ってウィジェットが存在するか検証
    // findOneWidget: 1つのウィジェットが見つかることを期待
    // findsNothing: ウィジェットが見つからないことを期待
    // findsWidgets: 1つ以上のウィジェットが見つかることを期待
    // findsNWidgets: N個のウィジェットが見つかることを期待
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);

    // 存在しないテキストの検証
    expect(find.text('存在しないテキスト'), findsNothing);

    // AppBar内のタイトルを正確に見つける
    expect(find.descendant(of: find.byType(AppBar), matching: find.text('テストタイトル')), findsOneWidget);
  });

  testWidgets('ボタンタップのテスト例 (Counterアプリ)', (WidgetTester tester) async {
    // Counterアプリのウィジェットをビルド (例)
    // await tester.pumpWidget(CounterApp());

    // 初期状態の確認
    // expect(find.text('0'), findsOneWidget);
    // expect(find.text('1'), findsNothing);

    // ボタンを見つけてタップ
    // await tester.tap(find.byIcon(Icons.add));
    // フレームを再描画 (setStateが呼ばれた後など)
    // await tester.pump();

    // タップ後の状態を確認
    // expect(find.text('0'), findsNothing);
    // expect(find.text('1'), findsOneWidget);

    // 複数回タップ
    // await tester.tap(find.byIcon(Icons.add));
    // await tester.pump();
    // await tester.tap(find.byIcon(Icons.add));
    // await tester.pump();
    // expect(find.text('3'), findsOneWidget);
  });
}
// 実行コマンド: flutter test test/my_widget_test.dart
            

プラットフォーム連携 📱⇔💻

ネイティブコード (Swift/Kotlin/Java) と連携する

  • MethodChannel: Flutter (Dart) からプラットフォーム固有のメソッドを呼び出し、結果を受け取るための仕組み。非同期。
    • Dart側: MethodChannel.invokeMethod('メソッド名', 引数)
    • iOS (Swift)側: FlutterMethodChannel.setMethodCallHandler で受け取り、result() で応答。
    • Android (Kotlin)側: MethodChannel.setMethodCallHandler で受け取り、result.success()result.error() で応答。
    • 一般的なデバイス機能(バッテリー残量、センサー情報、固有API)の呼び出しに使用。
  • EventChannel: プラットフォーム固有のイベントをFlutter (Dart) 側で継続的に受信するための仕組み。ストリームベース。
    • プラットフォーム側からイベント(センサーの更新、位置情報の変更など)を Dart に送信し続ける場合に使用。
    • Dart側: EventChannel.receiveBroadcastStream().listen() で購読。
    • プラットフォーム側: イベントが発生するたびに EventSink.success()EventSink.error() を呼び出す。
  • BasicMessageChannel: より柔軟なメッセージ送受信のための仕組み。任意のメッセージ(文字列、バイナリデータなど)を双方向に送受信できる。コーデックを指定可能。
  • Platform Views (AndroidView / UiKitView): ネイティブのUIコンポーネントをFlutterウィジェットツリー内に埋め込むための仕組み。地図表示、WebViewなど、Flutterでの実装が難しいまたは不可能な場合に利用。パフォーマンスに影響を与える可能性があるため注意が必要。
  • FFI (Foreign Function Interface): C/C++などのネイティブライブラリの関数を直接Dartから呼び出すための仕組み。低レベルな連携や既存のC/C++コード資産を活用する場合に強力。dart:ffi ライブラリを使用。
  • Pigeon パッケージ: MethodChannelなどの定型コードを型安全に自動生成するコードジェネレータ。Flutterチームが開発。プラットフォーム連携の記述を簡略化し、エラーを減らす。

MethodChannel の基本的な流れ

例: デバイスのバッテリー残量を取得する

Dart側 (Flutter):


import 'package:flutter/services.dart';

class BatteryService {
  // チャンネル名をプラットフォーム側と一致させる
  static const platform = MethodChannel('samples.flutter.dev/battery');

  Future<int> getBatteryLevel() async {
    try {
      // 'getBatteryLevel' メソッドを呼び出し、結果を待つ
      final int result = await platform.invokeMethod('getBatteryLevel');
      return result;
    } on PlatformException catch (e) {
      // プラットフォーム側でエラーが発生した場合
      print("バッテリーレベルの取得に失敗しました: '${e.message}'.");
      return -1; // または適切なエラー処理
    }
  }
}

// 呼び出し側ウィジェット
// final batteryService = BatteryService();
// int level = await batteryService.getBatteryLevel();
// print('現在のバッテリー残量: $level%');
            

iOS側 (Swift – AppDelegate.swift):


import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError("rootViewController is not type FlutterViewController")
    }
    // チャンネル名をDart側と一致させる
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)

    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // メソッド名を確認
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      self?.receiveBatteryLevel(result: result)
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    if device.batteryState == UIDevice.BatteryState.unknown {
      result(FlutterError(code: "UNAVAILABLE",
                          message: "バッテリー情報が利用できません",
                          details: nil))
    } else {
      result(Int(device.batteryLevel * 100)) // バッテリーレベルを 0-100 の整数で返す
    }
  }
}
            

Android側 (Kotlin – MainActivity.kt):


package com.example.my_flutter_app // 自分のパッケージ名に合わせる

import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

class MainActivity: FlutterActivity() {
    // チャンネル名をDart側と一致させる
    private val CHANNEL = "samples.flutter.dev/battery"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            // メソッド名を確認
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel) // 成功結果を返す
                } else {
                    result.error("UNAVAILABLE", "バッテリーレベルを取得できませんでした。", null) // エラー結果を返す
                }
            } else {
                result.notImplemented() // 未実装のメソッドの場合
            }
        }
    }

     private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
        return batteryLevel
    }
}
            

アニメーション ✨

UIに動きを与えて表現力を高める

暗黙的アニメーション (Implicit Animations)

プロパティの変更を検知して自動的にアニメーションするウィジェット群。手軽にアニメーションを導入できる。

  • AnimatedContainer: Container のプロパティ (width, height, color, padding, decoration など) が変更されると、指定した期間 (duration) とカーブ (curve) でアニメーションする。
  • AnimatedOpacity: 子ウィジェットの不透明度 (opacity) をアニメーションさせる。
  • AnimatedPadding: パディング (padding) の値をアニメーションさせる。
  • AnimatedPositioned: Stack 内の子ウィジェットの位置 (top, bottom, left, right) をアニメーションさせる。
  • AnimatedAlign: 子ウィジェットの配置 (alignment) をアニメーションさせる。
  • AnimatedDefaultTextStyle: 子孫のデフォルトテキストスタイル (style) をアニメーションさせる。
  • AnimatedSize: 子ウィジェットのサイズ変更に合わせて自身のサイズをアニメーションさせる。
  • AnimatedSwitcher: 子ウィジェットが切り替わる際にクロスフェードなどのトランジションアニメーションを提供する。
  • TweenAnimationBuilder: より汎用的な暗黙的アニメーションウィジェット。ターゲット値 (tween.end) と期間 (duration) を指定すると、その間の値を補間 (tween) し、builder 関数に渡してUIを構築する。

class ImplicitAnimationDemo extends StatefulWidget {
  @override
  _ImplicitAnimationDemoState createState() => _ImplicitAnimationDemoState();
}

class _ImplicitAnimationDemoState extends State<ImplicitAnimationDemo> {
  bool _isToggled = false;
  double _size = 100.0;
  Color _color = Colors.blue;
  double _opacity = 1.0;

  void _toggle() {
    setState(() {
      _isToggled = !_isToggled;
      _size = _isToggled ? 200.0 : 100.0;
      _color = _isToggled ? Colors.red : Colors.blue;
      _opacity = _isToggled ? 0.5 : 1.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('暗黙的アニメーション')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedContainer(
              duration: Duration(seconds: 1), // アニメーション期間
              curve: Curves.easeInOut, // アニメーションカーブ
              width: _size,
              height: _size,
              color: _color,
              alignment: _isToggled ? Alignment.topCenter : Alignment.bottomCenter,
              child: Center(child: Text('Container')),
            ),
            SizedBox(height: 20),
            AnimatedOpacity(
              duration: Duration(milliseconds: 500),
              opacity: _opacity,
              child: Text('Opacity', style: TextStyle(fontSize: 24)),
            ),
             SizedBox(height: 20),
            // TweenAnimationBuilder の例 (回転)
            TweenAnimationBuilder<double>(
              tween: Tween<double>(begin: 0, end: _isToggled ? 3.14159 * 2 : 0), // 0度から360度へ
              duration: Duration(seconds: 1),
              builder: (BuildContext context, double angle, Widget? child) {
                return Transform.rotate(
                  angle: angle,
                  child: child, // アニメーションしない部分はchildとして渡すと効率的
                );
              },
              child: Icon(Icons.star, size: 50, color: Colors.orange),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}
            

明示的アニメーション (Explicit Animations)

より細かな制御が可能なアニメーション。AnimationController を使ってアニメーションの開始、停止、進行度などを管理する。

  • AnimationController: アニメーションの心臓部。アニメーションの期間 (duration)、状態 (再生、停止、逆再生など)、現在の値 (0.0〜1.0) を管理する。TickerProvider (通常は SingleTickerProviderStateMixin または TickerProviderStateMixin) が必要。
  • Animation: アニメーション中の特定の型の値 (例: Animation<double>, Animation<Color>)。AnimationControllerTween を組み合わせて作成されることが多い。
  • Tween: 開始値 (begin) と終了値 (end) を定義し、その間の値を補間 (lerp または transform) するクラス。例: Tween<double>(begin: 0.0, end: 1.0), ColorTween(begin: Colors.blue, end: Colors.red)
  • Curve: アニメーションの変化率を定義する。例: Curves.linear, Curves.easeIn, Curves.elasticOutCurvedAnimation を使って AnimationController に適用する。
  • AnimatedBuilder: Animation オブジェクトをリッスンし、値が変更されるたびに builder 関数を呼び出してUIを再構築するウィジェット。AnimationController の値に基づいてUIを効率的に更新するのに便利。
  • SlideTransition, FadeTransition, ScaleTransition, RotationTransition, SizeTransition: 一般的なトランジションアニメーションを提供するウィジェット。内部で AnimatedBuilder を利用している。Animation<double>position, opacity, scale などのプロパティに渡す。
  • addListener()setState(): AnimationController にリスナーを追加し、値が変わるたびに setState() を呼んでUI全体を再描画する方法。単純だが、パフォーマンスが重要な場合は AnimatedBuilder の方が良い。

class ExplicitAnimationDemo extends StatefulWidget {
  @override
  _ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}

// TickerProviderStateMixin を追加
class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();
    // AnimationControllerを初期化
    _controller = AnimationController(
      duration: const Duration(seconds: 2), // アニメーション期間
      vsync: this, // TickerProviderを指定
    );

    // TweenとControllerを組み合わせてAnimationを作成
    // カーブを適用する場合はCurvedAnimationを使う
    final curvedAnimation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut // バウンドするカーブ
    );

    _sizeAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(curvedAnimation);
    _colorAnimation = ColorTween(begin: Colors.green, end: Colors.purple).animate(_controller); // カーブなし
    _rotationAnimation = Tween<double>(begin: 0.0, end: 2 * 3.14159).animate(curvedAnimation);

    // アニメーションの状態変化を監視 (オプション)
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // アニメーション完了時に逆再生
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        // 逆再生完了時に順再生
        _controller.forward();
      }
    });

    // アニメーションを開始
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose(); // Controllerを破棄する
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('明示的アニメーション')),
      body: Center(
        // 方法1: AnimatedBuilder を使う (推奨)
        child: AnimatedBuilder(
          animation: _controller, // Controller (または特定のAnimation) をリッスン
          builder: (BuildContext context, Widget? child) {
            return Transform.rotate(
              angle: _rotationAnimation.value, // Animationの現在の値を使用
              child: Container(
                width: _sizeAnimation.value,
                height: _sizeAnimation.value,
                color: _colorAnimation.value,
                child: Center(child: Text('Explicit!')),
              ),
            );
          },
          // アニメーションに影響されない部分はchildに渡す
          // child: const FlutterLogo(),
        ),

        // 方法2: addListener + setState (単純な場合に)
        // child: Transform.rotate(
        //   angle: _rotationAnimation.value,
        //   child: Container(
        //     width: _sizeAnimation.value,
        //     height: _sizeAnimation.value,
        //     color: _colorAnimation.value,
        //   ),
        // ),
        // initState で _controller.addListener(() => setState(() {})); が必要
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isAnimating) {
            _controller.stop();
          } else {
             // 現在の状態に応じて再生/逆再生を切り替え
            if (_controller.status == AnimationStatus.dismissed || _controller.status == AnimationStatus.reverse) {
               _controller.forward();
            } else {
               _controller.reverse();
            }
          }
        },
        child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}
            

Hero アニメーション 🦸

画面遷移時に、異なる画面にある同じ意味を持つウィジェット(画像など)間でスムーズな遷移アニメーションを実現する。

  • 遷移前と遷移後の両方の画面で、対応するウィジェットを Hero ウィジェットでラップする。
  • 両方の Hero ウィジェットに同じ tag プロパティ (一意なオブジェクト、通常は文字列) を設定する。
  • Flutterが自動的に画面遷移中にモーフィングアニメーションを行う。
  • リスト内のアイテムから詳細ページへ画像が拡大しながら移動するような場面でよく使われる。

// 画面1 (リスト表示など)
class Screen1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Screen 1')),
      body: Center(
        child: InkWell(
          onTap: () {
            Navigator.push(context, MaterialPageRoute(builder: (_) => Screen2()));
          },
          child: Hero(
            tag: 'imageHero', // 一意なタグ
            child: Image.network(
              'https://picsum.photos/id/1025/150/150', // サムネイル画像
              width: 150,
              height: 150,
              fit: BoxFit.cover,
            ),
          ),
        ),
      ),
    );
  }
}

// 画面2 (詳細表示など)
class Screen2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Screen 2')),
      body: Center(
        child: Hero(
          tag: 'imageHero', // Screen1と同じタグ
          child: Image.network(
             'https://picsum.photos/id/1025/400/400', // 詳細画像 (同じ画像IDが良いが見た目が変わるようにサイズ違い)
            width: 400,
            height: 400,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}
             

その他 🛠️

知っておくと便利な機能

テーマ設定 (Theming) 🎨

  • ThemeData: アプリ全体の配色 (primaryColor, accentColor/colorScheme)、フォント、コンポーネントスタイルなどを定義するクラス。
  • MaterialApp: theme プロパティに ThemeData を設定して、アプリ全体のデフォルトテーマを適用。darkTheme でダークモード時のテーマも指定可能。themeMode (ThemeMode.system, ThemeMode.light, ThemeMode.dark) でテーマの切り替え方を制御。
  • Theme: ウィジェットツリーの一部に異なるテーマを適用するためのウィジェット。Theme.of(context) で現在のテーマデータにアクセスできる。
  • ColorScheme: マテリアルデザイン3で推奨される、より体系的な色の定義方法。ThemeData.from(colorScheme: ...) で簡単に ThemeData を生成できる。

// main.dart
MaterialApp(
  title: 'Theme Demo',
  theme: ThemeData( // ライトモードのテーマ
    brightness: Brightness.light,
    primarySwatch: Colors.blue, // 基本色 (MaterialColor)
    // colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), // MD3推奨
    fontFamily: 'Georgia', // デフォルトフォント
    textTheme: TextTheme( // テキストスタイルをカスタマイズ
      displayLarge: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
      titleLarge: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic),
      bodyMedium: TextStyle(fontSize: 14.0, fontFamily: 'Hind'),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData( // ボタンのスタイル
        style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue, // 背景色
            foregroundColor: Colors.white, // テキスト色
        )
    ),
     // 他にも appBarTheme, cardTheme など多数
  ),
  darkTheme: ThemeData( // ダークモードのテーマ
    brightness: Brightness.dark,
    primarySwatch: Colors.orange,
    // colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange, brightness: Brightness.dark),
    // ダークモード用のスタイルを定義...
     elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
            backgroundColor: Colors.orange,
            foregroundColor: Colors.black,
        )
    ),
  ),
  themeMode: ThemeMode.system, // OSの設定に合わせる
  home: MyHomePage(),
)

// ウィジェット内でテーマの色やスタイルを使用
// Color primary = Theme.of(context).primaryColor;
// TextStyle? titleStyle = Theme.of(context).textTheme.titleLarge;
// ElevatedButton( ... style: Theme.of(context).elevatedButtonTheme.style ... );
            

国際化 (Internationalization / i18n) 🌍

  • アプリ内のテキストやフォーマット(日付、数値など)を多言語に対応させるプロセス。
  • flutter_localizations パッケージ: Flutter SDK に含まれる、基本的なローカライゼーションサポート(日付/時刻フォーマットなど)。MaterialApplocalizationsDelegatessupportedLocales を設定。
  • intl パッケージ: メッセージ(テキスト)の翻訳、複数形、性別による変化などを扱うための標準的な Dart ライブラリ。
  • arb ファイル (.arb): アプリケーションリソースバンドル。言語ごとにテキストメッセージをキーと値のペアで定義するJSONベースのファイル。
  • コード生成: intl_utils や Flutter の組み込み機能 (flutter gen-l10n) を使って、arb ファイルから Dart コード (AppLocalizations クラスなど) を自動生成する。

// 1. pubspec.yaml に依存関係を追加
// dependencies:
//   flutter:
//     sdk: flutter
//   flutter_localizations: # 追加
//     sdk: flutter
//   intl: ^0.18.0 # バージョンは適宜更新
//
// flutter:
//   uses-material-design: true
//   generate: true # コード生成を有効化

// 2. l10n.yaml ファイルを作成 (プロジェクトルート)
// arb-dir: lib/l10n # .arb ファイルを置くディレクトリ
// template-arb-file: app_en.arb # テンプレートとなる言語ファイル
// output-localization-file: app_localizations.dart # 出力されるDartファイル名
// output-class: AppLocalizations # 生成されるクラス名

// 3. lib/l10n/app_en.arb (英語) を作成
// {
//   "@@locale": "en",
//   "helloWorld": "Hello World!",
//   "@helloWorld": {
//     "description": "The conventional newborn programmer greeting"
//   },
//   "hello": "Hello {userName}",
//   "@hello": {
//     "description": "A greeting with a placeholder for the user's name",
//     "placeholders": {
//       "userName": {
//         "type": "String",
//         "example": "Bob"
//       }
//     }
//   }
// }

// 4. lib/l10n/app_ja.arb (日本語) を作成
// {
//   "@@locale": "ja",
//   "helloWorld": "こんにちは、世界!",
//   "hello": "{userName}さん、こんにちは"
// }

// 5. Flutterプロジェクトのルートでコマンド実行
// flutter gen-l10n

// 6. MaterialApp で設定
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // 生成されたファイルをインポート

MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: [
    AppLocalizations.delegate, // 生成されたデリゲート
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: AppLocalizations.supportedLocales, // サポートするロケールリスト
  // locale: Locale('ja'), // 強制的にロケールを指定する場合
  home: MyHomePage(),
)

// 7. ウィジェット内で翻訳済みテキストを使用
// Widget build(BuildContext context) {
//   // AppLocalizations.of(context)! でインスタンスを取得
//   return Scaffold(
//     appBar: AppBar(
//       // title: Text('Hardcoded Title'), // ハードコードせず
//       title: Text(AppLocalizations.of(context)!.helloWorld), // ローカライズされたテキストを使用
//     ),
//     body: Center(
//       child: Text(AppLocalizations.of(context)!.hello('Alice')), // Placeholder付き
//     ),
//   );
// }
             

デバッグとDevTools 🐛

  • print() / debugPrint(): 最も簡単なデバッグ出力。debugPrint は出力が多すぎる場合にスロットリングされる。
  • ログ (dart:developer log): より構造化されたログ出力。DevTools で確認できる。log('メッセージ', name: 'カテゴリ', level: 1000);
  • ブレークポイント: コードエディタ (VS Code, Android Studio) で設定し、実行を一時停止して変数の値などを確認できる。
  • Flutter DevTools: ブラウザベースの強力なデバッグ・プロファイリングツール。
    • Widget Inspector: ウィジェットツリーの構造、レイアウト、プロパティを確認・デバッグ。レイアウトエクスプローラーも便利。
    • Performance View: アプリのフレームレート (FPS)、CPU/GPU の使用状況をプロファイリングし、パフォーマンスボトルネックを特定。
    • CPU Profiler: DartコードのCPU使用状況を詳細に分析。どの関数に時間がかかっているか特定。
    • Memory View: メモリ使用量、オブジェクトの割り当て、リークの可能性を監視・分析。
    • Network View: HTTP/HTTPS 通信の履歴、詳細(ヘッダー、ボディ、タイミング)を確認。
    • Logging View: printdeveloper.log によるログを表示。
    • App Size Tool: アプリのビルドサイズの内訳を分析。
  • Flutter Doctor: Flutter のインストール状態や接続されているデバイス、依存関係などをチェックし、問題を診断するコマンドラインツール。flutter doctor -v で詳細表示。

コメント

タイトルとURLをコピーしました