Cómo escribir un complemento de Flutter

Te damos la bienvenida al codelab "Cómo escribir un complemento de Flutter".

¿Qué es es un complemento?

Un complemento es un software que agrega capacidades a tu app. Por ejemplo, es posible que desees que tu app para dispositivos móviles interactúe con la cámara de tu dispositivo. Los complementos forman una parte importante del ecosistema de Flutter. Primero debes consultar pub.dev para verificar si el complemento que necesitas ya existe. Los autores del SDK de Flutter, así como los miembros de la comunidad de Flutter, escribieron muchos complementos y los publicaron en pub.dev para compartirlos con la comunidad.

En particular, debes consultar los paquetes y complementos del programa Flutter Favorite. La etiqueta de favoritos de Flutter Favorite identifica los complementos que primero debes tener en cuenta cuando compilas tus apps.

Flutter facilita la interacción de los usuarios con las bibliotecas multiplataforma de Dart, pero, con frecuencia, es ideal interactuar con código específico de una plataforma. Por ejemplo, es posible que desees comunicarte con una base de datos que no cuenta con una biblioteca de Dart que se haya escrito para ella. Flutter brinda un mecanismo para crear complementos que te permiten comunicarte con código específico de la plataforma y también te permite publicar tus complementos en pub.dev, de modo que otros usuarios puedan usarlos.

En este codelab, aprenderás a crear tus propios complementos para iOS y Android. Implementarás un complemento simple de música que procesa audio en la plataforma host y crearás una app de ejemplo que usa el complemento para crear un teclado musical.

A continuación, te mostramos capturas de pantalla de la app definitiva:

f4275505c0be0bd7.png 746b8f48aa63e2ff.png

Qué aprenderás

  • Cómo escribir un complemento de Flutter para iOS y Android
  • Cómo crear una API para tu complemento
  • Cómo escribir una app que use tu complemento
  • Cómo publicar tu complemento para que otros puedan usarlo

¿Qué te gustaría aprender de este codelab?

Soy nuevo en el tema y me gustaría ver una buena descripción general. Tengo algunos conocimientos sobre este tema, pero me gustaría repasarlo. Estoy buscando código de ejemplo para usar en mi proyecto. Estoy buscando una explicación sobre un tema específico.

Para completar este lab, necesitas dos software: el SDK de Flutter y un editor. Puedes usar tu editor preferido, como Android Studio o IntelliJ, con los complementos de Flutter y Dart instalados, o Visual Studio Code con las extensiones de Flutter y el código Dart.

Algunas de las herramientas para el desarrollo de complementos cambiaron recientemente, por lo que este codelab sirve para la versión v1.15.19 del SDK de Flutter o posteriores. Puedes verificar tu versión con el siguiente comando:

$ flutter doctor

Puedes ejecutar el codelab con cualquiera de los siguientes dispositivos:

  • Un dispositivo físico iOS o Android conectado a tu computadora y configurado en modo de desarrollador
  • El simulador de iOS (requiere la instalación de herramientas de Xcode)
  • Android Emulator (requiere configuración en Android Studio)

Flutter se envía con plantillas para complementos que facilitan los primeros pasos. Cuando generas la plantilla del complemento, puedes especificar el lenguaje que desees usar. El lenguaje predeterminado es Swift para iOS y Kotlin para Android. Para este codelab, usarás Objective-C y Java.

Ejecuta los siguientes comandos en el directorio de trabajo para crear la plantilla del complemento y migrarla a seguridad nula:

$ flutter create --template=plugin --org com.example --template=plugin --platforms=android,ios -a java -i objc plugin_codelab
$ cd plugin_codelab
$ dart migrate --apply-changes
$ cd example
$ flutter pub upgrade
$ dart migrate --apply-changes

Con estos comandos, se genera la siguiente estructura de directorio:

