SizedBox, Padding and ColoredBox each do one job. Container is a convenience widget that combines size, padding, margin, decoration and transform, and builds those single-purpose widgets for you.
**SizedBox**: a fixed size or a gap between widgets; SizedBox.expand() fills the space, SizedBox.shrink() takes none
**Padding: empty space around its child, nothing else
ColoredBox**: a plain background colour; DecoratedBox adds a border, rounded corners, a shadow or a gradient
**Container**: for when you need several of these at once; its margin is space outside the decoration, padding is space inside it. Plain widgets have no margin, so you wrap them in a Padding
Why prefer the small ones: Container has no const constructor, while const SizedBox(height: 8) is created once and skipped on rebuild
FlutterJunior
Basic Widgets
Core layout widgets: Container, Row, Column, Stack, ListView
Questions closed
0 / 24
- 0Review
- 9Asked often
Questions
In debug it fails an assertion in the constructor: "Cannot provide both a color and a decoration". color is only a shorthand for decoration: BoxDecoration(color: ...), so move the colour into the BoxDecoration.
Why it is an error: two backgrounds would be painted on top of each other, and the framework refuses to guess which one you meant
The next surprise: borderRadius rounds the background but not the child: an image inside still has square corners until you add clipBehavior: Clip.antiAlias to the Container
The main axis is the direction the children are laid out in (horizontal for Row, vertical for Column); the cross axis is perpendicular to it. mainAxisAlignment places children along the main axis, crossAxisAlignment across it.
**mainAxisAlignment**: start, center, end, spaceBetween, spaceAround, spaceEvenly; it only has a visible effect when there is free space left
**crossAxisAlignment**: center by default; stretch forces every child to the full cross size; baseline lines up text and needs textBaseline: TextBaseline.alphabetic, otherwise it asserts
**mainAxisSize**: max (the default) takes all the space the parent allows, min shrinks to the children, e.g. a Column inside a dialog
**spacing**: since Flutter 3.27 a fixed gap between children, instead of a SizedBox between each pair
Direction-aware: in a Row, start means left in a left-to-right language and right in Arabic or Hebrew
Expanded is Flexible with fit: FlexFit.tight: the child is forced to fill exactly the share of free space it was given. Plain Flexible lets the child stay smaller than its share.
Both only work as a direct child of a Row, Column or Flex; anywhere else Flutter reports "Incorrect use of ParentDataWidget".
**flex**: defaults to 1; free space is split in proportion to the siblings' flex values, so flex: 2 gets twice as much as flex: 1
Which to use: Expanded for a panel that must fill the row, Flexible for a child that keeps its natural size and shrinks only when the row is full
**Spacer**: nothing but an Expanded around an empty SizedBox.shrink(); it pushes the next child to the far end
All three change size rather than draw anything: AspectRatio picks a size with a fixed width-to-height ratio, FittedBox scales its child to fit the space, and ConstrainedBox adds min/max limits on top of the parent's constraints.
**AspectRatio: a 16:9 video or a square image placeholder; it takes the largest width allowed and computes the height (or starts from the height if the width is unbounded), so it needs at least one bounded side
FittedBox**: scales a child that has its own natural size, e.g. a price that must stay on one line on a narrow phone; fit: BoxFit.scaleDown only ever shrinks. It scales pixels, so text gets smaller instead of wrapping
**ConstrainedBox**: BoxConstraints(maxWidth: 480) keeps a form readable on a tablet, minHeight: 48 guarantees a tap target
The catch: ConstrainedBox can only choose a size within what the parent allows; under a tight constraint it does nothing, so put a Center or Align above it
Stack paints its children on top of each other, the last one on top. Use it whenever widgets must overlap (a caption over a photo, a badge on an icon, a loader over a form), which Row and Column cannot do.
Non-positioned children: placed by alignment (top-start by default); the largest of them decides the Stack's size
**Positioned**: pins a child by top, left, right, bottom from the Stack's edges and does not affect its size; Positioned.fill stretches a child over the whole Stack, e.g. a gradient scrim
**fit: StackFit.expand: forces the non-positioned children to fill the Stack
Clipping**: clipBehavior defaults to Clip.hardEdge, so a child pushed past the edge with a negative offset is cut off; Clip.none paints it, but taps on the part outside still do not register
**IndexedStack**: shows one child at a time but keeps all of them alive, e.g. the pages behind a bottom bar
Wrap fills a line with children and then starts a new one, like words in a paragraph; a Row has exactly one line, so too many children overflow it with yellow stripes. Tag chips, filters and hashtags belong in a Wrap.
The knobs: spacing between children in a line, runSpacing between lines, alignment and runAlignment to position them; direction: Axis.vertical wraps into columns
Long children: Wrap gives each child at most the line width, so a long Text wraps onto several lines instead of overflowing; add maxLines and TextOverflow.ellipsis if a chip must stay on one line
No flex inside: Expanded and Flexible do not work in a Wrap ("Incorrect use of ParentDataWidget")
Not lazy: every child is built up front: a few dozen chips are fine, hundreds belong in a scrollable grid
**spacing on Row is not wrapping**: since Flutter 3.27 Row and Column also take spacing, but they still never start a new line
"A RenderFlex overflowed by N pixels on the right", drawn as yellow-and-black stripes. A Row lets a non-flexible child be as wide as it wants, so the long Text stays on one line and runs past the edge; wrap it in Expanded and add maxLines: 1 with TextOverflow.ellipsis.
**Why Expanded helps**: it gives the Text exactly the width left after the icons, so the text can truncate or wrap instead of overflowing
**Flexible instead: when a short name should not stretch and push the chevron to the far edge
The stripes are debug-only: a release build has the same broken layout, just without the warning
The same error in a Column**: usually the keyboard shrinking the screen; the fix there is a SingleChildScrollView, not Expanded
Other fixes: Wrap when children should move to a new line, FittedBox when content should scale down
SafeArea pads its child so it stays clear of the notch, the status bar, rounded corners and the home indicator. It reads those insets from MediaQuery.padding and passes down a MediaQuery with them removed, so a nested SafeArea adds nothing.
**Around the body, not the Scaffold**: the AppBar already handles the status bar; wrapping the whole Scaffold pushes the bar down and leaves a blank strip above it
Per edge: top: false, bottom: false and so on when only some sides need it; minimum: sets a floor for the padding even where the device reports no inset
Lists: often better left unwrapped so content scrolls under the status bar, with bottom padding for the home indicator; SliverSafeArea does the same job inside a CustomScrollView
ListView.builder creates rows on demand as they scroll into view, so ten thousand items cost about one screenful. ListView(children:) creates every child widget up front, and a Column in a SingleChildScrollView builds and lays out all of them.
How lazy building works: the viewport asks the builder only for indexes inside the visible area plus cacheExtent (250 pixels on each side by default); rows that scroll far away are disposed
**ListView(children:): fine for a short fixed list such as a settings screen; the widgets already exist, though only the visible ones are laid out
ListView.separated**: builder plus a separatorBuilder for dividers between rows
**itemExtent / prototypeItem: tell the list how tall every row is, so it can jump and size the scrollbar without laying rows out
SingleChildScrollView**: for a form or a page of mixed content that is a bit taller than the screen; never for a list that comes from data
They hide a widget at different levels: Visibility(visible: false) removes it from the tree, Opacity(opacity: 0) keeps it laid out and tappable but invisible, and Offstage keeps it alive but takes no space, paints nothing and ignores taps.
**Visibility**: by default the child is replaced with an empty SizedBox, so its State is lost; the maintainState, maintainSize flags (or Visibility.maintain) keep the state and the space
**Opacity**: the child still takes its space and still receives taps; add IgnorePointer if it should not. Values between 0 and 1 cost an extra compositing pass, so for fades use AnimatedOpacity or FadeTransition
**Offstage: the child keeps its state and its animations keep ticking, which costs CPU; use it to keep a widget warm or to measure it before showing
Simplest option**: if (show) const PromoBanner() inside a children list, which is what Visibility does by default
Material 3 buttons differ in emphasis, and you pick by how important the action is: FilledButton for the main action, OutlinedButton for a secondary one, TextButton for the quietest.
**FilledButton**: the primary action such as "Pay" or "Sign up", ideally one per screen; FilledButton.tonal for a second important action that should not compete with it
**OutlinedButton: a secondary action like "Cancel" next to a filled button
TextButton: low emphasis: "Forgot password?", actions inside a card or a dialog
ElevatedButton: the old Material 2 primary; in M3 keep it for a button that sits over busy content and needs a shadow
IconButton**: icon only; .filled, .filledTonal and .outlined give the same emphasis levels, and every button has an .icon constructor for icon plus label
Disabled state: pass onPressed: null; the button greys out and ignores taps, so there is no separate enabled flag
InkWell paints its ripple on the nearest Material ancestor, and the Container colour is painted above that Material, covering the splash. Use Ink(color: ...) so the colour is drawn on the Material under the splash.
First check: a ready widget may already do it: ListTile has onTap, buttons and ActionChip have onPressed, with the right feedback and accessibility built in
**Where the Material comes from**: a Scaffold, Card or Material supplies one; without any Material above, InkWell throws "No Material widget found"
Common mistake: taps on empty padding are lost, because only painted parts are hit; set behavior: HitTestBehavior.opaque on the GestureDetector
Image has a named constructor per source: Image.asset, Image.network, Image.memory and Image.file. Image.network is a widget; inside it uses NetworkImage, an ImageProvider that downloads and decodes and is the thing you pass where an API wants a provider, not a widget.
Assets: list the files or the folder under flutter: assets: in pubspec.yaml; variants in 2.0x/ and 3.0x/ folders are picked by screen density
Providers in use: CircleAvatar(backgroundImage: NetworkImage(url)), DecorationImage(image: AssetImage(...)), precacheImage(...)
Network: show something while loading and on failure with loadingBuilder and errorBuilder; Image.network caches in memory only, so apps usually add cached_network_image for a disk cache
**fit**: BoxFit.cover fills the box and crops, BoxFit.contain shows the whole picture
Limits: SVG needs the flutter_svg package; Image.file uses dart:io and does not work on the web
Call ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: ...)). ScaffoldMessenger sits above all routes (MaterialApp creates one) and owns the snackbar queue, so a message survives a route change and shows on whichever Scaffold is current.
History: snackbars used to go through Scaffold.of(context).showSnackBar, which tied them to one screen; that method was deprecated in favour of ScaffoldMessenger and later removed
Queue: three calls show three snackbars one after another; call hideCurrentSnackBar() first if only the latest matters
Action and style: SnackBarAction(label: 'Undo', onPressed: ...); behavior: SnackBarBehavior.floating lifts it off the bottom edge
No context at hand: give MaterialApp a scaffoldMessengerKey and call key.currentState?.showSnackBar(...)
**After an await**: check context.mounted before using the context
Wrap the screen in DefaultTabController(length: n), put a TabBar in AppBar.bottom and a TabBarView in the body. The controller keeps the selected tab and the swipe position in sync between the two.
The counts must match: length, the number of tabs and the number of TabBarView children; a mismatch throws
**Your own TabController**: when you switch tabs from code or react to changes: create it in a State with SingleTickerProviderStateMixin, pass vsync: this, and dispose it
Without your own controller: DefaultTabController.of(context) below it gives index and animateTo(...)
Tab state: TabBarView is a PageView, so a tab you switch away from is disposed and loses its scroll position and State; keep the data above the tabs or use AutomaticKeepAliveClientMixin in the page
Bottom navigation is different: NavigationBar plus an IndexedStack, not a TabBarView
Each parent passes its child min/max constraints, the child picks a size within them, and the parent decides where to place it. A child cannot break its constraints: under a tight constraint (min == max) there is exactly one allowed size, so width: 300 is silently clamped to it.
Who passes tight constraints: SizedBox.expand, Positioned.fill, Expanded along the main axis, crossAxisAlignment: stretch across it, and the root of the app (the screen forces its full size). A Scaffold body is loose (0 up to the screen size), so width: 300 works there
Getting the 300 back: wrap the child in Center or Align: they pass loose constraints (min = 0), so a smaller size is allowed
**double.infinity**: means "as big as allowed"; fine under a bounded constraint, but inside a Row the width is unbounded and Flutter throws "BoxConstraints forces an infinite width"
How to debug: print the constraints with LayoutBuilder; when a size is ignored, stop editing the child and walk up the tree to the parent that makes it tight
A Column gives its children unbounded height, and a ListView tries to be as tall as its constraints allow: here, infinite. Flutter stops with "Vertical viewport was given unbounded height".
**Expanded: the usual fix: the list takes the space left after the other children and stays lazy
SizedBox(height: ...)**: when the list is a strip of known size; this is also how a horizontal list sits inside a Column
**shrinkWrap: true + NeverScrollableScrollPhysics(): only for a short list inside an outer scroll view; it lays out every item up front, so the laziness is gone
CustomScrollView**: the right answer for "a header plus a long list": SliverToBoxAdapter plus SliverList, one scroll for both
Common mistake: wrapping the Column in a SingleChildScrollView does not help: the height is still unbounded
The grid delegate sets the tile size and the content gets no vote: with crossAxisCount, tile width = (available width − spacing) ÷ count, and height = width ÷ childAspectRatio. A narrower phone means a shorter tile while the text keeps its size, so it overflows there.
**mainAxisExtent: a height in pixels that overrides the aspect ratio; the honest fix when the design gives you a tile height
SliverGridDelegateWithMaxCrossAxisExtent: the column count follows the screen width instead of being fixed at 2
Lazy or not**: GridView.builder builds tiles on demand; GridView.count and GridView.extent take a ready list of children, fine for a dozen tiles
Make the tile survive: maxLines and TextOverflow.ellipsis on text, and a Spacer to absorb leftover height
Common mistake: tuning childAspectRatio to a magic 0.72 until the stripes go away on your phone; you wanted mainAxisExtent
Scaffold.of(context) searches upward from context, and the context passed to HomePage.build belongs to HomePage, which sits above the Scaffold it creates. Give the button a context below the Scaffold with a Builder, or move the button into its own widget.
**What Builder does**: a widget whose only job is to call your function with its own BuildContext, one level deeper in the tree
Extracting a widget: the cleaner fix in real code: a separate MenuButton widget gets a context that is inside the Scaffold
**GlobalKey<ScaffoldState>: works from anywhere, but a global key per screen is heavier and easy to misuse
Same trap elsewhere**: Theme.of, Navigator.of or any lookup in the widget that itself creates that Theme or Navigator; ScaffoldMessenger.of(context) works here only because MaterialApp put the messenger higher up
Often not needed: an AppBar in a Scaffold with a drawer already shows the menu button that opens it
body: _pages[_index] puts a different widget into the same slot on every switch, so Flutter unmounts the old page and disposes its State together with the scroll position and the loaded data. Use IndexedStack(index: _index, children: _pages) so every page stays mounted.
Why it happens: when a slot gets a widget of another type, the element is replaced, not updated: FeedPage runs dispose(), and coming back runs initState() again, which starts the fetch again. const does not help: it saves rebuilds, not State
**How IndexedStack fixes it**: it keeps all children in the tree but paints and hit-tests only the one at index, so hidden pages keep their State, scroll offset and running requests
The cost: every page is built at startup and stays in memory; for heavy tabs build a page on its first visit and show an empty placeholder until then. With go_router, StatefulShellRoute.indexedStack does the same per branch
Not a fix: a PageStorageKey on the list brings back only the scroll offset; the page is still recreated and loads its data again
Catching it: a widget test: scroll the feed, tap another destination and back, expect the same offset and one load call
An implicit animation animates a property for you whenever its value changes. An explicit one gives you an AnimationController that you start, stop, repeat and reverse yourself. Start with implicit; take a controller only when the animation has a life of its own.
Implicit: AnimatedContainer, AnimatedOpacity, AnimatedAlign, AnimatedSwitcher: change the value in setState, give a duration and a curve, and the widget animates there. No controller, nothing to dispose
Explicit: an AnimationController with vsync: this plus a Tween; needed for a looping animation, one driven by a drag, or several animations staged off one controller. Always dispose it
Rebuild scope: FadeTransition, ScaleTransition or AnimatedBuilder rebuild only the animated subtree; a listener that calls setState rebuilds the whole widget every frame
CustomPainter gives you a Canvas to draw what widgets cannot express: charts, progress rings, signatures, custom shapes. You extend it, implement paint(canvas, size) and shouldRepaint(oldDelegate), and pass it to a CustomPaint widget.
Coordinates: size is the box you were given and (0, 0) is its top-left corner; draw relative to size, never to fixed screen numbers
The Canvas API: drawLine, drawCircle, drawRect, drawArc, drawPath, drawImage; text needs a TextPainter that you lay out first
**Paint**: colour, strokeWidth, style (fill or stroke), shaders for gradients
**shouldRepaint**: return true only when something that affects the drawing changed; returning true always repaints on every rebuild of the parent
Animating: pass the animation to super(repaint: animation) so the painter repaints on each tick without rebuilding widgets, and put a RepaintBoundary around the CustomPaint
When not to: if a BoxDecoration, ClipRRect or an existing chart package already does it, stay with widgets
shouldRepaint always returns false, so when the parent rebuilds with a new painter, the render object keeps the old picture and the ring stays stuck. Return true when an input that affects the drawing changed: oldDelegate.progress != progress.
Why it happens: CustomPaint keeps the same RenderCustomPaint; given a new painter, it schedules a repaint only if the painter type changed or shouldRepaint(oldDelegate) returns true. A rebuild of the widget is not a repaint
Why it looks flaky: the new painter is still stored, so the ring jumps to the current value whenever something else repaints the same layer, such as a ripple nearby; behind a RepaintBoundary it never moves
The fix: compare every field paint reads (progress, colour, stroke width); a field left out of the comparison brings the bug back for that field. => true also works but repaints on every rebuild of the parent
For animations: pass the Animation to super(repaint: animation) and read its value in paint: the painter repaints on every tick without rebuilding widgets
Catching it: no lint flags it; a widget or golden test that pumps a second value and checks the painted result does