https://d226lax1qjow5r.cloudfront.net/blog/blogposts/getting-started-with-flutter-3-and-vonage-apis/flutter-3.png

Démarrer avec Flutter 3 et les API de Vonage

Publié le July 30, 2023

Temps de lecture : 15 minutes

Avec la sortie de Flutter 3.0 (qui comprend une série de améliorations en termes de stabilité et de performances), c'est le moment idéal pour examiner comment vous pouvez utiliser les API de communication pour améliorer votre expérience utilisateur et vos applications multiplateformes.

Grâce à la capacité de Flutter à utiliser les SDK des plateformes natives, nous pouvons utiliser de manière transparente les SDK Android et iOS de Vonage au sein de nos applications Flutter. Voyons comment nous pouvons créer une simple application Flutter capable de passer un appel téléphonique vocal vers un téléphone physique. À la fin de ce guide, vous aurez bien compris comment utiliser le SDK de Vonage pour passer un appel vocal et comment vous pouvez utiliser les SDK Android et iOS natifs dans votre application Flutter.

Pour ce guide, nous allons créer une application de base à partir de zéro, mais vous pouvez tout aussi bien intégrer les éléments ci-dessous dans votre application.

Le code source complet de ce projet est disponible sur GitHub.

Configuration de Vonage

Avant d'entrer dans le code, il y a quelques petites choses à faire pour configurer l'API de Vonage et l'utiliser.

Ouverture de compte

Commencez par vous inscrire pour obtenir un compte de développeur Vonage gratuit. Cela peut être fait via le tableau de bordUne fois inscrit, vous trouverez la clé et le secret de l'API de votre compte. Notez-les pour les étapes ultérieures.

Vonage dashboard home page showing API key and API secret location

Installer le CLI de Vonage

L'interface CLI de Vonage CLI de Vonage de Vonage vous permet d'effectuer de nombreuses opérations sur la ligne de commande. Les exemples incluent la création d'applications, l'achat de numéros et l'association d'un numéro à une application, ce que nous ferons tous aujourd'hui.

Pour installer le CLI avec NPM, exécutez :

npm install -g @vonage/cli

Configurez l'interface de programmation de Vonage pour qu'elle utilise votre clé et votre secret d'API de Vonage. Vous pouvez les obtenir à partir de la page des paramètres dans le tableau de bord.

Exécutez la commande suivante dans un terminal, en remplaçant API_KEY et API_SECRET par les vôtres :

vonage config:set --apiKey=API_KEY --apiSecret=API_SECRET

Acheter un Numbers Vonage

Ensuite, nous avons besoin d'un numéro Vonage que l'application peut utiliser, c'est le numéro de téléphone qui s'affichera sur le téléphone que nous appelons à partir de l'application.

Vous pouvez acheter un numéro en utilisant le CLI de Vonage. La commande suivante permet d'acheter un numéro disponible aux Etats-Unis. Spécifiez un autre code de pays à deux caractères pour acheter un numéro dans un autre pays.

vonage numbers:search US
vonage numbers:buy 15555555555 US

Créer un serveur Webhook

Lorsqu'un appel entrant est reçu, Vonage envoie une demande à une URL accessible au public de votre choix - que nous appelons le . answer_url. Vous devez créer un serveur webhook capable de recevoir cette demande et de renvoyer un NCCO contenant une connect qui transmettra l'appel au numéro de téléphone numéro de téléphone PSTN. Pour ce faire, vous devez extraire le numéro de destination du paramètre de requête to et en le renvoyant dans votre réponse.

Sur la ligne de commande, créez un nouveau dossier qui contiendra votre serveur web

mkdir app-to-phone-flutter
cd app-to-phone-flutter

À l'intérieur du dossier, initialisez un nouveau projet Node.js en exécutant cette commande :

npm init -y

Ensuite, installez les dépendances nécessaires :

npm install express localtunnel --save

Dans le dossier de votre projet, créez un fichier nommé server.js et ajoutez le code tel qu'illustré ci-dessous - assurez-vous de remplacer NUMBER par votre numéro Vonage (en E.164 ), ainsi que SUBDOMAIN par une valeur réelle. La valeur utilisée fera partie des URLs que vous définirez comme webhooks dans l'étape suivante.

'use strict';

const subdomain = 'SUBDOMAIN';
const vonageNumber = 'NUMBER';

