Rédiger un plug-in Flutter

Bienvenue dans l'atelier de programmation "Rédiger un plug-in Flutter" !

Qu'est-ce qu'un plug-in ?

Un plug-in est élément logiciel qui ajoute des fonctionnalités à votre appli. C'est ce qui permet, par exemple, à votre appli mobile d'interagir avec l'appareil photo de votre appareil. Les plug-ins jouent un rôle important dans l'écosystème Flutter. Commencez par consulter le site pub.dev pour vérifier si le plug-in dont vous avez besoin existe déjà. Les créateurs du SDK Flutter, ainsi que d'autres membres de la communauté Flutter, ont écrit de nombreux plug-ins et les ont publiés sur pub.dev pour les partager avec la communauté.

Plus spécifiquement, nous vous recommandons d'explorer les packages et plug-ins Flutter les plus populaires. La mention Flutter Favorite (Favori Flutter) identifie les plug-ins à considérer en premier pour la création de vos applis.

Flutter prend facilement en charge les interactions avec des bibliothèques Dart multiplates-formes, mais il est parfois préférable d'utiliser du code spécifique à une plate-forme (par exemple, si vous souhaitez communiquer avec une base de données pour laquelle aucune bibliothèque Dart n'a été écrite). Flutter fournit un mécanisme de création de plug-ins qui permet de communiquer avec du code spécifique à une plate-forme. Vous pouvez également publier vos plug-ins sur pub.dev pour les mettre à la disposition d'autres utilisateurs.

Dans cet atelier de programmation, vous apprendrez à créer vos propres plug-ins pour iOS et Android. Vous implémenterez un plug-in musical simple, qui traite des données audio sur la plate-forme hôte, puis créerez un exemple d'appli qui utilise votre plug-in pour créer un clavier musical.

Voici quelques captures d'écran de l'appli finale :

f4275505c0be0bd7.png 746b8f48aa63e2ff.png

Objectifs de l'atelier

  • Apprendre à rédiger un plug-in Flutter pour iOS et Android
  • Apprendre à créer une API pour votre plug-in
  • Apprendre à rédiger une appli qui utilise votre plug-in
  • Apprendre à publier votre plug-in afin que d'autres personnes puissent l'utiliser.

Qu'attendez-vous de cet atelier de programmation ?

Je suis novice en la matière et je voudrais avoir un bon aperçu. Je connais un peu le sujet, mais j'aimerais revoir certains points. Je recherche un exemple de code à utiliser dans mon projet. Je cherche des explications sur un point spécifique.

Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur. Vous pouvez utiliser l'éditeur de votre choix, par exemple Android Studio, IntelliJ avec les plug-ins Flutter et Dart, ou Visual Studio Code avec les extensions Dart Code et Flutter.

Certains des outils de développement de plug-ins ont récemment été modifiés. Cet atelier de programmation requiert la version 1.15.19 ou une version ultérieure du SDK Flutter. Vous pouvez vérifier votre version à l'aide de la commande suivante :

$ flutter doctor

Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :

  • Un appareil iOS ou Android physique connecté à votre ordinateur et réglé en mode développeur
  • Le simulateur iOS (les outils Xcode doivent être installés)
  • L'émulateur Android (doit être configuré dans Android Studio)

Flutter est fourni avec des modèles de plug-ins qui permettent de commencer facilement. Lorsque vous générez le modèle de plug-in, vous pouvez spécifier le langage que vous souhaitez utiliser. La valeur par défaut est Swift pour iOS et Kotlin pour Android. Pour cet atelier de programmation, vous utiliserez Objective-C et Java.

Exécutez les commandes suivantes dans votre répertoire de travail pour créer le modèle de plug-in, puis effectuez une migration vers Null Safety :

$ 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

Ces commandes génèrent l'arborescence de répertoires suivante :

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

