Flutter: ¿Cómo consumir servicios REST?

En la mayoría de las aplicaciones se tiene que interactuar con servicios rest externos, Flutter proporciona el paquete http para consumir recursos de este protocolo basada en el futuro (future-based) cuyas funciones se pueden utilizar en dos modos: espera (await) y asincrónicas (async). Cuenta con métodos de alto nivel y simplifica el desarrollo de aplicaciones móviles basadas en REST.

Algunos de los métodos principales son:

  • read(…): Solicita la URL a través del método GET y devuelve el respuesta como Future
  • get(…): Solicita la URL a través del método GET y devuelve la respuesta como Future. Response contiene la información de respuesta.
  • post(…): Solicita la URL a través del método POST enviando los datos y devuelve la respuesta como Future.
  • put(…): Solicita la URL a través del método PUT y devuelve la respuesta como Future.
  • head(…): Solicita la URL a través del método HEAD y devuelve la respuesta como Future
  • delete(…): Solicita la URL a través del método DELETE y devuelve la respuesta como Future

Este paquete también cuenta con un cliente HTTP más estándar para muchas solicitudes a un servidor en particular: client. Debe cerrarse correctamente utilizando el método de cierre (client.close().

Utilizando el paquete http

Para explicar un ejemplo de como utilizar este paquete vamos a consumir unos servicios REST sobre la Serie Nacional de Cuba, los cuales están disponibles en el siguiente enlace SNB , todos los endpoint son a través del método GET. La forma de utilizar este paquete puede ser de muchas maneras, en el siguiente código voy a compartir con ustedes una variante.

Archivo configuration.dart

//String BASE = "http://185.2.100.139:7002";
String BASE = "http://192.168.43.74:7002";

Este archivo tiene la configuración global de la base de los endpoints, esto puede ser útil para casos donde los desarrolladores tengan las APIs en entornos de pruebas y producción.

Archivo rest_client_service.dart

import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:http_apirest/configuration/configuration.dart';
class RestClientService {
  final _headers = {'Content-Type': 'application/json'};
  Future<GenericResponse> get(String path) async {
    try {
      final response =  await http.get(BASE + path, headers: _headers);
      if(response.statusCode == 200) {
        return _genericResponseFromJson(0, "", response.body);
      } else {
          return _genericResponseFromJson(1, response.body, null);
      }
    } catch (e) {
      if( e.toString().contains("No route to host") ||
          e.toString().contains("No address associated with hostname") ||
          e.toString().contains("Connection refused") ||
          e.toString().contains("Network is unreachable") ) {
        return _genericResponseFromJson(1, "Consulte la conexión de datos o wifi de su dispositivo, no es posible conectarse con el servidor.", null);
      } else {
          return _genericResponseFromJson(1, 'Error interno. Contacte con los desarrolladores. Detalles: ${e.toString()}', null);
      }
    }
  }
  GenericResponse _genericResponseFromJson(int statusCode, String message, dynamic data) {
    var genericResponse = new GenericResponse();
    genericResponse.statusCode = statusCode;
    genericResponse.message = message;
    genericResponse.data = data ;
    return genericResponse ;
  }
}
class GenericResponse {
  int statusCode ;
  String message ;
  dynamic data ;
  GenericResponse({
    this.statusCode,
    this.message,
    this.data
  });
}

Este archivo es quien interactúa directamente con el paquete http y tiene la responsabilidad de implementar las funcionalidades para consumir los servicios a partir de los distintos métodos, claro, lo mismo que hace http, pero además podemos tratar algunos errores y devolver en claro si hay errores (statusCode=1) o si todo fue bien (statusCode=0). A continuación mostramos un archivo que utiliza esta clase, pero de manera más ajustada al negocio.

Mostrar todos los partidos

Unos de los endpoint muestra la lista de partidos en directo, veamos la clase para el mapeo de un partido:

Archivo game.dart

class Game {
  String id;
  String status ;
  String outs;
  String team1Sigles;
  String team1Name;
  int team1Runs;
  String team2Sigles;
  String team2Name;
  int team2Runs;
  String date ;
  Game({this.id, this.status,this.outs, this.team1Sigles, this.team1Name, this.team1Runs, this.team2Sigles, this.team2Name, this.team2Runs, this.date});
  Game.fromMap(Map<String, dynamic> map) {
    id = map['id'];
    status = map['status'];
    outs = map['outs'];
    team1Sigles = map['team1Sigles'];
    team1Name = map['team1Name'];
    team1Runs = map['team1Runs'];
    team2Sigles = map['team2Sigles'];
    team2Name = map['team2Name'];
    team2Runs = map['team2Runs'];
    var s = DateTime.parse(map['updatedAt']).toLocal() ;
    date = "${s.year}/${s.month}/${s.day} ${s.hour}:${s.minute}:${s.second}" ;
  }
}
class GamesResponse {
  int statusCode;
  String message ;
  List<Game> games ;
  GamesResponse({
    this.statusCode,
    this.message,
    this.games,
  });
}

Aquí, además de la clase Game, tenemos la clase GamesResponse, que tiene como objetivo trasladar al widget que mostrará los partidos, si ocurrió un error (statusCode), el mensaje de error “Acomodado” si existe ((statusCode=1)) y la lista de los juegos games.

Archivo games_service.dart

import 'dart:async';
import 'dart:convert';
import 'package:http_apirest/models/game.dart';
import 'package:http_apirest/services/common/rest_client_service.dart';
class GamesService {
  RestClientService restClientService = RestClientService() ;
  final PATH = '/api/snb/livescore';
  Future<GamesResponse> games() async {
    var uri = '${PATH}?dateId=' ;
    GenericResponse response = await restClientService.get(uri) ;
    return _gamesResponseFromJson(response.statusCode, response.message, (response.statusCode==0) ? response.data : null);
  }
  GamesResponse _gamesResponseFromJson(int statusCode, String message, String json) {
    var gamesResponse = new GamesResponse();
    gamesResponse.statusCode = statusCode;
    gamesResponse.message = message;
    gamesResponse.games = (json != null) ? (jsonDecode(json) as List).map((game) => Game.fromMap(game)).toList() : [];
    return gamesResponse ;
  }
}

A través de la clase GamesService obtenemos todos los juegos en directo. Resaltar que estamos utilizando el método get de la clase RestClientService y convirtiendo la clase GenericResponse a una clase más particular GamesResponse. Este es el servicio que llamaremos en el widget que le presentará la información a los usuarios. Veamos solo una parte de ese archivo:

Archivo home.dart

import 'package:flutter/material.dart';
import 'package:http_apirest/models/game.dart';
import 'package:http_apirest/services/games_service.dart';
import 'package:http_apirest/ui/pages/game.dart';
import 'package:http_apirest/ui/widgets/message.dart';
class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);
  @override
  _HomeState createState() => new _HomeState();
}
class _HomeState extends State<HomePage> {
  GamesService gamesService = GamesService();
  List<Game> games = [];
  @override
  void initState() {
    _init();
    super.initState();
  }
  void _init() {
    this._fetchGames() ;
  }
  void _fetchGames() {
    gamesService.games().then((value) {
      if (value.statusCode == 0) {
        setState(() {
          games = value.games ;
        });
      } else {
        MessageWidget.error(context, "Ha ocurrido un error. Detalles: " + value.message, 4);
      }
    });
  }
  .
  .
  .

En la línea 3 estamos importando este servicio, en la línea 16 inicializando el servicio y en las líneas 30-39 utilizándolo. Este widget debe darle mantenimiento de estado a la variable game, ya que la misma no tendrá valor hasta que la función futuro games() del servicio GamesService no brinde una respuesta de la lista de los partidos.

También se implementa otro servicio get para obtener un partido en particular, como siempre pueden verificar todo el código en el siguiente enlace. github