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

Primeros pasos con Flutter 3 y las API de Vonage

Publicado el July 30, 2023

Tiempo de lectura: 15 minutos

Con el lanzamiento de Flutter 3.0 (que incluye una serie de mejoras de estabilidad y rendimiento) ahora es un buen momento para echar un vistazo a cómo puedes utilizar las API de comunicación para mejorar tu experiencia de usuario y potenciar tus aplicaciones multiplataforma.

Gracias a la capacidad de Flutter de usar SDK de plataformas nativas, podemos usar sin problemas los SDK de Vonage para Android e iOS dentro de nuestras aplicaciones Flutter. Echemos un vistazo a cómo podemos crear una sencilla aplicación Flutter que sea capaz de realizar una llamada telefónica de voz a un teléfono físico. Al final de esta guía, comprenderás bien cómo usar el SDK de Vonage para realizar una llamada de voz y cómo puedes usar los SDK nativos de Android e iOS en tu aplicación Flutter.

En esta guía, crearemos una aplicación básica desde cero, pero también puedes incorporar lo siguiente a tu aplicación.

El código fuente completo de este proyecto puede consultarse en GitHub.

Configuración de Vonage

Antes de entrar en el código, hay algunas cosas que debemos hacer para configurar la API de Vonage y hacer uso de ella.

Registro de la Account

Comienza por inscribirte para obtener una cuenta gratuita de Vonage Developer. Puedes hacerlo a través del PanelUna vez que te hayas registrado, encontrarás la clave de API y el secreto de API de tu Account. Anótalos para pasos futuros.

Vonage dashboard home page showing API key and API secret location

Instalar la CLI de Vonage

El CLI de Vonage te permite realizar muchas operaciones en la línea de comandos. Algunos ejemplos incluyen la creación de aplicaciones, la compra de números y la vinculación de un número a una aplicación, todo lo cual haremos hoy.

Para instalar el CLI con NPM ejecutar:

npm install -g @vonage/cli

Configura la CLI de Vonage para usar tu clave y secreto de API de Vonage. Puedes obtenerlos en la página de configuración en el panel.

Ejecute el siguiente comando en un terminal, sustituyendo API_KEY y API_SECRET por los suyos:

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

Comprar un número de Vonage

A continuación, necesitamos un número de Vonage que la aplicación pueda utilizar, este es el número de teléfono que aparecerá en el teléfono al que llamemos desde la aplicación.

Puedes comprar un número usando la CLI de Vonage. El siguiente comando compra un número disponible en los EE. UU. Especifica un código de país alternativo de dos caracteres para comprar un número en otro país.

vonage numbers:search US
vonage numbers:buy 15555555555 US

Crear un servidor Webhook

Cuando se recibe una llamada entrante, Vonage realiza una solicitud a una URL de acceso público de tu elección, que denominamos answer_url. Debes crear un servidor de webhook que sea capaz de recibir esta solicitud y devolver un archivo NCCO que contenga una connect acción que reenviará la llamada al número de teléfono PSTN. Para ello, extraiga el número de destino del parámetro de consulta to y devolviéndolo en la respuesta.

En la línea de comandos crea una nueva carpeta que contendrá tu servidor web

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

Dentro de la carpeta, inicializa un nuevo proyecto Node.js ejecutando este comando:

npm init -y

A continuación, instale las dependencias necesarias:

npm install express localtunnel --save

Dentro de la carpeta del proyecto, cree un archivo llamado server.js y agrega el código como se muestra a continuación; asegúrate de reemplazar NUMBER por tu número de Vonage (en E.164 ), así como SUBDOMAIN con un valor real. El valor utilizado formará parte de las URL que establecerás como webhooks en el siguiente paso.

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

Ahora puede iniciar el servidor ejecutando, en el terminal, el siguiente comando:

node server.js

Aparecerá un aviso indicándole que el servidor ya está disponible:

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

Crear una aplicación de Vonage

En este paso, crearás una aplicación de Vonage Vonage con capacidad para casos de uso de comunicación de voz dentro de la aplicación.

Abra un nuevo terminal y, si es necesario, navegue hasta el directorio de su proyecto.

Crea una aplicación de Vonage copiando y pegando el siguiente comando en el terminal. Asegúrate de cambiar los valores de --voice_answer_url y --voice_event_url sustituyendo SUBDOMAIN por el valor real utilizado en el paso anterior:

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

Se crea/actualiza un archivo llamado vonage_app.json en el directorio de tu proyecto y contiene el nuevo ID de aplicación de Vonage y la clave privada. También se crea un archivo de clave privada llamado app_to_phone_tutorial.key también se crea.

