Saltar al contenido principal

Scroll avanzado y slivers

Aprende a implementar scroll de alto rendimiento con slivers.

En esta lección, aprenderás sobre los slivers, los cuales son widgets especiales que pueden aprovechar el potente y compostable sistema de scroll de Flutter. Los slivers te permiten crear efectos de scroll sofisticados, incluyendo cabeceras colapsables, integración de búsqueda y comportamientos de scroll personalizados. Al final de esta sección, comprenderás cómo usar CustomScrollView, crear barras de navegación que se colapsan y organizar el contenido en secciones con scroll.

Qué lograrás

Entender los slivers y cómo difieren de los widgets
Construir layouts con scroll con CustomScrollView
Crear barras de navegación colapsables con búsqueda
Organizar contactos en secciones ordenadas alfabéticamente

Pasos

1

Slivers y widgets

Los slivers son áreas con scroll que se pueden componer juntas en un CustomScrollView u otras vistas de scroll. Piensa en los slivers como bloques de construcción donde cada uno aporta una parte del contenido total con scroll.

Aunque los slivers y los widgets son conceptos fundamentales de Flutter, sirven para propósitos diferentes y no son intercambiables.

  • Los Widgets son bloques de construcción generales de la UI que pueden usarse en cualquier lugar de tu árbol de widgets.

  • Los Slivers son widgets especializados diseñados específicamente para diseños con scroll y tienen algunas restricciones:

  • Los slivers solo pueden ser hijos directos de vistas de scroll, como CustomScrollView y NestedScrollView.

  • Algunas vistas de scroll solo aceptan slivers como hijos. No puedes pasar widgets regulares a CustomScrollView.slivers.

  • Para usar widgets regulares dentro de un contexto de sliver, envuélvelos en SliverToBoxAdapter o SliverFillRemaining.

Esta separación arquitectónica permite a Flutter optimizar el rendimiento del scroll mientras mantiene límites claros entre los diferentes tipos de componentes de la UI.

2

Añadir una estructura básica de slivers a los grupos de contactos

Primero, reemplaza el contenido del marcador de posición en tu página de grupos de contactos. Para evitar duplicar código entre el diseño del teléfono y la barra lateral de la tablet, puedes crear un widget privado y reutilizable.

Actualiza lib/screens/contact_groups.dart agregando _ContactGroupsView al final del archivo.

dart
import 'package:flutter/cupertino.dart';

import '../data/contact_group.dart';
import '../main.dart';

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

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: 0,
      onListSelected: (list) {
        debugPrint(list.toString());
      },
    );
  }
}

// ···
class _ContactGroupsView extends StatelessWidget {
  const _ContactGroupsView({required this.onListSelected, this.selectedListId});