plugin_codelab
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── local.properties
│   ├── plugin_codelab_android.iml
│   ├── settings.gradle
│   └── src
│       └── main
│           ├── AndroidManifest.xml
│           └── java
│               └── com
│                   └── example
│                       └── plugin_codelab
│                           └── PluginCodelabPlugin.java
├── example
│   ├── README.md
│   ├── android
│   ├── build
│   │   └── ios
│   │       └── Runner.build
│   │           └── Release-iphoneos
│   │               └── Runner.build
│   │                   └── dgph
│   ├── ios
│   ├── lib
│   │   └── main.dart
│   ├── plugin_codelab_example.iml
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
│       └── widget_test.dart
├── ios
│   ├── Assets
│   ├── Classes
│   │   ├── PluginCodelabPlugin.h
│   │   └── PluginCodelabPlugin.m
│   └── plugin_codelab.podspec
├── lib
│   └── plugin_codelab.dart
├── plugin_codelab.iml
├── pubspec.lock
├── pubspec.yaml
└── test
    └── plugin_codelab_test.dart

A continuación, se brinda una descripción de algunos archivos importantes:

  • pubspec.yaml: Es el archivo YAML que define el complemento. Especifica el nombre, las dependencias, la versión, los sistemas operativos compatibles del complemento, etc. Se usa en la página pub.dev de tu complemento.
  • CHANGELOG.md: Cada vez que desees publicar una versión nueva de un complemento, debes actualizar este archivo de Markdown para indicar los cambios en la versión nueva.
  • README.md: Este archivo de Markdown aparece en la primera página de la ficha de pub.dev del complemento. Debería describir en detalle cuál es el complemento y cómo usarlo.
  • lib/plugin_codelab.dart: Es el código Dart que implementa el frontend en el complemento. Los clientes de complementos tienen acceso a las funciones y a las clases públicas de este directorio.
  • android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java: Código nativo Java que implementa la característica de Android que se describe en plugin_codelab.dart.
  • ios/Classes/PluginCodelabPlugin.m: Es el código Objective-C que implementa la característica de iOS que se describe en plugin_codelab.dart. (también hay un archivo de encabezado que coincide).
  • example/: Este directorio contiene un cliente de tu complemento. A medida que desarrolles tu complemento, deberás editar este archivo para observar el complemento en funcionamiento.
  • example/lib/main.dart: Es el código Dart que ejecuta tu complemento. En este archivo, compilarás la IU de ejemplo.

Ejecuta los ejemplos en tu dispositivo Android o iOS con las siguientes instrucciones:

$ cd plugin_codelab/example
$ flutter run

Deberías ver algo como esto:

52b7d03a33b9cbfa.png

Verifica el código que se generó para el frontend del complemento:

lib/plugin_codelab.dart

class PluginCodelab {
  static const MethodChannel _channel =
      const MethodChannel('plugin_codelab');

  static Future<String> get platformVersion async {
    final String version =
      await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

cf1e10b838bf60ee.png Observaciones

  • PluginCodelab es la clase que invocan los usuarios de tu complemento.
  • Esta clase crea un elemento MethodChannel que permite que el código Dart se comunique con la plataforma host.
  • La API del complemento solo tiene un método: el método get de una propiedad platformVersion. Cuando se llama a este método get en Dart, el elemento MethodChannel invoca el método getPlatformVersion() y espera a que se muestre un objeto String de forma asíncrona.
  • Le corresponde al código específico de la plataforma interpretar el significado del mensaje getPlatformVersion, y se abordará este tema más adelante.

example/lib/main.dart

Future<void> initPlatformState() async {
  String platformVersion;
  // Platform messages may fail, so we use a try/catch PlatformException.
  try {
    platformVersion = await PluginCodelab.platformVersion;
  } on PlatformException {
    platformVersion = 'Failed to get platform version.';
  }

  // If the widget was removed from the tree while the asynchronous platform
  // message was in flight, we want to discard the reply rather than calling
  // setState to update our non-existent appearance.
  if (!mounted) return;

  setState(() {
    _platformVersion = platformVersion;
  });
}

cf1e10b838bf60ee.png Observaciones

  • Este es un cliente del complemento.
  • Este código llama al método get que se define en lib/plugin_codelab.dart.
  • Observa que la llamada se une en un elemento try-block. Si el código específico de la plataforma para iOS muestra FlutterError, o se arroja una excepción en Java, se vuelve a mostrar en Dart.

ios/Classes/PluginCodelabPlugin.m

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"plugin_codelab"
            binaryMessenger:[registrar messenger]];
  PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

cf1e10b838bf60ee.png Observaciones

