【Flutter】初心者向け 推奨されるWidgetの書き方

# はじめに
<strong>Flutterに関する動画</strong>を見て、感銘を受けたのでシェアしたいと思います。
<strong>その動画</strong>とは以下の動画です。
https://www.youtube.com/watch?v=IOyq-eTRhvo

題名は「<strong>Widgets vs helper methods</strong>」。
個人的にFlutterを書いていて、<strong>気を付けていてもWidgetのコードがごちゃごちゃするな</strong>と悩んでいました。
しかし、上記の動画を見て、はっ、と気づかされた部分があったので、
<strong>同じく悩んでいる初心者の方の参考になれば幸いです。</strong>

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [推奨されるWidgetの書き方](#推奨されるWidgetの書き方)
1. [メリット](#メリット)

<!-- 各チャプター -->
<a id="#推奨されるWidgetの書き方"></a>
# 推奨されるWidgetの書き方
<strong>推奨されるWidgetの書き方は、意味のある単位ごとにWidgetのコードを分けるという方法</strong>です。
すごく単純ですが、意外と私はできてませんでした。

<strong>まず何も考えずに書くと以下のようになると思います。</strong>
(作りこんでないので雑ですが、雰囲気だけ感じ取ってもらえれば）
```dart
// _UserListPageStateクラスの_userListをリスト表示するページ
class UserListPage extends StatefulWidget {
  const UserListPage({Key? key}) : super(key: key);

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  // リストに表示するユーザー情報
  final List<String> _userList = ["User1", "User2", "User3"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List'),),
      body: SafeArea(
        child: ListView.separated(
            itemBuilder: (context, index){
              final user = _userList[index];
              return Dismissible(
                key: ValueKey(user),
                direction: DismissDirection.endToStart,
                onDismissed: (DismissDirection direction){
                  if(direction == DismissDirection.endToStart){
                    setState((){
                      _userList.removeAt(index);
                    });
                  }
                },
                background: Container(color: Colors.red),
                child: InkWell(
                  child: ListTile(
                    title: Text(user),
                    onTap: () {
                      // TODO:go to edit user page
                    },
                  ),
                ),
              );
            },
            separatorBuilder: (context, index){
              return const SizedBox(height: 16,);
            },
            itemCount: _userList.length),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            _userList.add("new user");
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

<strong>上記のコードのままでは、可読性が悪く、メンテナンスしづらいため、
一通り、実装が終わったタイミングでよくやってしまうのが、下記のような書き方です。</strong>
（動画のタイトルにあるhelper methodsと言うらしいです）

```dart
class UserListPage extends StatefulWidget {
  const UserListPage({Key? key}) : super(key: key);

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  final List<String> _userList = ["User1", "User2", "User3"];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List'),),
      body: SafeArea(
        child: _buildUserListWidget(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            _userList.add("new user");
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildUserListWidget() {
    return ListView.separated(
      itemBuilder: (context, index){
        return _buildUserWidget(index);
      },
      separatorBuilder: (context, index){
        return const SizedBox(height: 16,);},
      itemCount: _userList.length,
    );
  }

  Widget _buildUserWidget(int index){
    final user = _userList[index];
    return Dismissible(
      key: ValueKey(user),
      direction: DismissDirection.endToStart,
      onDismissed: (DismissDirection direction){
        if(direction == DismissDirection.endToStart){
          setState((){
            _userList.removeAt(index);
          });
        }
      },
      background: Container(color: Colors.red),
      child: InkWell(
        child: ListTile(
          title: Text(user),
          onTap: () {
            // TODO:go to edit user page
          },
        ),
      ),
    );
  }
}
```
<strong>helperメソッドとは、要するに、長いbuildメソッドを「_build～」の形で分割するやり方です。</strong>

</br>

対して、推奨されるのは以下のように`StatelessWidget`や`StatefulWidget`に分ける書き方です。
<strong>※コード的におかしい部分がありますが、雰囲気だけ見てください。。。</strong>
```dart:user_list_page.dart
class UserListPage extends StatefulWidget {
  const UserListPage({Key? key}) : super(key: key);

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  final List<String> _userList = ["User1", "User2", "User3"];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List'),),
      body: SafeArea(
        child: UserListWidget(userList: _userList,),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            _userList.add("new user");
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
```
```dart:user_list_widget.dart
class UserListWidget extends StatefulWidget {
  final List<String> userList;
  const UserListWidget({Key? key, required this.userList}) : super(key: key);

  @override
  State<UserListWidget> createState() => _UserListWidgetState();
}

class _UserListWidgetState extends State<UserListWidget> {
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (context, index){
        final user = widget.userList[index];
        return UserWidget(
          user: user,
          onDismissible: () {
            setState((){
              widget.userList.removeAt(index);
            });
          },
        );
      },
      separatorBuilder: (context, index){
        return const SizedBox(height: 16,);},
      itemCount: widget.userList.length,
    );
  }
}
```

```dart:user_widget.dart
class UserWidget extends StatefulWidget {
  final String user;
  final void Function() onDismissible;

  const UserWidget({
    Key? key,
    required this.user,
    required this.onDismissible
  }) : super(key: key);

  @override
  State<UserWidget> createState() => _UserWidgetState();
}

class _UserWidgetState extends State<UserWidget> {
  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ValueKey(widget.user),
      direction: DismissDirection.endToStart,
      onDismissed: (DismissDirection direction){
        if(direction == DismissDirection.endToStart){
          widget.onDismissible?.call();
        }
      },
      background: Container(color: Colors.red),
      child: InkWell(
        child: ListTile(
          title: Text(widget.user),
          onTap: () {
            // TODO:go to edit user page
          },
        ),
      ),
    );
  }
}
```

<a id="#メリット"></a>
# メリット
サンプルのように`StatelessWidget`や`StatefulWidget`に分ける書き方のメリットは以下です。

### パフォーマンスが上がる
StatefulWidgetでは、状態変更時、buildメソッドで再描画されますが、クラスを分けることで描画範囲を小さくできるので、
パフォーマンスが上がります。

### テストしやすい
ヘルパーメソッドではWidgetとして分割されるわけではないので、Widgetを分割することと比べた際に、
Widget単体のテストが複雑になります。
Widgetを分割して小さくすれば、Widgetの目的もはっきりするため、テストコードも書きやすくなります。

# 最後に
`Provider`や`Riverpod`の開発者Remi Rousseletは、<strong>別のWidgetクラスに分けるか、ヘルパーメソッドに分けるかの問題</strong>に対して下記のように言ったらしいです。
<strong>「Classes have a better default behavior. The only benefit of methods is having to write a tiny bit less code. There's no functional benefit.」</strong>

直訳すると「別のWidgetクラスに分ける方が良い。ヘルパーメソッドの利点は少しコードが減ることだけで、機能的なメリットはない」という感じでしょうか。
どういうことかというと、
ヘルパーメソッドの場合、クラスが分割されるわけではないので、変数の使いまわしが容易です。
しかし別のWidgetクラスに分けた場合、変数を使いまわしするため引数として渡さなくてはならないため、その分、少しコードの記述量が増えます。
ただ、ヘルパーメソッドでは、Widgetに分割する恩恵であるパフォーマンスの向上やテストのしやすさなどは受けれません。
よって、機能的なメリットを受けたいのなら、Widgetに分割するのが推奨されるということです。