const express = require('express')
const app = express();
app.use(express.json());

app.get('/voice/answer', (req, res) => {
  console.log('NCCO request:');
  console.log(`  - callee: ${req.query.to}`);
  console.log('---');
  res.json([ 
    { 
      "action": "talk", 
      "text": "Please wait while we connect you."
    },
    { 
      "action": "connect",
      "from": vonageNumber,
      "endpoint": [ 
        { "type": "phone", "number": req.query.to } 
      ]
    }
  ]);
});

app.all('/voice/event', (req, res) => {
  console.log('EVENT:');
  console.dir(req.body);
  console.log('---');
  res.sendStatus(200);
});

app.listen(3000);

const localtunnel = require('localtunnel');
(async () => {
  const tunnel = await localtunnel({ 
      subdomain: subdomain, 
      port: 3000
    });
  console.log(`App available at: ${tunnel.url}`);
})();

Vous pouvez maintenant démarrer le serveur en exécutant, dans le terminal, la commande suivante :

node server.js

Un avis s'affiche pour vous indiquer que le serveur est désormais disponible :

App available at: https://SUBDOMAIN.loca.lt

Créer une application Vonage

Dans cette étape, vous allez créer une application Vonage Applications capable de répondre à des cas d'utilisation de communication vocale in-App.

Ouvrez un nouveau terminal et, si nécessaire, naviguez jusqu'au répertoire de votre projet.

Créez une application Vonage en copiant et en collant la commande ci-dessous dans le terminal. Veillez à modifier les valeurs de --voice_answer_url et --voice_event_url en remplaçant SUBDOMAIN par la valeur réelle utilisée à l'étape précédente :

vonage apps:create "App to Phone Tutorial" --voice_answer_url=https://SUBDOMAIN.loca.lt/voice/answer --voice_event_url=https://SUBDOMAIN.loca.lt/voice/event

Un fichier nommé vonage_app.json est créé/mis à jour dans le répertoire de votre projet et contient l'identifiant de l'application Vonage nouvellement créée et la clé privée. Un fichier de clé privée nommé app_to_phone_tutorial.key est également créé.

Notez l'ID de l'Application qui est affiché dans votre terminal lors de la création de votre application :

screenshot of the terminal with Application ID underlined

Lier un numéro Vonage

Une fois que vous avez un numéro approprié, vous pouvez le lier à votre application Vonage. Remplacez YOUR_VONAGE_NUMBER par votre nouveau numéro, remplacez APPLICATION_ID par l'identifiant de votre application et exécutez cette commande :

vonage apps:link APPLICATION_ID --number=YOUR_VONAGE_NUMBER

Créer un utilisateur

Les utilisateurs sont un concept clé lorsque l'on travaille avec les SDK clients de Vonage. Lorsqu'un utilisateur s'authentifie avec le Client SDK, les informations d'identification fournies l'identifient en tant qu'utilisateur spécifique. Chaque utilisateur authentifié correspondra généralement à un seul utilisateur dans votre base de données d'utilisateurs.

Pour créer un utilisateur nommé Aliceexécutez la commande suivante à l'aide de l'interface de gestion de Vonage :

vonage apps:users:create "Alice"

Cela renverra un identifiant similaire à celui qui suit :

User ID: USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

Générer un JWT

Le Client SDK utilise des JWT pour l'authentification. Le JWT identifie le nom de l'utilisateur, l'identifiant de l'application associée et les autorisations accordées à l'utilisateur. Il est signé à l'aide de votre clé privée pour prouver qu'il s'agit d'un jeton valide.

Exécutez les commandes suivantes, n'oubliez pas de remplacer la variable APPLICATION_ID par l'ID de votre application et PRIVATE_KEY par le nom de votre fichier de clé privée.

Vous générez un JWT à l'aide du CLI de Vonage en exécutant la commande suivante, mais n'oubliez pas de remplacer la variable APP_ID par votre propre valeur :

vonage jwt --app_id=APPLICATION_ID --subject=Alice --key_file=./PRIVATE_KEY --acl='{"paths":{"/*/users/**":{},"/*/conversations/**":{},"/*/sessions/**":{},"/*/devices/**":{},"/*/image/**":{},"/*/media/**":{},"/*/push/**":{},"/*/knocking/**":{},"/*/legs/**":{}}}'

