Flutter IV: Arquitectura MVP — [Flutter 1.0]

Fabio Santana
6 min readSep 12, 2016

--

Hasta este tutorial hemos visto como se crean interfaces de usuario en Flutter. En este nuevo tutorial dejamos por un momento las interfaces y vamos a ver como organizar nuestra app con una arquitectura MVP (Model-View-Presenter).

Actualizado a Flutter 1.0 — Diciembre 2018

Índice

Recuerda que dispones de todo el código que vamos viendo en este repo.

Planteamiento

Exactamente lo que vamos hacer es crea una capa en la app para la UI, otra para los Presenters y otra que contenga los repositorios de datos.

La capa de UI se encargará de contener nuestros widgets para seguir mostrando la lista de contactos.

Luego los Presenters los utilizaremos para enlazar la capa de UI con los datos que obtenemos desde los repositorios de datos.

Y por último, en la capa de repositorios de datos tendremos dos fuentes de datos: la actual de contactos dentro del código la cual nos servirá como datos mocks para hacer tests en un futuro y otra fuente de datos desde un servicio fuera del dispositivo, en este caso usaremos Random User una web que nos ofrece una API para obtener usuarios.

Obtención de datos

Vamos a empezar creándonos los clientes para las fuentes de datos. El primer paso que vamos a realizar es crear una carpeta data dentro de la carpeta lib. El siguiente paso es crear un archivo llamado contact_data.dart en la carpeta data.

En este archivo al igual que teníamos en el tutorial anterior vamos a tener la definición de un contacto con la clase Contact. También vamos a definir una interfaz que defina el método para obtener los contactos y un tipo de excepción propia para notificar de algún problema en la obtención de contactos.

Esta vez no pondremos la lista de contactos en este archivo sino que la moveremos a otro archivo en una implementación mock.

El resultado de este archivo es el siguiente:

Como hemos dicho tenemos la clase Contact, la interfaz ContactRepository con un método fetch que devuelve un Future del tipo lista de contactos y la excepción FetchDataException.

Para usar el Future debemos importar dart:sync, el Future nos permite trabajar con promesas en Dart. En este tutorial más adelante veremos un ejemplo de como utilizarlo.

Repositorio Mock

La primera implementación que haremos de nuestra interfaz ContactRepository será una implementación mock. En la carpeta data creamos otro archivo llamado contact_data_mock.dart. En ella crearemos una clase que implemente la interfaz ContactRepository y que devuelva en el método fetch la lista de contactos que creamos en el anterior tutorial.

Importamos tanto dart:async para usar los Future y contact_data.dart para usar la interfaz ContactRepository y la clase Contact.

Como vemos nos creamos una clase que implemente ContactRepository y en el método fetch devolvemos la lista de contactos en un Future.

Random User Repository

La segunda implementación será usando el servicio de Random User así veremos como obtener datos de internet en Dart.

Pero antes necesitamos la añadir la dependencia a la librería que nos va a proporcionar la funcionalidad para hacer peticiones http. En el archivo pubspec.yaml añadimos la siguiente linea.

dependencies: 
http: ^0.12.0

Y ejecutamos el comando:

flutter packages get

Luego nos creamos en la carpeta data un archivo llamado contact_data_impl.dart y añadimos lo siguiente.

En este caso creamos la clase RandomUserRepository que también implementa nuestra interfaz ContactRepository.

Dentro del método fetch vamos hacer una petición get a la URL definida en _kRandomUserUrl. Para esto utilizaremos el paquete package:http/http.dart el cual hemos importado al principio del archivo. Al utilizar el operador as estamos diciendo que vamos a usar ese paquete como la variable http.

Para hacer la petición get usamos el método get de http. El método get es una operación asíncrona por lo que ponemos usar el operador await y esperar en ese punto a que termine la petición.

Obtenemos: el body, el statusCode y comprobamos que todo haya ido correctamente. En caso contrario lanzamos la excepción FetchDataException.

El último paso es leer el json, para esto usamos dart:convert con la clase JsonDeconder al cual le pasamos el body de la respuesta.

El json de respuesta de Random User es entre otras cosas este.

{
“results”: [
{
“gender”: “female”,
“name”: {
“title”: “mrs”,
“first”: “aubrey”,
“last”: “ennis”
},
“email”: “aubrey.ennis@example.com”,
}
]
}