Description de certains fichiers importants :

  • pubspec.yaml : fichier YAML définissant votre plug-in. Il spécifie le nom du plug-in, ses dépendances, sa version, les systèmes d'exploitation compatibles, etc. Il est utilisé sur la page pub.dev de votre plug-in.
  • CHANGELOG.md : vous devez mettre à jour ce fichier Markdown pour indiquer les modifications apportées à chaque nouvelle version du plug-in que vous souhaitez publier.
  • README.md : fichier Markdown affiché sur la première page du référencement "pub.dev" du plug-in. Le fichier doit fournir une description détaillée du plug-in et de son utilisation.
  • lib/plugin_codelab.dart : code Dart qui implémente l'interface dans votre plug-in. Les clients du plug-in ont accès aux classes et fonctions publiques de ce répertoire.
  • android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java : code Java natif qui implémente la fonctionnalité Android décrite dans plugin_codelab.dart.
  • ios/Classes/PluginCodelabPlugin.m : code Objective-C qui implémente la fonctionnalité iOS décrite dans plugin_codelab.dart. (il existe également un fichier d'en-tête correspondant).
  • example/ : répertoire contenant le client de votre plug-in. Lors du développement, vous pouvez modifier ce fichier pour observer votre plug-in en action.
  • example/lib/main.dart : code Dart qui agit pour votre plug-in. Vous créerez l'exemple d'interface ici.

Suivez les instructions ci-dessous pour exécuter les exemples sur votre appareil iOS ou Android :

$ cd plugin_codelab/example
$ flutter run

L'écran ci-dessous doit s'afficher :

52b7d03a33b9cbfa.png

Consultez le code généré pour l'interface du 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 Observations

  • PluginCodelab est la classe appelée par les utilisateurs de votre plug-in.
  • Cette classe crée un objet MethodChannel qui permet au code Dart de communiquer avec la plate-forme hôte.
  • L'API du plug-in ne comporte qu'une seule méthode : la méthode getter de propriété platformVersion. Lorsque celle-ci est appelée dans Dart, MethodChannel appelle la méthode getPlatformVersion() et passe en attente asynchrone d'une réponse String.
  • C'est le code spécifique à la plate-forme qui doit interpréter le message getPlatformVersion. (Ce point sera abordé par la suite.)

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 Observations

  • Il s'agit d'un client du plug-in.
  • Ce code appelle la méthode getter définie dans lib/plugin_codelab.dart.
  • Notez que l'appel est encapsulé dans un try-block. Si le code spécifique à la plate-forme pour iOS renvoie une erreur FlutterError ou si une exception est générée en Java, le code réapparaît du côté 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 Observations

  • Ce code d'initialisation est appelé lorsqu'un nouveau moteur est configuré.
  • Ce code génère un canal pour communiquer avec le plug-in.
  • Notez que le nom du canal spécifié ici doit correspondre au nom défini dans lib/plugin_codelab.dart.
  • Si l'instance créée se définit comme methodCallDelegate, elle reçoit les messages associés au messager binaire fourni.

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 Observations

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 Observations

  • Ce code Java implémente getPlatformVersion() pour 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 Observations

  • Ce code Java traite les messages envoyés depuis Dart. Notez que la mise en forme de ce code est semblable à celle du plug-in iOS, avec quelques différences subtiles.

72ad6e60941a67c6.pngVous avez trouvé quelque chose de spécial !

Vous allez à présent fournir des implémentations spécifiques à la plate-forme pour un synthétiseur qui émet des sons lorsque l'on appuie sur les touches du clavier. Vous pouvez considérer ce code comme la bibliothèque que vous allez présenter à Flutter. Souvent, lorsque vous créez un plug-in, vous avez déjà défini une API pour la plate-forme à partir de laquelle vous allez travailler, comme dans le cas présent.

Vous disposez désormais de deux implémentations distinctes de la même fonctionnalité : une pour iOS et une pour Android. Vous devrez les intégrer à la compilation de votre appli, afin que le plug-in puisse les appeler.

Sur iOS

Ajoutez les fichiers suivants à votre projet :

Les fichiers placés dans ios/Classes seront compilés dans le build iOS de votre plug-in. Si vous consultez ios/plugin_codelab.podspec, vous constaterez que, par défaut, des globs sont utilisés pour définir les sources à compiler. Il vous suffit de placer les fichiers au bon endroit.

Sur Android

Ajoutez le fichier suivant à votre projet :

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

Les fichiers Java placés dans android/src/main/java/com/example seront compilés dans le build Android de votre plug-in. Si vous consultez le système de compilation Gradle, vous constaterez qu'il suffit de placer le fichier dans le bon répertoire pour le compiler.

Explication de l'interface Synthesizer

L'interface du synthétiseur est similaire sur iOS et Android. Elle se compose de quatre méthodes :

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

Les méthodes keyUp() et keyDown() représentent les événements envoyés lorsqu'une touche du clavier musical est pressée et relâchée. L'argument key représente la touche concernée. Il s'agit d'une énumération de toutes les touches du clavier musical. La norme MIDI définit une énumération pour ces touches, où 60 correspond au Do central, avec un incrément de 1 pour chaque touche noire ou blanche (demi-ton). Votre plug-in utilise cette définition.

L'étape suivante dans la création de votre plug-in consiste à déterminer le type d'informations que vous souhaitez échanger entre Flutter et la plate-forme hôte. Si la bibliothèque que vous voulez représenter dispose déjà d'une API définie, vous pouvez vous simplifier la vie et imiter cette interface.

Dans cet atelier de programmation, le code de synthétisation est fourni pour chaque plate-forme, pour vous permettre de l'imiter son interface dans le code 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;
  }
}