Les commandes ci-dessus fixent l'expiration du JWT à un jour à partir de maintenant, ce qui est le maximum.

terminal screenshot of a generated sample JWT

Nous avons maintenant tout ce qu'il faut pour utiliser l'API Voice de Vonage dans une application Flutter. Mettons maintenant en place l'application elle-même.

Mise en place de Flutter

Si vous ne l'avez pas encore fait, commencez par télécharger et installer Flutter et ses dépendances. Vous pouvez le faire en suivant le Guide d'installation. Une fois que Flutter est correctement installé, la prochaine chose à faire est de configurer votre IDE. La façon de procéder dépend de l'IDE que vous souhaitez utiliser, mais le Guide d'installation de Flutter vous aidera à le faire. Configurer un éditeur vous aidera à le faire.

Pour ce guide, nous utiliserons Android Studio.

Une fois que votre IDE est configuré, suivez la procédure test drive pour mettre en place une application Flutter de base avec un support pour Android et iOS. Nous utiliserons cette application de base comme point de départ de ce projet, mais bien sûr, si vous avez déjà un projet Flutter que vous souhaitez utiliser, vous pouvez également le faire.

Installation des SDK

Le projet étant maintenant configuré, nous pouvons installer le Client SDK de Vonage. Actuellement, le Client SDK n'est pas disponible en tant que paquetage Flutter, nous devrons donc utiliser le Client SDK natif Android natif pour Android et le Client SDK natif iOS. La communication entre Android/iOS et Flutter utilisera la méthode CanalMéthode - de cette manière, Flutter appellera les méthodes Android/iOS, Android/iOS appellera les méthodes Flutter.

SDK Android

Pour installer le SDK Android, commencez par augmenter la mémoire allouée à la JVM en modifiant la propriété org.gradle.jvmargs dans votre fichier gradle.properties de votre ordinateur. Nous recommandons de la fixer à au moins 4 Go :

org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8

Ensuite, ouvrez votre fichier build.gradle qui se trouve à l'adresse android/app/build.gradle et mettez en œuvre le SDK de Vonage comme suit :

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation "com.vonage:client-sdk-voice:1.0.3"
}

Enfin, assurez-vous que votre minSdkVersion est réglé sur au moins 23:

defaultConfig {
        applicationId "com.vonage.tutorial.voice.app_to_phone"
        minSdkVersion 23
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Le SDK Android est maintenant configuré et prêt à être utilisé pour la construction Android de l'application flutter.

SDK iOS

Pour installer le SDK iOS, commencez par générer le fichier PodFile en ouvrant une ligne de commande à la racine de votre projet Flutter et en exécutant les commandes ci-dessous :

cd ios/
pod init

Cela générera le fichier PodFileOuvrez ce fichier et ajoutez le pod ci-dessous :

pod 'VonageClientSDKVoice', '~> 1.0.3'

Veillez également à ce que la plate-forme soit au moins ios 10.

platform :ios, '10.0'

Votre fichier complet devrait ressembler à ceci :

platform :ios, '10.0'

target 'Runner' do
  use_frameworks!

  pod 'VonageClientSDKVoice', '~> 1.0.3'
end

Ensuite, à partir de la ligne de commande, toujours dans le répertoire iOS, exécutez :

pod update

Ceci téléchargera et installera le SDK de Vonage et ses dépendances.

Enfin, pour lier ceci à votre projet Flutter, depuis le répertoire racine de votre projet, exécutez la commande Flutter ci-dessous. Cela déclenchera une compilation iOS et générera les fichiers nécessaires à l'utilisation du SDK.

flutter build ios

Une fois terminé et construit avec succès, votre SDK est configuré et prêt à être utilisé.

Code

De par la nature de Flutter, le code peut facilement être divisé en trois parties, le code Flutter écrit en Dart, le code natif Android écrit en Kotlin et le code natif iOS écrit en Swift.

Flutter

Commençons par le code spécifique à Flutter, en remplaçant le contenu de lib/main.dart par le code ci-dessous :

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: CallWidget(title: 'app-to-phone-flutter'),
    );
  }
}

class CallWidget extends StatefulWidget {
  const CallWidget({Key key = const Key("any_key"), required this.title}) : super(key: key);
  final String title;

  @override
  _CallWidgetState createState() => _CallWidgetState();
}

