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

Erste Schritte mit Flutter 3 und Vonage APIs

Zuletzt aktualisiert am July 30, 2023

Lesedauer: 14 Minuten

Mit der Veröffentlichung von Flutter 3.0 (das eine Reihe von Stabilitäts- und Leistungsverbesserungen) ist jetzt ein guter Zeitpunkt, um einen Blick darauf zu werfen, wie Sie Kommunikations-APIs nutzen können, um Ihre Benutzererfahrung zu verbessern und Ihre plattformübergreifenden Applikationen zu erweitern.

Dank der Fähigkeit von Flutter, SDKs für native Plattformen zu verwenden, können wir die SDKs für Android und iOS von Vonage nahtlos in unseren Flutter-Applikationen verwenden. Schauen wir uns an, wie wir eine einfache Flutter-Anwendung erstellen können, die in der Lage ist, einen Sprachanruf an ein physisches Telefon zu tätigen. Am Ende dieses Leitfadens werden Sie ein gutes Verständnis dafür haben, wie Sie das Vonage SDK verwenden können, um einen Sprachanruf zu tätigen, und wie Sie native Android- und iOS-SDKs in Ihrer Flutter-Anwendung verwenden können.

In diesem Leitfaden werden wir eine einfache Anwendung von Grund auf erstellen, aber Sie können die folgenden Elemente genauso schnell in Ihre Anwendung integrieren.

Den vollständigen Quellcode für dieses Projekt finden Sie auf GitHub.

Vonage-Einrichtung

Bevor wir uns mit dem Code beschäftigen, müssen wir einige Dinge tun, um die Vonage-API einzurichten und sie zu nutzen.

Account-Anmeldung

Melden Sie sich zunächst für einen kostenlosen Vonage Developer Account an. Dies können Sie über das DashboardWenn Sie sich angemeldet haben, finden Sie dort Ihren Account-API-Schlüssel und Ihr API-Geheimnis. Notieren Sie sich diese für zukünftige Schritte.

Vonage dashboard home page showing API key and API secret location

Installieren Sie die Vonage CLI

Die Vonage CLI ermöglicht es Ihnen, viele Vorgänge über die Befehlszeile auszuführen. Beispiele hierfür sind die Erstellung von Applications, der Kauf von Numbers und die Verknüpfung einer Number mit einer Application, was wir heute tun werden.

Um die CLI mit NPM zu installieren, führen Sie aus:

npm install -g @vonage/cli

Richten Sie die Vonage CLI so ein, dass sie Ihren Vonage API-Schlüssel und Ihr API-Geheimnis verwendet. Sie erhalten diese Daten auf der Seite Seite Einstellungen im Dashboard.

Führen Sie den folgenden Befehl in einem Terminal aus und ersetzen Sie dabei API_KEY und API_SECRET durch Ihre eigenen:

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

Eine Vonage-Nummer kaufen

Als Nächstes benötigen wir eine Vonage-Nummer, die die Anwendung verwenden kann. Dies ist die Telefonnummer, die auf dem Telefon angezeigt wird, das wir von der Anwendung aus anrufen.

Sie können eine Nummer über die Vonage CLI kaufen. Mit dem folgenden Befehl kaufen Sie eine verfügbare Nummer in den USA. Geben Sie an. einen alternativen zweistelligen Ländercode um eine Nummer in einem anderen Land zu erwerben.

vonage numbers:search US
vonage numbers:buy 15555555555 US

Einen Webhook-Server erstellen

Wenn ein eingehender Anruf eingeht, stellt Vonage eine Anfrage an eine öffentlich zugängliche URL Ihrer Wahl - wir nennen dies die answer_url. Sie müssen einen Webhook-Server erstellen, der in der Lage ist, diese Anfrage zu empfangen und eine NCCO mit einer connect Aktion enthält, die den Anruf an die PSTN-Telefonnummer. Sie tun dies, indem Sie die Zielnummer aus dem to Abfrageparameter extrahieren und in Ihrer Antwort zurückgeben.

Erstellen Sie in der Befehlszeile einen neuen Ordner, der Ihren Webserver enthält

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

Innerhalb des Ordners initialisieren Sie ein neues Node.js-Projekt, indem Sie diesen Befehl ausführen:

npm init -y

Als nächstes installieren Sie die erforderlichen Abhängigkeiten:

npm install express localtunnel --save

Erstellen Sie in Ihrem Projektordner eine Datei mit dem Namen server.js und fügen Sie den unten gezeigten Code ein - bitte ersetzen Sie NUMBER durch Ihre Vonage-Nummer (in E.164 Format), sowie SUBDOMAIN durch einen aktuellen Wert. Der verwendete Wert wird Teil der URLs, die Sie im nächsten Schritt als Webhooks einrichten werden.

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

