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.
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 thei
th child.void paintChild(int i, { Matrix4 transform, double opacity = 1.0 })
: Used to paint thei
th 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:
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;
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:
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:
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:
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
.