《flutter 入門 Vo.1》インストールから起動, 基礎事項の確認(UI構成の裏側処理やツリー, Stateの話など)

12 min read読了の目安(約10800字

はじめに

flutter 始めたいけどよくわからん!との声があったのでインストールからシミュレータでのアプリ起動、基礎事項の確認までをできる記事を書こうと思います。また、なんとなく書いていてなんとなく動かして満足していた時に裏側の処理を勉強して「もっと早く知っておくべきだった、、」と思った基礎(裏側処理)事項も書いています。

macを使っているのでmacOS, iOS環境での実装を前提とした記事になります。

筆者は九州大学システム情報科学府の来年M1になる予定で、システム開発のバイトでフロントやモバイル開発をボチボチやっています。
本格的に開発を始めたのはここ半年くらいのことなので、多少荒い部分やわかりにくい部分があると思いますがご了承ください。
ちなみに、flutter は公式リファレンスやチュートリアルが充実しているのでもっと詳しく知りたい!や何言ってるのかわからん!と思った場合は一旦公式を参照してみてください。

では早速以下の手順で進めていこうと思います。

  1. flutter のインストール
  2. Simulator でのアプリ起動
  3. ツール設定
  4. アプリの構造の説明
  5. サンプリコード解説

1. flutter のインストール

公式サイトから手順に従って環境に合ったものをインストールしてください。

flutter doctor -v

を実行するとセットアップ完了のためにインストールする必要のあるものの詳細情報が出てきます。connected device を除いて全てに緑のチェックマークがつけばセットアップ終了です。

2. Simulator でのアプリ実行

Simulator の立ち上げ

open -a Simulator

でiOS Simulator の起動ができます。

flutter アプリの作成

適当なディレクトリを作り

flutter create my_app_name

を実行するとmy_app_name というアプリが作成されます。

シミュレータが起動されていることを確認して、

cd my_app_name
flutter run

でアプリが起動します。

または、VS Code の場合、シミュレータが起動している時に Run tab から実行することもできます。

console で r または VS Codeの⚡️マークで Hot Reload ができます。これは、エディタの変更点を即座に反映させることができる機能です。開発スピードが最速になります。神機能。
console で R または VS Codeの🔄マークで Restart ができます。これは Hot Reload と違い、アプリ起動のスタート時に戻ります。起動時までリセットしたい時にこれを使います。

実機にインストールしたい場合には, Xcode でApple ID や Apple developer Account の設定が必要となりますので、公式サイトに沿って設定を進めてください。

3. ツール設定

Editor 設定

VS Code の Install Extensions から flutter と検索し拡張機能をインストールしておくといろいろと便利です。

Effective Dart の導入

https://pub.dev/packages/effective_dart

以下、導入手順です。

  • flutter プロジェクトの pubspec.yaml を開き、以下を追記。
dev_dependencies:
  effective_dart: ^1.3.0

VS Code で保存をすると自動でパッケージをインストールしてくれます。

  • 同階層に analysis_options.yaml を作成し以下を追記。
include: package:effective_dart/analysis_options.yaml

実際にコードを書き始めると、青い波線が引かれることがあります。この時に該当コードにカーソルをあてるとより良い書き方を示してくれます。グレーの下線が引かれているところからサイトに飛ぶことができ、良い書き方と悪い書き方の例を確認できます。
また、カーソルを合わせた状態で command + . で変更の提案を確認でき、選択すると自動で修正してくれます。すごい。

4. アプリ構造説明

基礎事項

flutter では Widget と呼ばれる部品をツリー状に組み合わせてUIを表現します。flutter が画面を構築する時には以下の3つのツリーが関係しています。実際コードを書いたり操作するのは Widget のみなので Element や RenderObject は表には出てきません。ですがこれらの動作を知っておくことで、後々効率良い書き方ができるようになると思います(なりたい)。よくわからん!と思った場合は Widget 以外はすっ飛ばしても大丈夫です。

  • Widget Tree: UI のレンダリングに必要な情報の保持、設計図。主にこれを記述していく。
  • Element Tree: Widget と RenderObject の仲介役。状態を保持する。
  • RenderObject: 実際に UI のレイアウトと描画を行う。

以下それぞれについて詳しくみていきます。

Widget

Widget は UI の描画に必要な情報を保持し設計図の役割を果たしている immutable(不変) なオブジェクトです。自身と1対1のペアとなる状態管理をしてくれる Element の生成と、子 Widget の build も行います。また、Widget は再生成するためのコストが低く、頻繁に生成と破棄が行われます。
主に以下の2種類の Widget を使います。

StatelessWidget

StatelessWdiget とは、 State (状態) を持たない Widget で 別の Widget を返す build メソッドを持っています。Widget 自身のデータや Element が持つデータ以外に依存することなく UI を構築する時に使います。例えば、チュートリアル画面や、「ログイン」 か 「新規登録」 かを選択するだけの画面などの他の Widget から影響を受けない画面構成の場合です。よく 「静的」 な Widget を作りたい時に使われると表現されますが、必ずしも静的ではなく、自身や Element のデータが変更されるとリビルドされ、新しい UI を再構成します。

StatefulWidget

StatefulWidget は Widget に State の概念を追加したものであり、状態を保持しながら UI の描画をすることができます。StatelessWidget と違い、 StatefulWidget 自身は build メソッドを持たず、 createState メソッドで State と呼ばれるクラスを生成して、その State が Widget を返し、 Element とペアになり情報を参照します。State 内で setState が呼ばれると、その State 以下の Widget サブツリー全体がリビルドされます。これにより、ユーザ操作やタイマーなどによる値の変化を読み取ることで動的に UI の再構成を行うことができます。

上記2点の画面のリビルド周りについては後ほど詳しく説明します。

Element

Element は Widget と 1対1に紐づいた、UI の状態や生成した Widget への参照(ツリーを辿って親子 Widget 情報を取得する能力)を保持する mutable (可変) なオブジェクトです。親 Widget により子 Widget が build されると、その子 Widget とペアになる Element が生成され、 子 Element は 親 Element への参照を持ちます。Widget 自体には他の Widget への参照を持っていないので、ペアとなる Element から参照情報を取得します。Element は BuildContext と表現され、開発者は Element を直接参照するのでなく、BuildContext を通して参照を行います。親子関係の参照ができるのはこの Element ツリーだけです。

RenderObject

RenderObject は実際に UI のレイアウトや描画を行う mutable なオブジェクトです。この RenderObject が Widget の設計図をもとに実際に手を動かして UI をつくっているイメージです。Widget と同様、RenderObject 自身は親子関係を持たず、Element から参照を行います。

画面構成時の動き

UI 生成

Widget, Element, RenderObject について説明しましたがいまいちわからないと思います。そこで、flutter create で自動生成されるサンプルプログラムの動きをみながら説明をしていきます。
サンプルプログラムは以下のような画面で、右下のボタンを押すとカウントが1つずつアップしていくというものです。
screen shot

実際に画面が構成される順番と 、setState が呼び出された時の動きを順番に追っていきましょう。複雑な説明は避るためRenderObject は除き、大まかな部分だけをピックアップしています。

まずは画面が構成されるフローの説明です。

  1. MyApp が Widget Tree の root におかれる
  2. MyApp の Element が生成される
  3. MyApp が 子 Widget である MyHomePage を buildする
  4. MyHomaPage は Stateful Widget であるため子 Widget の build はせず、 createState()State を生成する
  5. State の Element が生成される
  6. State が 子 Widget である MyHomePageState を生成する
  7. MyHomePageState の Element が生成される
  8. 以下繰り返し

各 Element は RenderObject を生成し、Flutter はこれらの RenderObject を参照して、スクリーンに描画を行います。以上のフローで UI 画面が生成されます。

setState 呼び出し

次にボタンを押した時の挙動についてです。図中の赤い矢印がフローになります。

  1. ボタンを押すと カウントアップを行う関数内で setState が呼び出される
  2. State が数字を格納している変数: _counter を0から1に更新する
  3. Element が更新を読み取り、State 以下のサブツリーを再生成する
  4. 0を表示していた Text Widget が破棄され、1を表示する Text Widget が再生成される。
  5. 再生成された Widget をもとに Element, RenderObject が再生成される

各 Tree の要素はキャッシュされており、同一のものであれば再生成しない仕組みになっています。再生成コストが比較的高い Element や RenderObject はできるだけ再生成せずに再利用されます。

以上が画面生成と setState が呼び出された時の処理になります。

サンプルコード解説

上記のサンプルアプリのコードを部分に分けて解説していきます。

MyApp

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

StatelessWidge を引き継いだ MyApp では MaterialApp を build しています。BuildContext とは先述の通り、Elementです。context を通じて親子関係の情報を参照します。 MateirlaApp をトップレベルに使用することで、様々な flutter の Widget や機能が使用できるようになります。もし、黒い画面で赤文字に黄色い下線のテキストが現れたら、MaterialApp で全体がラップされていることを確認しましょう。MaterialApp ではタイトルやカラーなどのテーマ、子の Widget を指定する home などの要素があります。Widget にカーソルを当てると指定できる要素の一覧が表示されます。

material 要素を使うために全体を MaterialApp でラップする!

次は子 Widget の MyHomePage の説明です。

MyHomePage

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

  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

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

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

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

MyHomePage クラスは StatefulWidget を引き継いでいるので、状態の管理を行います。title を引数として受け取っているのでコンストラクタで初期化をしています。Key というのは簡単に説明すると Widget を識別するための ID です。Key は指定しない場合、null となります。Key についてはこちらの記事で詳しく解説されているため、一度目を通しておくと良いかもしれません。 MyHomePage は 子 Widget を build するのではなく createState() を呼び出して State クラスを生成します。この Widget の親要素が再生成され、State 以下のサブツリーが同じタイプ(同じキー)の Widget を使用する時、新しいState オブジェクトを生成する代わりに State オブジェクトを再利用します。

_MyHomePageState の中には動的な変更を含む UI や関数を記述します。以下の画面の各部品について上のコードを追いながら説明していきます。

カウンターの変数宣言とカウントアップをする function です。

int _counter = 0;

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

アンダースコア(_)をつけることでプライベートな変数宣言ができます。型も同時に定義しておきましょう。ここで _incrementCounter() 内部の setState() に注目してください。setState()が呼び出されると、State で変更されたことを Element に伝え build メソッドを再実行することで Widget が再生成され、更新された値を反映します。試しに、setState() を除外してみると、ボタンを押しても表示が変わらないことを確認できると思います。あれ?表示が変わらない、と思ったら対象の変数への動作が setState() でラップされているかを確認してみてください。

動的に要素を変更したいときは setState() の内部で変更を記述 !

UI を作っている Widget 部分です。


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

まず、ここで使われている Widget を軽く紹介しておきます。

  • Scaffold というのは「足場」という意味で、アプリの基本の骨組みとなります。要素として、ヘッダー部分の AppBar、メイン部分の body、ボタンを表示させる floatingActionButtion など他も様々なものがあります。
  • Center: child で指定される Widget を中央寄せします。
  • Column: children に含まれる Widget を縦に並べます。 mainAxisAlignment で Widget を揃える位置を指定できます。
  • Text: 指定された文字列を表示します。style でフォントサイズや色などを指定できます。
  • FloatingActionButton: 浮いているような見た目のボタンを表示します。onPressed でボタンを押した時に実行する関数を指定します。 childで中に含める情報を指定します。

各 Widget には上記以外の要素があります。詳しくは公式の Widget 一覧をみてください。

https://flutter.dev/docs/development/ui/widgets

以上を組み合わせてサンプルアプリの画面が構成されています。

まず、Scaffold でヘッダー部分の appbar とメイン部分の body を指定しています。body には Center が指定されており、子 Widget を全て中央に寄せています。 Centerchild で指定されいる Column Widget では指定された複数の Wdiget の List が縦に並べられます。 mainAxisAlignment.center となっているので、縦方向の位置揃えが中央から始まっています。これを mainAxisAlignment.start に変更すると、画面上部から並ぶことを確認できると思います。この子 Widget には2つの Text が並べられており、静的な説明文と、State により状態管理されている変数を表示させます。ボタンが押されることでこの変数が変わり、 Widget が再生成されることで表示が即座に反映されます。

以上が簡単な説明になります。サイズの変更や他の要素の追加など、自分でもいろいろ試してみてください。

おわりに

ざっとインストールから起動、基本事項までを書きました!(書いたつもり)
正直 Element や RenderObject のことは知らなくても開発はできますが、いつか壁にぶち当たって勉強することになるので、最初からなんとなくの処理を頭に入れておくと、実装したい処理のアテをつけやすくなりますし、より品質の良いアプリを作ることができると思います。
今回は自動生成のサンプルコードの解説のみだったので、次回は基本 Widget や基本機能を抑えたデモアプリを作りながら解説する記事を描こうと思います。

以下に参考にさせていただいた記事のリンクを貼っておきますので、ぜひ下記の記事から詳しい内容を見てみてください。
読んでいただきありがとうございました!

参考リンク

https://medium.com/flutter-jp/dive-into-flutter-4add38741d07
https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-24-flutter-rendering/
https://zenn.dev/chooyan/articles/77a2ba6b02dd4f
https://zenn.dev/chooyan/articles/cea8f55d9bbeec#fnref1
https://medium.com/flutter-jp/state-performance-7a5f67d62edd