· 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.
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.
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.
Pruébalo: por qué TODO es asíncrono
El punto que late y el contador son "la interfaz". Pulsa cada botón y observa si la interfaz se congela o sigue fluida mientras ocurre una tarea de 2 segundos.
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.
[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; }
}await dentro, tiene que ser async Task. 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 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.
Pruébalo: RAM vs. disco (por qué existe SQLite)
Agrega notas: van a la memoria y al disco. Ahora "cierra la app": la memoria se libera, pero la tabla de SQLite sobrevive. Reábrela y las notas vuelven desde el disco.
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.
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.
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 _db.CreateTableAsync<Nota>();
}
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);
}
}
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.
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:
Pruébalo: objeto ⇄ JSON
Edita los campos del objeto C#. A la derecha se genera el JSON (serializar): eso es lo que viaja por la red. Al recibir, la app hace el camino inverso (deserializar).
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 corta e inmutable de declarar una clase de solo datos (C# moderno). El deserializador lee el JSON y rellena estos objetos por ti.
// DTOs: la forma del JSON que devuelve la API (records = inmutables y 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.
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.
Pruébalo: una petición HTTP
Elige un verbo y envía. Verás la línea de la petición y el código de estado de la respuesta. Apaga el servidor para ver cómo se ven los errores que tu código debe atrapar.
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.
// 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();
}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.
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).
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>();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.
<?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>
7 C# moderno que vas a ver
Dos detalles de sintaxis reciente que aparecen en la app y conviene reconocer.
record — clases de datos, breves e inmutables
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 |
9 Autoevaluación
Responde para fijar los conceptos. Cada pregunta te explica el porqué al elegir.
async/await?await libera ese hilo mientras la tarea ocurre.200?new.Lo esencial que te llevas
- async/await existe para que la app nunca se congele: libera el hilo de la UI mientras algo tarda.
- SQLite hace que los datos sobrevivan al cierre; el ORM te deja usarlo con objetos y el repositorio lo aísla del resto.
- JSON es el idioma común con los servidores; serializar/deserializar es el puente, y los DTOs le dan forma.
- HttpClient conecta la app al mundo; lo difícil no es pedir datos, es manejar cuando la red falla.
- La inyección de dependencias une todo sin acoplarlo; los recursos + AppThemeBinding hacen el tema declarativo.
Con estos cimientos, el recorrido de la app deja de ser "código que funciona por magia" y pasa a ser un conjunto de decisiones que puedes explicar —y defender— una por una.