loading...

Chopper (Retrofit para Flutter) #1 – Primeros pasos

acanteror profile image Antonio Cantero Ruiz Updated on ・7 min read

Este tutorial y el resto de la serie son una traducción del genial trabajo de Matej Rešetár. Puedes leer el original aquí o si lo prefieres puedes ver el vídeo .

Trabajar con APIs RESTful y hacer peticiones HTTP es el pan de cada día para cualquier desarrollador. Si vienes de Android probablemente conocerás Retrofit. Los desarrolladores IOS tienen Alamofire.

En Flutter normalmente se usa el package http o algo como dio. Aunque esos packages hacen un trabajo asombroso, hacen que trabajes al nivel más bajo. La cuestión que surge es, por tanto -¿qué podemos usar nosotros, como desarrolladores de Flutter, para simplificar nuestra trabajo con APIs HTTP? ¡Chopper! 

Configurando el proyecto

Chopper es una librería que, además de otras cosas, genera código para simplificarnos el proceso de desarrollo. El chopper_generator es solo una dependencia de desarrollo - no necesitamos incluirla en la app final.

También vamos a utilizar el package Provider para simplificar la sintaxis del InheritedWidget.

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter
  chopper: ^2.4.0
  provider: ^3.0.0+1

...

dev_dependencies:
  flutter_test:
    sdk: flutter
  chopper_generator: ^2.3.4
  # No version number means the latest version
  build_runner:

...

Eligiendo una API REST

Antes de continuar tenemos que elegir una API con la que trabajar. En este proyecto usaremos JSONPlaceholder API. Fue creado específicamente para este tipo de tareas de aprendizaje. Esta API proporciona 100 post falsos que contienen un título y un body.

Esta primera parte de la serie de tutoriales sobre Chopper y las siguientes trabajarán con esos post falsos. En esta parte vamos a construir una aplicación básica con Flutter, que muestre una lista de todos los post y también el detalle de un post.

alt text

Creando un ChopperService

La mayor parte del código que escribimos con Chopper está dentro de una subclase de ChopperService. En este tendremos un PostApiService  que será una clase abstracta que contendrá únicamente las definiciones de sus métodos. Entonces el chopper_generator intervendrá, examinará estas definiciones y generará todo el código por nosotros.

///post_api_service.dart

import 'package:chopper/chopper.dart';

// Source code generation in Dart works by creating a new file which contains a "companion class".
// In order for the source gen to know which file to generate and which files are "linked", you need to use the part keyword.
part 'post_api_service.chopper.dart';

@ChopperApi(baseUrl: 'https://jsonplaceholder.typicode.com/posts')
abstract class PostApiService extends ChopperService {
  @Get()
  Future<Response> getPosts();

  @Get(path: '/{id}')
  // Query parameters are specified the same way as @Path
  // but obviously with a @Query annotation
  Future<Response> getPost(@Path('id') int id);

  // Put & Patch requests are specified the same way - they must contain the @Body
  @Post()
  Future<Response> postPost(
    @Body() Map<String, dynamic> body,
  );
}

El archivo anterior contiene todo lo que necesitamos para que el generador de código complete la implementación.

Apunte rápido sobre cabeceras HTTP

Aunque no vamos a usar cabeceras con la API de JSONPlaceholder, en la mayoría de las apps reales, conocer cómo trabajar con cabeceras es imprescindible.

TIP: Las cabeceras son un modo de pasar información adicional al servidor incluida en la petición. La autenticación es un ejemplo en el que la información se pasa a través de cabeceras. Una cabecera es un par clave-valor (Authorization: Bearer 123456).

Si tuviéramos que añadir cabeceras al método getPosts(), sería algo como el siguiente código.

///post_api_service.dart

// Headers (e.g. for Authentication) can be added in the HTTP method constructor
// or also as parameters of the Dart method itself.
@Get(headers: {'Constant-Header-Name': 'Header-Value'})
Future<Response> getPosts([
  // Parameter headers are suitable for ones which values need to change
  @Header('Changeable-Header-Name') String headerValue,
]);

Generando el código

La clase PostApiService es abstracta - su implementación será generada por el package chopper_generator. Para iniciar la generación de código tenemos que ejecutar un comando en la terminal.  

La última parte del comando puede ser "build" o “watch". Usaremos watch, de modo que la generación se ejecute automáticamente siempre que cambiemos el contenido del archivo post_api_service.dart.

flutter packages pub run build_runner watch

Al ejecutar este comando se ha generado un nuevo archivo llamado post_api_service.chopper.dart.

Instanciando un ChopperClient

Tener sólo los métodos que especifiquen qué pedir a la API no es suficiente. También necesitamos tener un medio para pedirlo. Necesitamos un cliente HTTP. El package Chopper usa su propio ChopperClient que está creado a partir del package http de Dart.

¿Cómo vamos a hacer que PostApiService trabaje junto al ChopperClient? Después de todo, esos tres métodos: getPosts, getPost y postPost,  cuyo cometido es simplificar el trabajo con la API HTTP, tienen que hacer peticiones desde el cliente.

La solución es simple - si echamos un vistazo a la clase generada
(_$PostApiService), su constructor tiene un parámetro opcional de tipo ChopperClient. Los métodos generados, por tanto, utilizan este cliente para hacer peticiones HTTP.

///post_api_service.chopper.dart

...

class _$PostApiService extends PostApiService {
  _$PostApiService([ChopperClient client]) {
    if (client == null) return;
    this.client = client;
  }

  ...

}