Sie können nun den Server starten, indem Sie im Terminal den folgenden Befehl ausführen:

node server.js

Es wird ein Hinweis angezeigt, dass der Server jetzt verfügbar ist:

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

Erstellen einer Vonage-Anwendung

In diesem Schritt erstellen Sie eine Vonage Applikation die für die In-App Voice Kommunikation geeignet ist.

Öffnen Sie ein neues Terminal und navigieren Sie, falls erforderlich, zu Ihrem Projektverzeichnis.

Erstellen Sie eine Vonage-Anwendung, indem Sie den unten stehenden Befehl kopieren und in das Terminal einfügen. Vergewissern Sie sich, dass Sie die Werte von --voice_answer_url und --voice_event_url Argumente zu ändern, indem Sie SUBDOMAIN durch den im vorherigen Schritt verwendeten Wert ersetzen:

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

Eine Datei namens vonage_app.json wird in Ihrem Projektverzeichnis erstellt/aktualisiert und enthält die neu erstellte Vonage Application ID und den privaten Schlüssel. Eine private Schlüsseldatei mit dem Namen app_to_phone_tutorial.key wird ebenfalls erstellt.

Notieren Sie sich die Anwendungs-ID, die bei der Erstellung Ihrer Anwendung in Ihrem Terminal angezeigt wird:

screenshot of the terminal with Application ID underlined

Verknüpfen einer Vonage-Nummer

Sobald Sie eine passende Nummer haben, können Sie diese mit Ihrer Vonage-Anwendung verknüpfen. Ersetzen Sie YOUR_VONAGE_NUMBER durch Ihre neu erworbene Nummer, ersetzen Sie APPLICATION_ID durch Ihre Anwendungs-ID und führen Sie diesen Befehl aus:

vonage apps:link APPLICATION_ID --number=YOUR_VONAGE_NUMBER

Einen Benutzer erstellen

Benutzer sind ein Schlüsselkonzept bei der Arbeit mit den Vonage Client SDKs. Wenn sich ein Benutzer mit dem Client SDK authentifiziert, identifizieren die angegebenen Anmeldeinformationen ihn als einen bestimmten Benutzer. Jeder authentifizierte Benutzer entspricht normalerweise einem einzelnen Benutzer in Ihrer Benutzerdatenbank.

Zum Erstellen eines Benutzers namens Alicezu erstellen, führen Sie den folgenden Befehl über die Vonage CLI aus:

vonage apps:users:create "Alice"

Das Ergebnis ist eine Benutzer-ID ähnlich der folgenden:

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

Erzeugen eines JWT

Das Client SDK verwendet JWTs für die Authentifizierung. Das JWT identifiziert den Benutzernamen, die zugehörige Anwendungs-ID und die dem Benutzer gewährten Berechtigungen. Es wird mit Ihrem privaten Schlüssel signiert, um zu beweisen, dass es ein gültiges Token ist.

Führen Sie die folgenden Befehle aus, wobei Sie die Variable APPLICATION_ID durch die ID Ihrer Anwendung und PRIVATE_KEY durch den Namen der Datei Ihres privaten Schlüssels.

Sie generieren ein JWT mit der Vonage CLI, indem Sie den folgenden Befehl ausführen, aber denken Sie daran, die Variable APP_ID Variable durch Ihren eigenen Wert zu ersetzen:

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

Die obigen Befehle setzen den Ablauf des JWT auf einen Tag ab jetzt, was das Maximum ist.

terminal screenshot of a generated sample JWT

Wir haben jetzt alles, was wir brauchen, um die Vonage Voice API in einer Flutter-Anwendung zu nutzen. Lassen Sie uns nun die Anwendung selbst einrichten.

Flattereinrichtung

Wenn Sie es noch nicht getan haben, laden Sie Flutter und seine Abhängigkeiten herunter und installieren Sie es. Sie können dies tun, indem Sie die Installationsanleitung. Sobald Sie Flutter korrekt eingerichtet haben, müssen Sie als nächstes Ihre IDE konfigurieren. Wie Sie dies tun, hängt von der IDE ab, die Sie verwenden möchten. Einen Editor einrichten Anleitung wird Ihnen dabei helfen.

Für diese Anleitung werden wir Android Studio verwenden.

Sobald Ihre IDE eingerichtet ist, folgen Sie dem Testfahrt Anleitung, um eine grundlegende Flutter-Anwendung mit Unterstützung für Android und iOS einzurichten. Wir werden diese Basisanwendung als Ausgangspunkt für dieses Projekt verwenden, aber wenn Sie bereits ein Flutter-Projekt haben, das Sie verwenden möchten, können Sie dies natürlich auch tun.

