はじめに
Flutterに関する動画を見て、感銘を受けたのでシェアしたいと思います。
その動画とは以下の動画です。
https://www.youtube.com/watch?v=IOyq-eTRhvo
題名は「Widgets vs helper methods」。
個人的にFlutterを書いていて、気を付けていてもWidgetのコードがごちゃごちゃするなと悩んでいました。
しかし、上記の動画を見て、はっ、と気づかされた部分があったので、
同じく悩んでいる初心者の方の参考になれば幸いです。
目次
推奨される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~」の形で分割するやり方です。
対して、推奨されるのは以下のようにStatelessWidget
やStatefulWidget
に分ける書き方です。
※コード的におかしい部分がありますが、雰囲気だけ見てください。。。
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),
),
);
}
}
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,
);
}
}
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
},
),
),
);
}
}
メリット
サンプルのようにStatelessWidget
やStatefulWidget
に分ける書き方のメリットは以下です。
パフォーマンスが上がる
StatefulWidgetでは、状態変更時、buildメソッドで再描画されますが、クラスを分けることで描画範囲を小さくできるので、
パフォーマンスが上がります。
テストしやすい
ヘルパーメソッドではWidgetとして分割されるわけではないので、Widgetを分割することと比べた際に、
Widget単体のテストが複雑になります。
Widgetを分割して小さくすれば、Widgetの目的もはっきりするため、テストコードも書きやすくなります。
最後に
Provider
やRiverpod
の開発者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に分割するのが推奨されるということです。