Como criar um plug-in do Flutter

Bem-vindo ao codelab "Como criar um plug-in do Flutter".

O que é exatamente um plug-in?

Um plug-in é um software que adiciona recursos ao seu aplicativo. Por exemplo, ele pode fazer com que o app para dispositivos móveis interaja com a câmera do dispositivo. Os plug-ins são uma parte importante do ecossistema do Flutter. Primeiro, confira no pub.dev (link em inglês) se o plug-in necessário já existe. Os autores do SDK do Flutter, assim como membros da comunidade do Flutter, já criaram muitos plug-ins e os publicaram no pub.dev para compartilhá-los com a comunidade.

Em particular, você precisa conferir os pacotes e plug-ins favoritos do Flutter (link em inglês). A tag Favoritos do Flutter (link em inglês) identifica plug-ins que você precisa considerar ao criar seus apps.

O Flutter facilita a interação dos usuários com bibliotecas Dart multiplataforma, mas, às vezes, é ideal para interagir com código específico da plataforma. Por exemplo, você pode querer se comunicar com um banco de dados que não possui uma biblioteca Dart criada para ele. O Flutter oferece um mecanismo para a criação de plug-ins que permite a comunicação com código específico da plataforma, além de possibilitar a publicação de seus plug-ins no pub.dev para que outras pessoas possam usá-los.

Neste codelab, você aprenderá a criar plug-ins para iOS e Android. Você implementará um plug-in de música simples que processa o áudio na plataforma de hospedagem. Depois, criará um aplicativo de exemplo que usa seu plug-in para criar um teclado de música.

Veja as capturas de tela do aplicativo final:

f4275505c0be0bd7.png 746b8f48aa63e2ff.png

O que você aprenderá

  • Como criar um plug-in do Flutter para iOS e Android.
  • Como criar uma API para seu plug-in.
  • Como criar um aplicativo que usa o plug-in.
  • Como publicar o plug-in para que outras pessoas possam usá-lo.

O que você quer aprender com este codelab?

Sou iniciante no assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero uma recapitulação. Estou procurando um exemplo de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor (links em inglês). Use seu editor preferido, como o Android Studio ou o IntelliJ com os plug-ins do Flutter e do Dart instalados ou o Visual Studio Code com as extensões do Dart Code e do Flutter (link em inglês).

Houve mudanças em algumas das ferramentas para desenvolvimento de plug-ins recentemente, então este codelab considera a v1.15.19 ou versões mais recentes do SDK do Flutter. Você pode conferir sua versão com o seguinte comando:

$ flutter doctor

É possível executar o codelab usando qualquer um dos seguintes dispositivos (links em inglês):

  • Um dispositivo físico iOS ou Android conectado ao seu computador e definido para o modo de desenvolvedor
  • O simulador do iOS (exige a instalação de ferramentas do Xcode)
  • O Android Emulator (requer configuração no Android Studio)

O Flutter vem com modelos de plug-ins que facilitam os primeiros passos. Ao gerar o modelo de plug-in, você pode especificar a linguagem que quer usar. O padrão é Swift para iOS e Kotlin para Android. Neste codelab, você usa Objective-C e Java.

Execute os seguintes comandos no seu diretório de trabalho para criar o modelo de plug-in e migrá-lo para um sistema de segurança contra nulidade (link em inglês):

$ 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

Esses comandos geram a seguinte estrutura de diretório:

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

Veja abaixo uma descrição de alguns arquivos importantes:

  • pubspec.yaml: o arquivo YAML que define seu plug-in. Ele especifica o nome, as dependências, a versão, os sistemas operacionais compatíveis etc. É usado na página pub.dev do plug-in.
  • CHANGELOG.md: sempre que você quiser publicar uma nova versão de um plug-in, atualize esse arquivo de marcação para indicar as mudanças na nova versão.
  • README.md: esse arquivo de marcação é exibido na primeira página da listagem pub.dev do plug-in. Ele deve descrever, em detalhes, o que é o plug-in e como usá-lo.
  • lib/plugin_codelab.dart: o código Dart que implementa o front-end no seu plug-in. Os clientes do plug-in têm acesso às classes e funções públicas nesse diretório.
  • android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java: o código Java nativo que implementa o recurso do Android descrito em plugin_codelab.dart.
  • ios/Classes/PluginCodelabPlugin.m: o código Objective-C que implementa o recurso do iOS descrito em plugin_codelab.dart.. Há também um arquivo principal correspondente.
  • example/: esse diretório contém um cliente do seu plug-in. Ao desenvolver o plug-in, edite esse arquivo para ver o plug-in em ação.
  • example/lib/main.dart: o código Dart que treina seu plug-in. Você cria a IU de exemplo aqui.

