Gnome: Desarrollo de aplicaciones. MVVM
Abordamos el desarrollo de nuestra aplicación de ejemplo usando un patrón de arquitectura MVVM. En los próximos artítulos iremos modificando y ampliando lo que aquí hagamos, solamente vamos a crear la base para poder desarrollar el resto de nuestra aplicación.
MVVM (Model-View-ViewModel) es un patrón de arquitectura que separa una aplicación en tres capas: el Model, que contiene los datos; la View, que es la interfaz de usuario y se limita a mostrar información y capturar eventos del usuario; y el ViewModel, que actúa como intermediario entre ambos, exponiendo los datos del Model en un formato listo para la View (normalmente mediante binding u observables) y manejando la lógica de presentación, de forma que la View se "suscribe" a los cambios del ViewModel sin conocer al Model directamente.
Como resumen podríamos decir que MVVM nos permite separar la lógica de negocio del modelo y la view usando un intermediario llamado ViewModel.
No existe una regla fija de cómo organizar el código para implementar una aplicación MVVM pero vamos a establecer una estructura que nos permita seguir todas las partes de código en este ejemplo y podamos detectar rápidamente donde está cada parte. En tus propios proyectos puedes organizar el código según tus preferencias o arquitectura.
| Dir | Descripción |
|---|---|
| Application | Todo el código que hace referencia al funcionamiento de la aplicación. Inicialización, handlers, utils, etc... |
| Model | Nuestros modelos de datos. En este caso solo tendremos uno |
| Service | Todo lo que sea susceptible a ser un servicio. En este caso, por ahora, solo tendremos uno que será al que le inyectemos el repositorio que obtendrá los datos de los feeds y los transformará a nuestro modelo de datos. |
| Repository | Repositorios de origen de datos. Por ahora tendremos uno que obtendrá un feed rss concreto y un dummy para realizar las pruebas. |
| ViewModel | El código que hará de intermediario entre el modelo y la view. Se encargará de llamar al servicio, pasarle el repositorio, obtener los datos y prepararlos para la view. |
| View | El código que se encargará de mostrar los datos del view model. Usaremos GtkTemplate por lo que tendremos un código en Vala en el que podremos tratar los datos y una plantilla xml para renderizarlos. |
Las templates xmls van dentro de data/ui y se referencian en el fichero resources como hemos visto con anterioridad.
Application.
src/Application/Main.vala
src/Application/Application.vala
src/Application/config.vala.in
Aquí vamos a colocar, por ahora, el código que ya tenemos para inicializar la aplicación, definición de la app, configuración, etc.
El código que hemos creado anteriormente para nuestra aplicación de ejemplo lo incluimos aquí y modificamos el meson.build.
src/meson.build
...
config_vala = configure_file(
input: 'Application/config.vala.in',
output: 'config.vala',
...
sources = files(
'Application/Main.vala',
'Application/Application.vala',
...Model
Nuestros modelos solo deben incluir la lógica que afecte a los propios datos del modelo, como puede ser formateo de fechas, transformaciones de textos y otras variantes que solo afectan al modelo.
El modelo se debe adaptar lo mejor posible al origen de los datos.
Por ahora solo vamos a tener un modelo Item que va a reflejar los campos básicos de un item de rss 2.0. En un futuro podremos incluir más datos al modelo como los namespaces MRSS.
src/Model/Item.vala
namespace Example
{
public class Item : Object
{
public string id { get; construct; }
public string title { get; set; default = ""; }
public string link { get; set; default = ""; }
public string description { get; set; default = ""; }
//TODO: Revisar si usar el DateTime para este campo
public string pub_date { get; set; default = ""; }
public Item (string title, string link, string description, string pub_date)
{
Object (id: GLib.Uuid.string_random);
this.title = title;
this.link = link;
this.description = description;
this.pub_date = pub_date;
}
}
}Services
Aquí podemos incluir cualquier cosa que sea susceptible de considerarse un servicio como podría ser un sistema de generación de imágenes dinámico, cache, etc.. Para nuestro ejemplo vamos a crear un servicio o adapter al que le inyectaremos el repositorio del que queramos obtener los datos. Las ViewModel no necesitan saber que hace, a quien llama o como trata los datos el repositorio es este servicio el que se encarga de normalizar la información para que el ViewModel no tenga que preocuparse de esa parte de la lógica de negocio.
En un principio vamos a crearlo como un pattern Adapter y crearemos una interfaz y un clase que implemente esa interfaz a la que inyectamos el repositorio del que queremos los datos.
src/Services/Service.vala
namespace Example
{
public interface Service : GLib.Object
{
public abstract Item[] find();
}
}src/Services/ImplRemoteFeedService.vala
namespace Example
{
public class ImplRemoteFeedService : Object, Service
{
public Repository repo { get; construct;}
public ImplRemoteFeedService (Repository repo)
{
Object(repo: repo);
}
public Item[] find()
{
return repo.find().to_array();
}
}
}Repository
Los Repositorios se van a encargar de realizar la obtención de datos, ya sea localmente (base de datos o ficheros) o remotamente. Para este ejemplo tenemos solo un tipo de origen de datos que es un xml RSS pero en el futuro podríamos tener otros orígenes como puede ser un json, base de datos o un csv... Nuestros repositorios solo deberían tener la tarea de recoger los datos y convertirlos en un formato que sea entendible para poder incluirlos en uno de nuestros modelos de datos para poder usarlos en las otras capas de nuestra aplicación como las Views o el ViewModel
Añadimos una nueva dependencia que es Gee. Gee es una librería de GObject para el manejo de ArrayList, HashMaps, Queue, Tree, etc...
src/meson.build
...
dependencies = [
dependency('gee-0.8'),
...Vamos a crear primero una interfaz para poder mantener una coherencia entre los diferentes repositorios que implementemos. Por ahora, solo necesitamos un método que llamaremos find() y que será el encargado de leer el feed que le indiquemos y devolvernos sus items. Usamos esta interfaz para implementar nuestros dos repo base uno que usaremos en el futuro para obtener un feed remoto y otro dummy para desarrollar nuestra aplicación hasta que implementemos la obtención de un feed remoto.
src/Repository/Repository.vala
using Gee;
namespace Example
{
public interface Repository : GLib.Object
{
public abstract ArrayList<Item> find();
}
}src/Repository/ImplRepositoryOne.vala
using Gee;
namespace Example
{
public class ImplRepositoryOne : Object, Repository
{
public ArrayList<Item> find()
{
return new ArrayList<Item> ();
}
}
}src/Repository/DummyRepository.vala
using Gee;
namespace Example
{
public class DummyRepository : Object, Repository
{
public ArrayList<Item> find()
{
var items = new ArrayList<Item>();
items.add(new Item("Elemento uno del RSS", "https://example.com/uno", "Descripción del elemento uno del RSS.", "2026-04-01"));
items.add(new Item("Elemento dos del RSS", "https://example.com/dos", "Descripción del elemento dos del RSS.", "2026-08-02"));
items.add(new Item("Elemento tres del RSS", "https://example.com/tres", "Descripción del elemento tres del RSS.", "2026-12-03"));
return items;
}
}
}ViewModel
Como hemos visto en la definición esta pieza es la encargada de obtener los datos, tratarlos y darselos a la View para esta no tenga que preocuparse de donde vienen o que hacen. Podemos tener tantos ViewModel como necesitemos y para nuestro ejemplo nos sirve tener solo una que obtenga el listado completo de items. Según vayamos avanzando en el desarrollo de nuestra aplicación es posible que tengamos otras ViewModels.
src/ViewModel/ItemListViewModel.vala
using Gee;
namespace Example
{
public class ItemListViewModel : Object
{
public ArrayList<Item> items { get; private set; default = new ArrayList<Item>(); }
private Service service = new ImplRemoteFeedService (new DummyRepository());
construct
{
load ();
}
public void load()
{
items.clear();
foreach (var item in service.find())
{
items.add(item);
}
}
}
}View
Volvemos a recordar la definición que hemos visto al principio de este artículo: Las Views solo se encargan de mostrar los datos de los modelos y deben carecer de cualquier lógica de negocio. Es simple, la View llama a un ViewModel y el ViewModel le da los datos y la View se encarga de tratarlos para renderizarlos con nuestras plantillas xml que tenemos en nuestro data/ui y referenciamos en nuestros objetos con GtkTemplate.
ItemRow renderiza cada elemento individual de nuestro listado identificándose como un elemento Gtk.Box y establecer los datos del item que se acceden a través de la definición del xml mediante tags bindings y lookup.
El listado se muestra, por ahora, dentro de nuestro MainWindow. MainWindow obtendrá los datos del ViewModel, definirá una propiedad Gtk.ListBox con un id item_list (esto se tiene que reflejar en el xml de la ui. Lo veremos un poco más adelante) con esto GtkTemplate sabrá que los elementos ItemRow que le pasemos van a ese listado para renderizarlos.
src/View/ItemRow.vala
namespace Example {
[GtkTemplate (ui = "/io/github/agustingv/example/ui/item-row.ui")]
public class ItemRow : Gtk.Box {
public Item item { get; construct; }
public ItemRow (Item item)
{
Object(item: item);
}
}
}src/View/MainWindow.vala
namespace Example {
[GtkTemplate (ui = "/io/github/agustingv/example/ui/main-window.ui")]
public class MainWindow : Adw.ApplicationWindow {
[GtkChild] unowned Gtk.ListBox item_list;
private ItemListViewModel view_model = new ItemListViewModel ();
public MainWindow (Gtk.Application app) {
Object (application: app);
}
construct
{
foreach (var item in view_model.items)
{
item_list.insert (new ItemRow (item), -1);
}
}
}
}data/ui/Item-row.ui
Cuando usemos GtkTemplate es importante definir los xml dentro de un tag template para que el sistema pueda reconocerlos como tal. En este xml del ItemRow podemos ver la definición de bindings y lookup que se corresponden con los datos de nuestro modelo dentro de los Gtk.Label.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="ExampleItemRow" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="xalign">0</property>
<property name="ellipsize">end</property>
<style><class name="heading"/></style>
<binding name="label">
<lookup name="title" type="ExampleItem">
<lookup name="item">ExampleItemRow</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkLabel" id="description_label">
<property name="xalign">0</property>
<property name="ellipsize">end</property>
<style><class name="dim-label"/><class name="caption"/></style>
<binding name="label">
<lookup name="description" type="ExampleItem">
<lookup name="item">ExampleItemRow</lookup>
</lookup>
</binding>
</object>
</child>
</template>
</interface>Lo importante en lo que nos tenemos que fijar aquí es en la definición del object GtkListBox y su id se corresponden con los definidos en nuestro MainWindow.
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hscrollbar-policy">never</property>
<property name="child">
<object class="GtkListBox" id="item_list">
<property name="selection-mode">none</property>
<style><class name="boxed-list"/></style>
</object>
</property>
</object>
</child>Más adelante lo veremos pero se puede usar css para el renderizado.
Nuestra aplicación de ejemplo va tomando forma y nos sirve como base para poder modificar y ampliar la funcionalidad por lo que es más que seguro que el código aquí expuesto sea muy diferente al resultado final. Continuamos en el siguiente artículo para crear un VAPI en nuestra aplicación.
Artículos recientes
Artículo
Gnome: Desarrollo de aplicaciones. Pango
04/09/2026
Artículo
Gnome: Desarrollo de aplicaciones. VAPI
03/09/2026
Artículo
Gnome: Desarrollo de aplicaciones. Construyendo una nueva aplicación
10/08/2026
Artículo
Linux: Guía de iniciación a Linux
08/08/2026
Noticia
Wayland
08/08/2026
Artículo
Gnome: Desarrollo de aplicaciones. Menús y acciones
08/08/2026