ISW-126 · Clase 2 · Desarrollo de Aplicaciones Móviles I

De un patrón a una app real que recuerda

La clase pasada instalamos el modelo mental: MVVM y el ciclo de vida. Hoy lo volvemos una app de verdad —con varias pantallas, una lista que se actualiza sola y datos que sobreviven al cierre.

El arco: en la clase 1 vieron el estado desaparecer cuando el sistema mataba el proceso. Hoy lo van a hacer sobrevivir.

1 De dónde venimos, a dónde vamos hoy

Construiremos una sola app a lo largo de toda la clase —un bloc de notas— y la haremos crecer pieza por pieza. Cada pieza resuelve algo que la anterior dejó pendiente.

Pieza de hoyQué problema resuelve
Grid (layout)Armar una pantalla real, no controles apilados.
ObservableCollection + CollectionViewMostrar una lista que se refresca sola al agregar o quitar.
Navegación con ShellPasar de una pantalla a otra y llevar datos entre ellas.
SQLiteQue los datos sigan ahí después de cerrar la app.

Todo se apoya en lo de la clase 1: seguimos en MVVM, seguimos usando [ObservableProperty] y [RelayCommand], y el cierre en SQLite es la versión en código del ciclo de vida y el offline-first que vimos como concepto.

2 Armar la pantalla: el Grid

En la clase 1 apilamos controles con VerticalStackLayout solo para no distraernos de MVVM. Una pantalla real casi siempre se estructura con un Grid: filas y columnas, como una tabla invisible.

Row 0 · Col 0-1Encabezado
Row 1 · Col 0Lista
Row 1 · Col 1Detalle

Defines las filas y columnas, y colocas cada control en su celda con Grid.Row y Grid.Column. * reparte el espacio sobrante; Auto ocupa solo lo necesario.

<Grid RowDefinitions="Auto,*,Auto"
      ColumnDefinitions="*,*">

    <Label Grid.Row="0" Grid.ColumnSpan="2"
           Text="Mis notas" />

    <CollectionView Grid.Row="1" Grid.Column="0" />
    <ContentView   Grid.Row="1" Grid.Column="1" />

    <Button Grid.Row="2" Grid.ColumnSpan="2"
            Text="Agregar" />
</Grid>
Regla práctica: * = "toma lo que sobre" (la lista, que debe crecer). Auto = "solo lo que necesito" (un encabezado o un botón). Casi toda pantalla móvil es Auto arriba, * en medio, Auto abajo.

3 Listas que se actualizan solas

Este es el corazón de la clase 2. En la clase 1, INotifyPropertyChanged hacía que un dato avisara sus cambios. Para una lista existe su gemelo: ObservableCollection, que avisa cuando agregas o quitas elementos. La UI (un CollectionView) se redibuja sola.

Colección en vivo

Agrega y quita notas. Nadie escribió código para tocar la lista de la pantalla: la colección avisa y el CollectionView reacciona. Es el binding de la clase 1, ahora sobre una lista.

