Saltar al contenido principal

Funciones avanzadas de UI

Una introducción suave a las funciones avanzadas de UI: layouts adaptativos, slivers, desplazamiento, navegación.

Obtén una vista previa de la aplicación Rolodex que construirás y configura un proyecto basado en Cupertino con modelos de datos.

Qué lograrás

Vista previa de la aplicación Rolodex que construirás
Configurar un proyecto con widgets de Cupertino
Crear modelos de datos para contactos y grupos

Pasos

1

Introducción

In this third installment of the Flutter tutorial series, you'll use Flutter's Cupertino library to build a partial clone of the iOS Contacts app.

Una captura de pantalla de la aplicación de gestión de contactos Rolodex
completada que muestra una lista de contactos organizados alfabéticamente.

Al final de este tutorial, habrás aprendido a crear layouts adaptativos, implementar un sistema de temas completo, construir patrones de navegación y utilizar técnicas avanzadas de desplazamiento.

Qué aprenderás

#

Este tutorial explora los siguientes temas:

  • Construcción de layouts responsivos con LayoutBuilder.
  • Uso de desplazamiento avanzado con slivers y búsqueda.
  • Implementación de patrones de navegación basados en stack.
  • Creación de temas completos con CupertinoThemeData.
  • Soporte para temas tanto claro como oscuro.
  • Creación de una UI al estilo iOS usando widgets de Cupertino.

Este tutorial asume que has completado los tutoriales de Flutter anteriores y te sientes cómodo con la composición básica de widgets, el state management y la estructura del proyecto Flutter.

2

Crear un nuevo proyecto Flutter

Para crear una aplicación Flutter, primero necesitas un proyecto Flutter. Puedes crear una nueva aplicación con la herramienta CLI de Flutter, que se instala como parte del Flutter SDK.

Abre tu terminal preferida y ejecuta el siguiente comando para crear un nuevo proyecto Flutter:

flutter create rolodex --empty
cd rolodex

Este comando crea un nuevo proyecto Flutter que utiliza la plantilla mínima "vacía".

3

Añadir la dependencia de Cupertino Icons

Este proyecto utiliza el paquete cupertino_icons, un paquete oficial de Flutter. Añádelo como dependencia ejecutando el siguiente comando:

flutter pub add cupertino_icons
4

Configurar la estructura del proyecto

Primero, crea la estructura de directorios básica para tu aplicación. En el directorio lib de tu proyecto, crea las siguientes carpetas:

mkdir lib/data lib/screens lib/theme

Este comando crea carpetas para organizar tu código en secciones lógicas: modelos de datos, widgets de pantalla y configuración del tema.

5

Reemplazar el código inicial

En tu IDE, abre el archivo lib/main.dart y reemplaza todo su contenido con el siguiente código inicial:

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

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

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Rolodex',
      theme: CupertinoThemeData(
        barBackgroundColor: CupertinoDynamicColor.withBrightness(
          color: Color(0xFFF9F9F9),
          darkColor: Color(0xFF1D1D1D),
        ),
      ),
      home: CupertinoPageScaffold(child: Center(child: Text('Hello Rolodex!'))),
    );
  }
}

A diferencia de los dos tutoriales anteriores, esta aplicación utiliza CupertinoApp en lugar de MaterialApp. El sistema de diseño Cupertino proporciona widgets y estilos al estilo iOS, lo cual es perfecto para crear aplicaciones que se sientan nativas en dispositivos Apple.

6

Ejecutar tu aplicación

En tu terminal en la raíz de tu aplicación Flutter, ejecuta el siguiente comando:

flutter run -d chrome

La aplicación se compila y se inicia en una nueva instancia de Chrome. Muestra "Hello Rolodex!" en el centro de la pantalla.

7

Crear los modelos de datos

Antes de construir la UI, crea las estructuras de datos y los datos de muestra que utilizará la aplicación. Esta sección se explica brevemente porque no es el foco de este tutorial.

Datos de contacto

#

Crea un nuevo archivo, lib/data/contact.dart, y añade la clase básica Contact:

dart
class Contact {
  Contact({
    required this.id,
    required this.firstName,
    this.middleName,
    required this.lastName,
    this.suffix,
  });

  final int id;
  final String firstName;
  final String lastName;
  final String? middleName;
  final String? suffix;
}

