Home Flutter UI 102 Adaptive Layouts

📱 Adaptive Layouts — Make Your App Look Great Everywhere!

⏱️ 40-45 minutes 📊 Advanced — Responsive design! 📦 2 Projects included 🏷️ LayoutBuilder, Slivers, Responsive, Contacts UI

🧒 One App, Many Screens — The Shape-Shifter Challenge!

📱➡️💻

Your App Needs to Work on Tiny Phones AND Giant Tablets!

Imagine designing a poster that needs to look perfect on a business card AND a highway billboard. That's the challenge of modern app development — your app runs on phones (small), tablets (medium), and desktops (large).

Flutter gives you two powerful tools to solve this:

📏

LayoutBuilder

Knows exactly how much space is available. You can say: "If the width is less than 600, show this. Otherwise, show that."

📜

Slivers

Advanced scrolling widgets that create fancy effects like sticky headers, collapsing toolbars, and parallax scrolling.

📏

LayoutBuilder — "How Much Room Do I Have?"

LayoutBuilder is a widget that gives you the constraints (max width and height) of its parent. You can make decisions based on the available space:

🔍 LayoutBuilder Pattern

LayoutBuilder example
LayoutBuilder(
    builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
            return PhoneLayout();     // Narrow = phone
        } else {
            return TabletLayout();   // Wide = tablet/desktop
        }
    },
)

What's happening: constraints.maxWidth tells you the available width in pixels. 600 is a common breakpoint — below it is a phone, above is a tablet. You can create completely different layouts for each!

📜

Slivers — The Secret to Fancy Scrolling Effects

Regular ListViews are like a simple roll of paper. Slivers are like a Swiss Army knife for scrolling — they can do sticky headers, collapsing toolbars, and parallax effects:

📌

SliverPersistentHeader

Headers that stick to the top while you scroll. Like the iOS Contacts app — "A", "B", "C" headers that stay visible!

📋

SliverList

A scrolling list, but with superpowers. Each child can have different heights and behaviors.

🎢

CustomScrollView

The container that holds all your slivers. Think of it as the "scroll track" that slivers ride on.

Today you'll use slivers to build the iOS Contacts-style list with alphabet headers that stick to the top!

1 Create the Contacts List Screen

We'll create the main screen that displays all contacts in an alphabetized, scrollable list — just like the iOS Contacts app.

Step 1.1 Create lib/screens/contacts_list.dart

Create a new file lib/screens/contacts_list.dart. This is the main screen widget:

🔍 ContactsListScreen Structure

lib/screens/contacts_list.dart
import 'package:flutter/cupertino.dart';
import '../data/contact_group.dart';

class ContactsListScreen extends StatelessWidget {
    const ContactsListScreen({super.key, required this.contactGroup});

    final ContactGroup contactGroup;

    @override
    Widget build(BuildContext context) {
        final alphabetized = contactGroup.alphabetizedContacts;

        return CupertinoPageScaffold(
            navigationBar: CupertinoNavigationBar(
                middle: Text(contactGroup.title),
            ),
            child: SafeArea(
                child: CustomScrollView(                    // ← The sliver container
                    slivers: [
                        // SliverList and sticky headers go here!
                    ],
                ),
            ),
        );
    }
}

Key parts:

  • CupertinoNavigationBar — iOS-style top navigation bar with the group title.
  • CustomScrollView — The special scroll container that holds slivers instead of regular children.
  • slivers: [...] — This is where we'll put SliverList and sticky headers. Regular ListView uses children; CustomScrollView uses slivers.

2 Build the Alphabetized Contact List with Sticky Headers

Now we'll fill in the slivers — one section per letter (A, B, C...) with a sticky header for each.

Step 2.1 Build SliverList Sections with Sticky Headers

Replace the empty slivers: [...] with this code that creates alphabet sections:

Complete slivers list
slivers: [
    for (final letter in alphabetized.entries)
        ...[
            SliverPersistentHeader(                     // ← Sticky letter header!
                pinned: true,                         // ← Stays at top while scrolling
                delegate: _ContactHeaderDelegate(letter: letter.key),
            ),
            SliverList(                                 // ← Contacts for this letter
                delegate: SliverChildBuilderDelegate(
                    (context, index) {
                        final contact = letter.value[index];
                        return Padding(
                            padding: const EdgeInsets.symmetric(
                                horizontal: 16, vertical: 8),
                            child: Text(
                                '${contact.firstName} ${contact.lastName}',
                                style: const TextStyle(fontSize: 17),
                            ),
                        );
                    },
                    childCount: letter.value.length,
                ),
            ),
        ],
],

What's happening:

  • for (final letter in alphabetized.entries) — Loops through each letter group (A: [Adam, Anna], B: [Ben, Bob], etc.).
  • SliverPersistentHeader(pinned: true) — Creates a header that sticks to the top when you scroll past it. The "A" header stays visible until "B" pushes it away!
  • SliverList — A scrollable list of contacts for one letter. Each contact is a simple Text widget for now.
  • SliverChildBuilderDelegate — Builds list items on-demand (lazy loading) for performance.

3 Create the Sticky Header Delegate

The SliverPersistentHeader needs a delegate — a class that tells it what to draw and how tall to be.

Step 3.1 Add the _ContactHeaderDelegate Class

Add this class below the ContactsListScreen in the same file:

Add to contacts_list.dart
class _ContactHeaderDelegate extends SliverPersistentHeaderDelegate {
    _ContactHeaderDelegate({required this.letter});

