把 ChatGPT 加入 Flutter 開發,會有怎樣的體驗?

語言: CN / TW / HK

前言

ChatGPT 最近一直都處於技術圈的討論焦點。它除了可作為普通用户的日常 AI 助手,還可以幫助開發者加速開發進度。聲網社區的一位開發者"小猿"就基於 ChatGPT 做了一場實驗。僅 40 分鐘就實現了一個互動直播 Demo。他是怎麼做的呢?他將整個過程記錄了下來。

(文章轉載自開發者的個人博客,以下為正文)


“遇事不決,AI 力學” ~ ChatGPT 可以説是 2023 開年最熱門的話題, 它不僅在極短時間內風靡了整個技術圈,更是病毒式地席捲了圈外的各個行業,並對各大企業都起到了實質性影響:

  • 谷歌緊急推出 “Bard” 對抗 ChatGPT
  • 微軟發佈新 Bing 集成 ChatGPT
  • 復旦發佈首個類 ChatGPT 模型 MOSS
  • 國內阿里、百度、崑崙萬維、網易、京東都開始新一輪 AI 軍備

那 ChatGPT 究竟有什麼魔力能讓“羣雄折腰”?這和 ChatGPT 的實現有很大關係:

與以往的統計模型不行,ChatGPT 不是那種「一切都從語料統計裏學習」的 AI,相反 ChatGPT 具備有臨場學習的能力,業內稱之為 in-context learning ,這也是為什麼 ChatGPT 可以在上下文中學習的原因。

ChatGPT 屬於 AI 領域在商用技術上的重大突破,當然,本篇我們不是要討論ChatGPT 的實現邏輯,而是 ChatGPT 會怎麼樣加速我們的開發?

PS:在此之前有人通過指示在 ChatGPT 界面下實現了一個虛擬機,雖然這是一個極端的例子,但是可以很直觀地感受到:「ChatGPT 對我們開發的影響是肉眼可見」。

那 ChatGPT 在實際工作中是如何影響我們的開發?為了更直觀,下面我們用一個開發場景來模擬這個流程。

基於 ChatGPT 開發

01 開發之前

假設我們現在有一個開發「直播」的需求,那我們可以直接求助 ChatGPT:

「開發一個直播app,是使用第三方SDK好還是自己從0開發好」?

如下圖所示,從回答上可以看到,AI 建議我們根據團隊實際情況去選擇,而在知曉「我的團隊只有 5 個人」的情況後,它建議我選擇採用 “接入第三方 SDK” 的方式更合理。

那麼選擇 “接入第三方 SDK” ,接下來的問題就是:「選擇做直播,在中國推薦使用哪些廠家的 SDK」?

如下圖所示,這個問題 ChatGPT 同樣提供了多個選項,從選項裏看*聲網、騰訊雲和阿里雲*好像都符合我們要求,而在接着的「優勢問題」對比上看,這三個選項都“不相伯仲”,那我們就在再細化問題。

假設我們希望直播可以有更多“互動能力”,那麼把問題修改為 「做互動直播,更推薦使用哪一個廠家的 SDK」 ,截圖如下圖所示,這次我們得到了更明確的答覆,看來聲網的 SDK 會更貼合我們的需求。

為了更放心這個選擇,我們通過 聲網 SDK 的優勢」「什麼產品使用了聲網SDK」 兩個問題進行提問,如下圖所示,從回覆上看聲網作為一個全球化的廠家,在音視頻領域還是值得相信。同時,還有包括小米、陌陌等產品都使用了聲網的服務。那麼就按照 AI 的建議選擇聲網 SDK 吧。

有沒有發現,在獲取資料的檢索方式上,ChatGPT 確實比搜索引擎更直觀且高效。

那麼敲定完 SDK ,接下來我們需要選擇應用的開發框架,我們把需求限定在 Android 和 iOS,更好是能兼容 Web,覆蓋整個移動端 ,因為團隊人數不多,所以我們希望採用跨平台開發來節約成本,那麼問題就是:

「移動端哪個跨平台框架更適合做直播」?