final johnAppleseed = Contact(id: 0, firstName: 'John', lastName: 'Appleseed'); final kateBell = Contact(id: 1, firstName: 'Kate', lastName: 'Bell'); final annaHaro = Contact(id: 2, firstName: 'Anna', lastName: 'Haro'); final danielHiggins = Contact( id: 3, firstName: 'Daniel', lastName: 'Higgins', suffix: 'Jr.', ); final davidTaylor = Contact(id: 4, firstName: 'David', lastName: 'Taylor'); final hankZakroff = Contact( id: 5, firstName: 'Hank', middleName: 'M.', lastName: 'Zakroff', ); final alexAnderson = Contact(id: 6, firstName: 'Alex', lastName: 'Anderson'); final benBrown = Contact(id: 7, firstName: 'Ben', lastName: 'Brown'); final carolCarter = Contact(id: 8, firstName: 'Carol', lastName: 'Carter'); final dianaDevito = Contact(id: 9, firstName: 'Diana', lastName: 'Devito'); final emilyEvans = Contact(id: 10, firstName: 'Emily', lastName: 'Evans'); final frankFisher = Contact(id: 11, firstName: 'Frank', lastName: 'Fisher'); final graceGreen = Contact(id: 12, firstName: 'Grace', lastName: 'Green'); final henryHall = Contact(id: 13, firstName: 'Henry', lastName: 'Hall'); final isaacIngram = Contact(id: 14, firstName: 'Isaac', lastName: 'Ingram'); final juliaJackson = Contact(id: 15, firstName: 'Julia', lastName: 'Jackson'); final kevinKelly = Contact(id: 16, firstName: 'Kevin', lastName: 'Kelly'); final lindaLewis = Contact(id: 17, firstName: 'Linda', lastName: 'Lewis'); final michaelMiller = Contact(id: 18, firstName: 'Michael', lastName: 'Miller'); final nancyNewman = Contact(id: 19, firstName: 'Nancy', lastName: 'Newman'); final oliverOwens = Contact(id: 20, firstName: 'Oliver', lastName: 'Owens'); final penelopeParker = Contact( id: 21, firstName: 'Penelope', lastName: 'Parker', ); final quentinQuinn = Contact(id: 22, firstName: 'Quentin', lastName: 'Quinn'); final rachelReed = Contact(id: 23, firstName: 'Rachel', lastName: 'Reed'); final samuelSmith = Contact(id: 24, firstName: 'Samuel', lastName: 'Smith'); final tessaTurner = Contact(id: 25, firstName: 'Tessa', lastName: 'Turner'); final umbertoUpton = Contact(id: 26, firstName: 'Umberto', lastName: 'Upton'); final victoriaVance = Contact(id: 27, firstName: 'Victoria', lastName: 'Vance'); final williamWilson = Contact(id: 28, firstName: 'William', lastName: 'Wilson'); final xavierXu = Contact(id: 29, firstName: 'Xavier', lastName: 'Xu'); final yasmineYoung = Contact(id: 30, firstName: 'Yasmine', lastName: 'Young'); final zacharyZimmerman = Contact( id: 31, firstName: 'Zachary', lastName: 'Zimmerman', ); final elizabethMJohnson = Contact( id: 32, firstName: 'Elizabeth', middleName: 'M.', lastName: 'Johnson', ); final robertLWilliamsSr = Contact( id: 33, firstName: 'Robert', middleName: 'L.', lastName: 'Williams', suffix: 'Sr.', ); final margaretAnneDavis = Contact( id: 34, firstName: 'Margaret', middleName: 'Anne', lastName: 'Davis', ); final williamJamesBrownIII = Contact( id: 35, firstName: 'William', middleName: 'James', lastName: 'Brown', suffix: 'III', ); final maryElizabethClark = Contact( id: 36, firstName: 'Mary', middleName: 'Elizabeth', lastName: 'Clark', ); final drSarahWatson = Contact( id: 37, firstName: 'Dr. Sarah', lastName: 'Watson', ); final jamesRSmithEsq = Contact( id: 38, firstName: 'James', middleName: 'R.', lastName: 'Smith', suffix: 'Esq.', ); final mariaCruz = Contact(id: 39, firstName: 'Maria', lastName: 'Cruz'); final pierreMartin = Contact(id: 40, firstName: 'Pierre', lastName: 'Martin'); final yukiTanaka = Contact(id: 41, firstName: 'Yuki', lastName: 'Tanaka'); final hansSchmidt = Contact(id: 42, firstName: 'Hans', lastName: 'Schmidt'); final priyaPatel = Contact(id: 43, firstName: 'Priya', lastName: 'Patel'); final carlosGarcia = Contact(id: 44, firstName: 'Carlos', lastName: 'Garcia'); final ninaVolkova = Contact(id: 45, firstName: 'Nina', lastName: 'Volkova'); final jenniferAdams = Contact(id: 46, firstName: 'Jennifer', lastName: 'Adams'); final michaelBaker = Contact(id: 47, firstName: 'Michael', lastName: 'Baker'); final sarahCooper = Contact(id: 48, firstName: 'Sarah', lastName: 'Cooper'); final christopherDaniel = Contact( id: 49, firstName: 'Christopher', lastName: 'Daniel', ); final jessicaEdwards = Contact( id: 50, firstName: 'Jessica', lastName: 'Edwards', );
final Set<Contact> allContacts = { johnAppleseed, kateBell, annaHaro, danielHiggins, davidTaylor, hankZakroff, alexAnderson, benBrown, carolCarter, dianaDevito, emilyEvans, frankFisher, graceGreen, henryHall, isaacIngram, juliaJackson, kevinKelly, lindaLewis, michaelMiller, nancyNewman, oliverOwens, penelopeParker, quentinQuinn, rachelReed, samuelSmith, tessaTurner, umbertoUpton, victoriaVance, williamWilson, xavierXu, yasmineYoung, zacharyZimmerman, elizabethMJohnson, robertLWilliamsSr, margaretAnneDavis, williamJamesBrownIII, maryElizabethClark, drSarahWatson, jamesRSmithEsq, mariaCruz, pierreMartin, yukiTanaka, hansSchmidt, priyaPatel, carlosGarcia, ninaVolkova, jenniferAdams, michaelBaker, sarahCooper, christopherDaniel, jessicaEdwards, };