    final String letter;

    @override
    Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
        return Container(
            color: CupertinoTheme.of(context).barBackgroundColor,
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            alignment: Alignment.centerLeft,
            child: Text(
                letter,
                style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
        );
    }

    @override double get maxExtent => 40;  // Full height of the header

    @override double get minExtent => 40;  // Minimum height when pinned

    @override bool shouldRebuild(_ContactHeaderDelegate oldDelegate) {
        return letter != oldDelegate.letter;
    }
}

What each override does:

  • build() — Draws the header: a colored container with the letter in bold.
  • maxExtent — The header's full height (40 pixels).
  • minExtent — The minimum height when pinned. Same as max = no shrinking.
  • shouldRebuild() — Only rebuild if the letter changed (performance optimization).

4 Connect the Screen to Your App

Update main.dart to show the ContactsListScreen instead of "Hello Rolodex!".

Step 4.1 Update RolodexApp

Updated main.dart
import 'package:flutter/cupertino.dart';
import 'data/contact_group.dart';
import 'screens/contacts_list.dart';  // ← Import screen

final contactGroupsModel = ContactGroupsModel();

void main() {
    runApp(const RolodexApp());
}

class RolodexApp extends StatelessWidget {
    const RolodexApp({super.key});

    @override
    Widget build(BuildContext context) {
        return CupertinoApp(
            title: 'Rolodex',
            theme: const CupertinoThemeData(
                barBackgroundColor: CupertinoDynamicColor.withBrightness(
                    color: Color(0xFFF9F9F9),
                    darkColor: Color(0xFF1D1D1D),
                ),
            ),
            home: ContactsListScreen(              // ← Show contacts!
                contactGroup: contactGroupsModel.lists.first,
            ),
        );
    }
}

Hot reload! You should now see an alphabetized contact list with sticky letter headers. Scroll down — the headers pin to the top as you pass each letter!

5 Add Adaptive Layout — Phone vs Tablet

Now use LayoutBuilder to show a different layout when there's enough width (like on a tablet).

Step 5.1 Wrap with LayoutBuilder

Update the home: in RolodexApp to use LayoutBuilder for adaptive layouts:

Adaptive home with LayoutBuilder
home: LayoutBuilder(
    builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
            // Phone layout: full screen contact list
            return ContactsListScreen(
                contactGroup: contactGroupsModel.lists.first,
            );
        } else {
            // Tablet layout: sidebar + detail (coming in next lesson!)
            return Row(
                children: [
                    SizedBox(
                        width: 300,
                        child: ContactsListScreen(
                            contactGroup: contactGroupsModel.lists.first,
                        ),
                    ),
                    const Expanded(
                        child: Center(child: Text('Select a contact')),
                    ),
                ],
            );
        }
    },
),

What this does:

  • Width < 600: Shows the contact list full screen (phone mode).
  • Width ≥ 600: Shows a two-column layout — contacts on the left (300px wide), detail area on the right. This is the standard tablet/desktop pattern!
Test it! Resize your browser window. When it's narrow (like a phone), you see the full contact list. When it's wide (like a tablet), you see a sidebar with the list and a detail area. One codebase, two layouts, zero duplication!

📝 What You Learned Today

LayoutBuilder

Used LayoutBuilder to read available screen width and switch between phone (full screen) and tablet (sidebar) layouts.

CustomScrollView & Slivers

Built a scrolling list with CustomScrollView, SliverList, and SliverPersistentHeader for sticky letter headers.

Sticky Headers

Created _ContactHeaderDelegate extending SliverPersistentHeaderDelegate with pinned: true.

Alphabetized Contact List

Displayed all 52 contacts organized by first letter (A-Z) with lazy-loading SliverChildBuilderDelegate.

🧠 Test Yourself!

Q1 What does LayoutBuilder provide to its builder function?

Q2 What does pinned: true do on a SliverPersistentHeader?

📦 Project 1: Add Contact Subtitles

Project 1Beginner

Objective: Show middle names and suffixes in the contact list

📋 Requirements:

  1. Update the contact Text widget in the SliverList to show the full name
  2. If the contact has a middleName, include it
  3. If the contact has a suffix, show it after the last name (e.g., "Jr.")
  4. Make middle names slightly smaller and gray using TextStyle
  5. Format: John [M.] Appleseed Jr.

🎯 Expected:

Contacts show their complete names: "Elizabeth M. Johnson", "William James Brown III", "Dr. Sarah Watson", etc.

📦 Project 2: Add a Third Layout for Desktop

Project 2Intermediate

Objective: Three breakpoints — phone, tablet, AND desktop

📋 Requirements:

  1. Add a third breakpoint at 900 pixels
  2. Phone (<600): Full screen contact list (current)
  3. Tablet (600-899): Sidebar (300px) + detail area
  4. Desktop (900+): Sidebar (350px) + detail area + extra info panel (250px)
  5. The extra panel can just show Text('Contact Info') for now

🎯 Expected:

Three distinct layouts as you resize the browser: phone (narrow), tablet (medium), desktop (wide with 3 columns).

🚀 What's Next?

Your app now adapts to any screen size! In the next lesson:

  • Scrolling and Slivers — Advanced scroll effects and search functionality
  • Build a search bar that filters contacts as you type
  • More sliver magic: collapsing headers, parallax, and beyond
🎉

Lesson Complete!

You built a responsive, adaptive Rolodex app with sticky alphabet headers and phone/tablet layouts! Your app now looks professional on any device!

Click the button above to track your progress!