フロントエンドエンジニアがFlutterを触ってみた

この記事は[Flutter Advent Calendar 2019](https://qiita.com/advent-calendar/2019/flutter)の24日目の記事です。
投稿が遅くなってすみません。

普段はWebのフロントエンドをやっていますが、最近、Flutterをよく聞くのでちょっと触ってみようかと思って勢いでアドベントカレンダーに登録しました！Flutterの知識は名前聞いたことあるくらいでほぼゼロです。

とりあえず、インストールしてサンプルアプリを少し改変してみたという内容の記事ですが、フロントエンドエンジニアの視点から書いてみるので参考になれば幸いです。

# 作業環境

- macOS High Sierra
- Android Studio 3.5.3 

# インストール

[オフィシャルの手順](https://flutter.dev/docs/get-started/install/macos)を参考に構築しました。

必要なシステム要件などが書いてありますが、Macだとほとんど標準で入ってるもののようなので、特にこの時点では他に何かをインストールしたりはしませんでした。

SDKをダウンロードして解凍。公式に倣って `development` フォルダを作成してそこに配置しました。

```bash
$ mkdir ~/development
$ cd ~/development
$ unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip
```

パスを通します。 `.bash_profile` に以下の記述を追加。

```.bash_profile
export PATH=$PATH:~/development/flutter/bin
```

ターミナルを再起動。

```bash
$ exec $SHELL -l
```

以下のコマンドを叩いて `/Users/{$USER}/development/flutter/bin/flutter`　が返って来ればOK。

```bash
$ which flutter
```

`flutter doctor` というコマンドを叩くと、関連するツールの状況が表示されるようです。
環境が整っている項目は[✓]で、ない箇所については [✗] か [!] と表示されます。

```bash
$ flutter doctor
```

細かい説明のところは削ってますが、僕の環境ではこうなりました。

```bash
$ [✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.13.6 17G65, locale ja-JP)
$ [✗] Android toolchain - develop for Android devices
$ [✗] Xcode - develop for iOS and macOS
$ [!] Android Studio (not installed)
$ [!] VS Code (version 1.41.1)
$ [!] Connected device
```

Xcodeはインストールされているのですが、古いバージョンのために[✗]になっているのかと思います。
諸事情でOSのバージョンが古いため最新のXcodeがインストールできず今回は諦めました。

iOS開発も試したかったのですが、今回はAndroidの開発を試しすので[Android studio](https://developer.android.com/studio/install?hl=ja)をインストールしました。

もう一度 `flutter doctor` を実行してみると

```bash
$ [✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.13.6 17G65, locale ja-JP) 
$ [!] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    ✗ Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
$ [✗] Xcode - develop for iOS and macOS
$ [!] Android Studio (version 3.5)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
$ [!] VS Code (version 1.41.1)
$ [!] Connected device
```

`Android toolchain` はライセンスの問題で [!] になっているようだったので

```bash
$ flutter doctor --android-licenses
```

でライセンスに同意。

Android StudioはFlutterのプラグインが必要なようだったので、Android Studioを立ち上げて、Preference →　plugin →　Flutter　でプラグインをインストール。この時に、Dartのプラグインも必要と聞かれるので一緒にインストールします。

ここまでやると、この状態になりました。

```bash
[✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✗] Xcode - develop for iOS and macOS
[✓] Android Studio (version 3.5)
[!] VS Code (version 1.41.1)
    ✗ Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[!] Connected device
    ! No devices available
```

`VS Code` は今回は使わないのでOK。 `Connected device` もエミュレータを使うのでOKです。

これで開発する準備ができたので、Android Studioを立ち上げます。

# プロジェクト作成

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/f442e59b-8e0f-fba4-2865-b3c210265316.png)

Start a new Flutter project を選択。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/0808b019-28b8-e322-cfde-1cda8432137b.png)

Flutter Applicationを選択。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/257c8b99-f898-c893-1cfc-68546fd170f1.png)

プロジェクト名や保存場所を設定。今回はそのまま進めました。

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/ae8028ef-939b-eb25-9e9d-2407b0c4c485.png)

Company domainは アプリのリリースの時にバンドルIDとして使われるもののようなので、今回はそのままexample.comで進めます。

![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/628d502a-859c-e5b0-2d36-c0c8e13472bb.png)

プロジェクトが立ち上がりました！

# エミュレータの設定

![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/d4ae0422-d93d-8076-5c7b-4634c333cd24.png)

