https://a.storyblok.com/f/270183/35313/a30e2b59c0/flutter-3.png

Introdução ao Flutter 3 e às APIs da Vonage

Publicado em July 30, 2023

Tempo de leitura: 15 minutos

Com o lançamento do Flutter 3.0 (que inclui uma série de melhorias de estabilidade e desempenho), agora é um ótimo momento para ver como você pode usar APIs de comunicação para melhorar a experiência do usuário e aprimorar seus aplicativos multiplataforma.

Graças à capacidade do Flutter de utilizar SDKs nativos da plataforma, podemos usar os SDKs do Vonage para Android e iOS de forma integrada em nossas Applications Flutter. Vamos ver como podemos criar um aplicativo Flutter simples, capaz de fazer uma chamada de voz para um telefone físico. Ao final deste guia, você terá uma boa compreensão de como usar o SDK da Vonage para fazer uma chamada de voz e de como pode utilizar os SDKs nativos do Android e do iOS em seu aplicativo Flutter.

Neste guia, vamos criar um aplicativo básico do zero, mas você também pode integrar rapidamente o que está abaixo ao seu aplicativo.

O código-fonte completo deste projeto pode ser encontrado em GitHub.

Configuração do Vonage

Antes de entrarmos no código, há algumas coisas que precisamos fazer para configurar a API da Vonage e começar a utilizá-la.

Criação de conta

Comece criando um account gratuito de desenvolvedor da Vonage. Isso pode ser feito pelo Painel de Controle; após o cadastro, você encontrará a chave de API e o segredo de API da sua Account. Anote-os para as etapas seguintes.

Vonage dashboard home page showing API key and API secret location

Instale o CLI da Vonage

O CLI da Vonage permite que você execute diversas operações na linha de comando. Exemplos incluem a criação de Applications, a compra de Numbers e a vinculação de um número a uma Application — tudo isso faremos hoje.

Para instalar a CLI com o NPM, execute:

npm install -g @vonage/cli

Configure o CLI da Vonage para usar sua chave API e seu segredo API da Vonage. Você pode obtê-los na página de configurações no Painel de Controle.

Execute o seguinte comando em um terminal, substituindo API_KEY e API_SECRET pelo seu próprio:

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

Compre um número da Vonage

Em seguida, precisamos de um número da Vonage que o aplicativo possa usar; esse é o número de telefone que aparecerá no aparelho de onde ligarmos pelo aplicativo.

Você pode adquirir um número usando a CLI da Vonage. O comando a seguir permite adquirir um número disponível nos EUA. Especifique um código de país alternativo de dois caracteres para adquirir um número em outro país.

vonage numbers:search US
vonage numbers:buy 15555555555 US

Criar um servidor de webhooks

Quando uma chamada recebida é atendida, a Vonage envia uma solicitação para uma URL de acesso público de sua escolha — chamamos isso de answer_url. Você precisa criar um servidor de webhook capaz de receber essa solicitação e retornar um NCCO contendo uma connect ação que encaminhará a chamada para o número de telefone PSTN. Para isso, extraia o número de destino do to parâmetro de consulta e retornando-o em sua resposta.

Na linha de comando, crie uma nova pasta que irá conter seu servidor web

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

Dentro da pasta, inicialize um novo projeto Node.js executando este comando:

npm init -y

Em seguida, instale as dependências necessárias:

npm install express localtunnel --save

Dentro da pasta do seu projeto, crie um arquivo chamado server.js e adicione o código conforme mostrado abaixo — certifique-se de substituir NUMBER pelo seu número da Vonage (em formato E.164 ), bem como SUBDOMAIN por um valor real. O valor utilizado fará parte das URLs que você definirá como webhooks na próxima etapa.

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

Agora você pode iniciar o servidor executando, no terminal, o seguinte comando:

node server.js

Será exibida uma mensagem informando que o servidor já está disponível:

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

Criar uma aplicação da Vonage

Nesta etapa, você criará um Vonage Applications capaz de atender a casos de uso de comunicação de voz dentro do aplicativo.

Abra um novo terminal e, se necessário, acesse o diretório do seu projeto.

Crie uma aplicação Vonage copiando e colando o comando abaixo no terminal. Certifique-se de alterar os valores de --voice_answer_url e --voice_event_url argumentos, substituindo SUBDOMAIN pelo valor real usado na etapa 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

Um arquivo chamado vonage_app.json é criado/atualizado no diretório do seu projeto e contém o ID de aplicativo da Vonage recém-criado e a chave privada. Um arquivo de chave privada chamado app_to_phone_tutorial.key também é criado.

Anote o ID da aplicação que é exibido no seu terminal quando a aplicação for criada:

