Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

    View Slide

  2. • What are hooks
    • Using a hook
    • Understanding hooks
    • Creating a hook
    Agenda
    Explaining the magic

    View Slide

  3. v
    “A Flutter implementation of
    React hooks.”
    - pub.dev

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 ...;
    }

    View Slide

  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 ...;
    }

    View Slide

  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 ...;
    }

    View Slide

  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 ...;
    }

    View Slide

  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 ...;
    }

    View Slide

  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

    View Slide

  20. • useState
    • useMemoized
    • useAnimationController
    • useTextEditingController
    • useFocusNode
    • useTabController
    • useScrollController
    • usePageController
    Subset of the whole list
    Existing Hooks

    View Slide

  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

    View Slide

  22. AnimationController
    using StatefulWidget

    View Slide

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

    View Slide

  24. class _ExampleState extends State 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();
    }
    }

    View Slide

  25. class _ExampleState extends State 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();
    }
    }

    View Slide

  26. class _ExampleState extends State 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();
    }
    }

    View Slide

  27. class _ExampleState extends State 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();
    }
    }

    View Slide

  28. AnimationController
    using Hooks

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    Or where did all the code go?
    How do they work

    View Slide

  33. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  34. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  35. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  36. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  37. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  38. class HookElement extends Element {
    List _hooks;
    int _hookIndex;
    T use(Hook hook) => _hooks[_hookIndex++].build(this);
    @override
    void performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
    }
    }

    View Slide

  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 calls is the same.
    Some rules
    Simple and easy to follow

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 ...
    }

    View Slide

  44. Let’s create a
    new Hook

    View Slide

  45. class _RandomNumberGenerator extends Hook {
    const _RandomNumberGenerator():
    @override
    State<_RandomNumberGenerator> createState() => _RandomNumberGeneratorState();
    }
    class _RandomNumberGeneratorState extends HookState {
    @override
    int build(BuildContext context) {
    return 0;
    }
    }

    View Slide

  46. class _RandomNumberGenerator extends Hook {
    const _RandomNumberGenerator():
    @override
    State<_RandomNumberGenerator> createState() => _RandomNumberGeneratorState();
    }
    class _RandomNumberGeneratorState extends HookState {
    @override
    int build(BuildContext context) {
    return 0;
    }
    }

    View Slide

  47. class _RandomNumberGenerator extends Hook {
    const _RandomNumberGenerator():
    @override
    State<_RandomNumberGenerator> createState() => _RandomNumberGeneratorState();
    }
    class _RandomNumberGeneratorState extends HookState {
    @override
    int build(BuildContext context) {
    return 0;
    }
    }

    View Slide

  48. class _RandomNumberGenerator extends Hook {
    const _RandomNumberGenerator():
    @override
    State<_RandomNumberGenerator> createState() => _RandomNumberGeneratorState();
    }
    class _RandomNumberGeneratorState extends HookState {
    @override
    int build(BuildContext context) {
    return 0;
    }
    }

    View Slide

  49. class _RandomNumberGenerator extends Hook {
    const _RandomNumberGenerator():
    @override
    State<_RandomNumberGenerator> createState() => _RandomNumberGeneratorState();
    }
    class _RandomNumberGeneratorState extends HookState {
    @override
    int build(BuildContext context) {
    return 0;
    }
    }

    View Slide

  50. class _RandomNumberGeneratorState extends HookState {
    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();
    }

    View Slide

  51. class _RandomNumberGeneratorState extends HookState {
    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();
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  55. class _RandomNumberGeneratorState extends HookState {
    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;
    }
    }

    View Slide

  56. @override
    Widget build(BuildContext context) {
    final random1 = useRandomNumber();
    final random2 = useRandomNumber();
    return ...;
    }

    View Slide

  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

    View Slide

  58. Questions?

    View Slide

  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

    View Slide