Básicamente lo que necesitamos es la instancia inicializada de _$PostApiService. El modo más elegante de obtenerla es a través de un método estático en PostApiService.

///post_api_service.dart

import 'package:chopper/chopper.dart';

part 'post_api_service.chopper.dart';

// This baseUrl is now changed to specify only the endpoint '/posts'
@ChopperApi(baseUrl: '/posts')
abstract class PostApiService extends ChopperService {

  ...

  static PostApiService create() {
    final client = ChopperClient(
      // The first part of the URL is now here
      baseUrl: 'https://jsonplaceholder.typicode.com',
      services: [
        // The generated implementation
        _$PostApiService(),
      ],
      // Converts data to & from JSON and adds the application/json header.
      converter: JsonConverter(),
    );

    // The generated class with the ChopperClient passed in
    return _$PostApiService(client);
  }
}

Ten en cuenta que ahora la baseUrl en la anotación @ChopperApi cambió por sólo ‘/posts'. La mayor parte de la URL ahora se define en el ChopperClient. Esto es una buena práctica que nos permite tener varios ChopperServices para diferentes endpoints de la misma API.

Construyendo la UI

Dado que ésta es sólo una app sencilla para mostrar Chopper no queremos complicarnos con ningún manejo real de estado. Puedes aprender acerca del manejo de estado con BLoC apropiado en un otro tutorial.

La UI consistirá en dos páginas - home y single post. Usaremos el package Provider para pasar fácilmente el PostApiService entre páginas. el provider envolverá el MaterialApp widget raíz.

///main.dart

import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'data/post_api_service.dart';
import 'home_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      // The initialized PostApiService is now available down the widget tree
      builder: (_) => PostApiService.create(),
      // Always call dispose on the ChopperClient to release resources
      dispose: (context, PostApiService service) => service.client.dispose(),
      child: MaterialApp(
        title: 'Material App',
        home: HomePage(),
      ),
    );
  }
}

HomePage

El widget principal de HomePage mostrará una lista de posts y un FloatingActionButton que usaremos para hacer la petición POST. Al pulsar en un post específico, el usuario navegará hasta SinglePostPage, que crearemos posteriormente.

alt text

///home_page.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:provider/provider.dart';

import 'data/post_api_service.dart';
import 'single_post_page.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chopper Blog'),
      ),
      body: _buildBody(context),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          // The JSONPlaceholder API always responds with whatever was passed in the POST request
          final response = await Provider.of<PostApiService>(context)
              .postPost({'key': 'value'});
          // We cannot really add any new posts using the placeholder API,
          // so just print the response to the console
          print(response.body);
        },
      ),
    );
  }

  FutureBuilder<Response> _buildBody(BuildContext context) {
    // FutureBuilder is perfect for easily building UI when awaiting a Future
    // Response is the type currently returned by all the methods of PostApiService
    return FutureBuilder<Response>(
      // In real apps, use some sort of state management (BLoC is cool)
      // to prevent duplicate requests when the UI rebuilds
      future: Provider.of<PostApiService>(context).getPosts(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          // Snapshot's data is the Response
          // You can see there's no type safety here (only List<dynamic>)
          final List posts = json.decode(snapshot.data.bodyString);
          return _buildPosts(context, posts);
        } else {
          // Show a loading indicator while waiting for the posts
          return Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }

  ListView _buildPosts(BuildContext context, List posts) {
    return ListView.builder(
      itemCount: posts.length,
      padding: EdgeInsets.all(8),
      itemBuilder: (context, index) {
        return Card(
          elevation: 4,
          child: ListTile(
            title: Text(
              posts[index]['title'],
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            subtitle: Text(posts[index]['body']),
            onTap: () => _navigateToPost(context, posts[index]['id']),
          ),
        );
      },
    );
  }

  void _navigateToPost(BuildContext context, int id) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => SinglePostPage(postId: id),
      ),
    );
  }
}

En esta línea (localizada en el FutureBuilder): final List posts = json.decode(snapshot.data.bodyString); podemos ver que no hay tipado seguro. La lista de post es simplemente List.
Chopper puede mejorar esto si lo integramos con BuiltValue o JSON Serializable. Aprenderemos como en próximas entregas de esta serie.

SinglePostPage

Esta página obtendrá el ID del post que mostrará a través de su constructor. Entonces llamará al método getPost(),  de nuevo con la ayuda de un FutureBuilder.

alt text

///single_post_page.dart

import 'dart:convert';

import 'package:chopper/chopper.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'data/post_api_service.dart';

class SinglePostPage extends StatelessWidget {
  final int postId;

  const SinglePostPage({
    Key key,
    this.postId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chopper Blog'),
      ),
      body: FutureBuilder<Response>(
        future: Provider.of<PostApiService>(context).getPost(postId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            final Map post = json.decode(snapshot.data.bodyString);
            return _buildPost(post);
          } else {
            return Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }

  Padding _buildPost(Map post) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: <Widget>[
          Text(
            post['title'],
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          Text(post['body']),
        ],
      ),
    );
  }
}

Conclusión

Hemos construido una app que muestra un listado de post de una API. Hasta ahora solamente hemos cubierto la funcionalidad básica de Chopper. Hay mucho más que aprender, incluyendo temas como interceptores y conversores personalizados. Al acabar esta serie de tutoriales sobre Chopper, sabrás cómo usar Chopper para permitir un tapado seguro de un modo sencillo.

Posted on by:

acanteror profile

Antonio Cantero Ruiz

@acanteror

Andaluz por parte de padres y mexicano por parte de hija. Enseñar para aprender, no hay más.

Discussion

markdown guide