screenshot of the terminal with Application ID underlined

Vincular um número da Vonage

Assim que tiver um número adequado, você poderá vinculá-lo ao seu aplicativo da Vonage. Substitua YOUR_VONAGE_NUMBER pelo número que você acabou de adquirir, substitua APPLICATION_ID pelo seu ID do aplicativo e execute este comando:

vonage apps:link APPLICATION_ID --number=YOUR_VONAGE_NUMBER

Criar um usuário

Usuários são um conceito fundamental ao se trabalhar com os Vonage Client SDKs. Quando um usuário se autentica com o Vonage Client SDK, as credenciais fornecidas o identificam como um usuário específico. Cada usuário autenticado corresponderá, normalmente, a um único usuário no seu banco de dados de usuários.

Para criar um usuário chamado Alice, execute o seguinte comando usando a CLI da Vonage:

vonage apps:users:create "Alice"

Isso retornará um ID de usuário semelhante ao seguinte:

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

Gerar um JWT

O Client SDK utiliza JWTs para autenticação. O JWT identifica o nome de usuário, o ID do aplicativo associado e as permissões concedidas ao usuário. Ele é assinado com sua chave privada para comprovar que se trata de um token válido.

Execute os comandos a seguir, lembre-se de substituir a APPLICATION_ID variável pelo ID do seu aplicativo e PRIVATE_KEY pelo nome do seu arquivo de chave privada.

Você está gerando um JWT usando a CLI da Vonage ao executar o comando a seguir, mas lembre-se de substituir a APP_ID variável pelo seu próprio valor:

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

Os comandos acima definem a validade do JWT para um dia a partir de agora, que é o prazo máximo.

terminal screenshot of a generated sample JWT

Agora temos tudo o que precisamos para usar a Voice API da Vonage em um aplicativo Flutter. Vamos, então, configurar o próprio aplicativo.

Configuração do Flutter

Se ainda não o fez, comece baixando e instalando o Flutter e suas dependências. Você pode fazer isso seguindo o Guia de Instalação. Depois de configurar o Flutter corretamente, o próximo passo é configurar seu IDE; a maneira de fazer isso vai depender do IDE que você deseja usar, mas o guia de configuração do editor vai te ajudar nisso.

Neste guia, usaremos o Android Studio.

Depois de configurar seu IDE, siga o guia de teste para configurar um aplicativo básico do Flutter com suporte tanto para Android quanto para iOS. Usaremos esse aplicativo básico como ponto de partida para este projeto, mas, é claro, se você já tiver um projeto do Flutter que queira usar, também poderá fazê-lo.

Instalação de SDKs

Com o projeto já configurado, podemos instalar o Vonage Client SDK. Atualmente, o Client SDK não está disponível como um pacote do Flutter; portanto, teremos que usar o Client SDK do cliente nativo do Android e o Client SDK do cliente nativo para iOS A comunicação entre Android/iOS e o Flutter será feita por meio do MethodChannel — dessa forma, o Flutter chamará métodos do Android/iOS, e o Android/iOS chamará métodos do Flutter.

SDK do Android

Para instalar o SDK do Android, comece aumentando a alocação de memória para a JVM, editando o org.gradle.jvmargs propriedade no seu gradle.properties arquivo. Recomendamos que esse valor seja definido para pelo menos 4 GB:

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

Em seguida, abra o arquivo build.gradle , que pode ser encontrado em android/app/build.gradle e implemente o SDK da Vonage da seguinte maneira:

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

Por fim, certifique-se de que sua minSdkVersion este valor esteja definido em pelo menos 23:

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

O SDK do Android já está configurado e pronto para ser usado na compilação para Android do aplicativo Flutter.

SDK do iOS

Para instalar o SDK do iOS, comece gerando o PodFile abrindo uma linha de comando na raiz do seu projeto Flutter e, em seguida, executando os comandos abaixo:

cd ios/
pod init

Isso irá gerar o PodFile, abra esse arquivo e adicione o pod abaixo:

pod 'VonageClientSDKVoice', '~> 1.0.3'

Certifique-se também de definir a plataforma para, no mínimo, o iOS 10

platform :ios, '10.0'

Seu arquivo completo deve ficar mais ou menos assim:

platform :ios, '10.0'

target 'Runner' do
  use_frameworks!

  pod 'VonageClientSDKVoice', '~> 1.0.3'
end

Em seguida, na linha de comando, novamente no diretório do iOS, execute:

pod update

Isso fará o download e a instalação do SDK da Vonage e de suas dependências.

Por fim, para integrar isso ao seu projeto Flutter, a partir do diretório raiz do seu projeto, execute o comando do Flutter abaixo. Isso iniciará uma compilação para iOS e gerará os arquivos necessários para utilizar o SDK.