Anote el ID de aplicación que aparece en su terminal cuando se crea su aplicación:

screenshot of the terminal with Application ID underlined

Vincular un número de Vonage

Una vez que tengas un número adecuado, podrás vincularlo con tu aplicación de Vonage. Reemplaza YOUR_VONAGE_NUMBER por tu número recién comprado, reemplaza APPLICATION_ID con el ID de tu aplicación y ejecuta este comando:

vonage apps:link APPLICATION_ID --number=YOUR_VONAGE_NUMBER

Crear un usuario

Los usuarios son un concepto clave cuando se trabaja con los SDK para clientes de Vonage. Cuando un usuario se autentica con el Client SDK, las credenciales provistas lo identifican como un usuario específico. Cada usuario autenticado normalmente corresponderá a un solo usuario en tu base de datos de usuarios.

Para crear un usuario llamado Aliceejecuta el siguiente comando desde la CLI de Vonage:

vonage apps:users:create "Alice"

Esto devolverá un ID de usuario similar al siguiente:

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

Generar un JWT

El Client SDK utiliza JWT para la autenticación. El JWT identifica el nombre de usuario, el ID de la aplicación asociada y los permisos concedidos al usuario. Se firma utilizando su clave privada para demostrar que se trata de un token válido.

Ejecute los siguientes comandos, recuerde sustituir la variable APPLICATION_ID por el ID de su aplicación y la variable PRIVATE_KEY por el nombre de tu archivo de clave privada.

Está generando un JWT utilizando la CLI de Vonage ejecutando el siguiente comando, pero recuerde sustituir la variable APP_ID por tu propio valor:

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

Los comandos anteriores establecen la caducidad del JWT en un día a partir de ahora, que es el máximo.

terminal screenshot of a generated sample JWT

Ahora tenemos todo lo que necesitamos para usar la Voice API de Vonage dentro de una aplicación Flutter. Ahora vamos a configurar la aplicación.

Configuración de Flutter

Si aún no lo has hecho, empieza por descargar e instalar Flutter y sus dependencias. Puede hacerlo siguiendo la Guía de instalación. Una vez que tengas Flutter configurado correctamente lo siguiente que tendrás que hacer es configurar tu IDE, la forma de hacerlo dependerá del IDE que desees utilizar pero el Configurar un editor le ayudará con esto.

Para esta guía, vamos a utilizar Android Studio.

Una vez que su IDE esté configurado, siga las instrucciones prueba para configurar una aplicación básica de Flutter compatible con Android e iOS. Usaremos esta app base como inicio de este proyecto, pero por supuesto, si ya tienes un proyecto Flutter que quieras usar también puedes hacerlo.

Instalación de SDK

Con el proyecto ya configurado, podemos instalar el Client SDK de Vonage. Actualmente, el Client SDK no está disponible como un paquete de Flutter, por lo que tendremos que utilizar el Client SDK nativo de Android y el Client SDK nativo de iOS La comunicación entre Android/iOS y Flutter utilizará MethodChannel - de esta forma, Flutter llamará a métodos de Android/iOS, Android/iOS llamará a métodos de Flutter.

SDK para Android

Para instalar el SDK de Android comience por aumentar la asignación de memoria para la JVM editando la propiedad org.gradle.jvmargs en su archivo gradle.properties archivo. Recomendamos que sea de al menos 4 GB:

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

A continuación, abra el archivo build.gradle que se encuentra en android/app/build.gradle e implementa el SDK de Vonage de la siguiente manera:

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

Por último, asegúrese de que su minSdkVersion esté configurado como mínimo en 23:

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

El SDK de Android está ahora configurado y listo para ser utilizado para la construcción de Android de la aplicación flutter.

SDK para iOS

Para instalar el SDK de iOS, comience por generar el archivo PodFile abriendo una línea de comandos en la raíz de su proyecto Flutter y ejecutando los comandos siguientes:

cd ios/
pod init

Esto generará el archivo PodFileabra este archivo y añada el siguiente pod:

pod 'VonageClientSDKVoice', '~> 1.0.3'

Asegúrese también de establecer la plataforma de al menos ios 10

platform :ios, '10.0'

El archivo completo debería tener este aspecto:

platform :ios, '10.0'

target 'Runner' do
  use_frameworks!

  pod 'VonageClientSDKVoice', '~> 1.0.3'
end

A continuación, desde la línea de comandos, de nuevo en el directorio de iOS ejecutar:

pod update

Esto descargará e instalará el SDK de Vonage y sus dependencias.

