Flutter再入門1

Created: Jul 4, 2025 | Lastmod: Jul 4, 2025 min read

初めに

Flutterでいくつかアプリを作ってきたが、あまりちゃんと理解せず感覚的に書いている個所が多くある。そのため、『Flutter実践開発 ── iPhone/Android両対応アプリ開発のテクニック』を読んで、知らなかったことなどを備忘録としてメモしていきたい。

文法

変数宣言

finalについて

final修飾子の場合、利用時までに初期化されていれば問題ない。


  final flag = DateTime.now().hour.isEven;

  final int number; // 宣言時に初期化しない(この場合もfinalで宣言可能)
  if (flag) {
    number = 0;
  } else {
    number = 255;
  }
  print(number);
  • 変数の初期化をDartコンパイラが正しく判断できない場合はlate修飾子でコンパイラのチェックを回避可能
    • グローバル変数の初期化など

final late

一度初期化されたら変更不可。

lateについて

  • 宣言時に初期化処理を記述すると、変数にアクセスされるまで初期化処理を遅延できる
    • 使用されるか分からない変数や、初期化処理の実行コストが高い場合に用いると効果的

組み込み型

文字列

変数の値を挿入する方法

final name = 'Taro';
final message = 'Hello, $name!';
print(message);
式の結果を挿入する方法

final a = 10;
final b = 20;
final message = 'a + b = ${a + b}';
print(message);
文字列リテラルの連結
  • 隣接する文字列リテラルは自動的に連結される(改行しても問題ない)が、+演算子を使ってもいい
複数行の文字列の定義

三重のシングルまたはダブルクオーテーションで囲む。

文字列リテラルの前にrを置くことで、エスケープシーケンスを無視できる

final message = r'Hello\nWorld';
print(message);
// => Hello\nWorld

コレクション

// List
final list1 = [0,1,2,3];
final list2 = [0,1,2,3,];
final list = <int>[0,1,2,3];

// 固定長の配列を作成
final fixedList = List.unmodifiable([0,1,2,3]);

// Set
final set1 = {'A', 'B', 'C'};
final set2 = {'A', 'B', 'C',};
final set = <String>{'A', 'B', 'C'};

// Map
final map1 = {'A': 1, 'B': 2, 'C': 3};
final map2 = {'A': 1, 'B': 2, 'C': 3,};
final map = <String, int>{'A': 1, 'B': 2, 'C': 3};

// 空の{}はMapとして推論される
final map = {};
print(setOrMap is Map); // => true

// Record
// 匿名型、タプル
// 位置フィールド(名前を付けないフィールド)
final record1 = (404, 'Not Found');
final (int, String) record2 = (404, 'Not Found');

// 名前付きフィールド
final record3 = (code: 404, message: 'Not Found');
final ({int code, String message}) record4 = (code: 404, message: 'Not Found');

// 型注釈によって、位置フィールドに名前を付ける
// 名前付きフィールドではない
final (int code, String message) record4 = (404, 'Not Found');

// 等値性
final record1 = (code: 404, message: 'Not Found');
final record2 = (message: 'Not Found', code: 404);
print(record1 == record2); // => true

final (int code, String name) record5 = (404, 'Not Found');
final (int x, String y) record6 = (404, 'Not Found');
print(record5 == record6); // => true

final ({int code, String name}) record7 = (code: 404, name: 'Not Found');
final ({int x, String y}) record8 = (x: 404, y: 'Not Found');
print(record7 == record8); // => false

// 名前付きフィールドと位置フィールドが混在した場合、型注釈では位置フィールドが常に先頭に配置される
final record9 = (code: 404, message: 'Not Found', "ページが見つかりません");

final (String description, {int code, String message}) record10 = record9;

Objectクラス

Dartの全てのクラスはObjectクラスを継承している。

// これはList<Object>として推論される
final list = [0, 'abc', true];
  • dynamicは、コンパイル時に型のチェックが行われない
    • 存在しないメソッドを呼び出してもコンパイルエラーにならない
    • null判断も行われない
  • 基本的にはobjectを使う