Obtenemos la lista dentro de results, recorremos cada elemento y con el map los convertimos en Contact para por último hacer un toList y devolver la lista.

Para esto debemos crear un nuevo constructor en la clase Contact para crear un contacto desde un json. La clase nos quedaría de la siguiente forma.

En este constructor con nombre asignamos los valores que extraemos del mapa a los atributos fullName y email.

Inyección de Dependencia

Para poder cambiar ente ambas implementaciones de ContactRepository vamos a crear una simple inyección de dependencia. En la carpeta lib creamos otra carpeta llamada injection y en esta un archivo dependency_injection.dart. Y añadimos el siguiente código.

El enumerador Flavor lo utilizaremos para definir dos entornos: uno mock y otro de producción. La clase Injector es un singleton con: un constructor privado _internal, un método configure para definir que entorno vamos a utilizar, un factory para devolver el singleton cuando se haga un new del Injector y un get para devolver la implementación del ContactRepository que hayamos elegido.

En el método main en el archivo main.dart, este archivo es igual que el que teníamos en el anterior tutorial, llamamos al configure antes del runApp y definimos el Flavor que queremos usar, en este caso PRO.

Injector.configure(Flavor.PRO);

Presenter

Una vez tenemos hecho la capa de repositorios vamos a crear el presenter. Nos creamos una carpeta module en lib y dentro de module otra carpeta contacts y en esta un archivo contact_presenter.dart.

Lo primero es añadir al archivo una interfaz Contract, esta interfaz nos ayudará para comunicar el presenter con la UI. Dispondremos de dos métodos: uno para comunicar cuando los contactos estén y otro para comunicar que se ha producido un error.

El segundo paso es crear el presenter.

E importamos lo siguiente al principio del archivo.

import ‘../../data/contact_data.dart’;
import ‘../../injection/dependency_injection.dart’;

En el constructor asignamos el contract de la vista y asignamos desde el Injector el actual tipo de ContactRepository que se esté utilizando ya sea MockContactRepository o RandomUserRepository.

Nos creamos un método loadContacts para que la vista pida los contactos. En este llamamos al método fetch que nos devuelve un Future del tipo List<Contact>, en el then le pasamos un lambda para que se ejecute cuando el repositorio acabe de obtener y convertir los datos a una lista de contactos.

En este lambda llamamos al método onLoadContactsComplete con la lista de contactos que nos ha devuelto el repositorio para comunicar a la vista los datos.

Si se producirá una excepción el Future iría por el catchError y no por el then, en este catchError imprimimos el error y lo comunicamos a la vista.

View

En la carpeta contacts vamos a poner nuestro archivo contact_view.dart del anterior tutorial. Y realizaremos algunos cambios al ContactList.

Hemos hecho que ContactList no sea un StatelessWidget ahora es un StatefulWidget por lo explicado en anteriores tutoriales. También hemos creado un State para el ContactList.

En la clase ContactListState hemos hecho que implemente el Contract creado en el paso anterior así como la implementación de sus dos métodos, onLoadContactsComplete y onLoadContactsError.

En el constructor creamos el presenter y en el método initState hacemos la petición al presenter para obtener los contactos.

En el método build generamos la lista como hasta ahora pero también generamos un CircularProgressIndicator para mostrar al usuario mientras obtenemos los contactos. La variable _isSearching la utilizamos para esto, si estamos buscando devolvemos un CircularProgressIndicator y sino un ListView.

Cuando el presenter obtenga los datos y llame al onLoadContactsComplete en el ContactList llamamos al setState con un lambda para invalidar el widget y obligar que se vuelva hacer el método build. En el setState ponemos _IsSearching a false para cuando se llame de nuevo al build devuelva un ListView y no el ProgressIndicator.

El tratamiento de errores lo haremos más adelante.

Resultado

Llegado a este punto si ejecutamos nuestra app este debería ser el resultado:

Progress indicator & Contact List example

Conclusión

Al construir una app con la arquitectura MVP conseguimos que nuestra app sea mejor mantenible además de ayudarnos a la hora de testear la app entre otras coas. Esto nos lleva a ver como con Flutter podemos crear apps robustas fácilmente.

El siguiente paso que veremos es navegar entre pantallas.

Este artículo corresponde al step3 de nuestro repositorio en GitHub.

--

--