Flutter

Flutter入門

Dartの基礎からWidget、レイアウト、ボタン、状態の更新まで、動く画面を作りながら学びます。

対象:Dart 3を同梱したFlutter 3系のstable版を対象に、まずChromeで動かすWebアプリを作ります。

01

FlutterとDartの違い

Flutterは1つのコードベースから複数の環境向けのアプリを作るためのフレームワークです。コードを書く言語はDartです。SwiftやKotlinを先に覚える必要はありません。この講座ではDartの文法を学んでからFlutterの画面へ進みます。

最初の実行対象はChromeです。AndroidではSDKと端末・エミュレーター、iOSではmacOSとXcodeなどの追加環境が必要です。すべてのOS向けの準備を最初に揃える必要はありません。
02

Flutterを準備する

公式の導入手順で、OSに合ったFlutter SDKとエディターを設定します。VS CodeならFlutter拡張機能を入れます。Chromeもインストールし、ターミナルで次を確認します。Dart SDKはFlutterに同梱されています。

環境を確認COMMAND
flutter --version
dart --version
flutter doctor
flutter devices

doctorは開発環境の診断、devicesは実行先の一覧です。Webで試す段階ではChromeが利用可能かを確認します。Android・iOSの診断が未完でも、Webの準備ができていれば本講座を進められます。

新しいプロジェクトを作成して起動COMMAND
flutter create study_app
cd study_app
flutter run -d chrome
確認する結果OUTPUT
Chromeに生成されたサンプルアプリが表示される

終了は実行中のターミナルでqです。以降のFlutter例はstudy_app内の lib/main.dart を全体ごと置き換えます。flutter run -d chrome で起動し、実行中は保存後にrでホットリロード、Rでホットリスタートできます。

コマンドが見つからない場合はSDKのbinへのPATHを確認します。Chromeが一覧にない場合はChromeの導入と公式Web設定を確認します。生成時のtest/widget_test.dartは元のカウンター用なので、後のテストページで新しい画面用へ置き換えます。
03

プロジェクトの構成

lib/main.dart はアプリの入口、pubspec.yaml は依存パッケージや画像の設定、test/ はテストの置き場です。buildは自動生成される出力なので、そこを編集してもソースの変更にはなりません。

次のDartの基礎例はFlutter画面とは別に試します。study_app直下に basics.dart を作り、各例ごとに全体を置き換えます。

basics.dartDART
void main() {
  print('こんにちは、Dart!');
}
study_app直下で実行COMMAND
dart run basics.dart
実行結果OUTPUT
こんにちは、Dart!

Dartのprintはターミナルへ出力します。FlutterのTextは画面へ文字を表示します。ここを区別して読み進めましょう。

04

Dartの変数と型

var は初期値から型を推論します。final は一度だけ代入でき、const はコンパイル時に決まる定数です。intは整数、doubleは小数、Stringは文字列、boolは真偽値です。文字列には単一・二重引用符を使えます。

basics.dart(全体を置き換え)DART
void main() {
  final String name = 'Mity';
  var age = 20;
  const double height = 170.5;
  final bool isStudent = true;
  age += 1;
  print('${name}さんは${age}歳です');
  print(height);
  print(isStudent);
}
実行結果OUTPUT
Mityさんは21歳です
170.5
true

$変数名 は識別子の範囲が曖昧になる場合があるため、日本語などへ続けるときは ${name} のように波かっこで囲むと確実です。Dartでは文末にセミコロンを付けます。

05

nullと型変換

String? は文字列またはnullです。?. は値がある場合だけアクセスし、?? はnullの代替値を指定します。int.tryParse は変換に失敗するとnullを返すため、入力を安全に扱えます。

basics.dart(全体を置き換え)DART
void main() {
  String? nickname;
  print(nickname?.length ?? 0);
  for (final text in ['20', 'abc']) {
    final age = int.tryParse(text);
    if (age != null && age >= 0) {
      print('来年は${age + 1}歳です');
    } else {
      print('整数を入力してください');
    }
  }
}
実行結果OUTPUT
0
来年は21歳です
整数を入力してください

末尾の ! はnullでないという強制で、実際にnullなら実行時エラーです。まずifや??で値がない場合を処理しましょう。画面からの入力はフォームの発展ページで扱います。

06

演算子と条件分岐

