Flutter - Flow

This tutorial is about how to use Flow widget in Flutter.

Flow is a widget that sizes and positions its children efficiently according to the logic of a FlowDelegate. This widget is useful when the children need to be repositioned using transformation matrices. For example, if you need to create an animation that transforms the position of the children.

In this tutorial, we are going to create a layout containing a floating menu which can expand/collapse using the Flow widget, like the image below.

Flutter - Flow

Using Flow Widget

Below is the constructor of Flow.

  Flow({
    Key key,
    @required this.delegate,
    List children = const [],
  })

To create an instance of Flow, the only required argument is delegate. You need to pass a FlowDelegate, which is responsible to control the appearance of the flow layout.

To create an instance of FlowDelegate, you have to create a custom class that extends FlowDelegate. The custom class needs to call the super constructor which is a const constructor. It has one parameter whose type is Listenable. It's quite common to pass an instance of Animation as the Listenable. By passing an Animation, it will listen to the animation and repaint whenever the animation ticks.

  const FlowDelegate({ Listenable? repaint }) : _repaint = repaint;

Below is an example of how to create a custom class that extends FlowDelegate and call the super constructor.

  class FlowExampleDelegate extends FlowDelegate {

    FlowExampleDelegate({this.myAnimation}) : super(repaint: myAnimation);

    final Animation<double> myAnimation;

    // Put overridden methods here
  }

Besides calling the constructor, there are some methods you can override, two of them must be overridden since they don't have default implementation.

The first method is paintChildren which returns void and accepts a parameter of type FlowPaintingContext. The method is responsible to define how the children should be painted.

  void paintChildren(FlowPaintingContext context);

FlowPaintingContext itself has some properties and methods.

  • Size get size: The size of the container in which the children can be painted.
  • int get childCount: The number of children available to paint.
  • Size? getChildSize(int i): The size of the ith child.
  • void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }): Used to paint the ith child.

paintChild needs to be called for painting each child based on the given transformation matrix. The first parameter i is the index of the child to be painted. You can paint the children in any order, but each child can only be painted once. The position of a child depends on the result of the container's coordinate system concatenated with the given transformation matrix. The upper left corner is set to be the origin of the parent's coordinate system. The value of x is increasing rightward, while the value of y is increasing downward.

The size and childCount properties as well as getChildSize method might be useful inside the FlowDelegate's paintChildren method. For example, if you need to perform a loop based on the number of children or translate the position of a child based on its size.

The code below is an example of paintChildren implementation. Each child is translated down based on its size multiplied by the current iterator, which then multiplied by the current animation value.

  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = context.childCount - 1; i >= 0; i--) {
      double dx = (context.getChildSize(i).height + 10) * i;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(0, dx * myAnimation.value + 10, 0),
      );
    }
  }

The other method that you have to override is shouldRepaint. The bool return value of the method is used to determine whether the children need to be repainted. The method has a parameter which will be passed with the old instance of the delegate. Therefore, you can compare the fields of the previous instance with the current fields in order to determine whether repainting should be performed.

  bool shouldRepaint(covariant FlowDelegate oldDelegate)

For example, if the delegate uses an Animation and stores it as a property, you can compare whether the animation of the previous delegate instance is the same with the current animation.

  @override
  bool shouldRepaint(FlowExampleDelegate oldDelegate) {
    return myAnimation != oldDelegate.myAnimation;
  }

Having created the custom FlowDelegate class, now we can call the constructor of Flow. For the delegate argument, pass an instance of FlowExampleDelegate. Since the constructor of FlowExampleDelegate requires an instance of Animation, we need to create an Animation. First, add with SingleTickerProviderStateMixin in the declaration of the state class. By doing so, it becomes possible to get the TickerProvider which is required when calling the constructor of AnimationController.

For the children, the example below creates a list of RawMaterialButton. When any button is clicked, it will toggle the state of the menu between expanded and collapsed.

  class _FlowExampleState extends State<FlowExample>
      with SingleTickerProviderStateMixin {
  
    AnimationController _myAnimation;
  
    final List<IconData> _icons = <IconData>[
      Icons.menu,
      Icons.email,
      Icons.new_releases,
      Icons.notifications,
      Icons.bluetooth,
      Icons.wifi,
    ];
  
    @override
    void initState() {
      super.initState();
  
      _myAnimation = AnimationController(
        duration: const Duration(seconds: 1),
        vsync: this,
      );
    }
  
    Widget _buildItem(IconData icon) {
      return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 10.0),
        child: RawMaterialButton(
          fillColor: Colors.teal,
          splashColor: Colors.grey,
          shape: CircleBorder(),
          constraints: BoxConstraints.tight(Size.square(50.0)),
          onPressed: () {
            _myAnimation.status == AnimationStatus.completed
                ? _myAnimation.reverse()
                : _myAnimation.forward();
          },
          child: Icon(AnimationController
            icon,
            color: Colors.white,
            size: 30.0,
          ),
        ),
      );
    }
  
    @override
    Widget build(BuildContext context) {
      return Flow(
        delegate: FlowExampleDelegate(myAnimation: _myAnimation),
        children: _icons
            .map<Widget>((IconData icon) => _buildItem(icon))
            .toList(),
      );
    }
  }