class _CallWidgetState extends State<CallWidget> {
  SdkState _sdkState = SdkState.LOGGED_OUT;
  static const platformMethodChannel = MethodChannel('com.vonage');

  _CallWidgetState() {
    platformMethodChannel.setMethodCallHandler(methodCallHandler);
  }

  Future<dynamic> methodCallHandler(MethodCall methodCall) async {
    switch (methodCall.method) {
      case 'updateState':
        {
          setState(() {
            var arguments = 'SdkState.${methodCall.arguments}';
            _sdkState = SdkState.values.firstWhere((v) {return v.toString() == arguments;}
            );
          });
        }
        break;
      default:
        throw MissingPluginException('notImplemented');
    }
  }

  Future<void> _loginUser() async {
    String token = "ALICE_TOKEN";

    try {
      await platformMethodChannel
          .invokeMethod('loginUser', <String, dynamic>{'token': token});
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

  Future<void> _makeCall() async {
    try {
      await requestPermissions();

      await platformMethodChannel.invokeMethod('makeCall');
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

  Future<void> requestPermissions() async {
    await [ Permission.microphone] .request();
  }

  Future<void> _endCall() async {
    try {
      await platformMethodChannel.invokeMethod('endCall');
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const SizedBox(height: 64),
            _updateView()
          ],
        ),
      ),
    );
  }

  Widget _updateView() {
    if (_sdkState == SdkState.LOGGED_OUT) {
      return ElevatedButton(
          onPressed: () { _loginUser(); },
          child: const Text("LOGIN AS ALICE")
      );
    } else if (_sdkState == SdkState.WAIT) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    } else if (_sdkState == SdkState.LOGGED_IN) {
      return ElevatedButton(
          onPressed: () { _makeCall(); },
          child: const Text("MAKE PHONE CALL")
      );
    } else if (_sdkState == SdkState.ON_CALL) {
      return ElevatedButton(
          onPressed: () { _endCall(); },
          child: const Text("END CALL")
      );
    } else {
      return const Center(
          child: Text("ERROR")
      );
    }
  }
}

enum SdkState {
  LOGGED_OUT,
  LOGGED_IN,
  WAIT,
  ON_CALL,
  ERROR
}

Il s'agit de la classe complète nécessaire pour construire l'interface utilisateur de l'application et déclencher les méthodes spécifiques à la plateforme que nous écrirons dans un instant. Décortiquons ce qui se passe dans chacune des méthodes de cette classe.

En commençant par les importations au sommet de cette classe, nous avons les importations normales de Flutter mais nous utilisons aussi le gestionnaire de permission . Celui-ci est utilisé pour gérer les demandes de permissions sur iOS et Android. Assurez-vous que vous l'avez installé en lançant la commande :

flutter pub add permission_handler

A la base de votre projet flutter.

Ensuite, nous construisons l'application, pour cette démo nous avons une application très simple avec juste un élément widget que nous avons appelé CallWidget

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: CallWidget(title: 'app-to-phone-flutter'),
    );
  }
}

Cette CallWidget prolonge le StatefulWidget en prenant le titre et en initialisant le CallWidgetState.

class CallWidget extends StatefulWidget {
  const CallWidget({Key key = const Key("any_key"), required this.title}) : super(key: key);
  final String title;

  @override
  _CallWidgetState createState() => _CallWidgetState();
}

L'interface CallWidgetState gère les éléments de l'interface utilisateur, l'état actuel de l'application et toute la communication avec le code de la plateforme native.

class _CallWidgetState extends State<CallWidget> {
  SdkState _sdkState = SdkState.LOGGED_OUT;
  static const platformMethodChannel = MethodChannel('com.vonage');

  _CallWidgetState() {
    platformMethodChannel.setMethodCallHandler(methodCallHandler);
  }

