はじめに
前回はFlutter Widget of the Weekの「#25 Align」、「#26 Positioned」、「#27 AnimatedBuilder」を紹介しました。
今回はその続きで「#28 Dismissible」、「#29 SizedBox」、「#30 ValueListenableBuilder」の3つです。
前回の記事はこちら
Flutter Widget of the Week
環境
- Flutter 2.10.3
記事にした時点でのバージョンです。GitHubに公開しているのは常に最新の安定版(stable)を使う予定です。
#28 Dismissible
Dismissibleとは
スマートフォンの操作で一覧表示されている項目を左右のどちらかにスワイプして削除する動きを実現する際に使います。
注意点として、keyに指定する内容はindex(配列の何番目)等の情報は使わないようにしてください。
以下のようなエラーが出やすくなります。必ず一意となるkeyを設定してください。
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building Dismissible-[<2>](dirty, dependencies: [Directionality], state: _DismissibleState#dd76d(tickers: tracking 2 tickers)):
A dismissed Dismissible widget is still part of the tree.
2
Make sure to implement the onDismissed handler and to immediately remove the Dismissible widget from the application once that handler has fired.
The relevant error-causing widget was
Dismissible-[<2>]
サンプルコード
import 'package:flutter/material.dart';
class SamplePage028 extends StatefulWidget {
const SamplePage028({
Key? key,
}) : super(key: key);
@override
State<SamplePage028> createState() => _SamplePage028State();
}
class _SamplePage028State extends State<SamplePage028> {
final _list = <String>[
'A',
'B',
'C',
'D',
'E',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dismissible'),
centerTitle: true,
),
body: SafeArea(
child: ListView.builder(
itemBuilder: (context, index) {
final str = _list[index];
return Dismissible(
key: ValueKey(str),
background: Container(
color: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(
Icons.notifications,
color: Colors.white,
),
),
secondaryBackground: Container(
color: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerRight,
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
child: ListTile(
title: Text(str),
),
onDismissed: (direction) {
setState(() {
_list.removeAt(index);
if (direction == DismissDirection.startToEnd) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$strを削除しました'),
),
);
}
});
},
);
},
itemCount: _list.length,
),
),
);
}
}
結果
左へスワイプすると削除
右へスワイプすると削除しつつ通知を出します。
動画
公式リファレンス
#29 SizedBox
SizedBoxとは
サイズを指定したウィジェットを作成する際に利用します。また子ウィジェットを必要としないのでスペースを埋めるのに利用したり、widthやheightにinfinityを指定したりSizedBox.expandを使うことで親の許容限界まで子ウィジェットを広げることができます。
サンプルコード
import 'package:flutter/material.dart';
class SamplePage029 extends StatelessWidget {
const SamplePage029({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dismissible'),
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: () {},
child: const Text('infinity指定だと画面サイズまで広げる'),
),
),
const SizedBox(
height: 100,
),
SizedBox(
width: 200,
height: 60,
child: ElevatedButton(
onPressed: () {},
child: const Text('↑スペース作る'),
),
),
],
),
),
),
);
}
}
結果
使うとこんな感じ。
動画
公式リファレンス
#30 ValueListenableBuilder
ValueListenableBuilderとは
ValueNotifierを継承したクラスを使う状態管理の方法の1つです。昨今ではStateNotifierを使ったほうが便利なのでValueNotifierを使っている人は減ってると思います。
ただValueNotifierを状態変数が1つなので状態管理クラスがシンプルに保てます。
アニメーションやInheritedWidgetと組み合わせて使うことが多いようです。
サンプルコード
import 'dart:math';
import 'package:flutter/material.dart';
class SamplePage030 extends StatefulWidget {
const SamplePage030({
Key? key,
}) : super(key: key);
@override
State<SamplePage030> createState() => _SamplePage030State();
}
class _SamplePage030State extends State<SamplePage030>
with SingleTickerProviderStateMixin {
final _notifier = _Sample030Notifier();
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint('_SamplePage030State#build');
final animation = Tween(
begin: 0,
end: 2 * pi,
).animate(_controller);
return Scaffold(
appBar: AppBar(
title: const Text('ValueListenableBuilder'),
centerTitle: true,
),
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
ValueListenableBuilder<int>(
valueListenable: _notifier,
builder: (context, value, child) {
return Text(value.toString());
},
),
const SizedBox(height: 200),
ValueListenableBuilder<num>(
valueListenable: animation,
child: Container(
width: 100,
height: 100,
color: Colors.green,
),
builder: (context, value, child) {
return Transform.rotate(
angle: animation.value.toDouble(),
child: child,
);
},
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_notifier.change(_notifier.value + 1);
},
child: const Icon(Icons.add),
),
);
}
}
class _Sample030Notifier extends ValueNotifier<int> {
_Sample030Notifier() : super(0);
void change(int i) {
value = i;
notifyListeners();
}
}
結果
ボタンを押しても不要なbuildが走らないのがわかります。
動画
公式リファレンス
さいごに
DismissibleはとSizedBoxは頻繁に使いそうなのでしっかり覚えておきたいですね。
ValueListenableBuilderは使い所がよくわかりません。
コメント