  • Se llama a este código de inicialización cuando se configura un motor nuevo.
  • Este código genera un canal para comunicarse con el complemento.
  • Ten en cuenta que el nombre del canal que se especifica aquí debe coincidir con el nombre que se define en lib/plugin_codelab.dart.
  • Configurarlo como methodCallDelegate implica que la instancia creada recibe mensajes que se asocian con el mensajero binario brindado.

ios/Classes/PluginCodelabPlugin.m

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else {
    result(FlutterMethodNotImplemented);
  }
}

cf1e10b838bf60ee.png Observaciones

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin_codelab");
  channel.setMethodCallHandler(this);
}

cf1e10b838bf60ee.png Observaciones

  • Este código Java implementa getPlatformVersion() para Android.

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else {
    result.notImplemented();
  }
}

cf1e10b838bf60ee.png Observaciones

  • Este código Java controla los mensajes enviados desde Dart. Ten en cuenta que este código tiene una forma similar al complemento de iOS, pero existen algunas diferencias sutiles.

72ad6e60941a67c6.png¡Encontraste algo especial!

Ahora, brindas las implementaciones específicas de la plataforma para un sintetizador que emite sonidos cuando se presionan teclas en el teclado. Puedes pensar en este código como la biblioteca que le mostrarás a Flutter. Con frecuencia, cuando creas un complemento, ya tienes una API de plataforma definida con la que trabajarás, como en este caso.

Ahora, tienes dos implementaciones separadas de la misma funcionalidad, una para iOS y otra para Android. Debes obtener estas compilaciones como parte de tu app para que esta pueda llamar al complemento.

Cómo agregar a iOS

Agrega los siguientes archivos a tu proyecto:

Cuando coloques estos archivos en la ubicación ios/Classes, se compilarán como parte de la compilación de iOS para tu complemento. Puedes consultar ios/plugin_codelab.podspec a fin de confirmar que, de forma predeterminada, usa globs para definir qué fuentes compilar. Lo único que debes hacer es colocar los archivos en la ubicación correcta.

Cómo agregar a Android

Agrega el siguiente archivo a tu proyecto:

android/src/main/java/com/example/plugin_codelab/Synth.java

Cuando coloques este archivo Java en la ubicación android/src/main/java/com/example, se compilará como parte de la compilación de Android para tu complemento. Puedes consultar el sistema de compilación de Gradle a fin de verificar si solo necesitas colocar el archivo en el directorio correcto para poder compilarlo.

Explicación de la interfaz del sintetizador

La interfaz del sintetizador es similar en iOS y Android, y consta de cuatro métodos:

class Synthesizer {
  void start();
  void stop();
  int keyDown(int key);
  int keyUp(int key);
}

Los métodos keyUp() y keyDown() representan los eventos que se envían cuando se presiona y se suelta una tecla del teclado musical. El argumento key representa qué tecla se presiona o se suelta. Es una enumeración de todas las teclas en el teclado musical. El estándar MIDI define una enumeración para esas teclas, en la que 60 es el valor de do central y aumenta uno para cada tecla negra o blanca (semitono). Tu complemento usa esta definición.

El siguiente paso para crear un complemento consiste en pensar qué tipo de información deseas enviar en ambos sentidos entre Flutter y la plataforma host. Si intentas representar una biblioteca que ya tiene una API definida, puedes simplificarte el proceso e imitar esa interfaz.