  Future<dynamic> methodCallHandler(MethodCall methodCall) async {
    switch (methodCall.method) {
      case 'updateState':
        {
          setState(() {
            var arguments = 'SdkState.${methodCall.arguments}';
            _sdkState = SdkState.values.firstWhere((v) {return v.toString() == arguments;}
            );
          });
        }
        break;
      default:
        throw MissingPluginException('notImplemented');
    }
  }

Ici, nous définissons l'état de départ de l'application comme étant SdkState.LOGGED_OUT, nous créons l'élément MethodChannel qui gérera toutes les communications entre Flutter et le code natif. Ensuite, nous définissons l'état methodCallHandler dans lequel l'état est fixé à l'état qui a été transmis à Flutter par le code natif.

L'interface utilisateur est ensuite construite à l'aide de la méthode build qui crée simplement un Box de hauteur 64. Nous mettrons à jour cet élément en fonction de l'état de l'application pour afficher différentes informations.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const SizedBox(height: 64),
            _updateView()
          ],
        ),
      ),
    );
  }

Ensuite, la méthode _updateView est utilisée pour modifier ce qui est affiché dans la boîte en fonction de l'état actuel de l'application. Ce modèle d'état permet d'avoir une interface utilisateur propre, ne montrant à l'utilisateur que ce qu'il a besoin de voir à un moment donné du cycle de vie de l'application.

Widget _updateView() {
    if (_sdkState == SdkState.LOGGED_OUT) {
      return ElevatedButton(
          onPressed: () { _loginUser(); },
          child: const Text("LOGIN AS ALICE")
      );
    } else if (_sdkState == SdkState.WAIT) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    } else if (_sdkState == SdkState.LOGGED_IN) {
      return ElevatedButton(
          onPressed: () { _makeCall(); },
          child: const Text("MAKE PHONE CALL")
      );
    } else if (_sdkState == SdkState.ON_CALL) {
      return ElevatedButton(
          onPressed: () { _endCall(); },
          child: const Text("END CALL")
      );
    } else {
      return const Center(
          child: Text("ERROR")
      );
    }
  }

Les méthodes _loginUser et _endCall sont très similaires en ce sens que tout ce que nous faisons ici est d'invoquer les méthodes loginUser/endCall dans le code natif. C'est ainsi que nous déclenchons le code natif lorsque l'utilisateur appuie sur un bouton de l'interface utilisateur. Dans l'élément _loginUser nous avons une variable token qui devrait être la valeur JWT que vous avez générée plus tôt en utilisant le CLI de Vonage.