如下圖所示,得到的答案有 React Native 和 Flutter ,而恰好在 Flutter 回覆裏可以看到聲網 SDK 的存在,所以我們可以敲定 App 開發框架就選 Flutter 了。

最後,在開發之前,我們還需要繼續提問 「如何獲取聲網 SDK「使用聲網 SDK 需要做什麼」,這樣我們就可以在開始開發之前提前準備好需要的東西。

關於註冊獲取 App ID 等步驟這裏就省略了,畢竟目前這部分 ChatGPT 也無能為力。

02 開始開發

那麼到這裏我們就假定大家已經準備好了開發環境,接下來可以直接進行開發。

我們還是繼續面向 ChatGPT 開發,首先我們的提問是:「用聲網的 Flutter SDK agora_rtc_engine 6.1.0 寫一個視頻通話頁面,給我 dart 代碼」 ,結果如下 GIF 所示,可以看到 ChatGPT 開始了瘋狂的輸出:

為什麼關鍵詞是「視頻通話」?因為它比直播場景更精準簡單,生成的代碼更靠譜(經過提問測試),而基於視頻通話部分,後面我們可以快速拓展為互動直播場景;而指定版本是為了避免 AI 使用舊版本 API。

從上門的代碼生成可以看到,ChatGPT 生產的代碼是自帶中文註釋,更貼心的是,如下圖所示,在生成的代碼末尾還給你解釋了這段代碼的實現邏輯,就像一個“知心大姐姐”。

從這裏也可以感覺到 ,ChatGPT 不是一個單純的完全只會基於語料答覆整合的 AI 。

當然,直接複製生成的代碼後會發現這段代碼會報錯,這和 ChatGPT 目前的模型數據版本有一定關係,所以針對生成的代碼我們需要做一定手動調整,比如:

  • 採用 createAgoraRtcEngineinitialize 創建和初始化 RtcEngine
  • setEventHandler 修改為最新的 registerEventHandler
  • AgoraRenderWidget 修改為 AgoraVideoView

最後修改代碼如下,其中 80% 以上的邏輯都來自 ChatGPT 的自動生成,雖然沒辦法做到“直出”,這無疑大大提高了開發的生產力。

class VideoCallPage extends StatefulWidget {
  final String channelName;

  const VideoCallPage({Key? key, required this.channelName}) : super(key: key);

  @override
  _VideoCallPageState createState() => _VideoCallPageState();
}

class _VideoCallPageState extends State<VideoCallPage> {
  late RtcEngine _engine;
  bool _localUserJoined = false;
  bool _remoteUserJoined = false;
  int? rUid;

  @override
  void initState() {
    super.initState();
    initAgora();
  }

  @override
  void dispose() {
    _engine.leaveChannel();
    super.dispose();
  }

  Future<void> initAgora() async {
    await [Permission.microphone, Permission.camera].request();

    _engine = createAgoraRtcEngine();
    await _engine.initialize(RtcEngineContext(
      appId: config.appId,
      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
    ));

  

    _engine.registerEventHandler(RtcEngineEventHandler(
      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
        setState(() {
          _localUserJoined = true;
        });
      },
      onUserJoined: (connection, remoteUid, elapsed) {
        setState(() {
          _remoteUserJoined = true;
          rUid = remoteUid;
        });
      },
      onUserOffline: (RtcConnection connection, int remoteUid,
          UserOfflineReasonType reason) {
        setState(() {
          _remoteUserJoined = false;
          rUid = null;
        });
      },
    ));

    await _engine.enableVideo();

    await _engine.startPreview();

    await _engine.joinChannel(
      token: config.token,
      channelId: widget.channelName,
      uid: config.uid,
      options: const ChannelMediaOptions(
        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
        clientRoleType: ClientRoleType.clientRoleBroadcaster,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("VideoCallPage"),),
      body: Center(
        child: Stack(
          children: [
            _remoteUserJoined ? _remoteVideoView(rUid) : _placeholderView(),
            _localUserJoined ? _localVideoView() : _placeholderView(),
          ],
        ),
      ),
    );
  }

  Widget _placeholderView() {
    return Container(
      color: Colors.black,
    );
  }

  Widget _remoteVideoView(id) {
    return AgoraVideoView(
      controller: VideoViewController.remote(
        rtcEngine: _engine,
        canvas: VideoCanvas(uid: id),
        connection: RtcConnection(channelId: widget.channelName),
      ),
    );
  }

  Widget _localVideoView() {
    return Positioned(
      right: 16,
      bottom: 16,
      width: 100,
      height: 160,
      child: AgoraVideoView(
        controller: VideoViewController(
          rtcEngine: _engine,
          canvas: const VideoCanvas(uid: 0),
        ),
      ),
    );
  }
}

