skip to content

In Flutter, how does AspectRatio choose its size from its constraints, and why can a 16:9 AspectRatio used as a GridView tile do nothing?

level: middleimportance: should knowfreq 42%

answer

  1. width first, height derived
  2. falls back to maxHeight if width infinite
  3. tight constraints win
  4. grid tiles are tight
  5. both unbounded is an error

basics

~20 s

AspectRatio takes the largest width allowed, derives the height from the ratio, then adjusts to fit the constraints. Under tight constraints, such as a GridView tile, it must take the forced size and the ratio is ignored.

solid answer

~50 s

`AspectRatio` computes a size in one pass, without laying the child out twice. If its constraints are **tight**, it simply takes `constraints.smallest` — there is no freedom, so the ratio is ignored. Otherwise it starts from `maxWidth` and sets `height = width / aspectRatio`; if `maxWidth` is infinite it starts from `maxHeight` instead. It then clamps against the max and min constraints, re-deriving the other side each time, and gives up on the ratio only when the constraints allow no matching size. A `GridView` tile gets tight constraints from its delegate, so an `AspectRatio` placed directly as the tile does nothing; the delegate's `childAspectRatio` (default 1.0) or `mainAxisExtent` sets the tile's shape. Inside the tile, wrapped in a `Column`, the widget works, because the `Column` gives it unbounded height. With both axes unbounded, it throws a debug error.

code

dart · 23 lines
dart
import 'package:flutter/material.dart';

class LectureTile extends StatelessWidget {
  const LectureTile({super.key, required this.title, required this.thumbnail});

  final String title;
  final ImageProvider thumbnail;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 16 / 9,
          child: Image(image: thumbnail, fit: BoxFit.cover),
        ),
        const SizedBox(height: 8),
        Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
      ],
    );
  }
}

go deeper

for a junior

Know that AspectRatio(aspectRatio: 16 / 9) sizes a child to that ratio and that the value is width divided by height.

for a middle

Walk through the width-first algorithm, the infinite-width fallback, the tight-constraints shortcut and the both-unbounded error.

for a senior

Diagnose ratio bugs from the constraints: tight grid tiles, FittedBox's unbounded child, min constraints that override the ratio.

for a principal

Encourage media components that own their aspect ratio internally so feature screens do not fight grid and list constraints.

## What AspectRatio is for `AspectRatio` sizes its child to a **width-to-height ratio**: `16 / 9` for a video thumbnail, `1.0` for a square avatar. The ratio is a `double` that must be positive and finite. It is a normal single-child layout widget: it receives constraints from its parent, chooses a size and hands that size to its child as **tight** constraints. ## The algorithm The render object, `RenderAspectRatio`, runs a short fixed sequence — no extra layout pass: 1. If the constraints are **tight**, return `constraints.smallest`. The parent has already decided the size. 2. Start with `width = maxWidth`. If it is finite, `height = width / aspectRatio`. 3. If `maxWidth` is infinite, start from the height instead: `height = maxHeight`, `width = height * aspectRatio`. 4. If `width > maxWidth`, clamp the width and re-derive the height. 5. If `height > maxHeight`, clamp the height and re-derive the width. 6. Apply the same steps for `minWidth` and `minHeight`. 7. Finally `constraints.constrain(...)` the result. If no size satisfies both the ratio and the constraints, the constraints win. In debug mode, if **both** width and height are unbounded, it throws a `FlutterError` saying it has unbounded constraints and does not know how much space to take. | Incoming constraints | Result for `16 / 9` | |---|---| | tight 300 x 300 | 300 x 300; the ratio is ignored | | width 0..320, height unbounded | 320 x 180 | | width unbounded, height 0..90 | 160 x 90 | | width 0..320, height 0..90 | 160 x 90, since 180 exceeds the max height | | both unbounded | debug error | ## Why it "does nothing" in a grid In a lecture-archive app you might render 16:9 video thumbnails in a `GridView`. Each tile is laid out with **tight** constraints computed by the grid delegate: the cross-axis extent from the column count, and the main-axis extent from `childAspectRatio`, which defaults to **1.0**, or from `mainAxisExtent`. Wrap a tile in `AspectRatio(aspectRatio: 16 / 9)` and step 1 fires: the tile is square anyway. Two fixes: - Set the tile shape on the delegate, `childAspectRatio: 16 / 9`, when the tile is only the thumbnail. - Make the tile a `Column` with the thumbnail in an `AspectRatio` and the title below. A `Column` gives its children unbounded height, so step 2 applies: full width, height derived. The tile height from the delegate must then leave room for the title. ## Other places it surprises people - **Inside `FittedBox`**, the child is laid out with fully unbounded constraints, so `AspectRatio` hits the both-unbounded case. The docs suggest a `SizedBox` of explicit size instead; the `FittedBox` then scales it. - **Inside a `Row`** without `Expanded`, width is unbounded. It then uses the height; if height is also unbounded, it fails. - **With a `ConstrainedBox` above it**, the min constraints can force a size that breaks the ratio, and step 6 accepts that. ## Interview summary Say "largest width, derived height, clamp, and tight constraints win", then use the grid tile as the example of tight constraints. That shows you know the ratio is a preference inside the constraint system, not an override of it.

  • What size does AspectRatio(aspectRatio: 2) choose when its constraints are width 0 to 100 and height 0 to 100?
    100 by 50. It starts from the maximum width, 100, and derives a height of 50, which fits inside the height limit, so nothing needs clamping.
  • What happens with AspectRatio(aspectRatio: 0.5) under the same 100 by 100 maximum?
    It starts at width 100, derives height 200, which exceeds the maximum height, so it clamps height to 100 and re-derives width as 50. The result is 50 by 100, still at the requested ratio.

saying these in an interview costs you the question

  • AspectRatio always enforces its ratio, whatever the constraints.
  • AspectRatio lays its child out twice to find the right size.
  • Wrapping a GridView tile in AspectRatio changes the tile's shape.
  • AspectRatio derives width from height first by default.
  • AspectRatio works fine when both width and height are unbounded.