Output:

Flutter - Flow

Set Size

If you try to inspect the size of the Flow, you'll find out that it occupies all the available space. That's because the default implementation of FlowDelegate's getSize method returns the biggest size that satisfies the constraints.

  Size getSize(BoxConstraints constraints) => constraints.biggest;

Flutter - Flow - Size -Default

To change that, you can override the getSize method. The code below sets the width of the widget to 70.

  @override
  Size getSize(BoxConstraints constraints) {
    return Size(70.0, double.infinity);
  }

Output:

Flutter - Flow - Size -Custom

Set Child Constraints

By default, the children will use the given size constraints. That's because the default implementation of getConstraintsForChild method returns the given constraints.

  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) => constraints;

By overriding the method, you can set different constraints for the children. The first parameter i which is the index of a child can be useful if you need to set different constraints for particular children.

  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return i == 0 ? constraints : BoxConstraints.tight(const Size(50.0, 50.0));
  }

Output:

Flutter - Flow - Child Constraints

Layout

In addition to changing the size of the Flow widget, you may also need to change how the widget should be laid out relative to other widgets. Usually, you can wrap it as the child of another widget. For example, you can use Align widget to set the alignment or use Stack widget if you want to make the button floating above other widgets.

  Stack(
    children: [
      Container(color: Colors.grey),
      Flow(
        delegate: FlowExampleDelegate(myAnimation: _myAnimation),
        children: _icons
            .map<Widget>((IconData icon) => _buildItem(icon))
            .toList(),
      ),
    ],
  )

Flow Parameters

  • Key key: The widget's key, used to control how a widget is replaced with another widget.
  • FlowDelegate delegate *: The delegate that controls the transformation matrices of the children.
  • List<Widget> children: The widgets below this widget in the tree.

*: required

Full Code

  import 'package:flutter/material.dart';
  import 'package:flutter/widgets.dart';
  
  void main() => runApp(MyApp());
  
  class MyApp extends StatelessWidget {
  
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Woolha.com Flutter Tutorial',
        home: Scaffold(
          appBar: AppBar(
            title: const Text('Woolha.com | Flow Example'),
            backgroundColor: Colors.teal,
          ),
          body: FlowExample(),
        ),
      );
    }
  }
  
  class FlowExample extends StatefulWidget {
  
    @override
    _FlowExampleState createState() => _FlowExampleState();
  }
  
  class _FlowExampleState extends State<FlowExample>
      with SingleTickerProviderStateMixin {
  
    AnimationController _myAnimation;
  
    final List<IconData> _icons = <IconData>[
      Icons.menu,
      Icons.email,
      Icons.new_releases,
      Icons.notifications,
      Icons.bluetooth,
      Icons.wifi,
    ];
  
    @override
    void initState() {
      super.initState();
  
      _myAnimation = AnimationController(
        duration: const Duration(seconds: 1),
        vsync: this,
      );
    }
  
    Widget _buildItem(IconData icon) {
      return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 10.0),
        child: RawMaterialButton(
          fillColor: Colors.teal,
          splashColor: Colors.grey,
          shape: CircleBorder(),
          constraints: BoxConstraints.tight(Size.square(50.0)),
          onPressed: () {
            _myAnimation.status == AnimationStatus.completed
                ? _myAnimation.reverse()
                : _myAnimation.forward();
          },
          child: Icon(
            icon,
            color: Colors.white,
            size: 30.0,
          ),
        ),
      );
    }
  
    @override
    Widget build(BuildContext context) {
      return Stack(
        children: [
          Container(color: Colors.grey),
          Flow(
            delegate: FlowExampleDelegate(myAnimation: _myAnimation),
            children: _icons
                .map<Widget>((IconData icon) => _buildItem(icon))
                .toList(),
          ),
        ],
      );
    }
  }
  
  class FlowExampleDelegate extends FlowDelegate {
  
    FlowExampleDelegate({this.myAnimation}) : super(repaint: myAnimation);
  
    final Animation<double> myAnimation;
  
    // Put overridden methods here
    @override
    bool shouldRepaint(FlowExampleDelegate oldDelegate) {
      return myAnimation != oldDelegate.myAnimation;
    }
  
    @override
    void paintChildren(FlowPaintingContext context) {
      for (int i = context.childCount - 1; i >= 0; i--) {
        double dx = (context.getChildSize(i).height + 10) * i;
        context.paintChild(
          i,
          transform: Matrix4.translationValues(0, dx * myAnimation.value + 10, 0),
        );
      }
    }
  
    @override
    Size getSize(BoxConstraints constraints) {
      return Size(70.0, double.infinity);
    }
  
    @override
    BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
      return i == 0 ? constraints : BoxConstraints.tight(const Size(50.0, 50.0));
    }
  }

Output:

Flutter - Flow - Full Code

Summary

The Flow widget is suitable when you need to create an animation that repositions the children. It can make the process more efficient because it only needs to repaint whenever the animation ticks. Therefore, the build and layout phases of the pipeline can be avoided. In order to use the Flow widget, you need to understand about FlowDelegate since the logic of how to paint the children mostly needs to be defined inside a class that extends FlowDelegate.