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

Flutter 3とVonage APIを使い始める

最終更新日 July 30, 2023

所要時間:3 分

Flutter 3.0のリリースに伴い(様々な安定性とパフォーマンスの改善が含まれている 安定性とパフォーマンスの改善をリリースした今こそ、ユーザーエクスペリエンスを向上させ、クロスプラットフォームアプリケーションを強化するために通信APIをどのように利用できるかを検討する絶好の機会です。

FlutterがネイティブプラットフォームSDKを使えるおかげで、Flutterアプリケーション内でVonageのAndroidとiOS SDKをシームレスに使うことができる。それでは、物理的な電話に音声電話をかけることができるシンプルなFlutterアプリケーションを作成する方法を見てみましょう。このガイドが終わるころには、Vonage SDKを使って音声通話をする方法と、FlutterアプリケーションでネイティブのAndroidとiOS SDKを使う方法をよく理解していることでしょう。

このガイドでは、基本的なアプリをゼロから作成するが、以下の内容をアプリケーションに組み込むこともできる。

このプロジェクトの全ソースコードは GitHub.

Vonageセットアップ

コードに入る前に、Vonage APIをセットアップして利用するために必要なことがいくつかある。

アカウント登録

まずは無料のVonage Developerアカウントにサインアップしてください。これは ダッシュボードサインアップすると、アカウントのAPIキーとAPIシークレットが表示されます。今後のステップのためにこれらをメモしておいてください。

Vonage dashboard home page showing API key and API secret location

Vonage CLIをインストールする

Vonage CLI Vonage CLIを使うと、コマンドラインで多くの操作を行うことができます。アプリケーションの作成、Numbersの購入、Numbersとアプリケーションのリンクなどがその例です。

CLIをNPMでインストールするには、以下を実行する:

npm install -g @vonage/cli

Vonage API KeyとAPI Secretを使用するようにVonage CLIを設定します。これらは 設定ページページで取得できます。

ターミナルで以下のコマンドを実行する。 API_KEYAPI_SECRETを自分のものに置き換えてください:

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

Vonage番号の購入

次に、アプリケーションが使用できるVonage番号が必要です。これは、アプリケーションから電話をかける際に表示される電話番号です。

Vonage CLIを使用して番号を購入できます。次のコマンドは、米国で利用可能な番号を購入します。指定する 別の2文字の国コードを指定します。

vonage numbers:search US
vonage numbers:buy 15555555555 US

Webhookサーバーの作成

インバウンドコールを受信すると、Vonageはお客様が選択した一般にアクセス可能なURLへのリクエストを行います。 answer_url.このリクエストを受信し、.NET Frameworkを返すことができるWebhookサーバーを作成する必要があります。 NCCOを含む connectアクションを含むNCCOを返します。 PSTN電話番号.これを行うには toクエリパラメータから宛先番号を抽出し、レスポンスで返します。

コマンドラインで、ウェブサーバーを格納する新しいフォルダを作成します。

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

フォルダ内で、以下のコマンドを実行して新しいNode.jsプロジェクトを初期化する:

npm init -y

次に、必要な依存関係をインストールする:

npm install express localtunnel --save

プロジェクトフォルダー内に server.jsという名前のファイルを作成し、以下のコードを追加します。 NUMBERをあなたのVonage番号に置き換えてください。 E.164形式)、および SUBDOMAINを実際の値に置き換えてください。使用した値は、次のステップでウェブフックとして設定するURLの一部になります。

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

ターミナルで以下のコマンドを実行すれば、サーバーを起動できる:

node server.js

サーバーが利用可能になったことを知らせる通知が表示されます:

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

Vonageアプリケーションの作成

このステップでは、Vonageアプリケーションを作成します。 アプリケーションを作成します。を作成します。

新しいターミナルを開き、必要であればプロジェクト・ディレクトリに移動する。

以下のコマンドをターミナルにコピー&ペーストしてVonageアプリケーションを作成する。必ず --voice_answer_url--voice_event_url引数の値を変更してください。 SUBDOMAINを前のステップで使用した実際の値に置き換えてください:

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

プロジェクトディレクトリに vonage_app.jsonという名前のファイルがプロジェクトディレクトリに作成/更新され、新しく作成されたVonageアプリケーションIDと秘密鍵が含まれます。という名前の秘密鍵ファイルも作成されます。 app_to_phone_tutorial.keyという名前の秘密鍵ファイルも作成されます。