Estos datos de muestra incluyen contactos con y sin segundos nombres y sufijos. Esto te brinda una variedad de datos con los que trabajar a medida que construyes la UI.

Datos del grupo de contacto

#

Ahora, crea los grupos de contactos que organizan tus contactos en listas. Crea un nuevo archivo lib/data/contact_group.dart y añade la clase ContactGroup:

dart
import 'dart:collection';

import 'package:flutter/cupertino.dart';

import 'contact.dart';

class ContactGroup {
  factory ContactGroup({
    required int id,
    required String label,
    bool permanent = false,
    String? title,
    List<Contact>? contacts,
  }) {
    final contactsCopy = contacts ?? <Contact>[];
    _sortContacts(contactsCopy);
    return ContactGroup._internal(
      id: id,
      label: label,
      permanent: permanent,
      title: title,
      contacts: contactsCopy,
    );
  }

  ContactGroup._internal({
    required this.id,
    required this.label,
    this.permanent = false,
    String? title,
    List<Contact>? contacts,
  }) : title = title ?? label,
       _contacts = contacts ?? const <Contact>[];

  final int id;
  final bool permanent;
  final String label;
  final String title;
  final List<Contact> _contacts;

  List<Contact> get contacts => _contacts;

  AlphabetizedContactMap get alphabetizedContacts {
    final contactsMap = AlphabetizedContactMap();
    for (final contact in _contacts) {
      final lastInitial = contact.lastName[0].toUpperCase();
      if (contactsMap.containsKey(lastInitial)) {
        contactsMap[lastInitial]!.add(contact);
      } else {
        contactsMap[lastInitial] = [contact];
      }
    }
    return contactsMap;
  }
}

Un ContactGroup representa una colección de contactos, como "Todos los contactos" o "Favoritos".

Añade el siguiente código auxiliar y los datos de muestra a lib/data/contact_group.dart:

dart
typedef AlphabetizedContactMap = SplayTreeMap<String, List<Contact>>;

/// Sorts a list of [contacts] alphabetically by
/// last name, then first name, then middle name.
/// If names are identical, sorts by contact ID to ensure consistent ordering.
void _sortContacts(List<Contact> contacts) {
  contacts.sort((a, b) {
    final checkLastName = a.lastName.compareTo(b.lastName);
    if (checkLastName != 0) {
      return checkLastName;
    }
    final checkFirstName = a.firstName.compareTo(b.firstName);
    if (checkFirstName != 0) {
      return checkFirstName;
    }
    if (a.middleName != null && b.middleName != null) {
      final checkMiddleName = a.middleName!.compareTo(b.middleName!);
      if (checkMiddleName != 0) {
        return checkMiddleName;
      }
    } else if (a.middleName != null || b.middleName != null) {
      return a.middleName != null ? 1 : -1;
    }

    // If both contacts have the exact same name, order by first created.
    return a.id.compareTo(b.id);
  });
}