カスケード記法

オブジェクトのメソッドやプロパティへドット2つでアクセスすると、そのオブジェクトそのものが戻り値になる。


final sb = StringBuffer()
  ..write('Hello')
  ..write(' ')
  ..write('World');

print(sb.toString());
// => Hello World

C#だと似たようなものでメソッドチェーンがあるが、メソッドチェーンはメソッドの戻り値をオブジェクト自身(this)にする必要があるのに対し、Dartのカスケード記法はその必要がない(writeメソッドの戻り値はvoid)。

コレクションのオペレータ

// Spread演算子
final list1 = [0,1,2];
final list2 = [-1, ...list1, 3];
print(list2);
// => [-1, 0, 1, 2, 3]

// 制御構文演算子
// flagがtrueの場合は3が追加される
final list3 = [0,1,2,if(flag) 3];

// forも記述できる
final list4 = [0, for(var i in list1) i * 2];
print(list4);
// -> [0, 0, 2, 4]

制御構文

if-case-when文

パターンマッチングをした上で変数へ分解する。


// responseのmessageかstatusCodeいずれかがnullの場合はelseに入る
final (String?, int?) response = ('OK', 200);
if (response case (String message, int statusCode)){
    print('message: $message, statusCode: $statusCode');
}else{
    print('response is null');
}

// caseの後whenを付けると条件式を記述できる
final (String?, int?) response = ('OK', 200);
if (response case (String message, int statusCode) when statusCode == 200){
    print('message: $message, statusCode: $statusCode');
}else{
    print('response is null');
}

switch文

  • 途中でswitch文を抜けて後の処理を続けて実行したときだけbreakを使う(毎回のケースの終わりにbreakを書く必要がない)
    • returnやthrowも使える
  • continueとラベルを使うと、ケースの順に関係なく直接フォールスルーできる
switch (color) {
  case 'red':
    doSomethingIfRed();
    continue other;
  case 'blue':
    doSomethingIfBlue();
  other:
  case 'black':
    throw 'Unexpected color'; // redとblackのときに実行される
}
  • whenを使うと、ケースの条件式を記述できる

final int? statusCode = null;
 switch (statusCode) {
   case (int statusCode) when 100 <= statusCode && statusCode < 200:
     print('informational');
   case (int statusCode) when 200 <= statusCode && statusCode < 300:
     print('successful');
   case (int statusCode) when 300 <= statusCode && statusCode < 400:
     print('redirection');
   case (int statusCode) when 400 <= statusCode && statusCode < 500:
     print('client error');
   case (int statusCode) when 500 <= statusCode && statusCode < 600:
     print('server error');
   case (null):
     print('no response received.');
   default:
     print('unknown status code');
 }
  • switchを式として使える
final int statusCode = 0;
final message = switch (statusCode) {
  >= 100 && < 200 => 'informational',
  >= 200 && < 300 => 'successful',
  >= 300 && < 400 => 'redirection',
  >= 400 && < 500 => 'client error',
  >= 500 && < 600 => 'server error',
  _ => 'unknown status code', // デフォルトケース
};
  • コレクションの一致判定ではリテラルにconst修飾子を付与する必要がある

switch (variable){
    case const [0,1,2]:
        doSomething();
    case const {3,4,5}:
        doSomethingElse();
    default:
        doSomethingDefault();
}

ループ

  • Iterableクラスに対してはfor-inやforEachメソッドが使える

パターン

  • 2つの機能がある
    • オブジェクトのマッチング
    • オブジェクトの分解宣言
// マッチング
final name = 'Taro';
switch(name){
    case 'Taro':
        doSomething();
}

// 分解宣言
final record = ('cake', 300);
final (name, price) = record;
print('${name}${price}円です');

// マッチした際に変数にバインドするパターン
// Setは利用不可
final [a, b, ..., c] = [0, 1, 2, 3, 4, 5];
print('a=$a, b=$b, c=$c');
// => a=0, b=1, c=5