Por último, para vincular esto a su proyecto Flutter, desde el directorio raíz de su proyecto ejecute el siguiente comando Flutter. Esto activará una compilación de iOS y generará los archivos necesarios para hacer uso del SDK.

flutter build ios

Una vez completado y construido con éxito su SDK está configurado y listo para ser utilizado.

Código

Por la naturaleza de Flutter, el código se puede dividir fácilmente en tres áreas, el código de Flutter que está escrito en Dart, el código nativo de Android que está escrito en Kotlin y el código nativo de iOS que está escrito en Swift.

Aleteo

Empecemos con el código específico de flutter, sustituye el contenido de lib/main.dart por el siguiente código:

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
}

Esta es la clase completa necesaria para construir la interfaz de usuario de la aplicación y activar los métodos específicos de la plataforma que escribiremos en un momento. Vamos a desglosar lo que sucede en cada uno de los métodos de esta clase.

Comenzando con las importaciones en la parte superior de esta clase, tenemos las importaciones normales de flutter pero también estamos utilizando el controlador de permisos paquete. Esto se utiliza para gestionar la solicitud de permisos en iOS y Android para nosotros. Asegúrese de que ha instalado este ejecutando el comando:

flutter pub add permission_handler

En la raíz de su proyecto de aleteo.

A continuación, construimos la aplicación, para esta demostración tenemos una aplicación muy simple con un solo elemento widget que hemos llamado 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'),
    );
  }
}

Este CallWidget amplía la StatefulWidget tomando el título e inicializando el 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();
}

El sitio CallWidgetState gestionará los elementos de la interfaz de usuario, el estado actual de la aplicación y toda la comunicación con el código de la plataforma nativa.

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

Aquí establecemos el estado inicial de la aplicación como SdkState.LOGGED_OUTcreamos el MethodChannel que se encargará de toda la comunicación entre Flutter y el código nativo. A continuación establecemos el estado methodCallHandler en el que el estado se establece a cualquier estado que se ha pasado de nuevo a Flutter desde el código nativo.

A continuación, la interfaz de usuario se construye utilizando el método build que simplemente crea un Box de altura 64. Actualizaremos este elemento dependiendo del estado de la app para mostrar información diferente.

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

A continuación, el método _updateView se utiliza para cambiar lo que se muestra en el cuadro en función del estado actual de la aplicación. Este modelo de estado permite una interfaz de usuario limpia que solo muestra al usuario lo que necesita ver en cada momento del ciclo de vida de la aplicación.

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

Los métodos _loginUser y _endCall son muy similares en el sentido de que todo lo que estamos haciendo aquí es invocar los métodos loginUser/endCall en el código nativo. Así es como activamos el código nativo cuando el usuario pulsa un botón en la interfaz de usuario. Dentro de _loginUser tenemos una variable token este debería ser el valor JWT que generaste anteriormente usando la 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);
      }
    }
  }

El método _makeCall método también implicaba un método en el código nativo, que llamaba al método makeCall método Sin embargo, antes de hacerlo utilizamos el método requestPermissions para solicitar al usuario los permisos necesarios en tiempo de ejecución. En este caso que es sólo el micrófono / grabación de 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();
  }

Y finalmente, tenemos un enum que contiene los diferentes estados en los que pueden estar el SDK y la app.

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

Android

A continuación, vamos a echar un vistazo al código específico de Android para esta aplicación. En primer lugar, tenemos que configurar los permisos que la aplicación necesitará del sistema Android. En su AndroidManifest.xml que se encuentra en android/app/src/main/AndroidManifest.xml añade los siguientes permisos:

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

A continuación vamos a abrir el archivo MainActivity.kt que se encuentra en android/app/src/main/kotlin/PACKAGE_NAME/MainActivity.kt

El contenido completo de este fichero es el siguiente:

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
}

Desglosemos esto y echemos un vistazo a lo que está pasando.

Lo primero que notarás es que estamos extendiendo la clase FlutterActivity esta es una clase de actividad proporcionada por Flutter que maneja gran parte del ciclo de vida adicional y la magia de Flutter que hace posible ejecutar código nativo.

A continuación tenemos dos variables que vamos a utilizar:

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

El VoiceClient es el objeto responsable de todas las interacciones del SDK, realizar una llamada telefónica, colgar, etc. El onGoingCallID se utilizará para realizar un seguimiento de la llamada telefónica actual mientras se está produciendo una.

A continuación anulamos el método configureFlutterEngine que nos permite ejecutar código cuando el motor de Flutter está creando la aplicación. Aquí usamos esto para ejecutar dos métodos, uno para añadir un receptor de canal y otro para configurar el canal NexmoClient.

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

        initClient()
        addFlutterChannelListener()
    }