Notas.Count = 0

    En MVVM, la colección vive en el ViewModel y el XAML solo la enlaza:

    public partial class NotasViewModel : ObservableObject
    {
        // la lista que avisa sus altas y bajas
        public ObservableCollection<Nota> Notas { get; } = new();
    
        [ObservableProperty] private string nuevoTexto;
    
        [RelayCommand]
        private void Agregar()
        {
            if (string.IsNullOrWhiteSpace(NuevoTexto)) return;
            Notas.Add(new Nota { Texto = NuevoTexto });  // la UI se entera sola
            NuevoTexto = "";
        }
    
        [RelayCommand]
        private void Quitar(Nota nota) => Notas.Remove(nota);
    }
    <CollectionView ItemsSource="{Binding Notas}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="model:Nota">
                <Label Text="{Binding Texto}" Padding="12" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
    El error que todos cometen una vez: ObservableCollection avisa cuando agregas o quitas un elemento, pero no cuando cambias una propiedad dentro de un elemento. Si haces notas[0].Texto = "editada"; la lista no se entera. Para que sí lo haga, la clase Nota también debe implementar INotifyPropertyChanged (o heredar de ObservableObject). Es el puente exacto con la clase 1.

    4 Varias pantallas: navegación con Shell

    Toda app real tiene más de una pantalla. Shell es el esqueleto de navegación de MAUI: registras rutas y te mueves entre páginas con una línea, pasando datos por el camino.

    Navegación en vivo

    Toca una nota para abrir su detalle; usa "Volver" para regresar. Abajo verás la ruta que MAUI ejecutaría en cada salto.

    detalle?id=—
    —

    Esta pantalla recibió el id por parámetro y cargó la nota.

    Cada pantalla secundaria se registra una vez como ruta con nombre.

    // AppShell.xaml.cs
    Routing.RegisterRoute("detalle", typeof(DetallePage));

    Desde el ViewModel de la lista, navegas al detalle pasando el id en la propia ruta:

    [RelayCommand]
    private async Task AbrirDetalle(Nota nota)
        => await Shell.Current.GoToAsync($"detalle?id={nota.Id}");

    La pantalla de detalle declara qué parámetro espera; MAUI se lo entrega solo.

    [QueryProperty(nameof(Id), "id")]
    public partial class DetalleViewModel : ObservableObject
    {
        [ObservableProperty] private int id;   // llega desde la ruta
    
        partial void OnIdChanged(int value)    // reacciona al recibirlo
        {
            // cargar la nota con ese id...
        }
    }

    5 Que sobreviva al cierre: SQLite

    El cierre que conecta con la clase 1. Una ObservableCollection vive en memoria: si el sistema mata el proceso, se va con él —justo lo que vimos en el simulador. Para que las notas persistan, las guardamos en una base de datos local: SQLite.

    Ejecutando

    Notas en memoria:

      Prueba la persistencia

      SQLite en disco: 0 notas

      Guarda una nota, mata el proceso y reábrelo…

      La idea: cada Guardar escribe en memoria y en SQLite. Al reabrir, la app relee la tabla y reconstruye la lista. Los datos sobreviven porque nunca vivieron solo en memoria.

      La misma clase Nota, con dos atributos que le dicen a SQLite cómo guardarla. Se instala con dotnet add package sqlite-net-pcl.

      public class Nota
      {
          [PrimaryKey, AutoIncrement]
          public int Id { get; set; }
          public string Texto { get; set; }
      }

      Un repositorio concentra el acceso a datos. Todo es asíncrono: en móvil nunca bloqueas la UI esperando el disco.

      public class NotaRepo
      {
          readonly SQLiteAsyncConnection _db;
      
          public NotaRepo()
          {
              var ruta = Path.Combine(FileSystem.AppDataDirectory, "notas.db3");
              _db = new SQLiteAsyncConnection(ruta);
              _db.CreateTableAsync<Nota>();
          }
      
          public Task<List<Nota>> ObtenerTodas() => _db.Table<Nota>().ToListAsync();
          public Task<int> Guardar(Nota n)        => _db.InsertAsync(n);
          public Task<int> Eliminar(Nota n)       => _db.DeleteAsync(n);
      }

      Al abrir la pantalla, se relee la tabla y se llena la colección. Aquí se cierra el círculo con el ciclo de vida de la clase 1.

      // al aparecer la pantalla (OnAppearing) o iniciar el ViewModel:
      var guardadas = await repo.ObtenerTodas();
      foreach (var n in guardadas)
          Notas.Add(n);   // la lista se reconstruye desde el disco

      6 Cierre y qué sigue

      Al terminar, tus estudiantes tienen una app pequeña pero completa: varias pantallas, una lista viva y datos que persisten. Ese salto —de "entiendo el patrón" a "construí algo que funciona y recuerda"— es lo que se llevan hoy.

      Lo que te llevas hoy

      • Un Grid estructura una pantalla real: Auto arriba y abajo, * en el medio.
      • ObservableCollection es la lista que avisa; el CollectionView se refresca solo. Es el INotifyPropertyChanged de las listas.
      • Cuidado: la colección no avisa cambios dentro de un elemento; para eso el elemento también debe notificar.
      • Shell es el esqueleto de navegación: registras rutas y usas GoToAsync pasando parámetros.
      • SQLite es la fuente de verdad local; los datos sobreviven al cierre —el offline-first de la clase 1, ahora en código.

      Nota de alcance: si en el aula sientes que las cuatro piezas aprietan en dos horas, SQLite (sección 5) es la que mejor se puede mover a una clase 3 sin romper el hilo —la app seguiría funcionando en memoria, y el "que sobreviva al cierre" se vuelve el gancho de apertura de la siguiente sesión.