Execute os exemplos no dispositivo iOS ou Android com as seguintes instruções:

$ cd plugin_codelab/example
$ flutter run

Você verá algo como:

52b7d03a33b9cbfa.png

Confira o código gerado para o front-end do plug-in:

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 Observações

  • PluginCodelab é a classe que os usuários do seu plug-in invocam.
  • Essa classe cria um MethodChannel que permite ao código Dart se comunicar com a plataforma de hospedagem.
  • A API do plug-in tem apenas um método, o getter de propriedade platformVersion. Quando alguém chama esse getter no Dart, o MethodChannel invoca o método getPlatformVersion() e aguarda de maneira assíncrona uma String ser retornada.
  • Cabe ao código específico da plataforma interpretar o significado da mensagem getPlatformVersion. Você verá isso mais tarde.

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 Observações

  • Esse é um cliente do plug-in.
  • Esse código chama o getter definido em lib/plugin_codelab.dart.
  • Observe que a chamada está incluída em um try-block. Se o código específico da plataforma para iOS retornar um FlutterError (link em inglês) ou uma exceção for lançada em Java, isso será recuperado no lado do 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 Observações

  • Esse código de inicialização é chamado quando um novo mecanismo é configurado.
  • Esse código gera um canal para se comunicar com o plug-in.
  • O nome do canal especificado aqui precisa corresponder ao nome definido em lib/plugin_codelab.dart.
  • Defini-lo como methodCallDelegate significa que a instância criada recebe mensagens associadas ao mensageiro binário fornecido.

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 Observações

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 Observações

  • Esse 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 Observações

  • Esse código Java processa as mensagens enviadas do Dart. O formato desse código é semelhante ao plug-in do iOS, mas tem algumas diferenças sutis.

72ad6e60941a67c6.pngVocê encontrou algo especial!

Agora, você forneçará implementações específicas da plataforma para um sintetizador que emite sons quando as teclas do teclado são pressionadas. Pense nesse código como a biblioteca que será exibida para o Flutter. Muitas vezes, ao criar um plug-in, já existe uma API de plataforma definida que você usará, como neste caso.

Agora você tem duas implementações separadas da mesma funcionalidade, uma para iOS e outra para Android. Você precisa compilá-las como parte do seu aplicativo para que o plug-in possa chamá-las.

Adicionar ao iOS

Adicione os seguintes arquivos ao projeto:

Ao colocar esses arquivos no local ios/Classes, eles serão compilados como parte do build do iOS para seu plug-in. Em ios/plugin_codelab.podspec, você pode ver que, por padrão, ele usa globs para definir quais fontes serão compiladas. Só é necessário colocar os arquivos no local certo.

Adicionar ao Android

Adicione o seguinte arquivo ao seu projeto:

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

Ao colocar esse arquivo Java no local android/src/main/java/com/example, ele será compilado como parte do build do Android para seu plug-in. Observe o sistema de compilação do Gradle para ver que você só precisa colocar o arquivo no diretório correto para compilá-lo.

Explicação sobre a interface do sintetizador

A interface do sintetizador é semelhante no iOS e no Android e consiste em quatro métodos:

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

Os métodos keyUp() e keyDown() representam os eventos enviados quando uma tecla do teclado musical é pressionada e solta. O argumento key representa qual tecla está sendo pressionada ou solta. Trata-se de uma enumeração de todas as teclas do teclado musical. O padrão MIDI define uma enumeração para essas teclas, em que 60 é o valor de Dó Central e é aumentado em um para cada tecla preta ou branca (semitom) (links em inglês). Seu plug-in usa essa definição.