flutter build ios

Depois de concluída e compilada com sucesso, sua SDK estará configurada e pronta para ser usada.

Código

Devido à natureza do Flutter, o código pode ser facilmente dividido em três partes: o código do Flutter, escrito em Dart; o código nativo do Android, escrito em Kotlin; e o código nativo do iOS, escrito em Swift.

Flutter

Vamos começar com o código específico do Flutter; substitua o conteúdo de lib/main.dart pelo código abaixo:

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 é a classe completa necessária para construir a interface do usuário do aplicativo e acionar os métodos específicos da plataforma, que escreveremos em breve. Vamos analisar o que acontece em cada um dos métodos desta classe.

Começando pelas importações no início desta classe, temos as importações normais do Flutter, mas também estamos usando o pacote de gerenciamento de permissões . Ele é usado para gerenciar as solicitações de permissões no iOS e no Android por nós. Certifique-se de tê-lo instalado executando o comando:

flutter pub add permission_handler

Na raiz do seu projeto Flutter.

Em seguida, vamos compilar o aplicativo; para esta demonstração, temos um aplicativo bem simples, com apenas um elemento widget, ao qual chamamos de 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'),
    );
  }
}

Isso CallWidget estende a StatefulWidget obter o título e inicializar o 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();
}

O CallWidgetState gerenciará os elementos da interface do usuário, o estado atual do aplicativo e toda a comunicação com o código da 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');
    }
  }

Aqui, definimos o estado inicial do aplicativo como SdkState.LOGGED_OUT, criamos o MethodChannel que irá lidar com toda a comunicação entre o Flutter e o código nativo. Em seguida, definimos o methodCallHandler em que o estado é definido como qualquer estado que tenha sido repassado de volta ao Flutter a partir do código nativo.

A interface do usuário é então construída usando o build método, que simplesmente criou um Box com altura de 64. Atualizaremos esse elemento de acordo com o estado do aplicativo para exibir informações diferentes.

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

Em seguida, o _updateView método é usado para alterar o que está sendo exibido na caixa com base no estado atual do aplicativo. Esse modelo de estado permite uma interface de usuário organizada, mostrando ao usuário apenas o que ele precisa ver em qualquer momento do ciclo de vida do aplicativo.

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

Os métodos _loginUser e _endCall são muito semelhantes, pois tudo o que fazemos aqui é chamar os métodos loginUser/endCall no código nativo. É assim que acionamos o código nativo quando o usuário pressiona um botão na interface do usuário. Dentro do _loginUser temos uma variável token que deve ser o valor do JWT que você gerou anteriormente usando a CLI da 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);
      }
    }
  }

O _makeCall método também envolvia um método no código nativo, chamando o makeCall método. No entanto, antes disso, usamos o requestPermissions método para solicitar ao usuário as permissões de tempo de execução necessárias. Nesse caso, trata-se apenas da gravação de áudio/microfone.

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

E, por fim, temos uma enumeração que contém os diferentes estados em que o SDK e o aplicativo podem se encontrar.

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

Android

A seguir, vamos dar uma olhada no código específico para Android deste aplicativo. Primeiro, precisamos definir as permissões que o aplicativo precisará do sistema Android. No seu AndroidManifest.xml que está localizado em android/app/src/main/AndroidManifest.xml , adicione as seguintes permissões:

    <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 seguir, vamos abrir o MainActivity.kt arquivo que está localizado em android/app/src/main/kotlin/PACKAGE_NAME/MainActivity.kt

O conteúdo completo deste arquivo é o seguinte:

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
}

Vamos analisar isso e ver o que está acontecendo.

A primeira coisa que você vai notar é que estamos estendendo a classe FlutterActivity essa é uma classe `Activity` fornecida pelo Flutter que lida com grande parte do ciclo de vida adicional e com a “mágica” do Flutter que torna possível executar código nativo.

A seguir, temos duas variáveis que vamos usar:

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

O VoiceClient é o objeto responsável por todas as interações com o SDK, como fazer uma ligação, desligar o telefone etc. O onGoingCallID será usado para acompanhar a chamada em andamento enquanto ela estiver ocorrendo.

Em seguida, sobrescrevemos o configureFlutterEngine método, o que nos permite executar código quando o aplicativo está sendo criado pelo mecanismo do Flutter. Aqui, usamos isso para executar dois métodos: um para adicionar um ouvinte de canal e outro para configurar o NexmoClient.

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

        initClient()
        addFlutterChannelListener()
    }

