MAUI Lab C# / .NET

01 / LEER

Lee para conectar las piezas.

El contenido de tus cuatro documentos, con ejemplos C# y XAML. Las demostraciones se encuentran en Practicar.

MÓDULO

Bases móviles y MVVM

1 Por qué lo móvil es una disciplina aparte

Cuatro restricciones definen la disciplina, y se mantienen sin importar la herramienta. Las dos primeras cambian la arquitectura de tu app —a esas dedicamos las secciones interactivas de abajo.

El ciclo de vida y la efemeridad del proceso

El sistema operativo puede detener o eliminar tu app en cualquier momento. El estado en memoria se pierde si no lo guardaste.

Simulador en la sección 5

Recursos y conectividad como restricción

Batería, memoria y una red que va y viene. De ahí nace la arquitectura offline-first: no es un detalle, es un cambio de diseño.

Detalle en la sección 6

El contexto humano

Una sola mano, atención de segundos, interrupciones constantes, pantallas de mil tamaños y toque en lugar de mouse.

El sistema operativo como guardián

Permisos que el usuario puede negar y distribución vía tiendas: no puedes forzar una actualización ni saltarte la revisión.

Las herramientas envejecen —el programa oficial todavía menciona Symbian, J2ME y Eclipse— pero estos conceptos siguen intactos.

2 ¿Qué es .NET MAUI?

El framework de Microsoft para construir apps nativas de móvil y escritorio —Android, iOS, macOS y Windows— desde un solo código base en C# y XAML. Es la evolución directa de Xamarin.Forms.

La diferencia central: Xamarin.Forms usaba Renderers (pesados y acoplados) para traducir cada control a nativo; MAUI usa Handlers, desacoplados con un mapa de propiedades: más livianos, rápidos y fáciles de personalizar. Esa es la razón técnica de fondo por la que MAUI existe.
Aspecto Xamarin.Forms .NET MAUI
Estructura Solución con varios proyectos (compartido + .Android + .iOS + .UWP) Un solo proyecto con carpetas Platforms/
UI → nativo Renderers (pesados, acoplados) Handlers (livianos, desacoplados)
Alcance Principalmente móvil Móvil y escritorio de primera clase
Plataforma Mono / .NET Framework .NET moderno unificado (.NET 8/9/10)
Arranque App.xaml + init manual MauiProgram.cs con inyección de dependencias
Soporte Sin soporte desde mayo 2024 Framework de primera línea de Microsoft

3 Dónde se ubica MAUI en el panorama

MAUI es una opción entre varias, no la única forma de hacer una app. El mapa actual, en trazos gruesos:

Opción En una frase
Nativo (Kotlin / Swift) La referencia. Máximo control y rendimiento, a cambio de mantener dos bases de código.
Flutter (Dart) Domina la conversación multiplataforma, con motor de render propio y comunidad enorme.
.NET MAUI (C#) La ruta natural para quien ya vive en el ecosistema .NET —como este curso.
Kotlin Multiplatform + Compose El que más creció: estable y en producción en empresas grandes; comparte lógica y ya la UI entre Android e iOS.

Elegimos MAUI por una razón concreta: ustedes ya conocen C# y MVVM de Programación Avanzada, así que la rampa es casi cero y podemos concentrarnos en lo verdaderamente móvil.

4 MVVM, del code-behind al patrón

Llegamos a MVVM por pasos, no de golpe. Para quien aún no ha visto enlaces (bindings), arrancamos por lo más intuitivo y construimos hacia el patrón. Cada pestaña resuelve un problema de la anterior.

El punto de partida para el grupo nuevo: le pones nombre al control con x:Name y lo manipulas directo desde el archivo .xaml.cs.

<VerticalStackLayout Padding="20" Spacing="12">
    <Entry x:Name="entradaNombre" Placeholder="Tu nombre" />
    <Button x:Name="btnSaludar" Text="Saludar" Clicked="btnSaludar_Clicked" />
    <Label  x:Name="lblSaludo" FontSize="20" />
</VerticalStackLayout>
private void btnSaludar_Clicked(object sender, EventArgs e)
{
    string nombre = entradaNombre.Text;    // leo el control
    lblSaludo.Text = $"¡Hola, {nombre}!";  // escribo el control
}

El problema: al crecer, la lógica queda amarrada a la pantalla, no se puede probar sin la UI y el code-behind se vuelve inmanejable. La UI y la lógica están acopladas. Eso es lo que MVVM resuelve.

Un binding es un puente automático entre una propiedad del control y una del objeto de datos. INotifyPropertyChanged es lo que dispara la actualización: el objeto avisa "cambié" y la UI se redibuja sola.

public class SaludoModel : INotifyPropertyChanged
{
    private string _saludo;
    public string Saludo
    {
        get => _saludo;
        set { _saludo = value; OnPropertyChanged(); }  // avisa a la UI
    }
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string n = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
<Label Text="{Binding Saludo}" FontSize="20" />
Un detalle que confunde a todos: el binding tiene dirección. Label.Text es OneWay (datos → UI). Un Entry.Text es TwoWay (UI ↔ datos): lo que el usuario escribe regresa al ViewModel solo. Si alguna vez no se refleja lo esperado, revisa el modo: {Binding Nombre, Mode=TwoWay}.

Pruébalo tú mismo abajo para ver el binding en acción.

El puente conceptual clave: el mismo binding se crea desde el code-behind, en C#. Sirve para entender qué hace {Binding} por debajo.

var modelo = new SaludoModel { Saludo = "¡Hola!" };
this.BindingContext = modelo;              // 1) fuente de datos

// 2) conecto Text del Label con la propiedad Saludo del modelo
lblSaludo.SetBinding(Label.TextProperty, "Saludo");

// esto ahora actualiza la UI SOLO, sin tocar el control:
modelo.Saludo = "¡Hola desde el binding!";

lblSaludo.SetBinding(Label.TextProperty, "Saludo") en C# es exactamente lo mismo que Text="{Binding Saludo}" en XAML. Una vez que se ve el enlace creado a mano, la sintaxis declarativa deja de ser magia.

Model: datos y reglas. View: el XAML, solo presentación. ViewModel: el intermediario con propiedades y comandos. Las acciones van por Command, no por eventos Clicked.

public class SaludoViewModel : INotifyPropertyChanged
{
    public string Nombre { get; set; }
    private string _saludo;
    public string Saludo { get => _saludo; set { _saludo = value; OnPropertyChanged(); } }

    public ICommand SaludarCommand { get; }
    public SaludoViewModel()
        => SaludarCommand = new Command(() => Saludo = $"¡Hola, {Nombre}!");
}
<Entry  Text="{Binding Nombre}" Placeholder="Tu nombre" />
<Button Text="Saludar" Command="{Binding SaludarCommand}" />
<Label  Text="{Binding Saludo}" FontSize="20" />

La View se conecta al ViewModel una sola vez: BindingContext = new SaludoViewModel();. A partir de ahí, pantalla y lógica evolucionan por separado.

En una app real no usas new: registras la View y el ViewModel en MauiProgram.cs y MAUI los inyecta por constructor. Así el ViewModel recibe sus dependencias —un repositorio, un servicio de red— sin acoplarse a nada.

// MauiProgram.cs
builder.Services.AddTransient<SaludoViewModel>();
builder.Services.AddTransient<MainPage>();

En proyectos reales casi nadie escribe INotifyPropertyChanged a mano: el CommunityToolkit.Mvvm genera esa plomería por ti a partir de atributos. Este es el "después"; justo abajo desglosamos cómo funciona y qué problema resuelve.

public partial class SaludoViewModel : ObservableObject
{
    [ObservableProperty] private string nombre;  // genera Nombre + notificación
    [ObservableProperty] private string saludo;

    [RelayCommand]                                // genera SaludarCommand
    private void Saludar() => Saludo = $"¡Hola, {Nombre}!";
}

El MVVM Toolkit de Microsoft: qué problema resuelve

MVVM funciona, pero tiene un costo escondido: escribir INotifyPropertyChanged a mano es repetitivo y frágil. Por cada propiedad necesitas un campo, un getter, un setter y una llamada a OnPropertyChanged(); por cada acción, una propiedad ICommand y su inicialización. En un ViewModel con 8 propiedades y 4 comandos, la mayor parte del código es plomería, no lógica —y si olvidas un solo OnPropertyChanged(), la UI deja de actualizarse y el error es silencioso: no truena, simplemente no pasa nada.

Cómo lo resuelve: el paquete CommunityToolkit.Mvvm —el .NET Community Toolkit, con soporte de Microsoft— usa generadores de código (source generators). En tiempo de compilación lee tus atributos y escribe la plomería por ti. Tú declaras la intención; el compilador genera el código real.

Tres reglas y ya:

  1. La clase hereda de ObservableObject (que ya implementa INotifyPropertyChanged) y se marca partial, para que el generador pueda agregarle código.
  2. [ObservableProperty] sobre un campo private string nombre; genera la propiedad pública Nombre con notificación incluida. La regla de nombres: el campo en camelCase, la propiedad generada en PascalCase.
  3. [RelayCommand] sobre un método Guardar() genera un ICommand llamado GuardarCommand, listo para enlazar en el XAML.
A mano ~22 líneas · 2 propiedades y 1 comando
public class ClienteViewModel : INotifyPropertyChanged
{
    private string _nombre;
    public string Nombre
    {
        get => _nombre;
        set { _nombre = value; OnPropertyChanged(); }
    }

    private string _correo;
    public string Correo
    {
        get => _correo;
        set { _correo = value; OnPropertyChanged(); }
    }

    private ICommand _guardar;
    public ICommand GuardarCommand =>
        _guardar ??= new Command(Guardar);
    private void Guardar() { /* ... */ }

    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string n = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
Con el toolkit mismo comportamiento · 6 líneas
public partial class ClienteViewModel : ObservableObject
{
    [ObservableProperty] private string nombre;
    [ObservableProperty] private string correo;

    [RelayCommand]
    private void Guardar() { /* ... */ }
}

Se instala con dotnet add package CommunityToolkit.Mvvm.

No es magia. Esto es, en esencia, lo que el generador escribe por ti a partir de [ObservableProperty] private string nombre;

// código generado automáticamente en tiempo de compilación
public string Nombre
{
    get => nombre;
    set => SetProperty(ref nombre, value);  // SetProperty compara y avisa a la UI
}

Lo que además te da, cuando lo necesites

Atributo Qué resuelve
[RelayCommand] sobre un async Task Comando asíncrono que expone IsRunning y evita el doble toque mientras trabaja.
[RelayCommand(CanExecute = nameof(PuedeGuardar))] Habilita o deshabilita el botón solo, según una condición.
[NotifyPropertyChangedFor(nameof(NombreCompleto))] Al cambiar esta propiedad, refresca también una propiedad derivada.

Por qué importa en móvil: menos código es menos superficie de bugs. Y como el ciclo de vida ya te complica bastante, quieres un ViewModel limpio y fácil de probar —no un muro de plomería donde esconderse.

5 El ciclo de vida y la efemeridad del proceso

El punto más importante del curso, y el que también requiere atención en escritorio, aunque las restricciones móviles son especialmente importantes: tu app no controla cuándo se detiene. Juega con el simulador —manda la app a segundo plano, deja que el sistema la elimine— y observa qué pasa con el estado.

Cerrada
App: Borrador de nota
Memoria: —

Eventos del sistema

Guardar el estado en OnSleep() → Preferences
El registro del ciclo de vida aparecerá aquí…

La correspondencia entre frameworks

Concepto Xamarin.Forms .NET MAUI (Window)
Inicio OnStart() Created / Activated
A segundo plano OnSleep() Deactivated / Stopped
Regresa OnResume() Resumed
Se destruye (no existía) Destroying
La regla de oro: OnSleep() (o Stopped en MAUI) es la oportunidad de guardado adicional, no una garantía ante una terminación abrupta para guardar. No hay un "OnKill": si el sistema elimina el proceso, lo hace después y sin avisar. Guarda lo importante durante el uso; utiliza estos eventos como apoyo, no como único punto de persistencia.

Dónde guardar el estado

Preferences Pares clave/valor simples (última pantalla, ajustes del usuario).
SecureStorage Datos sensibles cifrados (tokens, credenciales).
SQLite Datos estructurados y volumen: la base de datos local de la app.
// en OnSleep() / Stopped:
Preferences.Set("borrador", entradaNombre.Text);

// al reabrir, en OnStart():
entradaNombre.Text = Preferences.Get("borrador", "");

6 Recursos y conectividad: offline-first

La segunda restricción que cambia la arquitectura. Un móvil vive con batería finita, memoria limitada y una red que aparece y desaparece: un ascensor, un sótano, un plan agotado.

La respuesta es offline-first: la app funciona primero contra un almacén local y sincroniza cuando hay red. El almacén local —no el servidor— es la fuente de verdad inmediata para la interfaz. El usuario nunca espera a la red para ver o capturar datos.

if (Connectivity.Current.NetworkAccess == NetworkAccess.Internet)
    await SincronizarConServidor();
else
    GuardarLocalParaSincronizarLuego();   // la app sigue usable

Connectivity.Current.ConnectivityChanged += (s, e) => {
    if (e.NetworkAccess == NetworkAccess.Internet) _ = SincronizarPendientes();
};

El patrón práctico: escribir siempre en SQLite local, marcar cada registro como pendiente de sincronizar, y vaciar esa cola cuando vuelve la conexión. Es exactamente lo que exige cualquier app de campo seria —gestión de incidencias, control de pagos, toma de pedidos.

7 Cómo abordar la primera clase

Un arco de dos horas para que salgan con el modelo mental instalado, no solo con un botón funcionando.

  1. Gancho (15 min). ¿Por qué lo móvil es distinto? Las cuatro restricciones de la sección 1.
  2. Ubicación (10 min). Qué es MAUI, su relación con Xamarin.Forms y dónde encaja en el panorama.
  3. Construcción (45 min). MVVM en vivo, empezando por el code-behind directo y evolucionando hasta el patrón. Usa las pestañas y el binding en vivo de la sección 4.
  4. El demo que lo cambia todo (30 min). El simulador del ciclo de vida de la sección 5: mándalo a segundo plano, mátalo, y que vean desaparecer el estado no guardado.
  5. Cierre (20 min). Offline-first como idea de arquitectura y anticipo del proyecto del semestre.

Lo que te llevas hoy

  • Lo móvil es una disciplina aparte por el ciclo de vida: el sistema puede eliminar tu app; el estado que no hayas persistido oportunamente puede perderse.
  • Un binding conecta una propiedad de la UI con una del ViewModel, e INotifyPropertyChanged es lo que dispara la actualización.
  • MVVM separa la presentación (View) de la lógica (ViewModel) para poder probar y crecer sin enredos.
  • El MVVM Toolkit elimina la plomería con generadores de código: [ObservableProperty] y [RelayCommand].
  • Offline-first: escribe siempre local primero y sincroniza cuando haya red.

Sobre el programa oficial: los conceptos que la asignatura codificó —ciclo de vida, componentes, persistencia, APIs— siguen siendo correctos. Las tecnologías concretas que lista (Symbian, J2ME, Eclipse, Dalvik) quedaron atrás. Esa distinción es en sí misma una lección: en móvil, las herramientas cambian rápido; el modelo mental permanece.

Comprobación del módulo

MÓDULO

Pantallas, navegación y datos

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.

Row 0 · Col 0-1Encabezado
Row 1 · Col 0Lista
Row 1 · Col 1Detalle
Row 2 · Col 0-1Barra inferior

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.

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.

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);
            // En este fragmento falta esperar la inicialización.
            _db.CreateTableAsync<Nota>(); // Ver escenario «La primera consulta».
        }
    
        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.

    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.

    Comprobación del módulo

    MÓDULO

    Capacidades y permisos

    1 Por qué esto es lo más “móvil” de todo

    Un teléfono no es solo una pantalla: es una caja de sensores —cámara, GPS, micrófono, acelerómetro, red— que la app puede usar. Pero el usuario es el dueño de esos sensores, y el sistema operativo actúa como guardián: nada se usa sin permiso.

    Dos ideas que gobiernan todo el tema: primero, las capacidades se piden con una API multiplataforma llamada MAUI Essentials, así el mismo código funciona en Android y iOS. Segundo, cada acceso sensible pasa por el modelo de permisos en tiempo de ejecución: la app pregunta, el usuario decide, y tu código debe manejar el “no” con la misma seriedad que el “sí”.

    2 MAUI Essentials: el catálogo

    Una sola API para las capacidades del dispositivo. Estas son las que más se usan; la app de referencia utiliza las tres primeras.

    Capacidad API ¿Necesita permiso?
    Cámara / foto MediaPicker.Default Sí — cámara
    Ubicación Geolocation.Default Sí — ubicación
    Conectividad Connectivity.Current No
    Almacenamiento privado FileSystem, Preferences No
    Datos sensibles cifrados SecureStorage No
    Sensores Accelerometer, Compass… No (algunos sí)
    Regla mental: si el dato es del usuario o de su entorno (su cara, su ubicación, sus contactos), hay permiso de por medio. Si es de la app (sus propios archivos, sus preferencias), no.

    3 El modelo de permisos en tiempo de ejecución

    El corazón del tema. No basta con usar la cámara: primero se comprueba el estado, luego se pide, y —lo que casi nadie maneja bien— hay que contemplar que el usuario lo niegue, incluso para siempre. Juega con el simulador: intenta tomar la foto y niega el permiso un par de veces.

    CheckStatusAsync→ RequestAsync→ Granted / Denied→ Denied para siempre → Ajustes
    NotasApp · nueva nota
    Permiso: sin determinar
    Sin foto

    ¿Permitir que NotasApp use la cámara?

    Registro de las llamadas a la API

    Toca “Tomar foto” para empezar…

    Este es exactamente el patrón que la app de referencia encapsula en CapacidadesService, y sirve para cualquier permiso:

    private async Task<PermissionStatus> PedirPermisoAsync<T>()
        where T : Permissions.BasePermission, new()
    {
        // a) ¿Ya lo tenemos? Nunca pidas de más.
        var estado = await Permissions.CheckStatusAsync<T>();
        if (estado == PermissionStatus.Granted) return estado;
    
        // b) Pedirlo: el sistema muestra el diálogo nativo.
        estado = await Permissions.RequestAsync<T>();
    
        // c) Gestionar la negativa según plataforma e historial.
        //    Ofrecer una alternativa y Ajustes cuando corresponda.
        // No inferir bloqueo permanente solo de ShouldShowRationale.
        // Si procede, ofrecer Ajustes y abrirlos si el usuario lo elige.
    
        return estado;
    }
    El caso que separa a un junior de un senior: manejar el “denegado para siempre”. Si sigues llamando a RequestAsync, el sistema ya no muestra nada y tu app parece rota. Hay que detectar ese estado y mandar al usuario a Ajustes con AppInfo.Current.ShowSettingsUI().

    4 Cámara y ubicación

    Con el permiso resuelto, usar la capacidad es sorprendentemente corto. Todo es asíncrono: nunca bloqueas la UI esperando al hardware.

    Cámara (MediaPicker)

    if (!MediaPicker.Default.IsCaptureSupported) return null;
    
    var foto = await MediaPicker.Default.CapturePhotoAsync();  // abre la cámara
    if (foto is null) return null;                             // el usuario canceló
    
    // Copiar la foto al almacenamiento privado de la app para conservarla:
    var destino = Path.Combine(FileSystem.AppDataDirectory, foto.FileName);
    using var origen = await foto.OpenReadAsync();
    using var salida = File.OpenWrite(destino);
    await origen.CopyToAsync(salida);

    Ubicación (Geolocation)

    var request = new GeolocationRequest(GeolocationAccuracy.Medium,
                                         TimeSpan.FromSeconds(10));
    Location? loc = await Geolocation.Default.GetLocationAsync(request);
    // loc.Latitude, loc.Longitude
    Elige la precisión mínima que te sirva. Medium ubica “dónde escribí la nota” sin encender el GPS de alta precisión, que castiga la batería —una decisión de recursos, como vimos en la clase 1.

    5 Conectividad: el offline-first en acción

    No necesita permiso, pero es la capacidad que más cambia la arquitectura. La app debe saber si hay red y comportarse bien cuando no la hay. Prueba el interruptor.

    Con conexión
    Todo sincroniza normalmente.
    // Estado actual:
    bool hayInternet = Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
    
    // Reaccionar a los cambios en vivo:
    Connectivity.Current.ConnectivityChanged += (s, e) =>
    {
        bool online = e.NetworkAccess == NetworkAccess.Internet;
        // mostrar/ocultar el banner, reanudar la sincronización pendiente...
    };

    En la app de referencia, NotasViewModel lee HayInternet al cargar y enciende un banner “sin conexión”. Como todo se guarda en SQLite local, la app sigue siendo usable sin red: ese es el corazón del offline-first.

    6 Declaraciones de plataforma: el paso que todos olvidan

    Pedir el permiso en código no basta. Cada sistema exige declarar por adelantado qué capacidades usará la app. Si faltan estas líneas, la app compila pero el permiso falla en silencio —el bug más frustrante para un principiante.

    Android — Platforms/Android/AndroidManifest.xml

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    iOS — Platforms/iOS/Info.plist

    <key>NSCameraUsageDescription</key>
    <string>Para adjuntar una foto a tus notas.</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Para guardar dónde escribiste la nota.</string>
    En iOS el texto es obligatorio y visible: ese <string> es justo lo que el usuario lee en el diálogo. Un texto vago (“la app necesita ubicación”) baja la tasa de aceptación; uno claro (“para guardar dónde escribiste la nota”) la sube.

    7 Cierre

    Lo que te llevas hoy

    • MAUI Essentials da una API única para las capacidades del dispositivo en Android e iOS.
    • El permiso sigue un flujo: CheckStatusAsync → RequestAsync → manejar granted / denied / denegado para siempre.
    • El “denegado para siempre” se resuelve mandando al usuario a Ajustes con ShowSettingsUI().
    • La conectividad no pide permiso, pero define la arquitectura: offline-first, la app sigue usable sin red.
    • Sin declarar los permisos en el manifiesto (Android) y el Info.plist (iOS), nada funciona.

    Todo lo de este documento está implementado y comentado en la app de referencia NotasApp: mira Services/CapacidadesService.cs para el patrón de permisos, cámara y GPS, y ViewModels/DetalleViewModel.cs para verlo usado desde la pantalla. La guía completa mapea dónde vive cada pieza.

    Comprobación del módulo

    MÓDULO

    Conceptos de NotasApp

    · Antes de programar, entender

    La app NotasApp introduce varias ideas que van más allá de MAUI: cosas que valen para cualquier lenguaje y cualquier plataforma. Este documento las explica desde cero, una por una, de lo simple a lo complejo. No es sobre botones ni pantallas (eso está en el recorrido); es sobre los cimientos: por qué el código es asíncrono, cómo se guardan datos que sobreviven, cómo se habla con un servidor y cómo encaja todo.

    Ya dominas C# y MVVM de Programación Avanzada. Aquí construimos sobre eso. Cada sección trae una analogía, el concepto, el código real de la app y un demo para tocarlo. Al final hay una autoevaluación.

    1 Programación asíncrona: async / await

    Es el cimiento de todo lo demás. Guardar en disco y hablar con la red tardan, y en móvil no puedes permitirte esperar de brazos cruzados: la interfaz se congelaría.

    ️
    La analogía

    Un mesero toma tu orden y no se queda parado en la cocina esperando que cocinen: atiende otras mesas y vuelve cuando el plato está listo. await es exactamente eso: "empieza esta tarea larga y, mientras, deja libre el hilo para atender a la persona que usa la app". Un método bloqueante sería el mesero que se planta en la cocina hasta que salga el plato — nadie más recibe atención.

    Hay un solo hilo de interfaz (UI thread): el que dibuja la pantalla y responde a los toques. Si lo ocupas con una tarea larga, la app se "cuelga". La regla de oro móvil: nunca bloquees el hilo de la UI. Por eso casi todo método que toca disco o red es async y devuelve un Task, y lo llamas con await.

    Así se ve un método asíncrono real de la app. Fíjate en tres cosas: la firma async Task, cada await ante una operación lenta (red, base de datos), y el try/catch — porque una tarea asíncrona puede fallar (se cae la red, expira el tiempo) y hay que atrapar esa falla.

    NotasViewModel.cs · un método asíncrono real
        [RelayCommand]
        private async Task SincronizarAsync()
        {
            if (SinConexion) { Mensaje = "Sin conexión: se trabaja en local."; return; }
            if (IsBusy) return;
    
            IsBusy = true;
            Mensaje = "Sincronizando…";
            try
            {
                var remotas = await _api.ObtenerNotasAsync();
                int nuevas = 0;
                foreach (var r in remotas)
                {
                    if (!Notas.Any(n => n.Texto == r.Cuerpo))   // evitar duplicados
                    {
                        await _repo.GuardarAsync(new Nota { Texto = r.Cuerpo });
                        nuevas++;
                    }
                }
                if (nuevas > 0)
                {
                    Notas.Clear();
                    foreach (var n in await _repo.ObtenerTodasAsync()) Notas.Add(n);
                }
                Mensaje = nuevas > 0 ? $"Sincronizado: {nuevas} nota(s) nueva(s)." : "Todo al día.";
            }
            catch (TaskCanceledException) { Mensaje = "La API tardó demasiado (timeout)."; }
            catch (HttpRequestException)  { Mensaje = "No se pudo contactar la API."; }
            catch (Exception ex)          { Mensaje = $"Error: {ex.Message}"; }
            finally { IsBusy = false; }
        }
    Regla práctica: si un método usa await dentro, debe tener una firma asíncrona apropiada, por ejemplo async Task o async Task<T>. Y si llamas a algo async, casi siempre le pones await delante. El await no "para" la app: la libera.

    2 Persistencia local con SQLite

    "Persistir" es hacer que los datos sobrevivan a cerrar la app. Todo lo que vive en variables y listas está en memoria (RAM) y se borra al terminar el proceso. Para que una nota siga ahí mañana, hay que escribirla en disco.

    La analogía

    La memoria es tu mente mientras piensas: rápida, pero se vacía al dormir. SQLite es una libreta donde anotas lo importante: sigue ahí cuando despiertas. Cerrar la app es "dormir"; lo que no anotaste en la libreta, se pierde.

    ¿Qué es SQLite?

    Una base de datos que vive dentro de tu app, en un solo archivo (notas.db3), sin servidor ni instalación. Es la base de datos más usada del mundo justamente porque va incrustada en teléfonos, navegadores y apps. Guarda los datos en tablas: filas y columnas, como una hoja de cálculo con reglas.

    El ORM: hablar con la tabla usando objetos

    Escribir SQL a mano es tedioso y propenso a errores. Un ORM (Object-Relational Mapper) —aquí, la librería sqlite-net— hace de traductor: tu clase Nota se convierte en una tabla, y cada objeto en una fila. Los atributos [PrimaryKey] y [AutoIncrement] le dicen cómo construir la tabla. Tú trabajas con objetos C#; el ORM genera el SQL por debajo.

    La analogía

    El ORM es un intérprete en una reunión: tú hablas "objetos" y la base de datos habla "SQL / filas". Nadie tiene que aprender el idioma del otro; el intérprete traduce en ambos sentidos.

    Las cuatro operaciones: CRUD

    Toda la vida de un dato cabe en cuatro verbos: Create (insertar), Read (leer), Update (actualizar), Delete (borrar). En la app esto vive en el repositorio, la única clase que habla con SQLite. Nota el patrón: la conexión se crea una sola vez (inicialización perezosa), todo es async, y un mismo método GuardarAsync hace INSERT o UPDATE según si la nota ya tiene Id.

    NotaRepository.cs · SQLite de principio a fin
    using NotasApp.Models;
    using Microsoft.Maui.Storage;   // FileSystem
    using SQLite;
    
    namespace NotasApp.Data;
    
    // ─────────────────────────────────────────────────────────────────────────
    //  REPOSITORIO (capa de datos) — TODO el acceso a SQLite vive aquí.
    //  La UI nunca habla con la base de datos: habla con este repositorio. Eso lo
    //  hace testeable y reemplazable. Todo es asíncrono: en móvil no bloqueamos la UI.
    // ─────────────────────────────────────────────────────────────────────────
    public class NotaRepository
    {
        private SQLiteAsyncConnection? _db;
    
        // Inicialización perezosa: conexión y tabla se crean la 1ª vez que se usan.
        private async Task InitAsync()
        {
            if (_db is not null) return;
            var ruta = Path.Combine(FileSystem.AppDataDirectory, "notas.db3");
            _db = new SQLiteAsyncConnection(ruta);
            await // En este fragmento falta esperar la inicialización.
            _db.CreateTableAsync<Nota>(); // Ver escenario «La primera consulta».
        }
    
        public async Task<List<Nota>> ObtenerTodasAsync()
        {
            await InitAsync();
            return await _db!.Table<Nota>().OrderByDescending(n => n.Fecha).ToListAsync();
        }
    
        public async Task<Nota?> ObtenerPorIdAsync(int id)
        {
            await InitAsync();
            return await _db!.Table<Nota>().FirstOrDefaultAsync(n => n.Id == id);
        }
    
        // Un solo método: INSERT si es nueva (Id == 0), UPDATE si ya existe.
        public async Task<int> GuardarAsync(Nota nota)
        {
            await InitAsync();
            return nota.Id == 0 ? await _db!.InsertAsync(nota) : await _db!.UpdateAsync(nota);
        }
    
        public async Task<int> EliminarAsync(Nota nota)
        {
            await InitAsync();
            return await _db!.DeleteAsync(nota);
        }
    }
    
    ¿Por qué un repositorio? Para que el resto de la app no sepa que existe SQLite. Si mañana cambias a otra base de datos, tocas un solo archivo. Eso es separación de responsabilidades: cada pieza sabe lo justo.

    3 JSON, serialización y DTOs

    Para que dos programas distintos (tu app y un servidor) se entiendan, necesitan un idioma común de texto. Ese idioma, casi siempre, es JSON.

    La analogía

    No puedes enviar un mueble armado por correo: lo desarmas, lo empacas plano (con instrucciones), y quien lo recibe lo rearma. Serializar es empacar tu objeto como texto JSON para mandarlo; deserializar es rearmar el objeto a partir del JSON recibido.

    JSON es solo texto con una estructura de pares "clave": valor. Serializar = objeto → texto. Deserializar = texto → objeto. Mira cómo un objeto se vuelve JSON en vivo:

    DTOs: la forma del JSON, como objeto

    Un DTO (Data Transfer Object) es una clase cuya única misión es reflejar la forma del JSON que entra o sale. En la app usamos record, una forma concisa de declarar un tipo de datos; la mutabilidad depende de sus propiedades y objetos referenciados (C# moderno). El deserializador lee el JSON y rellena estos objetos por ti.

    ApiService.cs · los DTOs (records)
    // DTOs: la forma del JSON que devuelve la API (records = tipos de datos concisos).
    public record NotaRemota(string Titulo, string Cuerpo);
    public record PostDto(int Id, string Title, string Body);

    4 Consumir APIs REST con HttpClient

    Hasta aquí la app vivía sola en el teléfono. Una API le permite pedir y enviar datos a un servidor remoto por Internet. Es el tema que abre la puerta a apps conectadas de verdad.

    ‍
    La analogía

    Una API REST es un mesero entre tu app (cliente) y la cocina (servidor). Tú no entras a la cocina: le pides al mesero. GET = "tráeme el menú" (leer). POST = "anota este pedido" (crear). Y el mesero siempre responde con un código de estado: 200 "aquí tienes", 404 "eso no existe", 500 "se quemó la cocina".

    Los pilares: HTTP es el protocolo; los verbos dicen la intención (GET leer, POST crear, PUT actualizar, DELETE borrar); el endpoint es la dirección del recurso (/posts); y el código de estado resume el resultado. REST es simplemente el estilo de diseñar APIs así, alrededor de recursos y verbos.

    En C#, HttpClient es la herramienta para hacer esas peticiones. Con System.Net.Http.Json, pedir y deserializar es una sola línea: GetFromJsonAsync hace la petición GET y convierte el JSON de la respuesta en objetos. Todo asíncrono, claro.

    ApiService.cs · pedir y deserializar JSON
        // GET: trae notas de ejemplo. Deserializa el JSON directo a objetos tipados.
        public async Task<List<NotaRemota>> ObtenerNotasAsync(CancellationToken ct = default)
        {
            var posts = await _http.GetFromJsonAsync<List<PostDto>>("posts?_limit=5", ct);
            return posts?.Select(p => new NotaRemota(p.Title, p.Body)).ToList() ?? new();
        }
    El mundo real falla: la red se cae, el servidor tarda, no hay señal. Por eso la sincronización de la app envuelve la llamada en try/catch y distingue TaskCanceledException (timeout) de HttpRequestException (no se pudo conectar). Programar redes es, sobre todo, manejar lo que sale mal.

    5 Inyección de dependencias, en firme

    Lo viste en Clase 2; aquí lo consolidamos, porque es el pegamento que une todas las piezas anteriores sin acoplarlas.

    ‍
    La analogía

    A un chef le llegan los ingredientes ya listos a la estación: no sale a comprarlos a media receta. La inyección de dependencias hace eso con tus clases: cada una declara en su constructor lo que necesita, y el contenedor se lo entrega ya construido.

    En vez de que una clase haga new NotaRepository() por dentro (acoplándose a esa implementación), la pide en su constructor y el contenedor se la inyecta. Registras cada pieza una vez, eligiendo su vida: Singleton (una sola instancia para toda la app) o Transient (una nueva cada vez).

    MauiProgram.cs · registro de dependencias
            builder.Services.AddSingleton<NotaRepository>();
            builder.Services.AddSingleton<CapacidadesService>();
            builder.Services.AddSingleton<EnergiaService>();
    
            // HttpClient configurado (BaseAddress + timeout) e inyectado en ApiService.
            builder.Services.AddSingleton(sp => new HttpClient
            {
                BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
                Timeout = TimeSpan.FromSeconds(15)
            });
            builder.Services.AddSingleton<ApiService>();
    
            builder.Services.AddSingleton<NotasViewModel>();
            builder.Services.AddSingleton<NotasPage>();
            builder.Services.AddTransient<DetalleViewModel>();
            builder.Services.AddTransient<DetallePage>();
    Beneficio real: cada pieza queda desacoplada, reemplazable y testeable. Puedes cambiar el repositorio o el servicio de API por una versión falsa para pruebas sin tocar el resto.

    6 Recursos y tema: AppThemeBinding

    El modo claro/oscuro no se programa pantalla por pantalla: se declara. La clave son dos ideas: recursos con nombre y enlaces sensibles al tema.

    Un recurso es un valor reutilizable con nombre (un color, un tamaño) que vive en un diccionario de recursos (ResourceDictionary). Defines cada color en dos versiones —clara y oscura— y luego, en el XAML, {AppThemeBinding Light=..., Dark=...} elige la correcta según el tema vigente, y la cambia sola si el usuario alterna el modo del móvil.

    Tema.xaml · pares de color claro/oscuro
    <?xml version="1.0" encoding="UTF-8" ?>
    <ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
        <!-- Pares de color CLARO/OSCURO. Se usan con AppThemeBinding en las páginas,
             así cada color cambia solo cuando el móvil cambia de tema. -->
        <Color x:Key="FondoLight">#F5F7F8</Color>
        <Color x:Key="FondoDark">#12161A</Color>
        <Color x:Key="SuperficieLight">#FFFFFF</Color>
        <Color x:Key="SuperficieDark">#1C2229</Color>
        <Color x:Key="TextoLight">#22282D</Color>
        <Color x:Key="TextoDark">#E6EAED</Color>
        <Color x:Key="TenueLight">#5A6068</Color>
        <Color x:Key="TenueDark">#9AA4AC</Color>
        <Color x:Key="Acento">#3D6E78</Color>
    </ResourceDictionary>
    
    Es el mismo principio que las variables CSS en la web: defines el valor una vez, con nombre, y lo referencias en muchos lugares. Cambiar el tema es cambiar qué valor resuelve ese nombre.

    7 C# moderno que vas a ver

    Dos detalles de sintaxis reciente que aparecen en la app y conviene reconocer.

    record — tipos de datos concisos, sin garantía de inmutabilidad profunda

    Un record es una clase pensada para solo llevar datos. public record PostDto(int Id, string Title, string Body); declara en una línea lo que como clase normal serían varias propiedades. Son ideales para DTOs.

    Tipos anulables: el ?

    El ? en string? o double? significa "esto puede ser null". Es una señal explícita: RutaFoto es string? porque una nota puede no tener foto. El compilador te avisa si usas algo anulable sin comprobar antes que no sea nulo — menos errores en tiempo de ejecución.

    8 Resumen: concepto → dónde vive

    Concepto Qué es, en una frase Dónde en la app
    async / await No bloquear el hilo de la UI durante tareas lentas Todo método de repositorio, API y ViewModel
    SQLite Base de datos local en un archivo; los datos sobreviven NotaRepository
    ORM Traduce objetos ⇄ filas de tabla Atributos en Nota + sqlite-net
    CRUD Crear, leer, actualizar, borrar Métodos del repositorio
    JSON / serialización Objeto ⇄ texto para viajar por la red ApiService + DTOs
    DTO / record Clase que refleja la forma del JSON PostDto, NotaRemota
    HttpClient / REST Pedir y enviar datos a un servidor ApiService, registrado en MauiProgram
    Inyección de dep. Entregar piezas ya construidas, sin new MauiProgram.cs
    AppThemeBinding Colores que cambian solos con el tema Tema.xaml + las páginas

    Comprobación del módulo