Tutorial · 15 min · beginner

Layout that thinks in widgets

Build a profile card from the core layout vocabulary — Row, Column, Stack, Expanded — and learn how DartNative makes flexbox behave exactly like Flutter.

What you'll build — running on device.

Layout is where a “Flutter-compatible” claim gets tested hardest, because Flutter’s sizing rules are subtle: children fill a Column’s width, a Stack sizes to its first in-flow child, Expanded divides leftovers. DartNative runs layout on Yoga (flexbox) but enforces Flutter’s rules on top of it — so the code below is plain Flutter thinking, rendering as real native views. You’ll build a profile card that uses the whole core vocabulary.

What you need

Step 1 — The screen skeleton

A white screen with a scrolling body — every section is just a ListView child, spaced with SizedBox:

Scaffold(
  backgroundColor: const Color(0xFFFFFFFF),
  brightness: Brightness.light, // declare the light trait
  appBar: AppBar(title: const Text('Profile', style: ...)),
  body: ListView(
    padding: const EdgeInsets.all(16),
    children: const [
      _Banner(),
      SizedBox(height: 56), // clears the avatar overhang
      _NameBlock(),
      SizedBox(height: 20),
      _StatsRow(),
      // ...
    ],
  ),
)

A childless SizedBox is a spacer — that ports straight from Flutter. (One DartNative difference to remember: SizedBox(width:, child:) does not constrain its child — use Container(width:) for that.)

Step 2 — The banner, with Stack

The classic profile header: a gradient banner with the avatar overhanging its bottom edge.

Stack(
  clipBehavior: Clip.none, // let the avatar overhang
  children: [
    // In-flow child: defines the Stack's size.
    Container(
      height: 140,
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFFFA60A6), Color(0xFFB5C75E)],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(16),
      ),
    ),
    // Overlay: positioned against the Stack's box, hanging below it.
    Positioned(
      left: 20,
      bottom: -44,
      child: Container(
        width: 88, height: 88,
        decoration: BoxDecoration(
          color: const Color(0xFF7C6BAE),
          borderRadius: BorderRadius.circular(44),
          border: Border.all(color: const Color(0xFFFFFFFF), width: 4),
        ),
        child: const Center(child: Text('L', style: ...)),
      ),
    ),
  ],
)

This is the Flutter Stack contract, reproduced deliberately: the first in-flow child sizes the Stack, Positioned children float against that box. (Under the hood, positioned children become absolute-positioned Yoga nodes — but you never think about that.)

Step 3 — Name row: grow vs hug

Two intents in one Row: the name block should take all available width, the PRO badge should hug its content.

Row(
  children: [
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text('Lisa Taylor', style: ...),
          SizedBox(height: 2),
          Text('@lisa — building native things', style: ...),
        ],
      ),
    ),
    Container( // the badge: no Expanded → intrinsic width
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
      decoration: BoxDecoration(
        color: const Color(0xFFEDF7DD),
        borderRadius: BorderRadius.circular(999),
      ),
      child: const Text('PRO', style: ...),
    ),
  ],
)

Expanded = grow, plain child = hug. And note BorderRadius.circular(999) — the Flutter capsule idiom works (it’s clamped internally so it can’t degenerate).

Step 4 — The stats strip: equal thirds

Row(
  children: [
    stat('128', 'Posts'),      // each is Expanded(child: Column(...))
    stat('2.4k', 'Followers'),
    stat('310', 'Following'),
  ],
)

Three Expanded children with no flex argument split the row equally — the flexbox way to build any N-up strip, no width math.

Step 5 — Proportional buttons

Expanded(flex:) divides space by ratio — Follow gets two thirds, Message one:

Row(
  children: [
    Expanded(flex: 2, child: /* Follow button */),
    const SizedBox(width: 10),
    Expanded(child: /* Message button */),
  ],
)

Run it and rotate the device: everything re-flows proportionally, because nothing above uses a hard-coded width except the avatar.

Why this is native

The layout math is flexbox, but everything it positions is a real platform view: the texts are UILabels, the scroll is a real scroll view, the gradient is a native layer. DartNative’s reconciler enforces Flutter’s sizing contract on top of Yoga — children fill a Column’s width, Stacks size to their first in-flow child, double.infinity means fill — so layouts port from Flutter without relearning anything, and render without a canvas imitating anything.

The finished code

One file, in the public repo — dn create ., dn run.

Open the finished code →