A inicialização do VoiceClient é simples: basta passarmos o contexto atual do aplicativo. Em seguida, criamos um ErrorListener e um ‘ReconnectingListener’, que nos fornecerá o status atual do cliente; esses status correspondem aos valores que precisamos enviar de volta ao Flutter. Assim, usando uma instrução `when`, podemos enviar os valores conforme necessário.

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

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

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

O addFlutterChannelListener adiciona um ouvinte que ficará atento a quaisquer chamadas de método do Flutter. Como você pode ver, elas se referem aos três métodos que temos no Flutter; isso nos permite mapear essas chamadas para métodos específicos no 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()
                    }
                }
            }
        }
    }

O loginUser método é chamado quando o Flutter envia a chamada ao método `loginUser`, que passa o token JWT que definimos e, em seguida, aciona o método `login` no cliente.

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

O makeCall método é chamado quando o Flutter envia a chamada ao método makeCall; isso inicia uma ligação para o número de telefone especificado "PHONE_NUMBER" . Você deve substituir isso pelo número de telefone real para o qual deseja ligar. Novamente, aqui devolvemos o estado ao Flutter, dependendo se a chamada foi bem-sucedida e iniciada ou se ocorreu algum tipo de erro.

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

O endCall método é chamado quando o Flutter envia a endCall chamada de método; isso encerra a chamada telefônica atual (se houver alguma).

    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 fim, temos o nofityFlutter método; é aqui que usamos a “mágica” do Flutter para enviar de volta o estado atual do aplicativo, para que o Flutter possa atualizar a interface do usuário. Com isso, conseguimos envolver o método do Flutter updateState e passar o estado atual como uma variável.

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

E esse é todo o código nativo de que precisamos! Neste momento, temos um aplicativo Flutter em funcionamento que poderíamos compilar para o Android e que permite fazer uma ligação do aplicativo para um telefone físico. Mas, antes de testarmos o aplicativo, vamos dar uma olhada em como podemos fazer o mesmo para o iOS.

iOS

Primeiro, precisamos configurar as permissões de áudio no iOS; já temos o pacote no Flutter configurado para solicitá-las, então tudo o que precisamos fazer é abrir o ios/Runner/info.plist arquivo e adicionar Privacy - Microphone Usage Description chave com o valor "Make a call"

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

Em seguida, abra o arquivo ios/Runner/AppDelegate é aqui que vamos incluir o código para fazer a interface entre o Flutter e o SDK, da mesma forma que já fizemos para o Android. O código completo fica assim:

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

Esse é todo o código de que você vai precisar para também conseguir compilar para o iOS. Agora que já temos todo o código pronto, vamos compilar o aplicativo e testá-lo!

Compilar e testar

Com tudo já pronto, podemos compilar e executar o aplicativo; vamos compilar a versão para Android e executá-la no simulador do Android.

NOTA certifique-se de ter definido o JWT no código do Flutter e o PHONE_NUMBER no código nativo. Além disso, certifique-se de que seu servidor web ainda esteja em execução.

Inicie o emulador do Android para que o Flutter possa se conectar a ele; veja abaixo onde você pode fazer isso no Android Studio

Android studio with device manager selected

Assim que o processo estiver em execução, você poderá selecionar esse dispositivo como destino para a compilação do Flutter e clicar na seta verde para compilar e executar o aplicativo Flutter (com código nativo do Android).

android studio with emulator selected and main.dart

Assim que o aplicativo for compilado e instalado, você verá a tela à esquerda abaixo. Ao clicar no botão “Fazer login como Alice”, você será direcionado para a próxima tela. A partir daí, você pode pressionar o botão “Fazer ligação”, o que (na primeira vez que for executado) solicitará que você conceda as permissões de áudio. Em seguida, a ligação será iniciada e o número de telefone que você inseriu será discado, estabelecendo a conexão de áudio.

Quando quiser encerrar a ligação, basta pressionar o botão para encerrar a ligação.

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

E pronto! Agora você tem seu aplicativo de chamadas telefônicas totalmente funcional, escrito em Flutter e compatível tanto com Android quanto com iOS. Mas, é claro, isso não é o fim! Com o seu conhecimento sobre como usar os SDKs do Android e do iOS, dê uma olhada nos outros projetos de exemplo , que vão te ajudar a incorporar outros recursos de comunicação ao seu aplicativo Flutter. Se quiser mais detalhes, não deixe de conferir o portal do desenvolvedor , que contém toda a documentação e códigos de exemplo de que você possa precisar!

Compartilhar:

https://a.storyblok.com/f/270183/400x400/04765919bb/zachary-powell-1.png
Zachary PowellEx-funcionários da Vonage

Zach é um ex-membro da equipe da Vonage. Ele atuava como Promotor Sênior de Desenvolvimento Android na Vonage