Share
Embed
Flutter Hooks
A Flutter implementation of React hooks. Why would we want to use those and how do they work. We will explore the inner workings of that library to better understand how the “magic” happens and the impact they can have on the code we have to write and more importantly the performance cost of them.
GDG Montreal
March 19, 2022
Transcript
59 slides
-
Flutter Hooks Samuel Dionne Android Dev at Transit and GDG
Montreal Organizer Reduce boilerplate -
• What are hooks • Using a hook • Understanding
hooks • Creating a hook Agenda Explaining the magic -
Hooks are a way to share the same code with
multiple widgets, code that is usually duplicated or hard to share between stateful widgets. – Jimmy Aumard medium.com/flutter-community -
useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final
counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); } -
useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final
counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); } -
useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final
counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); } -
useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final
counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); } -
useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final
counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); } -
useMemoized Construct only once @override Widget build(BuildContext context) { final
expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); } -
useMemoized Construct only once @override Widget build(BuildContext context) { final
expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); } -
useMemoized Construct only once @override Widget build(BuildContext context) { final
expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); } -
useMemoized Construct only once @override Widget build(BuildContext context) { final
expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); } -
useState + useMemoized Update instance on key change @override Widget
build(BuildContext context) { final counter = useState(0); final expensiveObject = useMemoized(() { myExpensiveObject(counter.value) }, [counter.value]); return ...; } -
useState + useMemoized Update instance on key change @override Widget
build(BuildContext context) { final counter = useState(0); final expensiveObject = useMemoized(() { myExpensiveObject(counter.value) }, [counter.value]); return ...; } -
useState + useMemoized Update instance on key change @override Widget
build(BuildContext context) { final counter = useState(0); final expensiveObject = useMemoized(() { myExpensiveObject(counter.value) }, [counter.value]); return ...; } -
useState + useMemoized Update instance on key change @override Widget
build(BuildContext context) { final counter = useState(0); final expensiveObject = useMemoized(() { myExpensiveObject(counter.value) }, [counter.value]); return ...; } -
useState + useMemoized Update instance on key change @override Widget
build(BuildContext context) { final counter = useState(0); final expensiveObject = useMemoized(() { myExpensiveObject(counter.value) }, [counter.value]); return ...; } -
• Multiple animations • Long form with lots of text
fields There are no limit to the number of hooks you can define in your build method. Same type, different type, no problems! Multiple Hooks Even with the same type -
• useState • useMemoized • useAnimationController • useTextEditingController • useFocusNode
• useTabController • useScrollController • usePageController Subset of the whole list Existing Hooks -
• Hard to write reusable code in State classes •
Lot of required steps (init, dispose) that are always the same • Very verbose Writing boilerplate does not get you more users Problems with StatefulWidget -
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: Duration(seconds: 1)); } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } -
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: Duration(seconds: 1)); } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } -
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: Duration(seconds: 1)); } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } -
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: Duration(seconds: 1)); } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } -
class Example extends HookWidget { @override Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } } -
class Example extends HookWidget { @override Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } } -
class Example extends HookWidget { @override Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } } -
• Hooks are stored in the Element of a Widget
just like State • Unlike State which is unique, Hooks are stores in a List • The list is indexed by the number of call to use<SomeHook> Or where did all the code go? How do they work -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T
use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override void performRebuild() { _hookIndex = 0; super.performRebuild(); } } -
• Only use Hooks in the build method • No
conditions around Hooks • No Hooks in loops Basically the states are valids as long as the number of use<AnyHook> calls is the same. Some rules Simple and easy to follow -
@override Widget build(BuildContext context) { final loading = useState(true); final
error = useState(“”); return ... } -
@override Widget build(BuildContext context) { final message = useState(“”); final
loading = useState(true); final error = useState(“”); return ... } -
@override Widget build(BuildContext context) { final message = useState(“”); final
loading = useState(true); final error = useState(“”); return ... } -
@override Widget build(BuildContext context) { final message = useState(“”); final
loading = useState(true); if (!loading.value) { final error = useState(“”); ... } final controller = useAnimationController(duration: Duration(seconds: 1)); return ... } -
class _RandomNumberGenerator extends Hook<int> { const _RandomNumberGenerator(): @override State<_RandomNumberGenerator> createState()
=> _RandomNumberGeneratorState(); } class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { @override int build(BuildContext context) { return 0; } } -
class _RandomNumberGenerator extends Hook<int> { const _RandomNumberGenerator(): @override State<_RandomNumberGenerator> createState()
=> _RandomNumberGeneratorState(); } class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { @override int build(BuildContext context) { return 0; } } -
class _RandomNumberGenerator extends Hook<int> { const _RandomNumberGenerator(): @override State<_RandomNumberGenerator> createState()
=> _RandomNumberGeneratorState(); } class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { @override int build(BuildContext context) { return 0; } } -
class _RandomNumberGenerator extends Hook<int> { const _RandomNumberGenerator(): @override State<_RandomNumberGenerator> createState()
=> _RandomNumberGeneratorState(); } class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { @override int build(BuildContext context) { return 0; } } -
class _RandomNumberGenerator extends Hook<int> { const _RandomNumberGenerator(): @override State<_RandomNumberGenerator> createState()
=> _RandomNumberGeneratorState(); } class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { @override int build(BuildContext context) { return 0; } } -
class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { final Random random =
Random(); late Timer timer; int number = 0; @override void initHook() { time = Timer.periodic( Duration(seconds: 1), (timer) { setState(() { number = random.nextInt(9); }); } ); super.initHook(); } -
class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { final Random random =
Random(); late Timer timer; int number = 0; @override void initHook() { time = Timer.periodic( Duration(seconds: 1), (timer) { setState(() { number = random.nextInt(9); }); } ); super.initHook(); } -
@override void initHook() { time = Timer.periodic( Duration(seconds: 1), (timer)
{ setState(() { number = random.nextInt(9); }); } ); super.initHook(); } @override void dispose() { timer.cancel(); super.dispose(); } -
@override int build(BuildContext context) { return number; } } int
useRandomNumber() { return use(const _RandomNumberGenerator()); } -
@override int build(BuildContext context) { return number; } } int
useRandomNumber() { return use(const _RandomNumberGenerator()); } -
class _RandomNumberGeneratorState extends HookState<int, _RandomNumberGenerator> { final Random random =
Random(); late Timer timer; int number = 0; @override void initHook() { time = Timer.periodic( Duration(seconds: 1), (timer) { setState(() { number = random.nextInt(9); }); } ); super.initHook(); } @override void dispose() { timer.cancel(); super.dispose(); } @override int build(BuildContext context) { return number; } } -
@override Widget build(BuildContext context) { final random1 = useRandomNumber(); final
random2 = useRandomNumber(); return ...; } -
Tips and Tricks Avoiding headache - Always put Hooks at
the top of functions - Keep the separation of concerns in mind - Keep to Hooks as close as possible to the code that uses them