Inicializar el VoiceClient es sencilla, simplemente pasamos el contexto actual de la aplicación. Luego creamos un ErrorListener y un 'ReconnectingListener' que nos dará el estado actual del cliente, estos mapas de estado a los valores que necesitamos enviar de vuelta a Flutter. Así que usando una sentencia when podemos enviar los valores que necesitemos.

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

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

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

El addFlutterChannelListener añade un listener que estará atento a cualquier llamada a métodos desde Flutter. Como se puede ver estos se refieren a los tres métodos que tenemos en Flutter, esto nos permite asignar estas llamadas a métodos específicos dentro del código nativo.

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

El método loginUser es llamado cuando Flutter envía la llamada al método loginUser, este pasa el token JWT que establecimos y luego activa el método login en el cliente.

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

El método makeCall es llamado cuando Flutter envía la llamada del método makeCall, esto inicia una llamada telefónica al número de teléfono especificado "PHONE_NUMBER" usted debe reemplazar esto con un número de teléfono real que desea llamar. De nuevo, aquí pasamos de vuelta el estado a Flutter dependiendo de si la llamada tiene éxito y se inicia o si hay algún tipo de error.

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

El método endCall es llamado cuando Flutter envía la llamada al método endCall llamada al método, esto termina la llamada actual (si hay una).

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

Por último, tenemos el método nofityFlutter en el que utilizamos la magia de Flutter para devolver el estado actual de la aplicación y que Flutter pueda actualizar la interfaz de usuario. Usando esto somos capaces de involucrar al método Flutter updateState y pasar el estado actual como una variable.

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

¡Y ese es todo el código nativo que necesitamos! En este punto, tenemos una aplicación Flutter funcional que podríamos construir para Android y ser capaces de hacer una llamada telefónica desde la aplicación a un teléfono físico. Pero antes de probar la aplicación vamos a echar un vistazo a cómo podemos hacer lo mismo para iOS.

iOS

En primer lugar, tenemos que configurar los permisos de audio dentro de iOS, ya tenemos el paquete en la configuración de Flutter para solicitarlos por lo que todo lo que tenemos que hacer es abrir el archivo ios/Runner/info.plist y añadir Privacy - Microphone Usage Description con el valor de "Make a call"

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

A continuación, abra el archivo ios/Runner/AppDelegate aquí es donde incluiremos el código para interactuar entre flutter y el SDK de la misma manera que ya hemos hecho para Android. El código completo tiene este aspecto:

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

Este es todo el código que necesitarás para poder construir también para iOS, ahora que tenemos todo el código en su lugar ¡construyamos la aplicación y probémosla!

Construir y probar

Con todo ahora en su lugar podemos construir y ejecutar la aplicación, vamos a construir la versión de Android y ejecutarlo en la emulación de Android.

NOTA asegúrese de que ha establecido el JWT en el código de Flutter y el PHONE_NUMBER en el código nativo. Además, asegúrese de que su servidor web sigue funcionando.

Inicie el emulador de Android para que flutter pueda conectarse a él, a continuación se muestra donde se puede hacer esto en Android studio

Android studio with device manager selected

Una vez que esto se está ejecutando puede seleccionar este dispositivo como el objetivo para la construcción Flutter y pulse la flecha verde para construir y ejecutar la aplicación Flutter (con código nativo de Android).

android studio with emulator selected and main.dart

Una vez que la aplicación se haya compilado e instalado, aparecerá la pantalla de abajo a la izquierda. Pulsando el botón Iniciar sesión como Alice accederás a la siguiente pantalla. Desde aquí puedes pulsar el botón Hacer llamada telefónica que (en la primera ejecución) te pedirá que permitas los permisos de audio. Después de esto se iniciará la llamada telefónica y se llamará al número de teléfono introducido conectando la sesión de audio.

Cuando desee finalizar la llamada, puede hacerlo pulsando el botón de finalizar llamada.

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

¡Y eso es todo! Ya tienes tu app para llamar por teléfono completamente funcional escrita en Flutter con soporte tanto para Android como para iOS. Pero, por supuesto, ¡esto no es el final! Con su conocimiento de cómo utilizar Android y iOS SDK echa un vistazo a los otros proyectos de ejemplo que te ayudarán a construir otras características de comunicación en tu aplicación Flutter. Si quieres más detalles, asegúrate de consultar el portal para desarrolladores que contiene toda la documentación y el código de ejemplo que puedas necesitar.

Compartir:

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