final allPhone = ContactGroup(
  id: 0,
  permanent: true,
  label: 'All iPhone',
  title: 'iPhone',
  contacts: allContacts.toList(),
);

final friends = ContactGroup(
  id: 1,
  label: 'Friends',
  contacts: [allContacts.elementAt(3)],
);

final work = ContactGroup(id: 2, label: 'Work');

List<ContactGroup> generateSeedData() {
  return [allPhone, friends, work];
}

Este código crea tres grupos de muestra y una función para generar los datos iniciales para la aplicación.

Finalmente, añade una clase que gestione los cambios de state a lib/data/contact_group.dart:

dart
class ContactGroupsModel {
  ContactGroupsModel() : _listsNotifier = ValueNotifier(generateSeedData());

  final ValueNotifier<List<ContactGroup>> _listsNotifier;

  ValueNotifier<List<ContactGroup>> get listsNotifier => _listsNotifier;

  List<ContactGroup> get lists => _listsNotifier.value;

  ContactGroup findContactList(int id) {
    return lists[id];
  }

  void dispose() {
    _listsNotifier.dispose();
  }
}

Si no estás familiarizado con ValueNotifier, deberías completar el tutorial anterior sobre el state antes de continuar, el cual cubre el State Management.

8

Conectar los datos a tu aplicación

Actualiza tu main.dart para incluir el state global e importar el nuevo archivo de datos:

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

import 'data/contact_group.dart';

final contactGroupsModel = ContactGroupsModel();

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

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Rolodex',
      theme: CupertinoThemeData(
        barBackgroundColor: CupertinoDynamicColor.withBrightness(
          color: Color(0xFFF9F9F9),
          darkColor: Color(0xFF1D1D1D),
        ),
      ),
      home: CupertinoPageScaffold(child: Center(child: Text('Hello Rolodex!'))),
    );
  }
}

Con todo el código adicional fuera del camino, en la próxima lección, comenzarás a construir la aplicación en serio.

9

Revisión

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Vista previa de la aplicación Rolodex

Estás comenzando una nueva sección de tutoriales centrada en funciones avanzadas de UI. Para hacer que tu aplicación se sienta pulida y nativa en cualquier dispositivo, aprenderás sobre layouts adaptativos, slivers, navegación y tematización.

Configuración de un proyecto con widgets de Cupertino

A diferencia de las lecciones anteriores, esta aplicación utiliza CupertinoApp en lugar de MaterialApp. El sistema de diseño Cupertino proporciona widgets de estilo iOS que se sienten nativos en dispositivos Apple.

Modelos de datos creados para contactos y grupos

Creaste las clases Contact y ContactGroup con datos de muestra, además de un ContactGroupsModel para el State Management. Esta base sustenta la UI que construirás en las próximas lecciones.

10

Ponte a prueba

Quiz de Configuración Avanzada de UI

1 / 2
What is the main difference between CupertinoApp and MaterialApp?
  1. CupertinoApp solo funciona en dispositivos iOS.

    No exactamente.

    CupertinoApp puede ejecutarse en cualquier plataforma; simplemente proporciona widgets al estilo iOS.

  2. CupertinoApp proporciona widgets y estilos al estilo iOS, mientras que MaterialApp proporciona widgets de Material Design.

    ¡Así es!

    CupertinoApp utiliza widgets del sistema de diseño Cupertino que coinciden con el aspecto y la sensación de iOS.

  3. CupertinoApp es más ligera y tiene un mejor rendimiento.

    No exactamente.

    Ambas tienen un rendimiento similar; difieren en el estilo visual, no en la velocidad.

  4. MaterialApp requiere más configuración para configurarse.

    No exactamente.

    Ambas tienen requisitos de configuración similares; simplemente utilizan diferentes sistemas de diseño.

What is the purpose of a ValueNotifier in state management?
  1. Para validar los valores de entrada del usuario.

    No exactamente.

    ValueNotifier almacena y notifica sobre cambios de valor, no realiza validaciones.

  2. Para almacenar valores permanentemente en el almacenamiento local.

    No exactamente.

    ValueNotifier mantiene los valores en memoria; la persistencia requiere una implementación separada.

  3. Para convertir valores entre diferentes tipos de datos.

    No exactamente.

    La conversión de tipos no es el propósito de ValueNotifier.

  4. Para mantener un único valor y notificar a los escuchas cuando ese valor cambia.

    ¡Así es!

    ValueNotifier es un ChangeNotifier simple que envuelve un único valor y notifica a los escuchas cuando cambia.