/ は小数の割り算、~/ は整数の商、% は余りです。== は比較、&& は両方が真、|| は少なくとも一方が真です。ifの後の丸かっこに条件を書きます。

basics.dart(全体を置き換え)DART
void main() {
  print(7 / 2);
  print(7 ~/ 2);
  print(7 % 2);
  final score = 82;
  if (score >= 80) {
    print('よくできました');
  } else if (score >= 60) {
    print('合格');
  } else {
    print('再挑戦');
  }
  print(score >= 60 && score <= 100);
  print(score == 0 || score == 82);
  print(!(score < 60));
}
実行結果OUTPUT
3.5
3
1
よくできました
true
true
true

! を真偽値の前に置くと反転になります。nullの強制に使う末尾の!とは位置と役割が異なります。

07

リストと繰り返し

Listは順番のある値で、位置は0から数えます。add で追加し、length で数を調べます。for-inで順に取り出せます。isNotEmpty は空でないかの確認です。

basics.dart(全体を置き換え)DART
void main() {
  final fruits = <String>['りんご', 'みかん'];
  fruits.add('もも');
  if (fruits.isNotEmpty) { print(fruits[0]); }
  print(fruits.length);
  for (final fruit in fruits) { print(fruit); }
  var count = 2;
  while (count > 0) {
    print('残り $count');
    count -= 1;
  }
}
実行結果OUTPUT
りんご
3
りんご
みかん
もも
残り 2
残り 1

回数を指定するforは初期化・条件・更新をセミコロンで区切ります。continueは今回を飛ばし、breakは全体を終了します。

basics.dart(全体を置き換え)DART
void main() {
  for (var number = 1; number <= 5; number += 1) {
    if (number == 2) continue;
    if (number == 4) break;
    print(number);
  }
}
実行結果OUTPUT
1
3
finalのリストでも要素は変更できます。再代入と中身の変更は別です。定数リスト const ["りんご"] は変更できません。範囲外の位置へのアクセスはRangeErrorになります。
08

Map・Set・変換

Mapはキーと値の対応、Setは重複しない要素を扱います。map は各要素を変換し、toList() で結果をリストにします。(value) => 式 は短い関数の書き方です。

basics.dart(全体を置き換え)DART
void main() {
  final stocks = <String, int>{'りんご': 5, 'みかん': 8};
  print(stocks['みかん'] ?? 0);
  print(stocks['もも'] ?? 0);
  final tags = <String>{'Dart', 'Web', 'Dart'};
  print(tags.length);
  final doubled = [1, 2, 3].map((value) => value * 2).toList();
  print(doubled);
}
実行結果OUTPUT
8
0
2
[2, 4, 6]
09

関数と名前付き引数

関数は戻り値の型、名前、引数の順に書きます。波かっこ内の引数は名前付きです。required は必須、= 1 は省略したときの値です。FlutterのWidget作成でもこの書き方をよく使います。

basics.dart(全体を置き換え)DART
int total({required int price, int count = 1}) {
  return price * count;
}
void main() {
  print(total(price: 500, count: 3));
  print(total(price: 500));
}
実行結果OUTPUT
1500
500

文字列の前後を整えるtrimと、含むか調べるcontainsも試しましょう。処理した結果は戻り値で受け取ります。

basics.dart(全体を置き換え)DART
void main() {
  final text = ' Dart '.trim();
  print(text);
  print(text.toUpperCase());
  print(text.contains('art'));
}
実行結果OUTPUT
Dart
DART
true
10

クラスとコンストラクター

クラスは値と処理をまとめます。コンストラクターはインスタンス作成時の初期化です。this.name は受け取った引数を同名プロパティへ保存する省略形です。constコンストラクターは、定数として作れる型に使います。

basics.dart(全体を置き換え)DART
class Product {
  final String name;
  final int price;
  const Product({required this.name, required this.price});
  int total(int count) => price * count;
}
void main() {
  const pen = Product(name: 'ペン', price: 120);
  print(pen.name);
  print(pen.total(3));
}
実行結果OUTPUT
ペン
360
11

最初のFlutter画面

ここからはbasics.dartではなくlib/main.dartへ保存し、flutter run -d chrome で実行します。runAppがWidgetを画面の根元に置きます。MaterialAppはアプリの基本設定、Scaffoldは画面の枠組み、Textは文字の部品です。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';

void main() => runApp(const StudyApp());