En este codelab, brindamos el código del sintetizador para cada plataforma, de modo que puedas imitar su interfaz en el código Dart:

lib/plugin_codelab.dart

import 'dart:async';

import 'package:flutter/services.dart';

class PluginCodelab {
  static const MethodChannel _channel = const MethodChannel('plugin_codelab');

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<int?> onKeyDown(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyDown', [key]);
    return numNotesOn;
  }

  static Future<int?> onKeyUp(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyUp', [key]);
    return numNotesOn;
  }
}

Ten en cuenta que el segundo parámetro para invokeMethod() enumera los parámetros que se envían a la llamada del método.

Ahora, tienes bibliotecas específicas de la plataforma para emitir sonido y el código Dart que controla ese código, pero no están conectados. Si llamas a cualquiera de estos métodos de Dart ahora, arrojan excepciones "Sin implementar" porque no has implementado el host en el complemento. Ese es el próximo paso.

Cómo conectar todo en iOS

Primero, modifica el complemento para crear y, luego, iniciar una instancia de sintetizador:

ios/Classes/PluginCodelabPlugin.m

@implementation PluginCodelabPlugin {
  int _numKeysDown;
  FLRSynthRef _synth;
}
- (instancetype)init {
  self = [super init];
  if (self) {
    _synth = FLRSynthCreate();
    FLRSynthStart(_synth);
  }
  return self;
}

- (void)dealloc {
  FLRSynthDestroy(_synth);
}

Luego, empieza a controlar los mensajes que se envían a través del canal:

- (void)handleMethodCall:(FlutterMethodCall *)call
                  result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS "
        stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"onKeyDown" isEqualToString:call.method]) {
    FLRSynthKeyDown(_synth, [call.arguments[0] intValue]);
    _numKeysDown += 1;
    result(@(_numKeysDown));
  } else if ([@"onKeyUp" isEqualToString:call.method]) {
    FLRSynthKeyUp(_synth, [call.arguments[0] intValue]);

    _numKeysDown -= 1;
    result(@(_numKeysDown));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

Ten en cuenta que ahora el código también busca los mensajes onKeyDown y onKeyUp. Para obtener el argumento key, extráelo de call.arguments. El valor que se muestra se registra como un elemento NSNumber (que se describe en la documentación sobre los canales de la plataforma), por lo que es necesario convertirlo con intValue.

Consulta el archivo completo, PluginCodelabPlugin.m.

Cómo conectar todo en Android

Primero, modifica el complemento para crear y, luego, iniciar una instancia de sintetizador:

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

public class PluginCodelabPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  private Synth synth;
  private static final String channelName = "plugin_codelab";

  private static void setup(PluginCodelabPlugin plugin, BinaryMessenger binaryMessenger) {
    plugin.channel = new MethodChannel(binaryMessenger, channelName);
    plugin.channel.setMethodCallHandler(plugin);
    plugin.synth = new Synth();
    plugin.synth.start();
  }

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    setup(this, flutterPluginBinding.getBinaryMessenger());
  }

