ISW-126 · Desarrollo de Aplicaciones Móviles I

Desarrollo móvil con .NET MAUI

Programar para móvil no es programar para escritorio en una pantalla más chica. Es una disciplina con reglas propias. Este material las hace visibles —para tocarlas, no solo leerlas.

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.
AspectoXamarin.Forms.NET MAUI
EstructuraSolución con varios proyectos (compartido + .Android + .iOS + .UWP)Un solo proyecto con carpetas Platforms/
UI → nativoRenderers (pesados, acoplados)Handlers (livianos, desacoplados)
AlcancePrincipalmente móvilMóvil y escritorio de primera clase
PlataformaMono / .NET Framework.NET moderno unificado (.NET 8/9/10)
ArranqueApp.xaml + init manualMauiProgram.cs con inyección de dependencias
SoporteSin soporte desde mayo 2024Framework 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ónEn 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 + ComposeEl 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}!";
}

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}.

se enlaza con →
¡Hola…!

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

AtributoQué resuelve
[RelayCommand] sobre un async TaskComando 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.

Cerrada
App: Borrador de nota
Memoria: —

Eventos del sistema

El registro del ciclo de vida aparecerá aquí…

La correspondencia entre frameworks

ConceptoXamarin.Forms.NET MAUI (Window)
InicioOnStart()Created / Activated
A segundo planoOnSleep()Deactivated / Stopped
RegresaOnResume()Resumed
Se destruye(no existía)Destroying
La regla de oro: 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

PreferencesPares clave/valor simples (última pantalla, ajustes del usuario).
SecureStorageDatos sensibles cifrados (tokens, credenciales).
SQLiteDatos 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 guardes en OnSleep() / Stopped se pierde.
  • 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.