FlutterでiOS/Android/Webアプリをビルドする

flutteriosandroidwebdart

FlutterはGoogleによるクロスプラットフォームフレームワーク。 iOS/Androidに加え、今年の3月にリリースされた2.0でWebがstableになり、 Windows/Mac/Linuxはbetaとなっている。ネイティブのUIを用いるReact Nativeと異なり独自のUIで、 MaterialのほかにiOSスタイルのCupertinoも提供されているが、 分岐等しない限りはプラットフォームによらず同じ見た目になる。

環境構築

公式のGet startedに従って環境を構築していく。

まずFlutter SDKをインストールしてパスを通す。

$ mv ~/Downloads/flutter ~/
$ echo 'export PATH="$PATH:~/flutter/bin"' >> ~/.bash_profile 
$ source ~/.bash_profile
$ flutter doctor

Android

Android Studioをインストールし起動して依存コンポーネントをダウンロードする。 cmdline-tools component is missing が出ている場合はSDK Managerからインストールする。

ライセンスを承認する。

$ flutter doctor --android-licenses

AVD Managerからエミュレーターの設定を行う。Emulated PerformanceはHardware - GLES 2.0を選択する。

iOS

Xcodeをインストールして command-line toolsの設定を行い、CocoaPodsもインストールする。

$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch
$ sudo gem install cocoapods

VSCode

Flutter extensionをインストールし、Command PaletteからFlutter: New ProjectでApplicationを選ぶと次の構成のプロジェクトが作られる。

$ tree -L 2 .
.
├── README.md
├── analysis_options.yaml
├── android
│   ├── app
│   ├── build.gradle
│   ├── flutter_application_1_android.iml
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── local.properties
│   └── settings.gradle
├── flutter_application_1.iml
├── ios
│   ├── Flutter
│   ├── Runner
│   ├── Runner.xcodeproj
│   └── Runner.xcworkspace
├── lib
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml
├── test
│   └── widget_test.dart
└── web
    ├── favicon.png
    ├── icons
    ├── index.html
    └── manifest.json

lib/main.dartRun > Start Debugging すると選択されているデバイスでビルドが始まる。 立ち上がりは時間がかかるが、その後はコードを更新するとホットリロードされてすぐに反映される。

Webで実行するとHTMLではなくcanvasで描画されていた。この挙動は–web-rendererによって変えられる。 canvasはプラットフォームによる描画の差異が出ずパフォーマンスも高い一方、ダウンロードサイズが大きくなる。

ファイルの確認

pubspec.yamlに依存パッケージやflutterの設定などが記述されている。依存パッケージを追加してflutter pub getするとインストールされる。

$ cat pubspec.yaml
name: flutter_application_1
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  english_words: ^4.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^1.0.0

flutter:
  uses-material-design: true
  ...

エントリーポイントであるlib/main.dartを見ると次のようなWidgetが定義されている。

$ cat lib/main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
...

StatelessWidgetがあるならStatefulWidgetもあって、文字通りStateを持つ。

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Statebuild()で描画するWidgetを返す。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
    );
  }
}

例えば、Scaffoldのbodyを次のように書き換えるとリストが表示されるようになる。

body: ListView.builder(
  padding: const EdgeInsets.all(16.0),
  itemBuilder: (context, i) {
    return ListTile(title: Text(i.toString()));
  },
  itemCount: 10,
)