アプリケーションの作成時にターミナルに表示されるアプリケーションIDをメモしておいてください:

screenshot of the terminal with Application ID underlined

Vonage番号をリンクする

適当な番号が決まったら、Vonageアプリケーションとリンクさせることができます。新しい番号に置き換える YOUR_VONAGE_NUMBERを新しく購入した番号に置き換え APPLICATION_IDをアプリケーションIDに置き換えて、このコマンドを実行します:

vonage apps:link APPLICATION_ID --number=YOUR_VONAGE_NUMBER

ユーザーの作成

ユーザは、Vonage Client SDKs で作業する際の重要な概念です。ユーザが Client SDK で認証するとき、提供された認証情報はそのユーザを特定のユーザとして識別します。各認証されたユーザは、通常、ユーザのデータベース内の単一のユーザに対応します。

という名前のユーザを作成するには Aliceというユーザを作成するには、Vonage CLIを使用して次のコマンドを実行します:

vonage apps:users:create "Alice"

これにより、以下のようなユーザーIDが返される:

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

JWTの生成

Client SDKでは JWTを使用します。JWTは、ユーザー名、関連するアプリケーションID、およびユーザーに付与された権限を識別します。有効なトークンであることを証明するために、秘密鍵を使って署名されます。

以下のコマンドを実行する。 APPLICATION_ID変数をアプリケーションのIDに、そして PRIVATE_KEYを秘密鍵ファイルの名前に置き換えてください。

Vonage CLIを使用してJWTを生成するには、次のコマンドを実行します。 APP_ID変数を独自の値に置き換えることを忘れないでください:

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

上記のコマンドは、JWTの有効期限を最大で1日後に設定している。

terminal screenshot of a generated sample JWT

これでflutterアプリケーションでVonage Voice APIを使うために必要なものはすべて揃った。それではアプリケーション自体をセットアップしてみよう。

フラッターのセットアップ

まだなら、Flutterとその依存関係をダウンロードしてインストールすることから始めよう。これは インストールガイド.Flutterを正しくセットアップしたら、次に必要なのはIDEの設定です。 エディタのセットアップガイドを参考にしてください。

このガイドでは、Android Studioを使用します。

IDEがセットアップされたら テストドライブガイドに従って、AndroidとiOSの両方に対応した基本的なFlutterアプリケーションをセットアップします。もちろん、すでに使いたいFlutterプロジェクトがあれば、それを使うこともできます。

SDKのインストール

プロジェクトがセットアップされたので、Vonage Client SDKをインストールしよう。現在、Client SDKはFlutterのパッケージとして提供されていないので、AndroidネイティブのClient SDKである AndroidネイティブClient SDKiOSネイティブClient SDKを使用する必要があります。Android/iOSとFlutter間の通信には メソッドチャンネル- を使用し、FlutterがAndroid/iOSのメソッドを呼び出し、Android/iOSがFlutterのメソッドを呼び出す。

アンドロイドSDK

Android SDKをインストールするには、JVMのメモリ割り当てを増やすことから始めます。 org.gradle.jvmargsファイルの gradle.propertiesファイルのプロパティを編集して、JVMのメモリ割り当てを増やすことから始めます。少なくとも4GBに設定することをお勧めします:

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

次に、アプリレベルの build.gradleファイルを開き android/app/build.gradleを開き、Vonage SDKを次のように実装する:

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

最後に minSdkVersionが少なくとも 23:

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

これでAndroid SDKがセットアップされ、flutterアプリケーションのAndroidビルドに使えるようになった。

iOS SDK

iOS SDKをインストールするには PodFileFlutterプロジェクトのルートでコマンドラインを開き、以下のコマンドを実行する:

cd ios/
pod init

これで PodFileこのファイルを開き、以下のポッドを追加する:

pod 'VonageClientSDKVoice', '~> 1.0.3'

プラットフォームもios 10以上にしてください。

platform :ios, '10.0'

完成したファイルは次のようになるはずだ:

platform :ios, '10.0'

target 'Runner' do
  use_frameworks!

  pod 'VonageClientSDKVoice', '~> 1.0.3'
end

次にコマンドラインから、再びiOSのディレクトリで実行する:

pod update

これにより、Vonage SDKとその依存関係がダウンロードされ、インストールされます。