Future<void> _loginUser() async {
    String token = "ALICE_TOKEN";

    try {
      await platformMethodChannel
          .invokeMethod('loginUser', <String, dynamic>{'token': token});
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

  Future<void> _endCall() async {
    try {
      await platformMethodChannel.invokeMethod('endCall');
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

La méthode _makeCall implique également une méthode sur le code natif, appelant la méthode makeCall (méthode). Cependant, avant de le faire, nous utilisons la méthode requestPermissions pour demander à l'utilisateur les autorisations d'exécution requises. Dans le cas présent, il s'agit uniquement du microphone/de l'enregistrement audio.

Future<void> _makeCall() async {
    try {
      await requestPermissions();

      await platformMethodChannel.invokeMethod('makeCall');
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
  }

  Future<void> requestPermissions() async {
    await [ Permission.microphone] .request();
  }

Enfin, nous avons une énumération qui contient les différents états dans lesquels le SDK et l'application peuvent se trouver.

enum SdkState {
  LOGGED_OUT,
  LOGGED_IN,
  WAIT,
  ON_CALL,
  ERROR
}

Android

Voyons maintenant le code spécifique à Android pour cette application. Tout d'abord, nous devons définir les autorisations dont l'application aura besoin de la part du système Android. Dans votre fichier AndroidManifest.xml qui se trouve à android/app/src/main/AndroidManifest.xml ajoutez les permissions ci-dessous :

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

Ensuite, ouvrons le fichier MainActivity.kt qui se trouve à l'adresse suivante android/app/src/main/kotlin/PACKAGE_NAME/MainActivity.kt

Le contenu complet de ce fichier est le suivant :

import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import com.vonage.android_core.VGClientConfig
import com.vonage.clientcore.core.api.ClientConfigRegion
import com.vonage.voice.api.CallId
import com.vonage.voice.api.VoiceClient
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private lateinit var client: VoiceClient
    private var onGoingCallID: CallId? = null

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        initClient()
        addFlutterChannelListener()
    }

    private fun initClient() {
        client = VoiceClient(this)
        client.setConfig(VGClientConfig(ClientConfigRegion.US))

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

        client.setReconnectingListener {
            notifyFlutter(SdkState.WAIT)
        }
    }

    private fun addFlutterChannelListener() {
        flutterEngine?.dartExecutor?.binaryMessenger?.let {
            MethodChannel(it, "com.vonage").setMethodCallHandler { call, result ->

                when (call.method) {
                    "loginUser" -> {
                        val token = requireNotNull(call.argument<String>("token"))
                        loginUser(token)
                        result.success("")
                    }
                    "makeCall" -> {
                        makeCall()
                        result.success("")
                    }
                    "endCall" -> {
                        endCall()
                        result.success("")
                    }
                    else -> {
                        result.notImplemented()
                    }
                }
            }
        }
    }

    private fun loginUser(token: String) {
        client.createSession(token) { err, sessionId ->
            when(err) {
                null -> notifyFlutter(SdkState.LOGGED_IN)
                else -> notifyFlutter(SdkState.ERROR) // handle error

            }

        }
    }

    @SuppressLint("MissingPermission")
    private fun makeCall() {
        notifyFlutter(SdkState.WAIT)

        client.serverCall(mapOf("to" to "PHONE_NUMBER")) {
                err, outboundCall ->
            when {
                err != null -> {
                    notifyFlutter(SdkState.ERROR)
                } else -> {
                    onGoingCallID = outboundCall
                    notifyFlutter(SdkState.ON_CALL)
                }
            }
        }
    }

    private fun endCall() {
        notifyFlutter(SdkState.WAIT)

        onGoingCallID?.let {
            client.hangup(it) {
                    err ->
                when {
                    err != null -> {
                        notifyFlutter(SdkState.ERROR)
                    } else -> {
                        notifyFlutter(SdkState.LOGGED_IN)
                        onGoingCallID = null
                    }
                }
            }
        }
    }

    private fun notifyFlutter(state: SdkState) {
        Handler(Looper.getMainLooper()).post {
            flutterEngine?.dartExecutor?.binaryMessenger?.let {
                MethodChannel(it, "com.vonage")
                    .invokeMethod("updateState", state.toString())
            }
        }
    }
}

enum class SdkState {
    LOGGED_OUT,
    LOGGED_IN,
    WAIT,
    ON_CALL,
    ERROR
}

Voyons ce qu'il en est.

La première chose que vous remarquerez est que nous étendons la classe FlutterActivity Il s'agit d'une classe d'activité fournie par Flutter qui gère une grande partie du cycle de vie supplémentaire et de la magie de Flutter qui permet d'exécuter du code natif.

Nous avons ensuite deux variables que nous allons utiliser :

private lateinit var client: VoiceClient
   private var onGoingCallID: CallId? = null

L'objet VoiceClient est l'objet responsable de toutes les interactions du SDK : passer un appel téléphonique, raccrocher, etc. L'objet onGoingCallID sera utilisé pour garder une trace de l'appel téléphonique en cours.

Ensuite, nous surchargeons la méthode configureFlutterEngine qui nous permet d'exécuter du code lorsque l'application est créée par le moteur Flutter. Ici, nous l'utilisons pour exécuter deux méthodes, l'une pour ajouter un écouteur de canal et l'autre pour configurer le canal NexmoClient.

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        initClient()
        addFlutterChannelListener()
    }

L'initialisation de l'application VoiceClient est simple, nous passons simplement le contexte actuel de l'application. Ensuite, nous créons un ErrorListener et un "ReconnectingListener" qui nous donnera l'état actuel du client, ces états correspondent à des valeurs que nous devons renvoyer à Flutter. Ainsi, en utilisant une instruction when, nous pouvons envoyer les valeurs en fonction des besoins.

private fun initClient() {
        client = VoiceClient(this)
        client.setConfig(VGClientConfig(ClientConfigRegion.US))

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

        client.setReconnectingListener {
            notifyFlutter(SdkState.WAIT)
        }
    }

La fonction addFlutterChannelListener ajoute un listener qui surveillera tous les appels de méthode de Flutter. Comme vous pouvez le voir, ces appels sont liés aux trois méthodes que nous avons dans Flutter, ce qui nous permet de faire correspondre ces appels à des méthodes spécifiques dans le code natif.

    private fun addFlutterChannelListener() {
        flutterEngine?.dartExecutor?.binaryMessenger?.let {
            MethodChannel(it, "com.vonage").setMethodCallHandler { call, result ->

                when (call.method) {
                    "loginUser" -> {
                        val token = requireNotNull(call.argument<String>("token"))
                        loginUser(token)
                        result.success("")
                    }
                    "makeCall" -> {
                        makeCall()
                        result.success("")
                    }
                    "endCall" -> {
                        endCall()
                        result.success("")
                    }
                    else -> {
                        result.notImplemented()
                    }
                }
            }
        }
    }

La méthode loginUser est appelée lorsque Flutter envoie l'appel à la méthode loginUser, elle transmet le jeton JWT que nous avons défini et déclenche ensuite la méthode de connexion sur le client.

private fun loginUser(token: String) {
        client.createSession(token) { err, sessionId ->
            when(err) {
                null -> notifyFlutter(SdkState.LOGGED_IN)
                else -> notifyFlutter(SdkState.ERROR) // handle error
            }
        }
    }

La méthode makeCall est appelée lorsque Flutter envoie l'appel de la méthode makeCall, ce qui lance un appel téléphonique vers le numéro de téléphone spécifié "PHONE_NUMBER" vous devez remplacer ce numéro par un numéro de téléphone réel que vous souhaitez appeler. Ici encore, nous transmettons l'état à Flutter selon que l'appel est réussi et démarre ou qu'il y a une sorte d'erreur.

    private fun makeCall() {
        notifyFlutter(SdkState.WAIT)

        client.serverCall(mapOf("to" to "PHONE_NUMBER")) {
                err, outboundCall ->
            when {
                err != null -> {
                    notifyFlutter(SdkState.ERROR)
                } else -> {
                    onGoingCallID = outboundCall
                    notifyFlutter(SdkState.ON_CALL)
                }
            }
        }
    }

La méthode endCall est appelée lorsque Flutter envoie l'appel de méthode endCall ce qui met fin à l'appel téléphonique en cours (s'il y en a un).

    private fun endCall() {
        notifyFlutter(SdkState.WAIT)

        onGoingCallID?.let {
            client.hangup(it) {
                    err ->
                when {
                    err != null -> {
                        notifyFlutter(SdkState.ERROR)
                    } else -> {
                        notifyFlutter(SdkState.LOGGED_IN)
                        onGoingCallID = null
                    }
                }
            }
        }
    }

Enfin, nous avons la méthode nofityFlutter C'est ici que nous utilisons la magie de Flutter pour renvoyer l'état actuel de l'application afin que Flutter puisse mettre à jour l'interface utilisateur. En utilisant cela, nous sommes capables d'impliquer la méthode Flutter updateState et de passer l'état actuel en tant que variable.

private fun notifyFlutter(state: SdkState) {
        Handler(Looper.getMainLooper()).post {
            flutterEngine?.dartExecutor?.binaryMessenger?.let {
                MethodChannel(it, "com.vonage")
                    .invokeMethod("updateState", state.toString())
            }
        }
    }

Et c'est tout le code natif dont nous avons besoin ! À ce stade, nous avons une application Flutter fonctionnelle que nous pourrions construire pour Android et être en mesure de passer un appel téléphonique depuis l'application vers un téléphone physique. Mais avant de tester l'application, voyons comment nous pouvons faire la même chose pour iOS.

iOS

Tout d'abord, nous devons configurer les permissions audio dans iOS, nous avons déjà le paquet dans Flutter pour les demander, donc tout ce que nous avons à faire est d'ouvrir le fichier ios/Runner/info.plist et d'ajouter Privacy - Microphone Usage Description avec la valeur "Make a call"

Xcode showing the info file selected and pricacy microphone usage description set

Ensuite, ouvrez le fichier ios/Runner/AppDelegate c'est là que nous inclurons le code pour interfacer entre Flutter et le SDK de la même manière que nous l'avons déjà fait pour Android. Le code complet ressemble à ceci :

import UIKit
import Flutter
import VonageClientSDKVoice

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    enum SdkState: String {
        case loggedOut = "LOGGED_OUT"
        case loggedIn = "LOGGED_IN"
        case wait = "WAIT"
        case onCall = "ON_CALL"
        case error = "ERROR"
    }
    
    var vonageChannel: FlutterMethodChannel?
    var client: VGVoiceClient? = nil
    var callID: String?
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        initClient()
        addFlutterChannelListener()
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func initClient() {
        client = VGVoiceClient()
        let config = VGClientConfig(region: .US)
        client.setConfig(config)
    }
    
    func addFlutterChannelListener() {
        let controller = window?.rootViewController as! FlutterViewController
        
        vonageChannel = FlutterMethodChannel(name: "com.vonage",
                                             binaryMessenger: controller.binaryMessenger)
        vonageChannel?.setMethodCallHandler({ [weak self]
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            guard let self = self else { return }
            
            switch(call.method) {
            case "loginUser":
                if let arguments = call.arguments as? [String: String],
                   let token = arguments["token"] {
                    self.loginUser(token: token)
                }
                result("")
            case "makeCall":
                self.makeCall()
                result("")
            case "endCall":
                self.endCall()
                result("")
            default:
                result(FlutterMethodNotImplemented)
            }
        })
    }
    
    func loginUser(token: String) {
        client?.createSession(token, sessionId: nil) { error, sessionId in
            if (error != nil) {
                self.notifyFlutter(state: .error)
            } else {
                self.notifyFlutter(state: .loggedIn)
            }
        }
    }
    
    func makeCall() {
        client.serverCall(["to": "PHONE_NUMBER"]) { error, callId in
                    DispatchQueue.main.async { [weak self] in
                        guard let self else { return }
                        if error == nil {
                            self.callID = callId
                            self.notifyFlutter(state: .onCall)
                        } else {
                            self.notifyFlutter(state: .error)
                        }
                    }
                }
    }
    
    func endCall() {
        client.hangup(callID) { error in
                    DispatchQueue.main.async { [weak self] in
                        guard let self else { return }
                        if (error != nil) {
                            self.notifyFlutter(state: .error)
                        } else {
                            self.callID = nil
                            self.notifyFlutter(state: .loggedIn)
                        }
                    }
                }
    }
    
    func notifyFlutter(state: SdkState) {
        vonageChannel?.invokeMethod("updateState", arguments: state.rawValue)
    }
}

C'est tout le code dont vous aurez besoin pour pouvoir construire pour iOS. Maintenant que nous avons tout le code en place, construisons l'application et testons-la !

Construire et tester

Avec tout ce qui est maintenant en place, nous pouvons construire et exécuter l'application, nous allons construire la version Android et l'exécuter dans l'émulation Android.

REMARQUE assurez-vous d'avoir défini le JWT dans le code Flutter et le PHONE_NUMBER dans le code natif. Assurez-vous également que votre serveur web est toujours en cours d'exécution.

Démarrez l'émulateur Android pour que flutter puisse s'y attacher, voici comment faire dans Android studio

Android studio with device manager selected

Une fois que cela fonctionne, vous pouvez sélectionner cet appareil comme cible pour la construction de Flutter et appuyer sur la flèche verte pour construire et exécuter l'application Flutter (avec le code natif d'Android).

android studio with emulator selected and main.dart

Une fois l'application créée et installée, l'écran ci-dessous s'affiche. En cliquant sur le bouton Se connecter en tant qu'Alice, vous accéderez à l'écran suivant. À partir de là, vous pouvez appuyer sur le bouton Passer un appel téléphonique qui vous demandera (lors de la première exécution) d'autoriser les permissions audio. Ensuite, l'appel téléphonique commencera et le numéro de téléphone que vous avez saisi sera appelé pour connecter la session audio.

Une fois que vous souhaitez terminer l'appel, vous pouvez le faire en appuyant sur le bouton de fin d'appel.

The four UI screens of the app, from right to left. The App startup screen, the logged in screen, the permission request screen and finally the in call screen

Et c'est terminé ! Vous avez maintenant une application entièrement fonctionnelle pour les appels téléphoniques écrite en Flutter avec un support à la fois pour Android et iOS. Mais bien sûr, ce n'est pas tout ! Avec vos connaissances sur l'utilisation des SDK Android et iOS, jetez un coup d'œil aux autres projets d'exemple qui vous aideront à intégrer d'autres fonctionnalités de communication dans votre application Flutter. Si vous souhaitez plus de détails, n'hésitez pas à consulter le portail des développeurs qui contient toute la documentation et les exemples de code dont vous pourriez avoir besoin !

Partager:

https://a.storyblok.com/f/270183/400x400/04765919bb/zachary-powell-1.png
Zachary PowellDéveloppeur Android senior Advocate