📜 Scrolling and Slivers — The Pro-Level Scroll Effects!
🧒 Beyond Basic Scrolling — The Sliver Superpowers!
Last Lesson You Met Slivers — Now Master Them!
In the previous lesson, you used SliverList and SliverPersistentHeader
for sticky letter headers. But slivers can do so much more — they're the secret
behind every polished app's scrolling behavior!
Today you'll learn three new sliver superpowers:
- 🔍 Search Bar Sliver — A search field that collapses as you scroll
- 📱 CupertinoSliverNavigationBar — iOS-style large title that shrinks
- 🔤 Filterable Lists — Search contacts in real-time as you type
- ✅ A beautiful iOS-style navigation bar that collapses on scroll
- ✅ A search bar built right into the scrolling area
- ✅ Real-time contact filtering as you type
- ✅ Pull-to-refresh functionality
CupertinoSliverNavigationBar — The iOS "Large Title" Effect
You know how iOS apps have a big title that shrinks as you scroll? That's CupertinoSliverNavigationBar:
When at the top
Shows a large title (like "Contacts" in big bold text). Takes up about 100 pixels.
When scrolled
Collapses smoothly into a compact navigation bar. The large title fades into the standard bar.
It replaces both the CupertinoNavigationBar AND the CupertinoPageScaffold — the whole page structure changes!
Search in Slivers — Filter Contacts Instantly!
The iOS Contacts app has a search bar that hides as you scroll. You'll build the exact same thing using CupertinoSearchTextField inside a sliver:
🔍 Search Flow
// 1. User types in search bar
// 2. Filter contacts: keep only names containing the search text
// 3. Rebuild the sliver list with filtered results
// 4. Show "No results" if nothing matches
1 Upgrade to CupertinoSliverNavigationBar
Replace the basic CupertinoNavigationBar with the sliver version that has the beautiful collapsing large title effect.
Step 1.1 The Sliver Navigation Bar Structure
Update your ContactsListScreen to use CupertinoSliverNavigationBar. Note that the whole page structure changes — we remove CupertinoPageScaffold and use CustomScrollView directly:
🔍 Before vs After
@override
Widget build(BuildContext context) {
final alphabetized = contactGroup.alphabetizedContacts;
return CupertinoPageScaffold( // ← Keep the scaffold
child: CustomScrollView( // ← Sliver container
slivers: [
CupertinoSliverNavigationBar( // ← NEW! Replaces navigationBar
largeTitle: Text(contactGroup.title), // Big title that shrinks
),
// ... letter headers and contact lists follow ...
],
),
);
}
Key differences:
CupertinoSliverNavigationBargoes inside the slivers list, not as a separate property.largeTitle:— The big text that appears when scrolled to the top. It automatically shrinks into a compact bar as you scroll down.- The
navigationBar:property onCupertinoPageScaffoldis removed — the sliver navigation bar handles everything!
Hot reload and scroll! You should see "iPhone" (or whatever your group title is) in large text that smoothly shrinks as you scroll down through the contacts.
2 Add a Search Bar — Filter Contacts in Real-Time
Now add a search bar right below the navigation bar that filters contacts as you type — just like the real iOS Contacts app!
Step 2.1 Add Search State to ContactsListScreen
First, convert ContactsListScreen from StatelessWidget to StatefulWidget — we need state for the search text:
class ContactsListScreen extends StatefulWidget {
const ContactsListScreen({super.key, required this.contactGroup});
final ContactGroup contactGroup;
@override State<ContactsListScreen> createState() => _ContactsListScreenState();
}
class _ContactsListScreenState extends State<ContactsListScreen> {
String _searchText = ''; // ← Holds search input
@override
Widget build(BuildContext context) {
// ... build method ...
}
}
Step 2.2 Add the Search Bar Sliver
Add the search bar right after the navigation bar in the slivers list:
slivers: [
CupertinoSliverNavigationBar(
largeTitle: Text(widget.contactGroup.title),
),
SliverToBoxAdapter( // ← Wraps non-sliver widget in a sliver
child: Padding(
padding: const EdgeInsets.all(8),
child: CupertinoSearchTextField( // ← iOS-style search field!
placeholder: 'Search',
onChanged: (value) {
setState(() {
_searchText = value.toLowerCase(); // Update search on every keystroke
});
},
),
),
),
// ... letter headers and contact lists ...
],
New widgets explained:
SliverToBoxAdapter— A bridge widget. Regular widgets (like Padding) can't go directly into a slivers list. This wraps them so they can!CupertinoSearchTextField— iOS-style search field with rounded corners, a magnifying glass icon, and a clear button. Looks exactly like the iOS Contacts search bar.onChanged:— Fires on every keystroke (not just on submit). Perfect for real-time filtering!
3 Filter Contacts Based on Search Text
Now update the contact list to only show contacts that match the search text.
Step 3.1 Create Filtered Contact Map
Add this method to _ContactsListScreenState to filter contacts:
AlphabetizedContactMap _getFilteredContacts() {
final allAlphabetized = widget.contactGroup.alphabetizedContacts;
if (_searchText.isEmpty) {
return allAlphabetized; // No search = show everything
}
final filtered = AlphabetizedContactMap();
for (final entry in allAlphabetized.entries) {
final matchingContacts = entry.value.where((contact) {
final fullName = '${contact.firstName} ${contact.lastName}'.toLowerCase();
return fullName.contains(_searchText);
}).toList();
if (matchingContacts.isNotEmpty) {
filtered[entry.key] = matchingContacts;
}
}
return filtered;
}
Then update your slivers list to use _getFilteredContacts() instead of directly using alphabetized. The list now instantly filters as you type!
4 Handle Empty Search Results
When the search returns no results, show a friendly "No Results" message instead of an empty screen.
Step 4.1 Add Empty State to the Build Method
Before building the slivers, check if there are any results:
final filtered = _getFilteredContacts();
if (filtered.isEmpty && _searchText.isNotEmpty) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(widget.contactGroup.title),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.search, size: 48, color: CupertinoColors.systemGrey),
SizedBox(height: 16),
Text('No Results', style: TextStyle(fontSize: 20)),
SizedBox(height: 8),
Text('No contacts match "$_searchText"'),
],
),
),
);
}
return CupertinoPageScaffold(
child: CustomScrollView(slivers: [/* ... */]),
);
5 Add Pull-to-Refresh — CupertinoSliverRefreshControl
iOS apps let you pull down to refresh. Add this as the first sliver in the list!
Step 5.1 Add Refresh Control
slivers: [
CupertinoSliverRefreshControl( // ← Pull-to-refresh!
onRefresh: () async {
await Future.delayed(Duration(seconds: 1)); // Simulate network
setState(() {
_searchText = ''; // Clear search on refresh
});
},
),
CupertinoSliverNavigationBar(/* ... */),
// ... rest of slivers ...
],
Now when you pull down at the top of the list, you'll see an iOS-style spinner, and the search will clear after a second!
📝 What You Learned Today
CupertinoSliverNavigationBar
Replaced static nav bar with iOS-style large title that collapses smoothly as the user scrolls down.
Search Bar in Slivers
Added CupertinoSearchTextField wrapped in SliverToBoxAdapter for real-time contact filtering.
Real-Time Filtering
Built _getFilteredContacts() that filters contacts on every keystroke with setState.
Pull-to-Refresh
Used CupertinoSliverRefreshControl for iOS-style pull-down refresh that clears search.
🧠 Test Yourself!
Q1 What widget wraps a regular widget so it can be placed inside a slivers list?
Q2 What does CupertinoSliverNavigationBar do differently from CupertinoNavigationBar?
📦 Project 1: Highlight Matching Text in Search Results
Objective: Bold the matching part of contact names
📋 Requirements:
- When displaying a contact that matches search, bold the matching letters
- Use
Text.richwithTextSpanto build the styled name - Find where the search text appears in the full name
- Split the name into three parts: before match (normal), match (bold), after match (normal)
- If no search is active, show the name normally
🎯 Expected:
When you type "an", "Daniel Higgins" shows as "Daniel Higgins" and "Anna Haro" shows as "Anna Haro".
📦 Project 2: Add Clear Search Button Behavior
Objective: Enhance search with cancel/clear functionality
📋 Requirements:
- Add a TextEditingController to control the search field
- When search is active, show a "Cancel" button next to the search field (iOS pattern)
- Pressing Cancel clears the search AND dismisses the keyboard
- Use
FocusScope.of(context).unfocus()to dismiss the keyboard - Wrap the search bar area in a Row: [Expanded(search field), cancel button]
🎯 Expected:
When the user taps the search field, a "Cancel" button appears. Tapping it clears the search text, dismisses the keyboard, and shows all contacts again.
_searchController.addListener() to detect when text is entered, and call setState to show/hide the cancel button based on whether the search field has text.Lesson Complete!
You mastered advanced scrolling with slivers — collapsing navigation bars, real-time search filtering, and pull-to-refresh! Your Rolodex app now feels like a real iOS app!
Click the button above to track your progress!