最後に、これをFlutterプロジェクトにリンクさせるために、プロジェクトのルートディレクトリから以下のFlutterコマンドを実行する。これでiOSのビルドが開始され、SDKを利用するために必要なファイルが生成されます。

flutter build ios

SDKが完成し、ビルドに成功すれば、SDKはセットアップされ、使用できるようになります。

コード

Flutterの性質上、コードはDartで書かれたFlutterのコード、Kotlinで書かれたAndroidネイティブのコード、Swiftで書かれたiOSネイティブのコードの3つの領域に簡単に分けることができる。

フラッター

フラッター特有のコードから始めよう。 lib/main.dartの中身を以下のコードに置き換える:

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
}

これは、アプリのUIを構築し、これから書くプラットフォーム固有のメソッドをトリガーするために必要な完全なクラスだ。このクラスの各メソッドで何が起こっているのかを分解してみよう。

このクラスの一番上のインポートから始めると、通常のflutterのインポートがあるが、同時に パーミッションハンドラパッケージを使用しています。これはiOSとAndroidのパーミッションのリクエストを管理するのに使われる。コマンドを実行して、このパッケージがインストールされていることを確認してください:

flutter pub add permission_handler

あなたのフラッタープロジェクトの根底にあるもの。

次に、アプリをビルドします。このデモでは、非常にシンプルなアプリで、ウィジェット・エレメントは1つだけです。 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'),
    );
  }
}

これは CallWidgetを拡張します。 StatefulWidgetタイトルを取り 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();
}

そのため CallWidgetStateは、UI要素、アプリの現在の状態、そしてネイティブ・プラットフォームのコードに戻るすべての通信を管理する。

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

ここでは、アプリの開始状態を SdkState.LOGGED_OUTとして設定し MethodChannelを作成し、Flutterとネイティブコード間のすべての通信を処理する。次に methodCallHandlerを設定し、ネイティブコードからFlutterに戻された状態を設定する。

UIは buildメソッドを使って構築される。 Boxを作成するだけだ。アプリの状態に応じてこの要素を更新し、さまざまな情報を表示する。

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

次に _updateViewメソッドは、アプリの現在の状態に基づいて、現在ボックスに表示されているものを変更するために使用されます。このステートモデルによって、アプリのライフサイクルの任意の時点でユーザーが見る必要のあるものだけを表示する、すっきりとしたUIが可能になります。

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

loginUserメソッドと_endCallメソッドは非常に似ており、ここで行っていることは、ネイティブ・コードでloginUser/endCallメソッドを呼び出すことだけです。これは、ユーザーがUI上のボタンを押したときにネイティブ・コードをトリガーする方法です。メソッド内で _loginUserの中に変数 tokenこれは、Vonage CLIを使用して以前に生成したJWT値でなければなりません。

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

この _makeCallメソッドを呼び出すネイティブ・コード上のメソッドも関与していた。 makeCallメソッドを呼び出す。しかし、その前に requestPermissionsメソッドを使って、ユーザーに必要なランタイム権限を要求します。この場合、それはマイク/音声の録音だけである。

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

そして最後に、SDKとアプリの状態を保持するenumがあります。

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

アンドロイド

次に、このアプリケーションのAndroid固有のコードを見てみよう。まず、このアプリがAndroidシステムから必要とされるパーミッションを設定する必要があります。あなたの AndroidManifest.xmlにある android/app/src/main/AndroidManifest.xmlに以下のパーミッションを追加します:

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

次に MainActivity.ktファイルを開いてみよう。 android/app/src/main/kotlin/PACKAGE_NAME/MainActivity.kt

このファイルの全内容は以下の通り:

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
}

これを分解して、何が起こっているのか見てみよう。

まず気づくのは、クラスを拡張していることです。 FlutterActivityこれはFlutterが提供するActivityクラスで、ネイティブコードの実行を可能にする追加のライフサイクルとFlutterマジックの多くを処理する。

次は、これから使う2つの変数だ:

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

VoiceClientは、電話をかけたり切ったりといったSDKとのやりとりのすべてを担当するオブジェクトです。このオブジェクトは onGoingCallIDは現在の電話を追跡するために使われます。