Notez que le deuxième paramètre de invokeMethod() répertorie les paramètres envoyés à l'appel de méthode.

Vous disposez à présent de bibliothèques spécifiques à une plate-forme pour créer du son, et du code Dart qui contrôle ce code, mais ces éléments ne sont pas raccordés. Si vous appelez l'une de ces méthodes Dart, elle génère des exceptions "Not Implemented" (non implémentée), car le côté hôte du plug-in n'est pas implémenté. C'est l'étape suivante.

Raccrocher les wagons sur iOS

Commencez par modifier le plug-in pour créer et démarrer une instance du synthétiseur :

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

Ensuite, commencez à traiter les messages envoyés via le 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);
  }
}

Notez que le code recherche désormais les messages onKeyDown et onKeyUp. Vous pouvez obtenir l'argument key depuis call.arguments. La valeur renvoyée est présentée comme NSNumber (décrit dans la documentation sur les canaux de plate-forme). Convertissez-la avec intValue.

Consultez le fichier complété : PluginCodelabPlugin.m.

Raccrocher les wagons sur Android

Commencez par modifier le plug-in pour créer et démarrer une instance du synthétiseur :

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());
  }

Ensuite, commencez à traiter les messages envoyés via le 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();
  }
}

Comme pour iOS, le code recherche désormais les messages onKeyDown et onKeyUp. arguments.get() permet ici aussi d'obtenir la valeur key. Veillez à ce que votre plug-in Android gère toutes les exceptions potentielles.

Consultez le fichier complété : PluginCodelabPlugin.java.

Maintenant que le plug-in implémente tous les raccords, vous voudrez probablement le voir en action. Pour cela, vous implémentez un exemple d'appli d'interface de clavier 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),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Notez les points suivants :

  • Vous devez importer 'package:plugin_codelab/plugin_codelab.dart' pour utiliser le plug-in. La dépendance de l'exemple dans le plug-in est définie dans example/pubspec.yaml,, ce qui lui permet de fonctionner.
  • Dans main(),, le mode paysage est imposé pour permettre d'afficher tout le clavier.
  • Les méthodes _onKeyDown() et _onKeyUp() sont des clients de l'API du plug-in conçus aux étapes précédentes.
  • Le code utilise InkWell, de simples des rectangles interactifs, pour représenter les touches.

Exécutez l'appli pour voir votre clavier musical à l'œuvre :

cd example
flutter run

Le résultat devrait se présenter comme ceci :

f4275505c0be0bd7.png

Félicitations ! Vous avez créé un plug-in Flutter pour iOS et Android. Et un super clavier musical pour exprimer votre créativité. Si vous voulez comparer, vous pouvez télécharger le projet terminé sur la page https://github.com/flutter/codelabs/tree/master/plugin_codelab.

Étapes suivantes

  • Ajoutez des tests de bout en bout. L'équipe Flutter fournit une bibliothèque appelée e2e, qui permet de créer des tests d'intégration de bout en bout.
  • Publiez sur pub.dev. Lorsque vous créez un plug-in, vous pouvez le partager en ligne afin que d'autres personnes puissent l'utiliser. Vous trouverez la documentation complète sur la procédure de publication de votre plug-in sur pub.dev dans la section Développer des packages de plug-ins.

Aller plus loin

Vous pouvez jouer avec le synthétiseur et l'améliorer pour le plaisir. Voici quelques suggestions :

  • Pour le moment, le synthétiseur génère un signal sinusoïdal. Que diriez-vous d'un signal en dents de scie ?
  • Vous avez peut-être remarqué ces claquements lorsque vous appuyez sur une touche ou la relâchez. Ils sont causés par l'oscillateur, qui s'allume ou s'éteint de façon abrupte. Les synthétiseurs y remédient généralement à l'aide d'enveloppes sonores.
  • Pour le moment, vous ne pouvez utiliser qu'une touche à la fois. Votre système est monophonique. Les vrais pianos sont polyphoniques.