Flutter入門[4]: ステートフルとステートレスについて

今回も[前回](https://qiita.com/ShZgZ_Shogo/items/c6217899b674fcbfb431)に引き続きアプリを作成していきます。
ただ、今回は主にステートフルウィジェットとステートレスウィジェットを触っていきます。

前回から続けて作っているアプリは[公式サイトのチュートリアル](https://flutter.dev/docs/get-started/codelab)を参考に作っていますが、この中でステートフルウィジェットが出てきます。

公式の説明では以下の様に書いてありました。
> ステートレスウィジェットは不変で、プロパティを変更することができない-全ての値がfinalであるということ
> ステートフルウィジェットはそのウィジェットのライフタイムにおいて変化しうる状態を扱います。
> (公式チュートリアルより)

なんとなくはわかるんですが、もうちょっと具体的に知りたいと思ってたら[公式ドキュメントの"Adding interactivity"](https://flutter.dev/docs/development/ui/interactive)というページに以下のような説明で乗っていました。

***
ウィジェットはステートフルなウィジェットかステートレスなウィジェットに分けられます。ウィジェットが(例えばユーザの操作によって)変化するような場合、それはステートフルである。

<strong>ステートレスなウィジェット:</strong> 決して変化しない。例えば以下のウィジェットはステートレスなウィジェットであり、ステートレスなウィジェットはStatelessWidgetクラスのサブクラスである。

- Icon
- IconButton
- Text

<strong>ステートフルなウィジェット:</strong> 動的に変化する。例えばユーザの操作が行われた際やデータを受け取った際に見た目が変化する以下のようなウィジェットはステートフルなウィジェットである。また、ステートフルなウィジェットはStatefulWidgetクラスのサブクラスである。

- Checkbox
- Radio
- Slider
- InkWell
- Form
- TextField

ウィジェットの状態はStateオブジェクトに保存される。ここでいう状態とは、スライダーの今の値やチェックボックスのチェック有無といった変化しうる値によって構成されている。
ウィジェットの状態が変化する時、その状態オブジェクトはsetStateメソッドを呼び出し、フレームワークにウィジェットの再描画を依頼します。
***

では、ステートフルとステートフルなウィジェットを作っていきます。
IDEで開発している場合には"stful"や"stless"と打てば補完でボイラープレートを生成してくれます。補完してそのままウィジェット名を入力すれば内部処理では何もしないウィジェットの完成です。

```dart
// ステートフルウィジェットの実装
class TempStatefulWidget extends StatefulWidget {
  @override
  _TempStatefulWidgetState createState() => _TempStatefulWidgetState();
}
// ステートフルウィジェットに伴う状態を表すクラスの実装
class _TempStatefulWidgetState extends State<TempStatefulWidget> {

  @override
  Widget build(BuildContext context) {
    return Container(
    );
  }
}
// ステートレスウィジェットの実装
class TempStatelessWidget extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
    );
  }
}
```

で試しに状態を持たせてみたのが以下です。

```dart
class TempStatefulWidget extends StatefulWidget {
  @override
  _TempStatefulWidgetState createState() => _TempStatefulWidgetState();
}

class _TempStatefulWidgetState extends State<TempStatefulWidget> {
  // 状態(今回は背景色用の色)
  var bgColor = Colors.white;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: FlatButton(
        onPressed: () { 
          setState(() => bgColor = Colors.red);
        },
        child: Text('A'),
      ),
      color: bgColor,
    );
  }
}

class TempStatelessWidget extends StatelessWidget {
  // 状態
  var bgColor = Colors.white;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: FlatButton(
        onPressed: () {
          bgColor = Colors.red;
        },
        child: Text('B'),
      ),
      color: bgColor,
    );
  }
}
```

なんとなく見て貰えばわかるかと思いますが、共に押されたらbgColorの値を変更するような処理を行うウィジェットを作成しました。
ここでステートレスウィジェットに試しにクラス変数を持たせてみたところ、持たせることができかつ値の変更も行うことができました。(IDEからはステートレスにしない？とかmust_be_immutableを無視するようにアノーテーション入れない?みたいな警告もでっぱなしでした)

ただ、公式ドキュメントにもあるようにsetStateメソッドを実行する必要がありますが、StatelessWidgetクラスはsetStateメソッドを持っていないため実行することができませんでした。そのため、値は変更できるけどウィジェットの再描画ができないため、見た目に値の変更を反映させることができませんでした。

作成したウィジェットをメインとなるウィジェットに入れていきます。

```dart
class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
  
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter')
        ),
        body: ListView(
          children: <Widget>[
            TempStatefulWidget(),
            Divider(),
            TempStatelessWidget(),
          ],
        ),
      ),
    );
  }
}
```

ホットリロードすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656102/26dd7f52-0aa7-3ff1-0832-0f0ad09c7ffd.png)
こんな感じで表示されて、ステートフルなAをタップするとAの背景が赤くなり、ステートレスなBをタップしても何も変化しないことが確認できるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656102/cc60dc11-60a2-38ad-b2ee-da3122631352.png)

因みにまず作ろうとしていた公式チュートリアルのアプリですが、以下のようなコードで

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      home: RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = TextStyle(fontSize: 18.0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
      padding: EdgeInsets.all(16.0),
      itemBuilder: (context, i){
        if (i.isOdd) return Divider();

        final index = i ~/ 2;
        if (index >= _suggestions.length){
          debugPrint('loading word pairs');
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }

  Widget _buildRow(WordPair pair){
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}
```
(因みにこのコードはまんま貼り付けてもenglish_wardsが無いため動きません。もし動かしたければ、パッケージをインストールする必要があります。この記事の下の方に手順を載せておきます。)

起動するとランダムに英単語のリストが表示され、ある程度スクルールすると英単語がLazyロードされ無限にスクロールされるっていう感じなんですが、これだけならばStatefulWidgetでなくても実装できます。
公式チュートリアルでは続きの章でこの要素に状態を持たせる予定だからこのようにしています。

ListView.builderはスクレールされて見えるようになった時にitemBuilderの処理が呼び出されます。ListView自体はStatelessWidgetなので、リストの長さが伸びるっていうのは状態変わってるのでは？とも思いましたが、描画され途中なだけで描画された状態が変化しているわけでは無いという考え方かと思われます。

# まとめ
もうちょっと公式チュートリアルの作り方を見ていこうと思っていたのですが、ステートフルとステートレスの方が調べていったらどんどん沼にハマってしまって、結果的に殆どStateに関する記事になりました。

## 補足:パッケージのインストール方法
公式チュートリアルで使っているenglish_wordsパッケージのインストールを行います。
プロジェクトディレクトリ直下にあるpubspec.yamlファイルのdependenciesにenglish_wordsを追加し、保存すると勝手に始まります。

```yaml
dependencies:
  flutter:
    sdk: flutter
  english_words: ^3.1.5
```
あまり無いと思いますが、インストールが行われなかったり、CLIからやりたいというような場合にはコマンドパレットで"Flutter: Get Packages"を選ぶかターミナルで以下のコマンドを実行して下さい。

```bash
$ flutter pub get
```

VScodeではインストールについての右下に表示されますが、すぐ消えてしまって気付けないかもしれません。その場合にはpubspec.lockファイルにenglish_wordsがあることを確認して下さい。(Cmd + fで検索してみて下さい。)
インストールした後にホットリロードしてもパッケージが無いと言われると思います。一度停止して再度ビルドしてみて下さい。