Luego, empieza a controlar los mensajes que se envían a través del canal:

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else if (call.method.equals("onKeyDown")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyDown((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else if (call.method.equals("onKeyUp")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyUp((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else {
    result.notImplemented();
  }
}

De forma similar a iOS, el código ahora busca los mensajes onKeyDown y onKeyUp. También, usa arguments.get() para extraer el valor key aquí. Asegúrate de que, en Android, el complemento controle cualquier excepción que pueda surgir.

Consulta el archivo completo, PluginCodelabPlugin.java.

Ahora que el complemento implementa todos los detalles, es probable que desees verificar cómo funciona. Para ello, debes implementar una app de ejemplo de la IU de un teclado simple:

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:plugin_codelab/plugin_codelab.dart';

enum _KeyType { Black, White }

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight])
      .then((_) {
    runApp(new MyApp());
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String? platformVersion;
    try {
      platformVersion = await PluginCodelab.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  void _onKeyDown(int key) {
    print("key down:$key");
    PluginCodelab.onKeyDown(key).then((value) => print(value));
  }

  void _onKeyUp(int key) {
    print("key up:$key");
    PluginCodelab.onKeyUp(key).then((value) => print(value));
  }

  Widget _makeKey({@required _KeyType keyType, @required int key}) {
    return AnimatedContainer(
      height: 200,
      width: 44,
      duration: Duration(seconds: 2),
      curve: Curves.easeIn,
      child: Material(
        color: keyType == _KeyType.White
            ? Colors.white
            : Color.fromARGB(255, 60, 60, 80),
        child: InkWell(
          onTap: () => _onKeyUp(key),
          onTapDown: (details) => _onKeyDown(key),
          onTapCancel: () => _onKeyUp(key),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Color.fromARGB(255, 250, 30, 0),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('Running on: $_platformVersion\n'),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  _makeKey(keyType: _KeyType.White, key: 60),
                  _makeKey(keyType: _KeyType.Black, key: 61),
                  _makeKey(keyType: _KeyType.White, key: 62),
                  _makeKey(keyType: _KeyType.Black, key: 63),
                  _makeKey(keyType: _KeyType.White, key: 64),
                  _makeKey(keyType: _KeyType.White, key: 65),
                  _makeKey(keyType: _KeyType.Black, key: 66),
                  _makeKey(keyType: _KeyType.White, key: 67),
                  _makeKey(keyType: _KeyType.Black, key: 68),
                  _makeKey(keyType: _KeyType.White, key: 69),
                  _makeKey(keyType: _KeyType.Black, key: 70),
                  _makeKey(keyType: _KeyType.White, key: 71),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Ten en cuenta lo siguiente:

  • Debes importar 'package:plugin_codelab/plugin_codelab.dart' para usar el complemento. La dependencia del ejemplo en el complemento se define en example/pubspec.yaml, que permite que funcione.
  • En main(),, se fuerza a la orientación para que sea horizontal, de modo que todo el teclado se pueda ajustar a la pantalla.
  • Los métodos _onKeyDown() y _onKeyUp() son clientes de la API del complemento que se diseñó en los pasos anteriores.
  • El código usa InkWell, que solo son rectángulos interactivos, para dibujar las teclas individuales.

Ejecuta la app para verificar el funcionamiento del teclado musical:

cd example
flutter run

Debe tener el siguiente aspecto:

f4275505c0be0bd7.png

¡Felicitaciones! Creaste, de manera correcta, un complemento de Flutter para iOS y Android, y tienes un excelente teclado musical que puedes tocar. El proyecto completo se puede descargar en https://github.com/flutter/codelabs/tree/master/plugin_codelab para fines comparativos.

Próximos pasos

  • Agrega pruebas integrales. El equipo de Flutter brinda una biblioteca para crear pruebas completas de integración que se denominan e2e.
  • Publica en pub.dev. Después de crear un complemento, es posible que desees compartirlo en línea para que otros lo puedan usar. Puedes encontrar la documentación completa sobre cómo publicar tu complemento en pub.dev, en cómo desarrollar paquetes de complementos.

Cómo extender el sintetizador

Si deseas usar el sintetizador y mejorarlo por diversión, a continuación, se explican los pasos que debes seguir:

  • Por el momento, el sintetizador genera una onda sinusoidal. ¿Qué te parece la idea de que genere una onda de sierra?
  • ¿Notaste los sonidos que se emiten cuando presionas y sueltas una tecla? Esto se debe a que el oscilador se activa y desactiva de forma repentina. Por lo general, los sintetizadores corrigen este problema con los envolventes acústicos de amplitud.
  • Por el momento, solo puedes reproducir una tecla a la vez. Por este motivo, este teclado es monofónico. Los pianos reales son polifónicos.