$30 off During Our Annual Pro Sale. View Details »

Flutter Hooks

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
Tweet

More Decks by GDG Montreal

Other Decks in Programming

Transcript

  1. Flutter Hooks Samuel Dionne Android Dev at Transit and GDG

    Montreal Organizer Reduce boilerplate
  2. • What are hooks • Using a hook • Understanding

    hooks • Creating a hook Agenda Explaining the magic
  3. v “A Flutter implementation of React hooks.” - pub.dev

  4. 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
  5. useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final

    counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); }
  6. useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final

    counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); }
  7. useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final

    counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); }
  8. useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final

    counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); }
  9. useState Goodbye setState(() {}) @override Widget build(BuildContext context) { final

    counter = useState(0); return GestureDetector( child: Text(counter.value.toString()), onTap: () => counter.value++, ); }
  10. useMemoized Construct only once @override Widget build(BuildContext context) { final

    expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); }
  11. useMemoized Construct only once @override Widget build(BuildContext context) { final

    expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); }
  12. useMemoized Construct only once @override Widget build(BuildContext context) { final

    expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); }
  13. useMemoized Construct only once @override Widget build(BuildContext context) { final

    expensiveObject = useMemoized(() { myExpensiveObject() }); return Text(expensiveObject.toString()); }
  14. 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 ...; }
  15. 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 ...; }
  16. 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 ...; }
  17. 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 ...; }
  18. 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 ...; }
  19. • 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
  20. • useState • useMemoized • useAnimationController • useTextEditingController • useFocusNode

    • useTabController • useScrollController • usePageController Subset of the whole list Existing Hooks
  21. • 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
  22. AnimationController using StatefulWidget

  23. class Example extends StatefulWidget { @override _ExampleState createState() => _ExampleState();

    }
  24. 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(); } }
  25. 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(); } }
  26. 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(); } }
  27. 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(); } }
  28. AnimationController using Hooks

  29. class Example extends HookWidget { @override Widget build(BuildContext context) {

    final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } }
  30. class Example extends HookWidget { @override Widget build(BuildContext context) {

    final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } }
  31. class Example extends HookWidget { @override Widget build(BuildContext context) {

    final controller = useAnimationController(duration: Duration(seconds: 1)); return Container(); } }
  32. • 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
  33. 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(); } }
  34. 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(); } }
  35. 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(); } }
  36. 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(); } }
  37. 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(); } }
  38. 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(); } }
  39. • 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
  40. @override Widget build(BuildContext context) { final loading = useState(true); final

    error = useState(“”); return ... }
  41. @override Widget build(BuildContext context) { final message = useState(“”); final

    loading = useState(true); final error = useState(“”); return ... }
  42. @override Widget build(BuildContext context) { final message = useState(“”); final

    loading = useState(true); final error = useState(“”); return ... }
  43. @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 ... }
  44. Let’s create a new Hook

  45. 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; } }
  46. 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; } }
  47. 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; } }
  48. 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; } }
  49. 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; } }
  50. 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(); }
  51. 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(); }
  52. @override void initHook() { time = Timer.periodic( Duration(seconds: 1), (timer)

    { setState(() { number = random.nextInt(9); }); } ); super.initHook(); } @override void dispose() { timer.cancel(); super.dispose(); }
  53. @override int build(BuildContext context) { return number; } } int

    useRandomNumber() { return use(const _RandomNumberGenerator()); }
  54. @override int build(BuildContext context) { return number; } } int

    useRandomNumber() { return use(const _RandomNumberGenerator()); }
  55. 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; } }
  56. @override Widget build(BuildContext context) { final random1 = useRandomNumber(); final

    random2 = useRandomNumber(); return ...; }
  57. 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
  58. Questions?

  59. References: • https://pub.dev/packages/flutter_hooks • https://medium.com/flutter-community/fl utter-hooks-7754df814995 • https://medium.com/flutter-community/fl utter-hooks-say-goodbye-to-statefulwid get-and-reduce-boilerplate-code-8573d

    4720f9a Thank you! Happy to answer questions on Slack