Flutter入門[環境構築〜Todoアプリ]

たまたまFlutterを利用することになったのでこのテーマで投稿します。
今回はVSCodeとiOSシミュレータを使って入門レベルの説明を行います。


# 環境
macOS

インストールするもの（すでに入っている方は先へ）
Flutter SDK
VSCode
XCode


# Flutterとは
![スクリーンショット 2019-12-02 22.05.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/edc7a355-73ba-ead4-0ed3-7ca4e5b6dc74.png)

https://flutter.dev/
> Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Flutterは、単一のコードベースからモバイル、ウェブ、デスクトップ向けの、美しくネイティブにコンパイルされたアプリケーションを構築するためのGoogleのUIツールキットです。

# 環境構築
### インストール
インストールに関しては、公式のやり方を参考にすれば特に困ることはないと思います。


使用するエディタの候補は Android Studio、IntelliJ、VSCode などがあるみたいですが、私は普段VSCodeを利用しているので、そちらで進めていきます。

https://flutter.dev/docs/get-started/install
で自分のOSを選択し、FlutterSDKをインストールします。
解凍したflutterフォルダを ~/Developer 以下に移動させます。

ターミナルで`vim ~/.bash_profile`として、

```[~/.bash_profile]
export PATH="$PATH:/Users/[USER NAME]/Developer/flutter/bin"
```
を書き込み、`source ~/.bash_profile`で読み込みます。
ターミナルで`flutter --version`コマンドを入力して、バージョンが表示されればOKです。

flutterには`flutter doctor`コマンドがあり、これで、開発環境構築の状況がわかります。
何をしなくちゃいけないかが書いてあるので、指示通りやれば問題ないです。
例↓

```
[✗] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
```
### Xcodeインストール
XCodeがない方は、`flutter doctor`で

```
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
```
とある通り、Xcodeをインストールします。
gitが入っていないときはインストールするよう言われるかもしれませんので指示に従ってインストールしましょう。

### VSCode設定
VSCodeがない方は https://code.visualstudio.com/ からVSCodeインストールします。
VSCode内の左下のボタンをクリックして Flutter と検索し、一番上に出てくる拡張機能をインストールします。
<img width="300" alt="スクリーンショット 2019-12-02 22.10.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/0e30a49a-1d6a-18e8-3c70-9c4ebf826a94.png">

これで、Flutterが使えるようになりました。
Flutterの拡張機能に加えて、Dartの拡張機能を入れてもいいと思います。


# サンプルアプリを動かす
私は ~/flutter_apps というディレクトリを作りました。この中にアプリを作っていきます。

```
$ flutter create first_app
```
でアプリの雛形を作成します。
完了したら first_app フォルダをVSCodeで開きます。

lib/main.dartの中にはすでにコードが書かれているので、シミュレータで実行してみます。
<img width="947" alt="スクリーンショット 2019-12-03 14.42.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/b06b0fe7-a4b1-492d-9590-0809963d336b.png">

デバッグの開始、Start iOS Simulator をクリックします。（もしくはF5）
 

<img width="250" alt="スクリーンショット 2019-12-02 22.10.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/e3db8c22-2d73-0933-a4bd-e845796da620.png">

こんなカウンターアプリが立ち上がりました。プラスボタンをタップするとカウントが+1されます。

# Hello World

main.dartを以下の様に書き換えると

```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(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First App'),
      ),
      body: Center(
        child: Text('Hello World'),
      ),
    );
  }
}
```

<img width="250" alt="スクリーンショット 2019-12-02 22.10.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/15038aaa-cccc-c69c-d6b2-79d1ccd7d83f.png">

Hello World が表示されました。Flutterには Hot Reload があるので、コードがすぐ画面に反映されていいですね。開発がサクサク進みそうです。

コードの説明を簡単にしてみます。

```dart
void main() => runApp(MyApp());
```
一行目のこれは