Installieren von SDKs

Nachdem das Projekt nun eingerichtet ist, können wir das Vonage Client SDK installieren. Derzeit ist das Client SDK nicht als Flutter-Paket verfügbar, daher müssen wir das Android natives Client SDK und das iOS natives Client SDK Die Kommunikation zwischen Android/iOS und Flutter erfolgt über Methoden-Kanal - auf diese Weise ruft Flutter Android/iOS-Methoden auf, Android/iOS ruft Flutter-Methoden auf.

Android-SDK

Um das Android SDK zu installieren, erhöhen Sie zunächst die Speicherzuweisung für die JVM, indem Sie die org.gradle.jvmargs Eigenschaft in Ihrer gradle.properties Datei. Wir empfehlen, dies auf mindestens 4 GB zu setzen:

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

Als nächstes öffnen Sie Ihre App-Level build.gradle Datei, die Sie unter android/app/build.gradle zu finden ist, und implementieren Sie das Vonage SDK wie folgt:

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

Stellen Sie schließlich sicher, dass Ihr minSdkVersion mindestens auf folgende Werte gesetzt ist 23:

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

Das Android-SDK ist nun eingerichtet und kann für die Android-Version der Flutter-Anwendung verwendet werden.

iOS-SDK

Um das iOS SDK zu installieren, beginnen Sie mit der Generierung des PodFile indem Sie eine Befehlszeile im Stammverzeichnis Ihres Flutter-Projekts öffnen und dann die folgenden Befehle ausführen:

cd ios/
pod init

Dadurch wird die Datei PodFileöffnen Sie diese Datei und fügen Sie den unten stehenden Pod hinzu:

pod 'VonageClientSDKVoice', '~> 1.0.3'

Stellen Sie sicher, dass Sie auch die Plattform auf mindestens iOS 10 einstellen.

platform :ios, '10.0'

Ihre vollständige Datei sollte in etwa so aussehen:

platform :ios, '10.0'

target 'Runner' do
  use_frameworks!

  pod 'VonageClientSDKVoice', '~> 1.0.3'
end

Als Nächstes führen Sie über die Befehlszeile, wiederum im iOS-Verzeichnis, aus:

pod update

Dadurch werden das Vonage SDK und seine Abhängigkeiten heruntergeladen und installiert.

Um dies schließlich mit Ihrem Flutter-Projekt zu verknüpfen, führen Sie den folgenden Flutter-Befehl im Hauptverzeichnis Ihres Projekts aus. Dadurch wird ein iOS-Build ausgelöst und die für die Nutzung des SDK erforderlichen Dateien werden generiert.

flutter build ios

Nach Fertigstellung und erfolgreicher Erstellung ist Ihr SDK eingerichtet und kann verwendet werden.

Code

Es liegt in der Natur von Flutter, dass der Code leicht in drei Bereiche aufgeteilt werden kann: den Flutter-Code, der in Dart geschrieben ist, den nativen Android-Code, der in Kotlin geschrieben ist, und den nativen iOS-Code, der in Swift geschrieben ist.

Flattert

Beginnen wir mit dem flutter-spezifischen Code, ersetzen wir den Inhalt von lib/main.dart durch den untenstehenden Code:

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
}

Dies ist die komplette Klasse, die benötigt wird, um die Benutzeroberfläche der App zu erstellen und die plattformspezifischen Methoden auszulösen, die wir in einem Moment schreiben werden. Schauen wir uns an, was in den einzelnen Methoden dieser Klasse vor sich geht.

Beginnend mit den Importen am Anfang der Klasse haben wir die normalen Flutter-Importe, aber wir verwenden auch den Erlaubnis-Handler Paket. Dieses wird verwendet, um das Anfordern von Berechtigungen auf iOS und Android für uns zu verwalten. Stellen Sie sicher, dass Sie es installiert haben, indem Sie den Befehl ausführen:

flutter pub add permission_handler

An der Wurzel Ihres Flutter-Projekts.

Als nächstes erstellen wir die App. Für diese Demo haben wir eine sehr einfache App mit nur einem Widget-Element, das wir 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'),
    );
  }
}

Diese CallWidget erweitert die StatefulWidget um den Titel und die Initialisierung der 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();
}

Die CallWidgetState verwaltet die UI-Elemente, den aktuellen Zustand der App und die gesamte Kommunikation zurück zum Code der nativen Plattform.

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