メニュー → Tools → AVD Managerを選択。
Your Virtual Devicesという画面が立ち上がるのでCreate Virtual Deviceからデバイスを追加します。

![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/75cca385-0cb9-d0aa-a39a-5235882b35d9.png)

![09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/c805b4b6-3907-33a0-abaa-70fade949cec.png)

端末を選択してシステムをダウンロード。
途中、デフォルトの端末の向きの設定やライセンスの同意など求められましたが、全てデフォルトの設定で進めました。

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/04d9ccfc-c8ae-83fd-40af-c2013f45ada5.png)

Your Virtual Devicesの画面に追加した端末が表示されているので、右側の再生ボタンを押すとエミュレータが立ち上がります！

![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/0fe5dd01-5862-f2bc-f86e-379eb69a2110.png)

Android studioの上部のツールバーで `Android SDK built for x86(mobile)`が選択できるようになっているので選択。
`main.dart` を選択して再生ボタンを押すとサンプルアプリが起動します。

![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/177c4966-075f-7219-bf8e-14b594d84e09.png)

初回の実行は、数分時間がかかりました。

# サンプルアプリを触ってみる

Webのフロントエンドは、HTML/CSS/JSを使って書きますが、FlutterはDartだけで書いていくようです。
サンプルアプリの `main.dart` はコメントがたくさん入っているのですごく複雑に見えましたが、コメントを削除するとこんな感じ。

```main.dart
import 'package:flutter/material.dart';

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

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

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

このサンプルアプリが何をやっているのかは、[Flutter はじめの一歩](https://medium.com/flutter-jp/first-step-9b7f2c74fb08)がめちゃくちゃ詳しく書いてくれています。

この記事によると `Widget(UIレイアウトの設計図)を組み合わせてUIの階層構造(HTMLタグの入れ子に近い)を構築していくというのが本質` と書いてあって、WidgetがHTMLでいうタグみたいなものかなと思いました。

実際にIDEの右側の `Flutter Inspector` で `Widgets` のタブを見てみると、なるほど、かなりHTMLっぽいです。ここでだいぶ抵抗が薄れました。

![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/3d682839-61e5-7e8c-3805-16c16760ee2d.png)

## Widetを追加してみる

テキストを増やしてみました。

```main.dart
children: <Widget>[
    Text(
      'これはFlutterのサンプルアプリです',
    ),
    Text(
      'You have pushed the button this many times:',
    ),
    Text(
      '$_counter',
      style: Theme.of(context).textTheme.display1,
    ),
],
```
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/0091a18a-738e-ca35-ee57-25f3279df4ea.png)

ここは思った通りの挙動。
ホットリロードで、保存するだけで反映してくれるのが便利です！

##  テキストのスタイルを変えてみる

要素の追加ができたので、スタイルを変えてみます。

```main.dart
Text(
	'これはFlutterのサンプルアプリです',
	style: TextStyle(
		fontSize: 24,
		fontWeight: FontWeight.bold,
		color: Colors.red
	)
),
```

![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/31065648-671b-f94e-0381-965dcab6265e.png)


Text Widgetの引数にCSSっぽい感じで記述すれば変更できるようです。
とはいえ、プロパティの記述の仕方がわからなくて、ここですごく時間かかりました。

ですが、これだとインラインスタイルを書いているような感じなので、使いまわすことを考えるとclassのようにしたい。
オブジェクト(?)っぽい変数を作って、それを渡せば良いみたいです。

```main.dart
Widget build(BuildContext context) {

	var largeText = TextStyle(
	    fontSize: 24,
	    fontWeight: FontWeight.bold,
	    color: Colors.red
	);

	// 省略

	Text(
      'これはFlutterのサンプルアプリです',
      style: largeText
    ),

}
```

デフォルトで入っているカウンターの大きい文字のところは、最初に読み込んでいるマテリアルデザイン用のUIコンポーネントから参照しているようです。

```main.dart
style: Theme.of(context).textTheme.display1,
```

## 機能を追加してみる

サンプルは、数字を増やすカウンターアプリですが、減らす機能と、0にリセットする機能を追加してみました。
とりあえずボタンを追加します。ありそうだなと思って、`Butto` まで打ったら色々と候補が出てきたので `FlatButton` というのを使ってみました。

![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/f57f770e-0e41-3c64-759b-bd9de406f8b7.png)

```main.dart
child: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Text(
      'これはFlutterのサンプルアプリです',
      style: largeText
    ),
    Text(
      'You have pushed the button this many times:',
    ),
    Text(
      '$_counter',
      style: Theme.of(context).textTheme.display1,
    ),
    FlatButton(
      color: Colors.blue,
      child: Text(
        '足す',
        style: TextStyle(
          color: Colors.white
        )
      )
    ),
    FlatButton(
      color: Colors.blue,
      child: Text(
        '引く',
        style: TextStyle(
          color: Colors.white
        )
      )
    ),
    FlatButton(
      color: Colors.blue,
      child: Text(
        'リセット',
        style: TextStyle(
          color: Colors.white
        )
      )
    )
  ],
),
```

こんな感じになりました。

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/8ad2bc2a-416d-8afe-c526-cb031e6e9086.png)

ちなみに `Row` というWidgetがあるのを確認したので、横並びのボタンにできるかなと思い、こんな感じで書いてみたのですがエラーになりました。この辺はもうちょっと書き方を理解しないとダメですね…

```main.dart
body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text(
        'これはFlutterのサンプルアプリです',
        style: largeText
      ),
      // …
    ]
  ),
  child: Row(
    children: <Widget>[
      FlatButton(
        color: Colors.blue,
        child: Text(
          '足す',
          style: TextStyle(
            color: Colors.white
          )
        )
      ),
      // …
    ],
  ),
)
```

それぞれのボタンに関数をセットしていけばできるだろうというところで、元々のフローティングボタンに記載があったこの部分を参考にします。

```main.dart
int _counter = 0;

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