// Mapの場合はキーが一致するとバインドできる
final {200: successful, 404: notFound} = {
    200: 'OK',
    404: 'Not Found',
    500: 'Internal Server Error',
};
print('200 -> $successful, 404 => $notFound');
// => 200 -> OK, 404 -> Not Found

// Recordの場合はすべての構造が一致する必要がある
final record = (name: 'cake', price: 300);

// マッチする
final (name: n, price: p) = record;

// 名前付きフィールドの場合は名前まで一致する必要がある
// これはフィールド名がないのでマッチしない
final (String name, int price) = record;

// フィールド名を変数名で推論させる記法もある
// マッチする
final (:name :price) = record;

// クラスもゲッターからマッチ・分解宣言できる
class SomeClass {
    const SomeClass(this.x);
    final int x;
}

final someInstance = SomeClass(100);
final SomeClass(x: number) = someInstance;
// これも同じ(フィールド名を変数名で推論)
final SomeClass(:x) = someInstance;
print(number);
// => 100

// for-inでの分解宣言
final list = [0,1,2,3,4,5];
for (final (index, value) in list.indexed){
    print('index: $index, value: $value');
}
// => index: 0, value: 0
// => index: 1, value: 1

// MapはIterableのサブクラスではない
// entriesプロパティを使うことで、キーとバリューのペアを変数にバインドしてループを回せる(MapEntryをObjectとして分解宣言)
final map = {'A': 1, 'B': 2, 'C': 3};
for (var MapEntry(key: key, value: value) in map.entries){
    print('key: $key, value: $value');
}
// => key: A, value: 1
// => key: B, value: 2
// => key: C, value: 3

パターンを補助する構文

// キャスト
final List<Object> list = [0, 'abc'];
final [number as int, str as String] = list;

// nullチェック
switch (code){
    case final i? when i >= 0:
        print('i is positive');
    case final i?:
        print('i is null');
    default:
        print('i is negative');
}

// nullアサーション
// null時は実行時エラーになる
switch (code){
    case final i! when i >= 0:
        print('i is positive');
    default:
        print('i is negative');
}

// ワイルドカード
final record = ('cake', 300);
final (name, _) = record;

switch (variable){
    case SomeClass _:
        doSomething();
    case String _:
        doSomethingElse();
}

例外処理

例外の型

  • DartにはError型とException型がある
    • Error型
      • プログラムの失敗、プログラム上の問題によりスローされるもの
          • 間違った関数の使い方
          • 無効な引数が渡された
        • 呼び出し元で捕捉する必要なし
    • Exception型
      • 捕捉されることを目的としている
      • エラーに関する情報を持たせるべき
  • 任意のオブジェクトを例外としてスローできるが、推奨されていない

例外の捕捉

try{
    doSomething();
} on MyException{
    print('MyException');
}

try {
  doSomething();
} catch(e, st) {
  print('catch $e');
  print('stackTrace $st');
}

try {
  doSomething();
} on MyException catch(e) {
  print('catch $e');
  rethrow; // 呼び出し元へ例外を再スロー
} finally {
    doCleanUp();
}

assert

assertの第1引数がfalseの場合、プログラム実行が中断された上で、指定している場合は第2引数のメッセージが出力される。

assert(variable != null, 'variable is null');

debugビルドの時だけassert文が処理される性質を利用して、デバッグ時のみ実行したい処理を以下のように記述可能。

assert(() {
  print('debug mode');
  return true;
}());

Flutterの例外処理

void main() {
  FlutterError.onError = (errorDetails) {
    //Flutterのフレームワーク自身がトリガーするコールバック(レンダリング処理やウィジェットのbuildメソッドなど)で発生した例はここにルーティングされる
    //デフォルトではログをコンソールに出力する
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    // それ以外のFlutter内で発生した例外(ボタンのタップイベントハンドラなど)はここでハンドリングする
    print(error);
    return true; // 例外を処理した場合はtrueを返す
  };

  runApp(const MyApp());
}

null関連演算子

str1 ?? = 'new string';と書くと、変数がnullの場合だけ代入される。

comments powered by Disqus