Hier setzen wir den Startzustand der App als SdkState.LOGGED_OUTund erstellen die MethodChannel die die gesamte Kommunikation zwischen Flutter und nativem Code abwickeln wird. Dann setzen wir die methodCallHandler in dem der Zustand auf den Zustand gesetzt wird, der vom nativen Code an Flutter zurückgegeben wurde.

Die Benutzeroberfläche wird dann mit der build Methode erstellt, die einfach eine Box mit der Höhe 64 erstellt. Wir werden dieses Element je nach Zustand der App aktualisieren, um verschiedene Informationen anzuzeigen.

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

Als nächstes wird die _updateView Methode verwendet, um zu ändern, was derzeit in der Box angezeigt wird, basierend auf dem aktuellen Zustand der App. Dieses Zustandsmodell ermöglicht eine saubere Benutzeroberfläche, die dem Benutzer nur das anzeigt, was er zu einem bestimmten Zeitpunkt im Lebenszyklus der App sehen muss.

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

Die Methoden _loginUser und _endCall sind insofern sehr ähnlich, als wir hier nur die Methoden loginUser/endCall im nativen Code aufrufen. Auf diese Weise lösen wir den nativen Code aus, wenn der Benutzer eine Schaltfläche auf der Benutzeroberfläche drückt. Innerhalb der _loginUser haben wir eine Variable token Dies sollte der JWT-Wert sein, den Sie zuvor mit der Vonage CLI generiert haben

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

Die Methode _makeCall Methode beinhaltete auch eine Methode des nativen Codes, die die makeCall Methode. Bevor dies jedoch geschieht, verwenden wir die requestPermissions Methode, um die erforderlichen Laufzeitberechtigungen vom Benutzer anzufordern. In diesem Fall ist das nur die Mikrofon-/Audioaufnahme.

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

Und schließlich haben wir eine Aufzählung, die die verschiedenen Zustände enthält, die das SDK und die App annehmen können.

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

Android

Werfen wir nun einen Blick auf den Android-spezifischen Code für diese Anwendung. Zunächst müssen wir die Berechtigungen einrichten, die die Anwendung vom Android-System benötigt. In Ihrem AndroidManifest.xml die sich unter android/app/src/main/AndroidManifest.xml befinden, fügen Sie die folgenden Berechtigungen hinzu:

    <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" />

Als nächstes öffnen wir die Datei MainActivity.kt Datei, die unter folgender Adresse zu finden ist android/app/src/main/kotlin/PACKAGE_NAME/MainActivity.kt

Der vollständige Inhalt dieser Datei lautet wie folgt:

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
}

Schauen wir uns an, was hier vor sich geht.

Das erste, was Sie bemerken werden, ist, dass wir die Klasse FlutterActivity Dies ist eine von Flutter bereitgestellte Activity-Klasse, die einen Großteil der zusätzlichen Lebenszyklus- und Flutter-Magie übernimmt, die es ermöglicht, nativen Code auszuführen.

Als Nächstes haben wir zwei Variablen, die wir verwenden werden:

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

Die VoiceClient ist das Objekt, das für alle SDK-Interaktionen zuständig ist, also für das Tätigen eines Anrufs, das Auflegen usw. Die onGoingCallID wird verwendet, um den aktuellen Telefonanruf zu verfolgen, während ein solcher stattfindet.

Als nächstes überschreiben wir die configureFlutterEngine Methode. Damit können wir Code ausführen, wenn die App von der Flutter-Engine erstellt wird. Hier verwenden wir dies, um zwei Methoden auszuführen, eine, um einen Channel Listener hinzuzufügen und eine weitere, um die NexmoClient.

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

        initClient()
        addFlutterChannelListener()
    }

Die Initialisierung der VoiceClient ist einfach, wir übergeben einfach den aktuellen Kontext der Anwendung. Dann erstellen wir einen ErrorListener und einen 'ReconnectingListener', der uns den aktuellen Status des Clients liefert. Dieser Status entspricht den Werten, die wir an Flutter zurücksenden müssen. Mit einer when-Anweisung können wir also die Werte nach Bedarf senden.

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

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

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

Die addFlutterChannelListener fügt einen Listener hinzu, der auf alle Methodenaufrufe von Flutter achtet. Wie Sie sehen können, beziehen sich diese auf die drei Methoden, die wir in Flutter haben. Dies ermöglicht es uns, diese Aufrufe auf bestimmte Methoden innerhalb des nativen Codes zuzuordnen.

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

Die loginUser Methode wird aufgerufen, wenn Flutter den loginUser-Methodenaufruf sendet. Dieser übergibt das von uns festgelegte JWT-Token und löst dann die Login-Methode auf dem Client aus.

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