A próxima etapa na criação de um plug-in é pensar no tipo de informação que você quer enviar entre o Flutter e a plataforma de hospedagem. Se estiver tentando representar uma biblioteca que já tem uma API definida, você poderá imitar essa interface para facilitar sua vida.

Neste codelab, fornecemos o código do sintetizador para cada plataforma, de modo que seja possível simular a interface no 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;
  }
}

Observe que o segundo parâmetro de invokeMethod() lista os parâmetros enviados para a chamada de método.

Agora você tem bibliotecas específicas da plataforma para criar som e código Dart que controla esse código, mas eles não estão associados. Se você chamar qualquer um desses métodos Dart agora, eles resultarão em exceções "Não implementado" porque você não implementou o lado do host no plug-in. Esse é o próximo passo.

Associar os elementos no iOS

Primeiro, modifique o plug-in para criar e iniciar uma instância do 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);
}

Em seguida, comece a processar as mensagens enviadas pelo 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);
  }
}

Observe que o código agora também procura as mensagens onKeyDown e onKeyUp. Para receber o argumento key, extraia-o de call.arguments. O valor retornado vem como NSNumber (descrito na documentação dos canais de plataforma), portanto, converta-o com intValue (links em inglês).

Veja o arquivo completo, PluginCodelabPlugin.m.

Associar os elementos no Android

Primeiro, modifique o plug-in para criar e iniciar uma instância do 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());
  }

Em seguida, comece a processar as mensagens enviadas pelo 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();
  }
}

Assim como no iOS, o código agora procura as mensagens onKeyDown e onKeyUp. Use arguments.get() para extrair o valor de key aqui também. Garanta que, no Android, seu plug-in lide com todas as exceções que possam surgir.

Veja o arquivo completo, PluginCodelabPlugin.java.

Agora que o plug-in implementa toda a estrutura básica, é provável que você queira vê-lo em ação. Para isso, implemente um app de exemplo de IU de teclado simples:

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),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Observe o seguinte:

  • É preciso importar 'package:plugin_codelab/plugin_codelab.dart' para usar o plug-in. A dependência do exemplo no plug-in é definida em example/pubspec.yaml,, que faz esse trabalho.
  • Em main(),, a orientação é forçada para paisagem, de modo que todo o teclado possa caber na tela.
  • Os métodos _onKeyDown() e _onKeyUp() são clientes da API do plug-in desenvolvidos nas etapas anteriores.
  • O código usa InkWell (link em inglês), que é apenas um retângulo interativo, para desenhar as teclas individuais.

Execute o app para ver o teclado de música em funcionamento:

cd example
flutter run

Ele ficará assim:

f4275505c0be0bd7.png

Parabéns! Você criou um plug-in do Flutter para iOS e Android e tem um teclado musical interessante para usar. Faça o download do projeto concluído em https://github.com/flutter/codelabs/tree/master/plugin_codelab para comparar.

Próximas etapas

  • Adicione testes completos. A equipe do Flutter oferece uma biblioteca para criar testes de integração completos chamada e2e (link em inglês).
  • Publique em pub.dev. Depois de criar um plug-in, você pode querer compartilhá-lo on-line para que outras pessoas possam usá-lo. Encontre a documentação completa sobre como publicar seu plug-in no pub.dev em Como desenvolver pacotes de plug-in (link em inglês).

Estender o sintetizador

Para se divertir com o sintetizador e aprimorá-lo, considere as próximas etapas:

  • No momento, o sintetizador gera uma onda senoidal. Que tal gerar uma onda de serra (links em inglês)?
  • Você percebeu os sons de estalo ao pressionar e soltar uma tecla? Isso acontece porque o oscilador liga e desliga de forma abrupta. Normalmente, os sintetizadores corrigem isso com envelopes de amplitude (link em inglês).
  • No momento, só é possível tocar uma tecla por vez. Isso é chamado de monofônico. Os pianos reais são polifônicos (link em inglês).