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 5Recursos 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 6El 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.
| 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" />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}!";
}Binding en vivo
Escribe en el campo y mira cómo la etiqueta se actualiza sola. Nadie escribió lbl.Text = ...: eso es lo que hace {Binding}.
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.
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:
- La clase hereda de
ObservableObject(que ya implementaINotifyPropertyChanged) y se marcapartial, para que el generador pueda agregarle código. [ObservableProperty]sobre un campoprivate string nombre;genera la propiedad públicaNombrecon notificación incluida. La regla de nombres: el campo en camelCase, la propiedad generada en PascalCase.[RelayCommand]sobre un métodoGuardar()genera unICommandllamadoGuardarCommand, 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 no existe en escritorio: 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.
Eventos del sistema
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 |
OnSleep() (o Stopped en MAUI) es la última oportunidad garantizada para guardar. No hay un "OnKill": si el sistema elimina el proceso, lo hace después y sin avisar. Todo lo que no puedas perder, guárdalo ahí.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.
- Gancho (15 min). ¿Por qué lo móvil es distinto? Las cuatro restricciones de la sección 1.
- Ubicación (10 min). Qué es MAUI, su relación con Xamarin.Forms y dónde encaja en el panorama.
- 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.
- 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.
- 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 guardes en
OnSleep()/Stoppedse pierde. - Un binding conecta una propiedad de la UI con una del ViewModel, e
INotifyPropertyChangedes 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.