📱 Adaptive Layouts — Make Your App Look Great Everywhere!
🧒 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(
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
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 useschildren; CustomScrollView usesslivers.
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:
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:
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
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:
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!
📝 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
Objective: Show middle names and suffixes in the contact list
📋 Requirements:
- Update the contact Text widget in the SliverList to show the full name
- If the contact has a middleName, include it
- If the contact has a suffix, show it after the last name (e.g., "Jr.")
- Make middle names slightly smaller and gray using
TextStyle - 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
Objective: Three breakpoints — phone, tablet, AND desktop
📋 Requirements:
- Add a third breakpoint at 900 pixels
- Phone (<600): Full screen contact list (current)
- Tablet (600-899): Sidebar (300px) + detail area
- Desktop (900+): Sidebar (350px) + detail area + extra info panel (250px)
- 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).
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!