Die Methode makeCall Methode wird aufgerufen, wenn Flutter den makeCall-Methodenaufruf sendet, der einen Telefonanruf an die angegebene Telefonnummer startet "PHONE_NUMBER" Sie sollten diese durch eine tatsächliche Telefonnummer ersetzen, die Sie anrufen möchten. Auch hier wird der Status an Flutter zurückgegeben, je nachdem ob der Anruf erfolgreich ist und startet oder ob ein Fehler auftritt.

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

Die Methode endCall Methode wird aufgerufen, wenn Flutter den endCall Methodenaufruf sendet, wird das aktuelle Telefongespräch beendet (falls es eines gibt).

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

Schließlich haben wir die nofityFlutter Methode. Hier nutzen wir die Magie von Flutter, um den aktuellen Zustand der Anwendung zurückzusenden, damit Flutter die Benutzeroberfläche aktualisieren kann. Mit dieser Methode können wir die Flutter updateState Methode einzubinden und den aktuellen Zustand als Variable zu übergeben.

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

Und das ist alles, was wir an nativem Code brauchen! Jetzt haben wir eine funktionierende Flutter-Anwendung, die wir für Android erstellen können und mit der wir einen Anruf von der App auf ein physisches Telefon tätigen können. Aber bevor wir die App testen, schauen wir uns an, wie wir das Gleiche für iOS machen können.

iOS

Zuerst müssen wir die Audio-Berechtigungen innerhalb von iOS einrichten. Wir haben bereits das Paket in Flutter eingerichtet, um sie anzufordern, also müssen wir nur noch die ios/Runner/info.plist Datei und fügen Sie Privacy - Microphone Usage Description Schlüssel mit dem Wert von "Make a call"

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

Als nächstes öffnen Sie die Datei ios/Runner/AppDelegate hier werden wir den Code für die Schnittstelle zwischen Flutter und dem SDK einfügen, so wie wir es bereits für Android getan haben. Der vollständige Code sieht wie folgt aus:

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

Dies ist der gesamte Code, den Sie benötigen, um auch für iOS zu erstellen. Jetzt, wo wir den Code haben, können wir die App erstellen und testen!

Erstellen und Testen

Da nun alles vorhanden ist, können wir die Anwendung erstellen und ausführen. Wir werden die Android-Version erstellen und sie in der Android-Emulation ausführen.

HINWEIS Stellen Sie sicher, dass Sie das JWT im Flutter-Code und die PHONE_NUMBER im nativen Code gesetzt haben. Stellen Sie außerdem sicher, dass Ihr Webserver noch läuft.

Starten Sie den Android-Emulator, damit sich Flutter mit ihm verbinden kann. Im Folgenden sehen Sie, wie Sie dies in Android Studio tun können

Android studio with device manager selected

Sobald dieser ausgeführt wird, können Sie dieses Gerät als Ziel für den Flutter-Build auswählen und den grünen Pfeil drücken, um die Flutter-App (mit nativem Android-Code) zu erstellen und auszuführen.

android studio with emulator selected and main.dart

Sobald die Anwendung erstellt und installiert ist, wird Ihnen der unten links abgebildete Bildschirm angezeigt. Wenn Sie auf die Schaltfläche Anmelden als Alice klicken, gelangen Sie zum nächsten Bildschirm. Von hier aus können Sie auf die Schaltfläche Anruf tätigen klicken, woraufhin Sie (beim ersten Durchlauf) aufgefordert werden, die Audiorechte zuzulassen. Daraufhin wird der Anruf gestartet und die von Ihnen eingegebene Telefonnummer wird angerufen, um die Audiositzung zu verbinden.

Wenn Sie das Gespräch beenden möchten, können Sie dies durch Drücken der Taste Anruf beenden tun.

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

Und das ist ein Wrap! Sie haben jetzt Ihre voll funktionsfähige App zum Telefonieren in Flutter geschrieben mit Unterstützung für Android und iOS. Aber das ist natürlich nicht das Ende! Mit Ihrem Wissen über die Verwendung von Android und iOS SDKs werfen Sie einen Blick auf die anderen Beispielprojekte die Ihnen helfen werden, andere Kommunikationsfunktionen in Ihre Flutter-Anwendung einzubauen. Wenn Sie mehr Details erfahren möchten, schauen Sie sich bitte das Entwickler-Portal wo Sie die gesamte Dokumentation und den Beispielcode finden, den Sie jemals brauchen könnten!

Teilen Sie:

https://a.storyblok.com/f/270183/400x400/04765919bb/zachary-powell-1.png
Zachary PowellSenior Android Entwickler Advocate