1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

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

目次

  1. 推奨されるWidgetの書き方
  2. メリット

推奨されるWidgetの書き方

推奨されるWidgetの書き方は、意味のある単位ごとにWidgetのコードを分けるという方法です。
すごく単純ですが、意外と私はできてませんでした。

まず何も考えずに書くと以下のようになると思います。
(作りこんでないので雑ですが、雰囲気だけ感じ取ってもらえれば)

// _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),
      ),
    );
  }
}

上記のコードのままでは、可読性が悪く、メンテナンスしづらいため、
一通り、実装が終わったタイミングでよくやってしまうのが、下記のような書き方です。

(動画のタイトルにあるhelper methodsと言うらしいです)

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

helperメソッドとは、要するに、長いbuildメソッドを「_build~」の形で分割するやり方です。

対して、推奨されるのは以下のようにStatelessWidgetStatefulWidgetに分ける書き方です。
※コード的におかしい部分がありますが、雰囲気だけ見てください。。。

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

メリット

サンプルのようにStatelessWidgetStatefulWidgetに分ける書き方のメリットは以下です。

パフォーマンスが上がる

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

テストしやすい

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

最後に

ProviderRiverpodの開発者Remi Rousseletは、別のWidgetクラスに分けるか、ヘルパーメソッドに分けるかの問題に対して下記のように言ったらしいです。
「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.」

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?