class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('学習メモ')),
        body: const Center(child: Text('こんにちは、Flutter!')),
      ),
    );
  }
}
画面で確認する結果OUTPUT
上部に「学習メモ」
中央に「こんにちは、Flutter!」

extends StatelessWidget は状態を自身で管理しないWidgetを定義します。buildは表示内容を返し、@overrideは親のメソッドを実装する印です。super.key はWidgetを識別するキーを親へ渡します。BuildContextはWidgetツリー上の場所を表します。

12

縦・横・余白のレイアウト

Column は縦、Row は横に配置します。children は複数の子、child は1つの子です。Paddingは余白、SizedBoxは間隔を作ります。長い文字はExpandedで横幅に合わせて折り返します。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';

void main() => runApp(const StudyApp());

class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('学習メモ')),
        body: const Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('今日の目標'),
              SizedBox(height: 12),
              Row(
                children: [
                  Text('01'),
                  SizedBox(width: 12),
                  Expanded(child: Text('Dartの変数と型を復習する')),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}
画面で確認する結果OUTPUT
「今日の目標」の下に「01」と学習内容が横並びになる
狭い画面では学習内容が折り返される

Columnに大量の項目を置いても自動でスクロールしません。長い一覧は次の章のListViewや発展ページで扱います。

13

一覧を表示する

ListView.builder は必要な行を作ってスクロール表示します。itemCountは件数、itemBuilderのindexは0から始まる位置です。ListTileは1行分の表示に使えます。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';
void main() => runApp(const StudyApp());
class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    const lessons = ['変数', '関数', 'Widget'];
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('学習一覧')),
        body: ListView.builder(
          itemCount: lessons.length,
          itemBuilder: (context, index) {
            return ListTile(title: Text('${index + 1}: ${lessons[index]}'));
          },
        ),
      ),
    );
  }
}
画面で確認する結果OUTPUT
「1: 変数」「2: 関数」「3: Widget」が縦に並ぶ
14

ボタンと状態の更新

押した回数は変化する状態なので、StatefulWidgetとStateに分けます。setState で状態を変更すると画面の再構築が予約され、buildが新しい値で呼ばれます。onPressedへ渡す関数はボタンを押したときに動きます。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';

void main() => runApp(const StudyApp());

class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: CounterPage());
  }
}

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('学習カウンター')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('学習回数: $_count'),
            ElevatedButton(
              onPressed: () {
                setState(() { _count += 1; });
              },
              child: const Text('1回追加'),
            ),
          ],
        ),
      ),
    );
  }
}
画面で確認する結果OUTPUT
初期表示は「学習回数: 0」
「1回追加」を1回押すと「学習回数: 1」
もう1回押すと「学習回数: 2」

先頭に_が付く名前は同じDartライブラリ内だけから使えます。_countはbuildの外で宣言し、再描画のたびに0へ戻らないようにします。ホットリロードは状態を保つため、0から試すならホットリスタートします。

setStateは保存機能ではありません。アプリを再起動するとカウントは0に戻ります。永続保存は「テストとパッケージ」で扱います。
15

練習問題

問題1:Dartで[60, 80, 100]の平均を求め、空のリストなら「データなし」と表示します。basics.dartへ保存して試してください。

解答例と結果を確認する

まず自分で書いてから、処理の順番と結果を比べましょう。

basics.dart(全体を置き換え)DART
void main() {
  final scores = <int>[60, 80, 100];
  if (scores.isEmpty) {
    print('データなし');
  } else {
    var sum = 0;
    for (final score in scores) { sum += score; }
    print(sum / scores.length);
  }
}
実行結果OUTPUT
80.0

問題2:直前のカウンターへ「リセット」ボタンを追加します。2回追加してからリセットすると0になることを確かめてください。次のコードはchildren内の「1回追加」のElevatedButtonの直後へ追加します。

追加するコードと結果
lib/main.dart:Columnのchildrenへ追加DART
ElevatedButton(
  onPressed: () {
    setState(() { _count = 0; });
  },
  child: const Text('リセット'),
),
画面で確認する結果OUTPUT
「1回追加」を2回押す → 学習回数: 2
「リセット」を押す → 学習回数: 0
16

次に学ぶこと

基礎の次は、以下の4ページを順に進めましょう。各ページに手順、コード例、確認する結果と練習問題があります。

公式ドキュメントで確認する:Flutterの導入 / Dartの言語入門 / Flutterの学習ガイド