将本机Twilio Android SDK与Flutter集成在一起

Tra*_*ean 5 android twilio flutter

我正在尝试使用flutter创建IP语音(VOIP)移动应用程序。我还没有看到twilio语音api的flutter插件的实现,因此我使用MethodChannel将我的应用程序与本机android语音api集成在一起.twilio SDK似乎没有像它正确集成一样,我无法访问脚本中的twilio类和方法。这些是我得到的错误。

Running Gradle task 'assembleDebug'...
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees  /voip20/MainActivity.java:23: error: package android.support.annotation does not exist
import android.support.annotation.NonNull;
                             ^
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees   /voip20/MainActivity.java:295: error: cannot find symbol
public void onRequestPermissionsResult(int requestCode, @NonNull   String[] permissions, @NonNull int[] grantResults) {
                                                         ^
symbol:   class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:295: error: cannot find symbol
   public void onRequestPermissionsResult(int requestCode, @NonNull  String[] permissions, @NonNull int[] grantResults) {
                                                                                         ^
symbol:   class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees   /voip20/MainActivity.java:117: error: cannot find symbol
    soundPoolManager =   SoundPoolManager.getInstance(this.MainActivity);
                                                        ^
 symbol: variable MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:186: error: cannot find symbol
        public void onReconnecting(@NonNull Call call, @NonNull   CallException callException) {
                                    ^
  symbol: class NonNull
  /home/kudziesimz/voip20/android/app/src/main/java/com/workerbees  /voip20/MainActivity.java:186: error: cannot find symbol
          public void onReconnecting(@NonNull Call call, @NonNull CallException callException) {
                                                        ^
           symbol: class NonNull
         /home/kudziesimz/voip20/android/app/src/main/java /com/workerbees/voip20/MainActivity.java:191: error: cannot find symbol
            public void onReconnected(@NonNull Call call) {
                                   ^
        symbol: class NonNull
  /home/kudziesimz/voip20/android/app/src/main/java/com/workerbees    /voip20/MainActivity.java:279: error: cannot find symbol
      int resultMic = ContextCompat.checkSelfPermission(this,    Manifest.permission.RECORD_AUDIO);
                    ^
   symbol:   variable ContextCompat
   location: class MainActivity
  /home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:284: error: method    shouldShowRequestPermissionRationale in class Activity cannot be applied  to given types;
         if (MainActivity.shouldShowRequestPermissionRationale(this,  Manifest.permission.RECORD_AUDIO)) {
                     ^
           required: String
    found: MainActivity,String
     reason: actual and formal argument lists differ in length
    /home/kudziesimz/voip20/android/app/src/main/java/com/workerbees   /voip20/MainActivity.java:287: error: method requestPermissions in class    Activity cannot be applied to given types;
             MainActivity.requestPermissions(
                    ^
           required: String[],int
           found: MainActivity,String[],int
           reason: actual and formal argument lists differ in length
          Note: /home/kudziesimz/voip20/android/app/src/main/java  /com/workerbees/voip20/MainActivity.java uses or overrides a deprecated   API.
                    Note: Recompile with -Xlint:deprecation for details.
                    10 errors
Run Code Online (Sandbox Code Playgroud)

我遵循此处显示的voice-quickstart-android指南https://github.com/twilio/voice-quickstart-android

这是我的代码:main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';

 //This is a test application which allows clients to make Voice Over    The Internet Cal
 void main() => runApp(MaterialApp(
   home: MyApp(),
    ));

 class MyApp extends StatefulWidget {
  @override
 _MyAppState createState() => _MyAppState();
  }

 class _MyAppState extends State<MyApp> {
 static const platform = const     MethodChannel("com.voip.call_management/calls");

  @override
  Widget build(BuildContext context) {
     return Scaffold(
     appBar: AppBar(
      title: Text("Call Management"),
       ),
     bottomNavigationBar: Center(
     child: IconButton(
        icon: Icon(Icons.phone),
        onPressed: () {
          _makeCall;
           }),
       ),
      );
     }

  Future<void> _makeCall() async {
     return showDialog<void>(
         context: context,
          barrierDismissible: false, // user must tap button!
          builder: (BuildContext context) {
          return AlertDialog(
          title: Row(
             children: <Widget>[
                Text('Call'),
                Icon(
                   Icons.phone,
                  color: Colors.blue,
                 )
             ],
           ),
          content: SingleChildScrollView(
           child: ListBody(
                children: <Widget>[
                 TextField(
                  decoration: InputDecoration(
                  hintText: "client identity or phone number"),
                  ),
                SizedBox(
                    height: 20,
                    ),
                 Text(
                     'Dial a client name or number.Leaving the field      empty will result in an automated response.'),
              ],
             ),
           ),
           actions: <Widget>[
            FlatButton(
               child: Text('Cancel'),
                   onPressed: () {
                        Navigator.of(context).pop();
                       },
                    ),
             IconButton(icon: Icon(Icons.phone), onPressed:()async {
                try {
                final result = await platform.invokeMethod("makecall");
          } on PlatformException catch (e) {
            print(e.message);
          }
        })
        ],
      );
    },
    );
   }
  }
Run Code Online (Sandbox Code Playgroud)

MainActivity.java

package com.workerbees.voip20;

 import android.os.Bundle;

import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

//javacode imports

import android.Manifest;
import android.content.Context;

import android.content.pm.PackageManager;

import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.NonNull;

import android.util.Log;


import com.google.firebase.iid.FirebaseInstanceId;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.twilio.voice.Call;
import com.twilio.voice.CallException;
import com.twilio.voice.CallInvite;
import com.twilio.voice.ConnectOptions;
import com.twilio.voice.RegistrationException;
import com.twilio.voice.RegistrationListener;
import com.twilio.voice.Voice;

import java.util.HashMap;

//sound pool imports
import android.media.SoundPool;


import static android.content.Context.AUDIO_SERVICE;


public class MainActivity extends FlutterActivity {
   private static final String CHANNEL = "com.workerbees.voip/calls";        // MethodChannel Declaration
   private static final String TAG = "VoiceActivity";
   private static String identity = "alice";
   private static String contact;
   /*
    * You must provide the URL to the publicly accessible Twilio     access token server route
    *
    * For example: https://myurl.io/accessToken
    *
    * If your token server is written in PHP,    TWILIO_ACCESS_TOKEN_SERVER_URL needs .php extension at the end.
     *
     * For example : https://myurl.io/accessToken.php
     */
     private static final String TWILIO_ACCESS_TOKEN_SERVER_URL = "https://bd107744.ngrok.io/accessToken";

private static final int MIC_PERMISSION_REQUEST_CODE = 1;


private String accessToken;
private AudioManager audioManager;
private int savedAudioMode = AudioManager.MODE_INVALID;


// Empty HashMap, never populated for the Quickstart
HashMap<String, String> params = new HashMap<>();

private SoundPoolManager soundPoolManager;
private Call activeCall;

Call.Listener callListener = callListener();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);

    new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(

            new MethodCallHandler() {
                @Override
                public void onMethodCall(MethodCall call, Result result) {
                    // Note: this method is invoked on the main thread.
                    // TODO
                    if(call.method.equals("makecall")){

                        params.put("to", contact);
                        ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
                                .params(params)
                                .build();
                        activeCall = Voice.connect(MainActivity.this, connectOptions, callListener);

                    }
                    else if(call.method.equals("hangup")){
                        disconnect();
                    }
                    else if(call.method.equals("mute")){
                        mute();
                    }
                    else if (call.method.equals("hold")){
                        hold();
                    }
                    else{
                        Log.d(TAG,"invalid API call");
                    }
                }
            });


    soundPoolManager = SoundPoolManager.getInstance(this.MainActivity);

    /*
     * Needed for setting/abandoning audio focus during a call
     */
    audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    audioManager.setSpeakerphoneOn(true);

    /*
     * Enable changing the volume using the up/down keys during a conversation
     */
    setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);



    /*
     * Displays a call dialog if the intent contains a call invite
     */
    //handleIncomingCallIntent(getIntent());

    /*
     * Ensure the microphone permission is enabled
     */
    if (!checkPermissionForMicrophone()) {
        requestPermissionForMicrophone();
    } else {
        retrieveAccessToken();
    }

}


private Call.Listener callListener() {
    return new Call.Listener() {
        /*
         * This callback is emitted once before the Call.Listener.onConnected() callback when
         * the callee is being alerted of a Call. The behavior of this callback is determined by
         * the answerOnBridge flag provided in the Dial verb of your TwiML application
         * associated with this client. If the answerOnBridge flag is false, which is the
         * default, the Call.Listener.onConnected() callback will be emitted immediately after
         * Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the
         * call to emit the onConnected callback only after the call is answered.
         * See answeronbridge for more details on how to use it with the Dial TwiML verb. If the
         * twiML response contains a Say verb, then the call will emit the
         * Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is
         * raised, irrespective of the value of answerOnBridge being set to true or false
         */
        @Override
        public void onRinging(Call call) {
            Log.d(TAG, "Ringing");
        }

        @Override
        public void onConnectFailure(Call call, CallException error) {
            setAudioFocus(false);
            Log.d(TAG, "Connect failure");
            String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
            Log.e(TAG, message);

        }

        @Override
        public void onConnected(Call call) {
            setAudioFocus(true);
            Log.d(TAG, "Connected");
            activeCall = call;
        }

        @Override
        public void onReconnecting(@NonNull Call call, @NonNull CallException callException) {
            Log.d(TAG, "onReconnecting");
        }

        @Override
        public void onReconnected(@NonNull Call call) {
            Log.d(TAG, "onReconnected");
        }

        @Override
        public void onDisconnected(Call call, CallException error) {
            setAudioFocus(false);
            Log.d(TAG, "Disconnected");
            if (error != null) {
                String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
                Log.e(TAG, message);
            }
        }
    };
}


private void disconnect() {
    if (activeCall != null) {
        activeCall.disconnect();
        activeCall = null;
    }
}

private void hold() {
    if (activeCall != null) {
        boolean hold = !activeCall.isOnHold();
        activeCall.hold(hold);

    }
}

private void mute() {
    if (activeCall != null) {
        boolean mute = !activeCall.isMuted();
        activeCall.mute(mute);

    }
}

private void setAudioFocus(boolean setFocus) {
    if (audioManager != null) {
        if (setFocus) {
            savedAudioMode = audioManager.getMode();
            // Request audio focus before making any device switch.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                AudioAttributes playbackAttributes = new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .build();
                AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
                        .setAudioAttributes(playbackAttributes)
                        .setAcceptsDelayedFocusGain(true)
                        .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() {
                            @Override
                            public void onAudioFocusChange(int i) {
                            }
                        })
                        .build();
                audioManager.requestAudioFocus(focusRequest);
            } else {
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.FROYO) {
                    int focusRequestResult = audioManager.requestAudioFocus(
                            new AudioManager.OnAudioFocusChangeListener() {

                                @Override
                                public void onAudioFocusChange(int focusChange)
                                {
                                }
                                  }, AudioManager.STREAM_VOICE_CALL,
                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
                }
            }
            /*
             * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
             * required to be in this mode when playout and/or recording starts for
             * best possible VoIP performance. Some devices have difficulties with speaker mode
             * if this is not set.
             */
            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        } else {
            audioManager.setMode(savedAudioMode);
            audioManager.abandonAudioFocus(null);
        }
    }
}

private boolean checkPermissionForMicrophone() {
    int resultMic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
    return resultMic == PackageManager.PERMISSION_GRANTED;
}

private void requestPermissionForMicrophone() {
    if (MainActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {

    } else {
        MainActivity.requestPermissions(
                this,
                new String[]{Manifest.permission.RECORD_AUDIO},
                MIC_PERMISSION_REQUEST_CODE);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    /*
     * Check if microphone permissions is granted
     */
    if (requestCode == MIC_PERMISSION_REQUEST_CODE && permissions.length > 0) {
        if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {

            Log.d(TAG, "Microphone permissions needed. Please allow in your application settings.");
        } else {
            retrieveAccessToken();
        }
    }
}


/*
 * Get an access token from your Twilio access token server
 */
private void retrieveAccessToken() {
    Ion.with(this).load(TWILIO_ACCESS_TOKEN_SERVER_URL + "?identity=" + identity).asString().setCallback(new FutureCallback<String>() {
        @Override
        public void onCompleted(Exception e, String accessToken) {
            if (e == null) {
                Log.d(TAG, "Access token: " + accessToken);
                MainActivity.this.accessToken = accessToken;

            } else {
                Log.d(TAG, "Registration failed");
            }
        }
    });
           }
      }


class SoundPoolManager {

private boolean playing = false;
private boolean loaded = false;
private boolean playingCalled = false;
private float actualVolume;
private float maxVolume;
private float volume;
private AudioManager audioManager;
private SoundPool soundPool;
private int ringingSoundId;
private int ringingStreamId;
private int disconnectSoundId;
private static SoundPoolManager instance;

private SoundPoolManager(Context context) {
    // AudioManager audio settings for adjusting the volume
    audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
    actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
    maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    volume = actualVolume / maxVolume;

    // Load the sounds
    int maxStreams = 1;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        soundPool = new SoundPool.Builder()
                .setMaxStreams(maxStreams)
                .build();
    } else {
        soundPool = new SoundPool(maxStreams, AudioManager.STREAM_MUSIC, 0);
    }

    soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
        @Override
        public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
            loaded = true;
            if (playingCalled) {
                playRinging();
                playingCalled = false;
            }
        }

    });
    ringingSoundId = soundPool.load(context, R.raw.incoming, 1);
    disconnectSoundId = soundPool.load(context, R.raw.disconnect, 1);
}

public static SoundPoolManager getInstance(Context context) {
    if (instance == null) {
        instance = new SoundPoolManager(context);
    }
    return instance;
}

public void playRinging() {
    if (loaded && !playing) {
        ringingStreamId = soundPool.play(ringingSoundId, volume, volume, 1, -1, 1f);
        playing = true;
    } else {
        playingCalled = true;
    }
}

public void stopRinging() {
    if (playing) {
        soundPool.stop(ringingStreamId);
        playing = false;
    }
}

public void playDisconnect() {
    if (loaded && !playing) {
        soundPool.play(disconnectSoundId, volume, volume, 1, 0, 1f);
        playing = false;
    }
}

pub

Oma*_*att 1

您还有这个问题吗?按照Flutter 平台渠道指南,我能够毫无问题地使用 Twilio Android SDK。我在这个基于Twilio 的 Android 快速入门的演示中集成了 Twilio 所需的最少组件。

\n

主程序.dart

\n
import \'package:flutter/material.dart\';\nimport \'package:flutter/services.dart\';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: \'Flutter Demo\',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: MyHomePage(title: \'Flutter Demo Home Page\'),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key key, this.title}) : super(key: key);\n\n  final String title;\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  static const platform = const MethodChannel(\'samples.flutter.dev/twilio\');\n\n  Future<void> callTwilio() async{\n    try {\n      final String result = await platform.invokeMethod(\'callTwilio\');\n      debugPrint(\'Result: $result\');\n    } on PlatformException catch (e) {\n      debugPrint(\'Failed: ${e.message}.\');\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: <Widget>[\n            Text(\n              \'Hello\',\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => callTwilio(),\n        tooltip: \'Call\',\n        child: Icon(Icons.phone),\n      ),\n    );\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

android/app/src/main/kotlin/{PACKAGE_NAME}/MainActivity.kt

\n
class MainActivity : FlutterActivity() {\n    private val CHANNEL = "samples.flutter.dev/twilio"\n    private val TAG = "MainActivity"\n\n    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->\n            if (call.method == "callTwilio") {\n                executeTwilioVoiceCall()\n                result.success("Hello from Android")\n            } else {\n                result.notImplemented()\n            }\n        }\n    }\n\n    private val accessToken = ""\n    var params = HashMap<String, String>()\n    var callListener: Call.Listener = callListener()\n    fun executeTwilioVoiceCall(){\n        val connectOptions = ConnectOptions.Builder(accessToken)\n                .params(params)\n                .build()\n        Voice.connect(this, connectOptions, callListener)\n    }\n\n    private fun callListener(): Call.Listener {\n        return object : Call.Listener {\n            override fun onRinging(call: Call) {\n                Log.d(TAG, "Ringing")\n            }\n\n            override fun onConnectFailure(call: Call, error: CallException) {\n                Log.d(TAG, "Connect failure")\n            }\n\n            override fun onConnected(call: Call) {\n                Log.d(TAG, "Connected")\n            }\n\n            override fun onReconnecting(call: Call, callException: CallException) {\n                Log.d(TAG, "onReconnecting")\n            }\n\n            override fun onReconnected(call: Call) {\n                Log.d(TAG, "onReconnected")\n            }\n\n            override fun onDisconnected(call: Call, error: CallException?) {\n                Log.d(TAG, "Disconnected")\n            }\n\n            override fun onCallQualityWarningsChanged(call: Call,\n                                                      currentWarnings: MutableSet<CallQualityWarning>,\n                                                      previousWarnings: MutableSet<CallQualityWarning>) {\n                if (previousWarnings.size > 1) {\n                    val intersection: MutableSet<CallQualityWarning> = HashSet(currentWarnings)\n                    currentWarnings.removeAll(previousWarnings)\n                    intersection.retainAll(previousWarnings)\n                    previousWarnings.removeAll(intersection)\n                }\n                val message = String.format(\n                        Locale.US,\n                        "Newly raised warnings: $currentWarnings Clear warnings $previousWarnings")\n                Log.e(TAG, message)\n            }\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

至于Android中的依赖项,我已将它们添加到build.gradle配置中

\n

android/build.gradle

\n
ext.versions = [\n    \'voiceAndroid\'       : \'5.6.2\',\n    \'audioSwitch\'        : \'1.1.0\',\n]\n
Run Code Online (Sandbox Code Playgroud)\n

安卓/应用程序/build.grade

\n
dependencies {\n    ...\n    implementation "com.twilio:audioswitch:${versions.audioSwitch}"\n    implementation "com.twilio:voice-android:${versions.voiceAndroid}"\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是我的flutter doctor详细日志供参考

\n
[\xe2\x9c\x93] Flutter (Channel master, 1.26.0-2.0.pre.281, on macOS 11.1 20C69 darwin-x64)\n    \xe2\x80\xa2 Flutter version 1.26.0-2.0.pre.281\n    \xe2\x80\xa2 Framework revision 4d5db88998 (3 weeks ago), 2021-01-11 10:29:26 -0800\n    \xe2\x80\xa2 Engine revision d5cacaa3a6\n    \xe2\x80\xa2 Dart version 2.12.0 (build 2.12.0-211.0.dev)\n\n[\xe2\x9c\x93] Android toolchain - develop for Android devices (Android SDK version 29.0.2)\n    \xe2\x80\xa2 Platform android-30, build-tools 29.0.2\n    \xe2\x80\xa2 Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java\n    \xe2\x80\xa2 Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)\n    \xe2\x80\xa2 All Android licenses accepted.\n\n[\xe2\x9c\x93] Xcode - develop for iOS and macOS (Xcode 12.0.1)\n    \xe2\x80\xa2 Xcode at /Applications/Xcode.app/Contents/Developer\n    \xe2\x80\xa2 Xcode 12.0.1, Build version 12A7300\n    \xe2\x80\xa2 CocoaPods version 1.10.0\n\n[\xe2\x9c\x93] Chrome - develop for the web\n    \xe2\x80\xa2 Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n\n[\xe2\x9c\x93] Android Studio (version 4.1)\n    \xe2\x80\xa2 Android Studio at /Applications/Android Studio.app/Contents\n    \xe2\x80\xa2 Flutter plugin can be installed from:\n       https://plugins.jetbrains.com/plugin/9212-flutter\n    \xe2\x80\xa2 Dart plugin can be installed from:\n       https://plugins.jetbrains.com/plugin/6351-dart\n    \xe2\x80\xa2 Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)\n\n[\xe2\x9c\x93] VS Code (version 1.52.1)\n    \xe2\x80\xa2 VS Code at /Applications/Visual Studio Code.app/Contents\n    \xe2\x80\xa2 Flutter extension version 3.18.1\n\n[\xe2\x9c\x93] Connected device (2 available)\n    \xe2\x80\xa2 AOSP on IA Emulator (mobile) \xe2\x80\xa2 emulator-5554 \xe2\x80\xa2 android-x86    \xe2\x80\xa2 Android 9 (API 28) (emulator)\n    \xe2\x80\xa2 Chrome (web)                 \xe2\x80\xa2 chrome        \xe2\x80\xa2 web-javascript \xe2\x80\xa2 Google Chrome 88.0.4324.96\n\n\xe2\x80\xa2 No issues found!\n
Run Code Online (Sandbox Code Playgroud)\n

以下是示例应用程序运行时的外观。由于 API 密钥集无效,日志会抛出“Connect failure”和“Forbidden:403”错误,但这证明 Twilio Android SDK 可以通过 Flutter 平台通道正常运行。

\n

演示

\n

您还可以在 pub.dev 中查看社区制作的可能适合您的用例的 Twilio Flutter 插件。

\n