Home Flutter UI 102 Scrolling and Slivers

📜 Scrolling and Slivers — The Pro-Level Scroll Effects!

⏱️ 40-45 minutes 📊 Advanced — Pro scroll techniques! 📦 2 Projects included 🏷️ Slivers, Search, NavigationBar, Collapse, Filter

🧒 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
🎯
By the end, your Rolodex will have:
  • ✅ 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

Search logic
// 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

Updated ContactsListScreen structure
@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:

  • CupertinoSliverNavigationBar goes 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 on CupertinoPageScaffold is 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:

Convert to StatefulWidget
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:

Add search bar sliver
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:

Filter method
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:

Empty state check
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

Add as first sliver
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

Project 1Intermediate

Objective: Bold the matching part of contact names

📋 Requirements:

  1. When displaying a contact that matches search, bold the matching letters
  2. Use Text.rich with TextSpan to build the styled name
  3. Find where the search text appears in the full name
  4. Split the name into three parts: before match (normal), match (bold), after match (normal)
  5. 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

Project 2Intermediate

Objective: Enhance search with cancel/clear functionality

📋 Requirements:

  1. Add a TextEditingController to control the search field
  2. When search is active, show a "Cancel" button next to the search field (iOS pattern)
  3. Pressing Cancel clears the search AND dismisses the keyboard
  4. Use FocusScope.of(context).unfocus() to dismiss the keyboard
  5. 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.

💡
Hint: Use _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.

🚀 What's Next?

Your Rolodex now has professional scrolling with search! In the next lesson:

  • Stack Based Navigation — Push and pop screens for contact details
  • Navigate from the contact list to a detail view
  • Implement iOS-style back navigation
🎉

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!