// …

floatingActionButton: FloatingActionButton(
  onPressed: _incrementCounter,
  tooltip: 'Increment',
  child: Icon(Icons.add),
), 
```

`_incrementCounter` という関数があって、フローティングボタンの `onPressed` に設定してるみたいですね。
この辺は、HTML要素に対してJSのイベントを設定しているような感じでわかりやすいです。

これを参考に、関数を作ってそれぞれのイベントに設定します。

```main.dart
void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

void _decrementCounter() {
  setState(() {
    _counter--;
  });
}

void _resetCounter() {
  setState(() {
    _counter = 0;
  });
}

// …

FlatButton(
  onPressed: _incrementCounter,
  color: Colors.blue,
  child: Text(
    '足す',
    style: TextStyle(
      color: Colors.white
    )
  )
),
FlatButton(
  onPressed: _decrementCounter,
  color: Colors.blue,
  child: Text(
    '引く',
    style: TextStyle(
      color: Colors.white
    )
  )
),
FlatButton(
  onPressed: _resetCounter,
  color: Colors.blue,
  child: Text(
    'リセット',
    style: TextStyle(
      color: Colors.white
    )
  )
)
```

![19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/27c75856-df4e-4ac5-1d69-7962729ce8ff.gif)

無事に動きました。 関数の中にある `setState()` は状態を更新してくれる関数のようです。
消した場合、内部的に値は変わってるけど画面に反映されませんでした。

### デバッグのためにログを出したいときは？

`print()` でConsoleに表示することができるようでした。

![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531/9173ba70-b89f-daba-d4bb-b71c3bf2d56e.png)

# コードまとめ

```main.dart
import 'package:flutter/material.dart';

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

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

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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

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

  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  void _resetCounter() {
    setState(() {
      _counter = 0;
    });
  }

  @override
  Widget build(BuildContext context) {

    var largeText = TextStyle(
        fontSize: 24,
        fontWeight: FontWeight.bold,
        color: Colors.red
    );

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'これはFlutterのサンプルアプリです',
              style: largeText
            ),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            FlatButton(
              onPressed: _incrementCounter,
              color: Colors.blue,
              child: Text(
                '足す',
                style: TextStyle(
                  color: Colors.white
                )
              )
            ),
            FlatButton(
              onPressed: _decrementCounter,
              color: Colors.blue,
              child: Text(
                '引く',
                style: TextStyle(
                  color: Colors.white
                )
              )
            ),
            FlatButton(
              onPressed: _resetCounter,
              color: Colors.blue,
              child: Text(
                'リセット',
                style: TextStyle(
                  color: Colors.white
                )
              )
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

# まとめ・所感

本当はTodoアプリくらいまで作りたかったのですが、思ったより時間がかかってしまったのと記事も長くなってしまったので、また別で書こうと思います。

まだ、ちょっと触ったくらいで機能なども全然使えてませんが、Widgetを組み合わせて構築していくところに慣れれば、僕のような普段はフロントエンドをやってるエンジニアでも簡単なアプリだったら作れそうな気がしたので、もうちょっと触ってみようと思います。