Basic Flutter layout concepts
Welcome to the Flutter layout codelab, where you learn how to build a Flutter UI without downloading and installing Flutter or Dart!
Flutter is different from other frameworks because its UI is built in code, not (for example) in an XML file or similar. Widgets are the basic building blocks of a Flutter UI. As you progress through this codelab, you’ll learn that almost everything in Flutter is a widget. A widget is an immutable object that describes a specific part of a UI. You’ll also learn that Flutter widgets are composable, meaning that you can combine existing widgets to make more sophisticated widgets. At the end of this codelab, you’ll get to apply what you’ve learned into building a Flutter UI that displays a business card.
Estimated time to complete this codelab: 45-60 minutes.
Row and Column classes
Row
and Column
are classes that contain and lay out widgets.
Widgets inside of a Row
or Column
are called children,
and Row
and Column
are referred to as parents.
Row
lays out its widgets horizontally,
and Column
lays out its widgets vertically.
Example: Creating a Column
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
runApp(MyApp());
final controller = LiveWidgetController(WidgetsBinding.instance!);
final columns = controller.widgetList(find.byType(Column));
if (columns.length == 0) {
_result(false, ['The Row contains three BlueBox widgets and lays them out horizontally.']);
return;
}
if (columns.length > 1) {
_result(false, ['Found ${columns.length} Rows, rather than just one.']);
return;
}
final column = columns.first as Column;
if (column.children.length != 3 || column.children.any((w) => w is! BlueBox)) {
_result(false, ['Row/Column should contain three children, all BlueBox widgets.']);
return;
}
_result(true, ['The Column contains three BlueBox widgets and lays them out vertically.']);
}
{$ end test.dart $}
Axis size and alignment
So far, the BlueBox
widgets have been squished together
(either to the left or at the top of the UI Output).
You can change how the BlueBox
widgets are spaced
out using the axis size and alignment properties.
mainAxisSize property
Row
and Column
occupy different main axes.
A Row
’s main axis is horizontal,
and a Column
’s main axis is vertical.
The mainAxisSize
property determines how much
space a Row
and Column
can occupy on their main axes.
The mainAxisSize
property has two possible values:
MainAxisSize.max
-
Row
andColumn
occupy all of the space on their main axes. If the combined width of their children is less than the total space on their main axes, their children are laid out with extra space. MainAxisSize.min
-
Row
andColumn
only occupy enough space on their main axes for their children. Their children are laid out without extra space and at the middle of their main axes.
Example: Modifying axis size
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.min) {
_result(false, ['Row lays out the BlueBox widgets with extra space. Change MainAxisSize.max to MainAxisSize.min']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox)) {
_result(false, ['There should only be three children, all BlueBox widgets.']);
return;
}
_result(true, ['Row lays out the BlueBox widgets without extra space, and the BlueBox widgets are positioned at the middle of Row\'s main axis.']);
}
{$ end test.dart $}
mainAxisAlignment property
When mainAxisSize
is set to MainAxisSize.max
,
Row
and Column
might lay out their children with extra space.
The mainAxisAlignment
property determines how Row
and Column
can position their children in that extra space.
mainAxisAlignment
has six possible values:
MainAxisAlignment.start
- Positions children near the beginning of the main axis.
(Left for
Row
, top forColumn
) MainAxisAlignment.end
- Positions children near the end of the main axis.
(Right for
Row
, bottom forColumn
) MainAxisAlignment.center
- Positions children at the middle of the main axis.
MainAxisAlignment.spaceBetween
- Divides the extra space evenly between children.
MainAxisAlignment.spaceEvenly
- Divides the extra space evenly between children and before and after the children.
MainAxisAlignment.spaceAround
- Similar to
MainAxisAlignment.spaceEvenly
, but reduces half of the space before the first child and after the last child to half of the width between the children.
Example: Modifying main axis alignment
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox)) {
_result(false, ['The Row should have three children, all BlueBox widgets.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.start) {
_result(false, ['MainAxisAlignment.start positions the BlueBox widgets on the left of the main axis. Change the value to MainAxisAlignment.end.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.end) {
_result(true, ['MainAxisAlignment.end positions the BlueBox widgets on the right of the main axis.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.center) {
_result(true, ['MainAxisAlignment.center positions the BlueBox widgets at the middle of the main axis.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceBetween) {
_result(true, ['The extra space is divided between the BlueBox widgets.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(true, ['The extra space is divided evenly between the BlueBox widgets and before and after them.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceAround) {
_result(true, ['Similar to MainAxisAlignment.spaceEvenly, but reduces half of the space before the first BlueBox widget and after the last BlueBox widget to half of the width between the BlueBox widgets.']);
}
}
{$ end test.dart $}
crossAxisAlignment property
The crossAxisAlignment
property determines
how Row
and Column
can position their children
on their cross axes.
A Row
’s cross axis is vertical,
and a Column
’s cross axis is horizontal.
The crossAxisAlignment
property has five possible values:
CrossAxisAlignment.start
- Positions children near the start of the cross axis. (Top for
Row
, Left forColumn
) CrossAxisAlignment.end
- Positions children near the end of the cross axis. (Bottom for
Row
, Right forColumn
) CrossAxisAlignment.center
- Positions children at the middle of the cross axis. (Middle for
Row
, Center forColumn
) CrossAxisAlignment.stretch
- Stretches children across the cross axis.
(Top-to-bottom for
Row
, left-to-right forColumn
) CrossAxisAlignment.baseline
- Aligns children by their character baselines.
(
Text
class only, and requires that thetextBaseline
property is set toTextBaseline.alphabetic
. See the Text widget section for an example.)
Example: Modifying cross axis alignment
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlueBox(),
BiggerBlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
class BiggerBlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox && w is! BiggerBlueBox)) {
_result(false, ['The Row should have three children, all BlueBox or BiggerBlueBox widgets.']);
return;
}
if (row.crossAxisAlignment == CrossAxisAlignment.start) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are positioned at the top of the cross axis.']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.end) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are positioned at the bottom of the cross axis']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.center) {
_result(false, ['The widgets are positioned at the middle of the cross axis. Change CrossAxisAlignment.center to CrossAxisAlignment.start.']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.stretch) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are stretched across the cross axis. Change the Row to a Column, and run again.']);
} else if(row.crossAxisAlignment == CrossAxisAlignment.baseline) {
_result(false, ['Couldn\t find a text class.']);
}
}
{$ end test.dart $}
Flexible widget
As you’ve seen, the mainAxisAlignment
and
crossAxisAlignment
properties determine
how Row
and Column
position widgets along both axes.
Row
and Column
first lay out widgets of a fixed size.
Fixed size widgets are considered inflexible because
they can’t resize themselves after they’ve been laid out.
The Flexible
widget wraps a widget,
so the widget becomes resizable.
When the Flexible
widget wraps a widget,
the widget becomes the Flexible
widget’s child
and is considered flexible.
After inflexible widgets are laid out,
the widgets are resized according to their
flex
and fit
properties.:
flex
- Compares itself against other
flex
properties before determining what fraction of the total remaining space eachFlexible
widget receives. fit
- Determines whether a
Flexible
widget fills all of its extra space.
Example: Changing fit properties
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
Flexible(
fit: FlexFit.loose,
flex: 1,
child: BlueBox(),
),
Flexible(
fit: FlexFit.loose,
flex: 1,
child: BlueBox(),
),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBox or Flexible widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['Row\'s first child should be a BlueBox.']);
return;
}
if (row.children[1] is! Flexible) {
_result(false, ['Row\'s second child should be a Flexible class.']);
return;
}
if (row.children[2] is! Flexible) {
_result(false, ['Row\'s third child should be a Flexible class.']);
return;
}
final flexibleWidget = row.children[2] as Flexible;
if (flexibleWidget.child is! BlueBox) {
_result(false, ['The Flexible classes should have BlueBox widgets as their children.']);
return;
}
if (flexibleWidget.fit != FlexFit.tight) {
_result(false, ['The fit properties set the Flexible widgets to their preferred size. Change both fit values to FlexFit.tight.']);
return;
}
_result(true, ['The Flexible widgets now occupy the space determined by their flex values.']);
}
{$ end test.dart $}
Example: Testing flex values
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
Flexible(
fit: FlexFit.tight,
flex: 1,
child: BlueBox(),
),
Flexible(
fit: FlexFit.tight,
flex: 1,
child: BlueBox(),
),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBlox or Flexible widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! Flexible) {
_result(false, ['The Row\'s second child should be a Flexible widget.']);
return;
}
if (row.children[2] is! Flexible) {
_result(false, ['The Row\'s third child should be a Flexible widget.']);
return;
}
final flexibleWidget = row.children[1] as Flexible;
if (flexibleWidget.child is! BlueBox) {
_result(false, ['The Flexible should have a BlueBox widget as its child.']);
return;
}
if (flexibleWidget.flex != 1) {
_result(false, ['Notice how the flex properties divide the extra space between the two Flexible widgets.']);
return;
}
_result(true, ['Both Flexible widgets receive half of the total remaining space.']);
}
{$ end test.dart $}
Expanded widget
Similar to Flexible
, the Expanded
widget can
wrap a widget and force the widget to fill extra space.
Example: Filling extra space
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBox widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! Expanded) {
_result(false, ['Notice how Row contains extra space on its main axis. Wrap the second BlueBox widget in an Expanded widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a Flexible widget.']);
return;
}
_result(true, ['Expanded forces second BlueBox widget to fill the extra space.']);
}
{$ end test.dart $}
SizedBox widget
The SizedBox
widget can be used in one of two ways when
creating exact dimensions.
When SizedBox
wraps a widget, it resizes the widget
using the height
and width
properties.
When it doesn’t wrap a widget,
it uses the height
and width
properties to
create empty space.
Example: Resizing a widget
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
BlueBox(),
SizedBox(
width: 100,
child: BlueBox(),
),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3) {
_result(false, ['The Row should end up with three children.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! SizedBox) {
_result(false, ['The Row\'s second child should be a SizedBox widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a BlueBox widget.']);
return;
}
final sizedBox = row.children[1] as SizedBox;
if (sizedBox.width != 100) {
_result(false, ['The SizedBox should have a width of 100.']);
return;
}
if (sizedBox.height != 100) {
_result(false, ['The SizedBox widget resizes the BlueBox widget to 100 logical pixels wide. Add a height property inside SizedBox equal to 100 logical pixels.']);
return;
}
_result(true, ['The SizedBox widget resizes the BlueBox widget to 100 logical pixels wide and tall.']);
}
{$ end test.dart $}
Example: Creating space
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
SizedBox(width: 50),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.spaceAround
|| row.mainAxisAlignment == MainAxisAlignment.spaceBetween
|| row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(false, ['It\'s best to use MainAxisAlignment.start, MainAxisAlignment.end, or MainAxisAlignment.center to see how the SizedBox widgets work in a Row.']);
return;
}
if (row.children.length != 5) {
_result(false, ['The SizedBox widget creates space at 50 logical pixels wide. Add another SizedBox class between the second and third BlueBox widgets with a width property equal to 25 logical pixels.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! SizedBox) {
_result(false, ['The Row\'s second child should be a SizedBox widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a BlueBox widget.']);
return;
}
if (row.children[3] is! SizedBox) {
_result(false, ['The Row\'s fourth child should be a SizedBox widget.']);
return;
}
if (row.children[4] is! BlueBox) {
_result(false, ['The Row\'s fifth child should be a BlueBox widget.']);
return;
}
final sizedBox = row.children[1] as SizedBox;
if (sizedBox.width != 50) {
_result(false, ['The SizedBox should have a width of 50.']);
return;
}
final sizedBox2 = row.children[3] as SizedBox;
if (sizedBox2.width != 25) {
_result(false, ['SizedBox should have a width of 25.']);
return;
}
_result(true, ['The SizedBox widgets create space between the BlueBox widgets, one space at 50 logical pixels and one at 25 logical pixels.']);
}
{$ end test.dart $}
Spacer widget
Similar to SizedBox
, the Spacer
widget also
can create space between widgets.
Example: Creating more space
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
Spacer(flex: 1),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.spaceAround
|| row.mainAxisAlignment == MainAxisAlignment.spaceBetween
|| row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(false, ['It\'s best to use MainAxisAlignment.start, MainAxisAlignment.end, or MainAxisAlignment.center to see how the SizedBox widgets work in a Row.']);
return;
}
if (row.children.length != 5) {
_result(false, ['What do you think would happen if you added another Spacer widget with a flex value of 1 between the second and third BlueBox widgets?']);
return;
}
if (row.children[0] is! BlueBox ||
row.children[1] is! Spacer ||
row.children[2] is! BlueBox ||
row.children[3] is! Spacer ||
row.children[4] is! BlueBox) {
_result(false, ['Not quite. Row should contain five children in this order: BlueBox, Spacer, BlueBox, Spacer, BlueBox.']);
return;
}
final spacer = row.children[3] as Spacer;
if (spacer.flex != 1) {
_result(false, ['The Spacer class should have a flex equal to 1.']);
return;
}
_result(true, ['Both Spacer widgets create equal amounts of space between all three BlueBox widgets.']);
}
{$ end test.dart $}
Text widget
The Text
widget displays text and can be configured
for different fonts, sizes, and colors.
Example: Aligning text
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'Hey!',
style: TextStyle(
fontSize: 30,
fontFamily: 'Futura',
color: Colors.blue,
),
),
Text(
'Hey!',
style: TextStyle(
fontSize: 50,
fontFamily: 'Futura',
color: Colors.green,
),
),
Text(
'Hey!',
style: TextStyle(
fontSize: 40,
fontFamily: 'Futura',
color: Colors.red,
),
),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! Text)) {
_result(false, ['The Row should have three children, all Text widgets.']);
return;
}
if (row.textBaseline == null) {
_result(false, ['To use CrossAxisAlignment.baseline, you need to set the Row\'s textBaseline property.']);
return;
}
if (row.crossAxisAlignment != CrossAxisAlignment.baseline) {
_result(false, ['The Text widgets are positioned at the middle of the cross axis. Change CrossAxisAlignment.center to CrossAxisAlignment.baseline.']);
return;
}
_result(true, ['The Text widgets are now aligned by their character baselines.']);
}
{$ end test.dart $}
Icon widget
The Icon
widget displays a graphical symbol
that represents an aspect of the UI.
Flutter is preloaded with icon packages for
Material and Cupertino applications.
Example: Creating an Icon
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: [
Icon(
Icons.widgets,
size: 50,
color: Colors.blue,
),
Icon(
Icons.widgets,
size: 50,
color: Colors.red,
),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! Icon)) {
_result(false, ['Row should have three children, all Icon widgets.']);
return;
}
final icon = row.children[2] as Icon;
if (icon.color != Colors.amber) {
_result(false, ['Add a third Icon. Give the Icon a size of 50 and a color of Colors.amber.']);
return;
}
_result(true, ['The code displays three Icons in blue, red, and amber.']);
}
{$ end test.dart $}
Image widget
The Image
widget displays an image.
You either can reference images using a URL,
or you can include images inside your app package.
Since DartPad can’t package an image,
the following example uses an image from the network.
Example: Displaying an image
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network('[Place an image link here!]'),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xffeeeeee),
child: Center(
child: Container(
child: MyWidget(),
color: Color(0xffcccccc),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
final rows = controller.widgetList(find.byType(Row));
if (rows.length == 0) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
}
{$ end test.dart $}
Putting it all together
You’re almost at the end of this codelab. If you’d like to test your knowledge of the techniques that you’ve learned, why not apply those skills into building a Flutter UI that displays a business card!
You’ll break down Flutter’s layout into parts, which is how you’d create a Flutter UI in the real world.
In Part 1,
you’ll implement a Column
that contains the name and title.
Then you’ll wrap the Column
in a Row
that contains the icon,
which is positioned to the left of the name and title.
In Part 2, you’ll wrap the Row
in a Column
,
so the code contains a Column
within a Row
within a Column
.
Then you’ll tweak the outermost Column
’s layout,
so it looks nice.
Finally, you’ll add the contact information
to the outermost Column
’s list of children,
so it’s displayed below the name, title, and icon.
In Part 3, you’ll finish building the business card display by adding four more icons, which are positioned below the contact information.
Part 1
Exercise: Create the name and title
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
TODO('Begin implementing the Column here.');
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headline5,
),
Text('Experienced App Developer'),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: Color(0xffeeeeee),
textTheme: TextTheme(
bodyText2: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: Color(0xffffffff),
border: Border.all(),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance!);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Column) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check Column has correct properties
final innerColumnElement = myWidgetChildElements[0];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column that contains the name and title should use CrossAxisAlignment.start as its CrossAxisAlignment value.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column that contains the name and title should use MainAxisSize.min as its MainAxisSize value.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column that contains the name and title should have two children, both Text widgets.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headline5" textStyle.']);
return;
}
_result(true);
}
{$ end test.dart $}