```dart
void main() {
  runApp(new MyApp());
}
```
と同じ意味です。main関数内で MyAppインスタンスを引数としてrunApp関数を呼んでいます。
インスタンス生成時の「new」は書かなくてもいいみたいです。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}
```
MyAppクラスはStatelessWidgetクラスを継承しています。
StatelessWidgetクラスとはなんでしょう。
https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html

公式ドキュメントには詳しい説明がありますが、ここでは簡単に、
state(状態)がなく、immutableな(再描画が必要ない)Widgetと考えます。
（もし状態を持たせたいなら StatefulWidgetを使うことを検討しましょう。）

ちなみにWidget（ウィジェット）とは、コンピュータの操作画面を構成する、何らかの機能を持った表示・操作要素のことです。

StatelessWidgetはbuildメソッドを持っています。これでWidgetクラス等を返すようです。
上のコードでは、MaterialApp Widgetを返しています。

Material Widgetの引数で、MyHomePageインスタンスが呼ばれています。
続いてMyHomePageクラスを見てみます。

```dart
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First App'),
      ),
      body: Center(
        child: Text('Hello World'),
      ),
    );
  }
}
```

こちらもStateless Widgetを継承しているのでbuildメソッドを持っています。
同じように、Scaffold Widgetを返しており、引数で AppBar、Center Widgetが呼ばれています。

Flutterではこんな感じでWidgetの中でWidgetを呼んで、入れ子状にして構築するようです。

# Todoアプリ

ここから、追加だけの簡単なTodoアプリを作りたいと思います。
main.dartを書き換えます。

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller = TextEditingController();
  final List<TodoItem> _items = List();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todo App'),
      ),
      body: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              Expanded(
                flex: 3,
                child: TextField(
                  controller: _controller,
                ),
              ),
              Expanded(
                flex: 1,
                child: RaisedButton(
                  child: Text("追加"),
                  color: Colors.orange[600],
                  textColor: Colors.white,
                  onPressed: () {
                    setState(() {
                      _items.add(TodoItem(content: _controller.text,));
                      _controller.clear();
                    });
                  },
                ),
              )
            ],
          ),
          Expanded(
            child: Column(
              children: _items,
            ),
          ),
        ],
      ),
    );
  }
}


class TodoItem extends StatelessWidget {
  final String content;

  TodoItem({@required this.content});

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(height: 50.0),
      child: Container(
        decoration: BoxDecoration(
          border: Border(bottom: BorderSide(color: Colors.lightBlue[200]))
        ),
        child: Center(
          child: Text(
            content,
            style: TextStyle(
              fontSize: 18.0,
            )
          ),
        ),
      ),
    );
  }
}
```
<img width="250" alt="スクリーンショット 2019-12-02 22.10.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282309/00cd264e-f18d-122a-4fdd-d3bcd7c55d33.png">

編集も削除もできない追加だけのシンプルなTodoアプリができました。

簡単にコードの説明をします。

```dart
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
```
今回は再描画が必要なので MyHomePageクラスは StatefulWidgetを継承しています。
StateクラスがUIの状態を持っているので、ここでは特に処理を書かず、_MyHomePageStateクラスに処理を書いていきます。

```dart
class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller = TextEditingController();
  final List<TodoItem> _items = List();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todo App'),
      ),
      body: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              Expanded(
                flex: 3,
                child: TextField(
                  controller: _controller,
                ),
              ),
              Expanded(
                flex: 1,
                child: RaisedButton(
                  child: Text("追加"),
                  color: Colors.orange[600],
                  textColor: Colors.white,
                  // ボタンを押したときの状態の変化をここに書く
                  onPressed: () {
                    setState(() {
                      _items.add(TodoItem(content: _controller.text,),);
                      _controller.clear();
                    });
                  },
                ),
              )
            ],
          ),
          Expanded(
            child: Column(
              children: _items,
            ),
          ),
        ],
      ),
    );
  }
}
```
build関数の中は先ほどと同じ様にWidgetを入れ子にしてUIを作っています。

onPressd: のところに状態がどう変わるかを書いています。
_itemsというTodoItemのListを入れる変数をつくり、そこにTodoItemを追加していく感じです。

追加していくTodoItemの内容は、 TextEditingControllerのインスタンスを入れる\_controller変数をTextFieldのcontroller: に設定しておけば、_controller.textで取り出すことができます。


# 終わりに
データの永続化にはFirebaseが相性が良いようです。
今後、FirebaseやBLoCパターン等も学習していけたらと思います。

私も最近触り始めたレベルなので間違っている部分があるかもしれませんが、そのときはコメントお願いします。
最後まで読んでいただき、ありがとうございました。