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 hoy | Qué problema resuelve |
|---|---|
| Grid (layout) | Armar una pantalla real, no controles apilados. |
| ObservableCollection + CollectionView | Mostrar una lista que se refresca sola al agregar o quitar. |
| Navegación con Shell | Pasar de una pantalla a otra y llevar datos entre ellas. |
| SQLite | Que 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.
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>* = "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.
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>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.
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.
Notas en memoria:
Prueba la persistencia
SQLite en disco: 0 notas
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 disco6 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:
Autoarriba y abajo,*en el medio. - ObservableCollection es la lista que avisa; el
CollectionViewse refresca solo. Es elINotifyPropertyChangedde 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
GoToAsyncpasando 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.
Hacia la clase 3
La app ya recuerda localmente. El siguiente paso natural es conectarla al mundo: consumir una API real con HttpClient, sincronizar lo local con un servidor y manejar los estados de cargando / error / sin conexión. Ahí el offline-first deja de ser un concepto y se vuelve una arquitectura completa —el mismo patrón que sostiene tu app de control de pagos y el SaaS de residenciales.
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.