接下來,如下圖所示,在將項目運行到手機和 PC 端之後,可以看到我們就完成了最簡單的直播視頻場景,而基於我們打算做直播的念頭僅僅過去了 40 分鐘,這其中還包含了註冊聲網賬號和申請 App ID 的過程,我們通過簡單的提問、複製、粘貼、修改,就完成了一個直播需求的 demo。

紅色方塊是後期加上的打碼~

那麼到這裏,雖然目前為止 demo 項目還不是互動直播,但是基於這個 demo 實現互動直播場景不會太難,因為你已經跑通了整個 SDK 的鏈路流程了。

03 進階開發

那假設我們需要繼續往互動直播的方向開發,那麼我們肯定會遇到“互動”這個需求,比如「收到用户發送的一段內容後畫面彈出一個動畫」 這樣的需求。

那麼首先我們要知道聲網 SDK 如何監聽用户發送的內容,所以接下來我們繼續提問:「如何使用聲網的 agora_rtc_engine 6.1.0 監聽別人發送的文本消息」 ?

這裏為什麼還強制寫 agora_rtc_engine 6.1.0 ?因為如果不寫,默認可能會輸出 4.x 版本的老 API。

儘管得到的答案並不是 Dart 代碼而是 OC ,但是關鍵詞 registerEventHandlerMessage 我們捕抓到了,簡單對比一下,就是 Flutter SDK 裏的 registerEventHandler 對象,可以發現平替的接口就是 onStreamMessage 回調。

那麼接着就是彈出什麼內容,因為我們沒有素材,假設還沒有設計師,那不如就讓 ChatGPT 幫我們畫一隻兔子吧,不過測試結果並不好,如下圖所示,從輸出結果上看 ,這並不是我們想要的。

這裏是我自己加的粉色,不然都是白色會糊成一坨,不得不説 ChatGPT 在繪製能力上“很抽象”。

所以 ChatGPT 有時候也不是很智能,可能目前在繪畫理解上它還沒那麼成熟, 但是沒問題, ChatGPT 是可以通過上下文學習“調教”的,比如我們覺得兔子的耳朵形狀太離譜,那麼我們可以讓 ChatGPT 給我們調整。

如下所示,雖然調整之後依然不對,但是比起一開始是不是好很多了?

這就是 ChatGPT 在每次會話上下文裏學習的表現。

然後我們在兔子耳朵的基礎上再讓 ChatGPT 補全兔子頭,雖然最終的效果依然不理想,但是比起一開始已經進步了很多。

同時我們還讓 ChatGPT 給我們畫了一個“星星”,然後結合這兩個 Canvas 繪製的素材,我們在代碼裏設置接收到 "兔子" 和 星星 文本的時候,就彈出一個放大動畫效果。

最終運行後效果如下 GIF 所示,看起來很簡陋,但是要知道,我們只是經過了簡單的複製/粘貼就完成了這樣的效果,這難道不是開發效率的極大提高?

源碼在後面。

來自 ChatGPT 的兔子頭代碼:

class StarPaint extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: HeartPainter(),
      size: Size(50, 50),
    );
  }
}

class StarPaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;
    final path = Path();
    final halfWidth = size.width / 2;
    final halfHeight = size.height / 2;
    final radius = halfWidth;

    path.moveTo(halfWidth, halfHeight + radius);
    path.arcToPoint(
      Offset(halfWidth + radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight - radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth - radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight + radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant StarPaint oldDelegate) {
    return false;
  }
}

來自 ChatGPT 的星星代碼:

class StarPaint extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: HeartPainter(),
      size: Size(50, 50),
    );
  }
}

class StarPaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;
    final path = Path();
    final halfWidth = size.width / 2;
    final halfHeight = size.height / 2;
    final radius = halfWidth;

    path.moveTo(halfWidth, halfHeight + radius);
    path.arcToPoint(
      Offset(halfWidth + radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight - radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth - radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight + radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant StarPaint oldDelegate) {
    return false;
  }
}

自己補充的監聽文本、發送文本和動畫效果代碼:

onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
    Uint8List data, int length, int sentTs) {
  var message = utf8.decode(data);
  if (message == "兔子") {
    showDialog(
        context: context,
        builder: (context) {
          return AnimaWidget(Rabbit());
        });
  } else if (message == "星星") {
    showDialog(
        context: context,
        builder: (context) {
          return Center(
            child: AnimaWidget(StarPaint()),
          );
        });
  }
  Future.delayed(Duration(seconds: 3), () {
    Navigator.pop(context);
  });
},

Future<void> _onPressSend() async {
  try {
    final streamId = await _engine.createDataStream(
        const DataStreamConfig(syncWithAudio: false, ordered: false));
    var txt = (Random().nextInt(10) % 2 == 0) ? "星星" : "兔子";
    final data = Uint8List.fromList(utf8.encode(txt));
    await _engine.sendStreamMessage(
        streamId: streamId, data: data, length: data.length);
  } catch (e) {
    print(e);
  }
}

class AnimaWidget extends StatefulWidget {
  final Widget child;

  const AnimaWidget(this.child);

  @override
  State<AnimaWidget> createState() => _AnimaWidgetState();
}

class _AnimaWidgetState extends State<AnimaWidget> {
  double animaScale = 1;

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 1), () {
      animaScale = 5;
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
        scale: animaScale,
        duration: Duration(seconds: 1),
        curve: Curves.bounceIn,
        child: Container(child: widget.child));
  }
}

相信到這裏大家應該可以感受到 ChatGPT 提高開發效率的魅力,甚至你還可以把 ChatGPT 集成到你的直播場景裏,通過 Flutter 上的 chatgpt_api_client 插件,你可以在 App 裏直接向 ChatGPT 提問,比如通過 OpenAI 的 API 實現一個可以互動的虛擬主播。

我怎麼知道這個插件?肯定也是問 ChatGPT 的啊~

04 最後

到這裏,相信大家應該能感受到,在使用 ChatGPT 之後,**整個開發效率能夠得到很大的提升,特別是內容檢索的高效和準確上比搜索引擎更加靠譜,**另外也能幫我們完成一些“體力活”形式的代碼。

當然我們也看到了目前 ChatGPT 並不能完全替代人工,因為它在很多方面生成的內容並不完美,特別是很多代碼還是需要我們人工調整,但是這並不影響 ChatGPT 的價值。

最後引用我曾經看到過的關於 ChatGPT 的一些評價:

「當你抱怨 ChatGPT鬼話連篇滿嘴跑火車的時候,這可能有點像你看到一隻猴子在沙灘上用石頭寫下1+1=3。它確實算錯了,但這不是重點。它有一天會算對的。」

我相信 AI 並不是直接取代人類的方式,因為它對社會的擠壓不是從水平上碾壓,而是劣幣驅逐良幣,比如有位大佬就説過:「乙方最討厭甲方什麼都不懂還bb,但乙方的議價權恰恰來源於甲方什麼都不懂還 bb」 ,而現在 ChatGPT 在慢慢消磨掉整個議價權。

總的來説「ChatGPT 只是一個產品,它不代表的整個技術的“上限” ,它代表的是技術已經到達商用的臨界點」。

現在,它在慢慢成為開發圈子裏的習慣,和曾經的 Copilot 一樣,而同時它在其他領域如文字編排等的能力,甚至遠超它在開發領域的價值。


歡迎開發者們也嘗試體驗聲網 SDK,實現實時音視頻互動場景。現註冊聲網賬號下載 SDK,可獲得每月免費 10000 分鐘使用額度。如在開發過程中遇到疑問,可在聲網開發者社區與官方工程師交流。