次に configureFlutterEngineメソッドをオーバーライドし、アプリがFlutterエンジンによって作成されるときにコードを実行できるようにする。1つはチャンネルリスナーを追加するメソッド、もう1つは NexmoClient.

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

        initClient()
        addFlutterChannelListener()
    }

を初期化するのは簡単だ。 VoiceClientアプリの現在のコンテキストを渡すだけだ。次に ErrorListenerと'ReconnectingListener'を作り、クライアントの現在のステータスを取得する。このステータスはFlutterに送り返す必要のある値に対応しているので、whenステートメントを使って必要に応じて値を送ることができる。

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

        client.setSessionErrorListener {
            notifyFlutter(SdkState.ERROR)
        }

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

addFlutterChannelListenerはFlutterからのメソッド呼び出しを監視するリスナーを追加する。見てわかるように、これらはFlutterにある3つのメソッドに関連しており、これによってこれらの呼び出しをネイティブコード内の特定のメソッドにマッピングすることができる。

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

この loginUserメソッドはFlutterがloginUserメソッドコールを送信するときに呼び出され、設定したJWTトークンを渡し、クライアントのログインメソッドをトリガーします。

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

この makeCallメソッドはFlutterがmakeCallメソッドコールを送信するときに呼び出され、指定された電話番号に電話をかけます。 "PHONE_NUMBER"これは指定された電話番号への電話を開始するもので、これを実際に電話をかけたい電話番号に置き換える必要がある。ここでも、通話が成功して開始されるか、何らかのエラーが発生するかによって、Flutterに状態を引き渡します。

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

この endCallメソッドはFlutterが endCallメソッドが呼び出されると、現在の電話が終了します(もしあれば)。

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

最後に nofityFlutterここでFlutterのマジックを使ってアプリケーションの現在の状態を送り返し、FlutterがUIを更新できるようにする。これを使ってFlutterの updateStateメソッドに関与させ、現在の状態を変数として渡すことができます。

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

必要なネイティブコードはこれだけだ!この時点で、私たちはAndroid用にビルドし、アプリから物理的な電話に電話をかけることができる、機能するFlutterアプリを持っている。しかしアプリをテストする前に、iOSでも同じことができる方法を見てみよう。

iOS

まず、iOSのオーディオパーミッションを設定する必要がある。 ios/Runner/info.plistファイルを開き Privacy - Microphone Usage Descriptionキーに"Make a call"

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

次に ios/Runner/AppDelegateここに、Androidですでにやったのと同じように、flutterとSDK間のインターフェイスのコードを入れる。完全なコードは以下のようになる:

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

これでiOS用にビルドするのに必要なコードはすべて揃ったので、アプリをビルドしてテストしてみよう!

ビルドとテスト

これですべての準備が整ったので、アプリケーションをビルドして実行することができる。Androidバージョンをビルドし、Androidエミュレーションで実行してみよう。

注意FlutterのコードでJWTを設定し、ネイティブコードでPHONE_NUMBERを設定していることを確認してください。また、ウェブサーバーがまだ動いていることを確認してください。

Androidエミュレータを起動し、flutterがエミュレータに接続できるようにする。

Android studio with device manager selected

これが実行されたら、Flutterビルドのターゲットとしてこのデバイスを選択し、緑の矢印を押してFlutterアプリ(Androidネイティブコード)をビルドして実行することができます。

android studio with emulator selected and main.dart

アプリケーションのビルドとインストールが完了すると、左下の画面が表示されます。アリスとしてログインボタンをクリックすると、次の画面が表示されます。ここから電話をかけるボタンを押すと、(初回実行時に)音声許可を求めるプロンプトが表示されます。この後、通話が開始され、入力した電話番号が音声セッションに接続されます。

通話を終了したい場合は、通話終了ボタンを押してください。

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

これで終わりです!これで、AndroidとiOSの両方に対応した、Flutterで書かれた完全な機能を持つ電話アプリが完成した。しかし、もちろんこれで終わりではありません!AndroidとiOSのSDKの使い方を学んだら、他の サンプルプロジェクト他のコミュニケーション機能をFlutterアプリケーションに組み込むのに役立ちます。もっと詳しく知りたい場合は 開発者ポータルには必要なドキュメントやサンプルコードがすべて掲載されています!

シェア:

https://a.storyblok.com/f/270183/400x400/04765919bb/zachary-powell-1.png
Zachary Powellシニア・アンドロイド・デベロッパー・アドボケイト