  final int? selectedListId;
  final void Function(ContactGroup) onListSelected;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      backgroundColor: CupertinoColors.extraLightBackgroundGray,
      child: CustomScrollView(
        slivers: [
          const CupertinoSliverNavigationBar(largeTitle: Text('Lists')),
          SliverFillRemaining(
            child: ValueListenableBuilder<List<ContactGroup>>(
              valueListenable: contactGroupsModel.listsNotifier,
              builder: (context, contactLists, child) {
                return CupertinoListSection.insetGrouped(
                  header: const Text('iPhone'),
                  children: [
                    for (final ContactGroup contactList in contactLists)
                      CupertinoListTile(
                        title: Text(contactList.label),
                        onTap: () => onListSelected(contactList),
                      ),
                  ],
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Este widget privado contiene la UI compartida para mostrar la lista de grupos de contactos. En pantallas pequeñas, se usará como una página, y en pantallas grandes se usará para llenar la columna izquierda.

Este widget introduce varios slivers:

  • CupertinoSliverNavigationBar: Una barra de navegación con opinión que se colapsa a medida que se hace scroll en la página.
  • SliverList: Una lista con scroll de elementos.
  • SliverFillRemaining: Un sliver que ocupa el espacio restante en el área de scroll, y cuyo hijo es un widget que no es sliver.

Acepta una función callback, onListSelected, para manejar toques, lo que la hace adaptable tanto para navegación como para selección en la barra lateral.

Ahora, actualiza ContactGroupsPage en lib/screens/contact_groups.dart para usar tu nuevo widget _ContactGroupsView:

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

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: 0,
      onListSelected: (list) {
        debugPrint(list.toString());
      },
    );
  }
}

Esta estructura mantiene el ContactGroupsPage limpio y enfocado en su responsabilidad principal: la navegación, sobre la cual aprenderás en la siguiente sección de este tutorial.

3

Mejorar la lista con iconos y elementos visuales

Ahora, añade iconos y recuentos de contactos para hacer la lista más informativa. Añade este método auxiliar _buildTrailing a tu clase _ContactGroupsView en lib/screens/contact_groups.dart:

dart
Widget _buildTrailing(List<Contact> contacts, BuildContext context) {
  final TextStyle style = CupertinoTheme.of(
    context,
  ).textTheme.textStyle.copyWith(color: CupertinoColors.systemGrey);

  return Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Text(contacts.length.toString(), style: style),
      const Icon(
        CupertinoIcons.forward,
        color: CupertinoColors.systemGrey3,
        size: 18,
      ),
    ],
  );
}

Este ayudante crea el contenido final para cada elemento de la lista. Muestra el número de contactos y una flecha hacia adelante.

Ahora, actualiza el CupertinoListSection en _ContactGroupsView para usar iconos y el ayudante final. Actualiza el código dentro del callback ValueListenableBuilder.builder en el método build:

dart
child: ValueListenableBuilder<List<ContactGroup>>(
  valueListenable: contactGroupsModel.listsNotifier,
  builder: (context, contactLists, child) {
    const groupIcon = Icon(
      CupertinoIcons.group,
      weight: 900,
      size: 32,
    );

    const pairIcon = Icon(
      CupertinoIcons.person_2,
      weight: 900,
      size: 24,
    );

    return CupertinoListSection.insetGrouped(
      header: const Text('iPhone'),
      children: [
        for (final ContactGroup contactList in contactLists)
          CupertinoListTile(
            leading: contactList.id == 0 ? groupIcon : pairIcon,
            title: Text(contactList.label),
            trailing: _buildTrailing(contactList.contacts, context),
            onTap: () => onListSelected(contactList),
          ),
      ],
    );
  },
),

El código actualizado ahora muestra iconos que diferencian entre el grupo principal "All iPhone" y los grupos creados por el usuario, junto con el número de contactos e indicadores de navegación.

4

Crear scroll avanzado para contactos

A continuación, implementarás la página de la lista de contactos.

En la siguiente lección, implementarás la navegación para pantallas pequeñas. Para ver tu progreso en la página de lista de contactos mientras tanto, primero actualiza lib/screens/adaptive_layout.dart para mostrar la página de lista de contactos:

dart

import 'package:flutter/cupertino.dart';

import 'contacts.dart';

const largeScreenMinWidth = 600;

class AdaptiveLayout extends StatefulWidget {
  const AdaptiveLayout({super.key});

  @override
  State<AdaptiveLayout> createState() => _AdaptiveLayoutState();
}

class _AdaptiveLayoutState extends State<AdaptiveLayout> {
  int selectedListId = 0;

  void _onContactListSelected(int listId) {
    setState(() {
      selectedListId = listId;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isLargeScreen = constraints.maxWidth > largeScreenMinWidth;

        if (isLargeScreen) {
          return _buildLargeScreenLayout();
        } else {
          return const ContactListsPage(listId: 0); // New, temporary
        }
      },
    );
  }

  Widget _buildLargeScreenLayout() {
    return CupertinoPageScaffold(
      backgroundColor: CupertinoColors.extraLightBackgroundGray,
      child: SafeArea(
        child: Row(
          children: [
            const SizedBox(width: 320, child: Text('Sidebar placeholder')),
            Container(width: 1, color: CupertinoColors.separator),
            const Expanded(child: Text('Details placeholder')),
          ],
        ),
      ),
    );
  }
}

Actualiza lib/screens/contacts.dart añadiendo _ContactListView al final del archivo:

dart
class _ContactListView extends StatelessWidget {
  const _ContactListView({
    required this.listId,
    this.automaticallyImplyLeading = true,
  });

  final int listId;
  final bool automaticallyImplyLeading;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          return CustomScrollView(
            slivers: [
              CupertinoSliverNavigationBar(
                largeTitle: Text(contactList.title),
                automaticallyImplyLeading: automaticallyImplyLeading,
              ),
              SliverFillRemaining(
                child: Center(
                  child: Text(
                    '${contactList.contacts.length} contacts in ${contactList.label}',
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

Ahora, actualiza ContactListsPage para usar esta vista:

dart
class ContactListsPage extends StatelessWidget {
  const ContactListsPage({super.key, required this.listId});

  final int listId;

  @override
  Widget build(BuildContext context) {
    return _ContactListView(listId: listId);
  }
}

Esta implementación básica demuestra cómo usar slivers con datos dinámicos en un componente reutilizable.

5

Añadir integración de búsqueda con slivers

Ahora, mejora la página de contactos con una UI de funcionalidad de búsqueda integrada. Actualiza el CustomScrollView en _ContactListView para usar el constructor CupertinoSliverNavigationBar.search en lugar del constructor por defecto CupertinoSliverNavigationBar:

dart
class _ContactListView extends StatelessWidget {
  // ···
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          return CustomScrollView(
            slivers: [
              // Now using a search bar:
              CupertinoSliverNavigationBar.search(
                largeTitle: Text(contactList.title),
                searchField: const CupertinoSearchTextField(
                  suffixIcon: Icon(CupertinoIcons.mic_fill),
                  suffixMode: OverlayVisibilityMode.always,
                ),
              ),
              SliverFillRemaining(
                child: Center(
                  child: Text(
                    '${contactList.contacts.length} contacts in ${contactList.label}',
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

El constructor CupertinoSliverNavigationBar.search proporciona una funcionalidad de búsqueda integrada. A medida que haces scroll hacia abajo, el campo de búsqueda realiza la transición de forma suave hacia la barra de navegación colapsada.

6

Crear secciones de contactos ordenadas alfabéticamente

Las aplicaciones de contactos del mundo real organizan los contactos alfabéticamente. Para hacer esto, crea secciones para cada letra. Añade el siguiente widget al final de tu archivo contacts.dart. Este widget no contiene slivers.

dart
class ContactListSection extends StatelessWidget {
  const ContactListSection({
    super.key,
    required this.lastInitial,
    required this.contacts,
  });

  final String lastInitial;
  final List<Contact> contacts;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsetsDirectional.fromSTEB(20, 0, 20, 0),
      child: Column(
        children: [
          const SizedBox(height: 15),
          Align(
            alignment: AlignmentDirectional.bottomStart,
            child: Text(
              lastInitial,
              style: const TextStyle(
                color: CupertinoColors.systemGrey,
                fontSize: 15,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
          CupertinoListSection(
            backgroundColor: CupertinoColors.systemBackground,
            dividerMargin: 0,
            additionalDividerMargin: 0,
            topMargin: 4,
            children: [
              for (final Contact contact in contacts)
                CupertinoListTile(
                  padding: const EdgeInsets.all(0),
                  title: Text('${contact.firstName} ${contact.lastName}'),
                ),
            ],
          ),
        ],
      ),
    );
  }
}

Este widget crea las secciones alfabéticas familiares que ves en la aplicación Contactos de iOS.

7

Usar SliverList para las secciones alfabetizadas

Ahora, reemplaza el contenido del marcador de posición en _ContactListView con las secciones alfabetizadas:

dart
class _ContactListView extends StatelessWidget {
  // ···
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          final contacts = contactList.alphabetizedContacts;

          return CustomScrollView(
            slivers: [
              CupertinoSliverNavigationBar.search(
                largeTitle: Text(contactList.title),
                automaticallyImplyLeading: automaticallyImplyLeading,
                searchField: const CupertinoSearchTextField(
                  suffixIcon: Icon(CupertinoIcons.mic_fill),
                  suffixMode: OverlayVisibilityMode.always,
                ),
              ),
              SliverList.list(
                children: [
                  const SizedBox(height: 20),
                  ...contacts.keys.map(
                    (initial) => ContactListSection(
                      lastInitial: initial,
                      contacts: contacts[initial]!,
                    ),
                  ),
                ],
              ),
            ],
          );
        },
      ),
    );
  }
}

SliverList.list te permite proporcionar una lista de widgets que pasan a formar parte del contenido con scroll. Esta es la forma más sencilla de añadir una lista de widgets normales a un área de sliver con scroll.

En la siguiente lección, aprenderás sobre la navegación basada en pila y actualizarás la UI en pantallas pequeñas para navegar entre la vista de lista de contactos y la vista de contactos.

8

Revisión

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Entendí los slivers y cómo difieren de los widgets

Los slivers son widgets especializados para diseños con scroll. Solo pueden ser hijos directos de vistas de scroll como CustomScrollView. En CustomScrollView y otros contextos de sliver, los widgets regulares deben envolverse en SliverToBoxAdapter o SliverFillRemaining.

Construí layouts con scroll con CustomScrollView

CustomScrollView te permite componer múltiples slivers juntos. Usaste CupertinoSliverNavigationBar, SliverFillRemaining y SliverList para crear interfaces con scroll sofisticadas.

Creé barras de navegación colapsables con búsqueda

Utilizaste el constructor CupertinoSliverNavigationBar.search para crear una barra de navegación colapsable con funcionalidad de búsqueda integrada.

Organicé contactos en secciones alfabetizadas

Creaste widgets ContactListSection agrupados por la inicial del apellido, luego usaste SliverList.list para añadirlos al área con scroll. Esto refleja la experiencia familiar de la aplicación Contactos de iOS.

9

Ponte a prueba

Quiz de slivers

1 / 2
What is the key difference between slivers and regular widgets?
  1. Los slivers son más rápidos de renderizar que los widgets normales.

    No exactamente.

    Ambos están optimizados; la diferencia es su propósito y contexto.

  2. Los slivers pueden tener un número ilimitado de hijos.

    No exactamente.

    Algunos slivers como SliverList pueden tener muchos hijos, pero eso no es lo que los distingue.

  3. Los slivers manejan automáticamente los gestos del usuario.

    No exactamente.

    El manejo de gestos es independiente; los slivers se encargan de la composición de layouts con scroll.

  4. Los slivers son widgets especializados diseñados para diseños con scroll y solo pueden ser hijos directos de vistas de scroll.

    ¡Así es!

    Los slivers funcionan dentro de vistas de scroll como CustomScrollView; los widgets regulares se pueden usar en cualquier lugar.

How do you use a regular widget inside a CustomScrollView's slivers list?
  1. Pásalo a la propiedad child en lugar de slivers.

    No exactamente.

    CustomScrollView utiliza la propiedad slivers; no hay propiedad child para este propósito.

  2. Envuélvelo en un SliverToBoxAdapter o SliverFillRemaining.

    ¡Así es!

    Estos adaptadores convierten widgets regulares en slivers para que puedan usarse en contextos de sliver.

  3. Solo agrégalo directamente; CustomScrollView acepta cualquier widget.

    No exactamente.

    CustomScrollView solo acepta slivers; los widgets regulares deben envolverse.

  4. Convierte el widget en un sliver llamando a .toSliver() en él.

    No exactamente.

    No existe el método .toSliver(); utilizas widgets adaptadores como SliverToBoxAdapter.