引言

ChatGPT 介绍

ChatGPT(Chatbot based on Generative Pre-trained Transformer)是一种基于GPT(Generative Pre-trained Transformer)架构的聊天机器人。GPT是一种自然语言处理(NLP)模型,由OpenAI开发。GPT的原理是使用大量文本数据进行预训练,以生成能够理解和生成自然语言的深度学习模型。这些模型可以捕捉到语言中的语法、语义和上下文信息,从而在各种任务中表现出卓越的性能。

ChatGPT由多个GPT版本组成,包括GPT-2、GPT-3、GPT-3.5、GPT-4 等。随着版本的升级,模型变得更大、更强大,可以生成更加连贯、准确和有趣的回答。这些模型可以用于创建各种应用,如智能对话系统、内容生成、翻译、问答系统等。

使用ChatGPT的过程通常包括以下步骤:

  1. 准备数据:GPT模型需要大量的文本数据进行预训练。数据可以来自网络、书籍、新闻等各种来源。数据需要进行清洗、预处理和格式化,以便模型可以正确地从中学习。
  2. 预训练:使用预处理后的文本数据对GPT模型进行预训练。这一步会让模型学会如何生成连贯的句子和理解上下文信息。预训练过程通常在大型GPU集群上进行,需要消耗大量的计算资源。
  3. 微调:对预训练模型进行微调,使其适应特定任务,如回答问题或生成文本。微调过程可以在小型GPU上进行,需要较少的计算资源。
  4. 应用:将微调后的模型部署到应用程序中,如聊天机器人、内容生成器等。用户可以与模型进行交互,提问或发送指令,模型会根据输入生成相应的回答或操作。

ChatGPT是一种强大且灵活的聊天机器人,可以用于创建多种自然语言处理应用。然而,它仍然存在一些局限性,如可能生成不准确或不相关的回答、容易受到输入数据偏见的影响等。尽管如此,随着研究和技术的不断发展,这些问题有望得到解决。

为什么使用 Flutter 进行开发

Flutter是Google开发的一个开源UI框架,用于创建高质量、跨平台的原生应用程序。选择Flutter进行开发有很多理由,以下是一些主要的优势:

  1. 跨平台开发:Flutter允许您使用单一的代码库开发Android和iOS应用程序,这可以大大减少开发和维护成本。此外,Flutter还支持Web、macOS、Windows和Linux平台,使其成为一个真正的全平台开发框架。
  2. 快速开发与热重载:Flutter提供了热重载功能,这意味着在开发过程中,您可以实时查看对代码所做的更改,而无需重新启动应用程序。这大大加快了开发速度,并提高了开发者的生产力。
  3. 丰富的组件库:Flutter提供了丰富的预构建组件库,包括Material Design和Cupertino风格的组件。这些组件可以帮助您快速搭建应用程序的界面,而无需从零开始。
  4. 高性能:Flutter使用Dart语言进行开发,Dart代码会被编译成本地ARM或x86代码,这使得Flutter应用程序具有接近原生应用程序的性能。
  5. 定制能力:Flutter提供了高度可定制的UI组件,使您可以轻松地调整应用程序的外观和感觉。此外,由于Flutter的绘制引擎是基于GPU的,这意味着您可以创建具有复杂动画和视觉效果的应用程序,而不会对性能造成太大影响。
  6. 社区支持:Flutter拥有一个庞大的开发者社区,这意味着您可以在遇到问题时轻松地找到解决方案和资源。此外,社区还为Flutter贡献了大量的第三方库和插件,以帮助您更快地开发功能丰富的应用程序。
  7. Google支持:由于Flutter是Google的一个项目,因此它得到了Google的大力支持。这意味着您可以期待不断的更新和改进,以及对新技术的及时适配。

综上所述,使用Flutter进行开发可以带来许多优势,包括跨平台开发、快速开发、高性能、丰富的组件库、定制能力、庞大的社区支持以及Google的支持。这些优点使Flutter成为一个值得考虑的应用程序开发框架。

本书目标和受众

本书的目标是向读者介绍如何使用Flutter构建一个全平台的ChatGPT客户端。我们将详细讨论Flutter框架的基础知识、Dart编程语言、UI设计、状态管理、API集成和性能优化等方面。此外,我们还将深入探讨如何实现语音输入与输出功能,使应用程序更具交互性。

本书的受众包括以下几类:

  1. 初学者:对移动应用程序开发和Flutter感兴趣,希望从零开始学习如何使用Flutter构建跨平台应用程序的初学者。
  2. 有经验的开发者:已经具备一定的移动应用开发经验,希望了解更多关于Flutter和Dart的知识,并探索如何使用Flutter构建聊天机器人客户端的开发者。
  3. 聊天机器人爱好者:对聊天机器人技术感兴趣,希望学习如何构建一个基于ChatGPT的聊天机器人客户端的读者。
  4. 产品经理和设计师:希望了解更多关于Flutter和聊天机器人技术的产品经理和设计师,以便更好地规划和设计跨平台应用程序。

通过阅读本书,您将了解到Flutter框架的核心概念,掌握使用Dart进行开发的技巧,以及如何实现一个功能丰富、性能优越的ChatGPT客户端。本书将为您提供实际的示例和实践经验,帮助您迅速上手并成功构建您自己的聊天机器人客户端。

Flutter 环境安装

首先,确保你的开发环境满足以下系统要求:

  • Windows 7 SP1或更高版本、macOS 10.13.6或更高版本、Linux (Ubuntu 20.04, Debian 10, Fedora 33, Arch Linux)。
  • 至少需要2.8 GB可用磁盘空间。

下载Flutter SDK

  1. 访问Flutter官方网站:安装和环境配置 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
  2. 选择你的操作系统(Windows、macOS或Linux)。
  3. 根据提示下载最新版本的Flutter SDK压缩包。

解压和配置Flutter SDK

  1. 解压下载的Flutter SDK压缩包到合适的位置,例如C:\src\flutter(Windows)或~/development/flutter(macOS或Linux)。

  2. 将Flutter SDK的bin目录添加到系统的PATH环境变量中。

    • Windows:

      1. 在开始菜单中搜索"环境变量"并点击"编辑系统环境变量"。
      2. 在"系统属性"对话框中,点击"环境变量"按钮。
      3. 在"系统变量"下找到"Path",然后点击"编辑"。
      4. 点击"新建",然后将Flutter SDK的bin目录路径(如C:\src\flutter\bin)添加到列表中。
    • macOS或Linux:

      1. 打开终端。

      2. 在终端中运行以下命令:

        echo 'export PATH="$PATH:[PATH_TO_FLUTTER_SDK]/flutter/bin"' >> ~/.bashrc
        

        其中,[PATH_TO_FLUTTER_SDK]需要替换为实际的Flutter SDK路径,例如/Users/username/development

      3. 关闭并重新打开终端,使更改生效。

安装Flutter命令行工具

  1. 在终端或命令提示符中,运行以下命令:

    flutter doctor

    此命令将检查你的开发环境,并显示可能需要安装或配置的任何依赖项。

  2. 根据flutter doctor的输出,按照提示安装或配置所需的依赖项,例如Android Studio、Xcode、iOS Simulator等。

设置开发环境

根据你的喜好选择合适的集成开发环境(IDE)。以下是一些建议:

  • Android Studio或IntelliJ IDEA:安装Flutter插件和Dart插件。
  • Visual Studio Code:安装Flutter扩展和Dart扩展。

在本节中,我们重点介绍如何在Visual Studio Code中安装Flutter和Dart插件。

  1. 安装Visual Studio Code:

    如果尚未安装Visual Studio Code,请访问官方网站 (https://code.visualstudio.com/) 下载并安装适用于你操作系统的版本。

  2. 打开Visual Studio Code:

    安装完成后,启动Visual Studio Code。

  3. 安装Flutter和Dart插件:

    1. 点击左侧边栏中的Extensions图标(或按Ctrl+Shift+X),以打开Extensions视图。

    2. 在搜索框中输入"Flutter",找到名为"Flutter"的插件,此插件由Dart Code开发。点击"Install"按钮以安装插件。安装Flutter插件时,Dart插件会自动安装,无需额外操作。

    3. 安装完成后,重启Visual Studio Code以使更改生效。

至此,你已经成功在Visual Studio Code中安装了Flutter和Dart插件。现在,你可以创建、运行和调试Flutter项目了。以下是一些基本的Flutter命令:

  • 创建新项目:按Ctrl+Shift+P打开命令面板,输入"Flutter: New Project",然后按Enter,按照提示输入项目名称和路径。
  • 运行项目:在项目文件夹下,按F5启动调试会话。确保设备模拟器或实际设备已连接。
  • 查看已连接设备:按Ctrl+Shift+P打开命令面板,输入"Flutter: List Devices",然后按Enter。
  • 查看Flutter大纲:打开一个Flutter代码文件,在右侧边栏中可以看到Flutter大纲,显示了当前文件的组件和结构。

现在,你可以开始使用Visual Studio Code进行Flutter开发了。

Dart 基础

Dart product logo

Dart是一种面向对象、类定义、垃圾回收的编程语言,由Google开发并用于构建移动、桌面、服务器和Web应用。Flutter框架就是使用Dart语言编写的。Dart的语法类似于C和Java,因此对于熟悉这些语言的开发者来说很容易上手。下面是一些Dart语言的基本概念和特性。

  1. 变量和类型:

    • 变量使用var关键字声明,类型可以是静态(如intStringbool等)或动态(dynamic)。
    • Dart支持类型推断,因此在声明变量时可以省略类型。
    • 使用finalconst关键字定义不可变变量。
  2. 控制流程:

    • ifelse ifelse用于条件判断。
    • forwhile用于循环,breakcontinue用于控制循环流程。
    • switchcase用于多条件判断。
  3. 函数:

    • 使用void关键字定义没有返回值的函数,其他返回类型直接使用类型名。
    • 支持匿名函数(lambda表达式)和箭头函数(单行函数)。
    • 支持可选参数(位置可选参数和命名可选参数)和默认参数值。
  4. 面向对象编程:

    • 使用class关键字定义类,extends关键字用于继承。
    • 支持抽象类(abstract关键字)和接口。
    • 构造函数可以有多个(命名构造函数),可以使用初始化列表。
    • 支持getter和setter方法,用于属性访问和修改。
  5. 异步编程:

    • 使用Futureasync/await进行异步编程。
    • 使用Stream处理异步数据流。
  6. 错误处理:

    • 使用trycatchfinally处理异常。
    • 使用throw抛出异常,自定义异常类可以继承自Exception
  7. 集合:

    • Dart中的集合有List(列表)、Set(集合)和Map(映射)。
    • 支持集合字面量和构造函数创建集合。
    • 支持集合操作,如添加、删除、查找和修改元素。
  8. 导入和库:

    • 使用import关键字导入库,支持导入核心库、外部库和自定义库。
    • 使用librarypartpart of关键字构建模块化代码。

了解这些基本概念和特性后,你就可以开始使用Dart语言进行Flutter开发了。要深入了解Dart语言,请参阅官方文档

Flutter框架和组件

Flutter是一个用于构建高性能、高保真的移动、Web和桌面应用程序的UI工具包。它使用Dart编程语言,并提供了一系列预制的组件,使开发过程更加快捷。在本节中,我们将概述Flutter框架的基本结构和常用组件。

  1. Flutter基本结构

    • Widgets(组件):在Flutter中,一切皆为组件。组件是构建用户界面的基本单元。组件可以嵌套在一起以构建复杂的UI。
    • StatelessWidget(无状态组件):不需要维护内部状态的组件。这些组件只需要根据给定的配置渲染UI即可。
    • StatefulWidget(有状态组件):需要维护内部状态的组件。这些组件可以根据状态变化来更新UI。
  2. 布局组件

    • Container:一个包含子组件的矩形容器,可以设置大小、边距、填充、背景等属性。
    • Row和Column:水平或垂直方向上排列子组件的线性布局。
    • Stack:允许子组件重叠的布局,可以用来创建具有重叠元素的界面。
    • Expanded:根据父组件的可用空间分配给子组件的空间,常用于Row和Column中。
    • SizedBox:一个固定尺寸的组件,用于调整子组件的大小或添加空白间隔。
    • SingleChildScrollView:使子组件具有滚动功能的组件,当子组件超出屏幕范围时,可以滚动查看。
  3. UI组件

    • Text:用于显示文本内容的组件,可以设置字体、大小、样式等。
    • Image:用于显示图片的组件,支持本地资源、网络资源和内存中的图片数据。
    • IconButton:一个图标按钮,可以设置点击事件和图标样式。
    • RaisedButton/FlatButton:常用的矩形按钮组件,可以设置点击事件、背景、阴影等样式。
    • FloatingActionButton:一个圆形按钮,通常位于屏幕底部,并具有主要操作功能。
    • TextField:用于输入文本的组件,支持键盘输入、文本样式、输入类型等。
  4. 导航和路由

    • MaterialApp:应用程序的根组件,包含了主题、导航和路由等全局配置。
    • Scaffold:一个基本的视觉结构,包含了app bar、底部导航栏、抽屉菜单等通用布局元素。
    • Navigator:用于管理应用程序的页面堆栈和导航的组件。
    • MaterialPageRoute:表示应用程序中的一条导航路径,通常对应一个页面。
  5. 状态管理

状态管理是构建应用程序时的关键部分,尤其是当应用程序的规模和复杂性增加时。在Flutter中,有多种状态管理方法和库可供选择。以下是一些常用的状态管理技术:

  1. setState

    在有状态组件(StatefulWidget)内部使用setState来更新状态。这是最简单的状态管理方法,适用于简单的场景。当状态变化时,Flutter将重新构建整个组件树。

  2. InheritedWidget

    一个特殊类型的组件,允许其数据在组件树中向下传递。子组件可以通过BuildContext访问InheritedWidget中的数据。这种方法适用于跨多个组件共享数据的情况。

  3. Provider

    Provider是一个Flutter状态管理库,用于在组件树中向下传递数据和创建可重用的状态对象。它基于InheritedWidget,但提供了更简洁的API和更丰富的功能。以下是一些Provider的核心概念:

    • ChangeNotifier:一个可通知侦听器的数据模型,通常用于存储应用程序状态。
    • ChangeNotifierProvider:将ChangeNotifier对象插入到组件树中,并在需要时销毁它。
    • Consumer:一个组件,可重建以响应ChangeNotifier中的状态更改。
  4. Riverpod

    Riverpod是一个声明式状态管理库,旨在解决Provider的一些限制。它具有以下特点:

    • 类型安全:Riverpod具有更好的类型安全性,可以减少错误。
    • 灵活性:允许多个提供者共享相同的状态,并简化了依赖注入。
    • 静态分析:提供的静态分析工具可以帮助你在编写代码时发现错误。

    Riverpod提供了类似于Provider的API,但具有更高的灵活性和可组合性。

了解这些状态管理方法后,你可以根据应用程序的需求和复杂性选择合适的策略。初学者可以从setStateInheritedWidget开始,然后逐步尝试使用ProviderRiverpod等更高级的库。

布局和UI

在本节中,我们将介绍Flutter中的布局和UI设计。布局是应用程序的基础,它决定了组件在屏幕上的位置和尺寸。UI设计则涉及到组件的外观和交互。以下是一些在Flutter中创建布局和UI的关键概念。

  1. 基本布局组件

    • Padding:用于给子组件添加内边距的组件。使用EdgeInsets类设置上、下、左、右的边距值。
    • Center:用于将子组件置于其父组件中心的组件。
    • Align:用于根据指定对齐方式对齐子组件的组件。使用Alignment类设置对齐方式,如topLeftcenter等。
    • AspectRatio:用于调整子组件的尺寸,使其保持指定的宽高比。
    • ConstrainedBox:用于限制子组件的尺寸范围。使用BoxConstraints类设置最大/最小宽度和高度。
    • FractionallySizedBox:用于根据父组件尺寸的百分比设置子组件尺寸的组件。
  2. 自适应布局

    • MediaQuery:用于查询屏幕信息(如尺寸、方向、亮度等)的组件。根据屏幕信息调整布局使其适应不同设备。
    • LayoutBuilder:根据其父组件的约束条件构建布局。可以用来创建响应式布局,适应不同屏幕尺寸。
    • OrientationBuilder:根据设备方向(横屏或竖屏)构建布局。
  3. 主题和样式

    • ThemeData:用于定义应用程序的全局主题样式,包括颜色、字体、组件样式等。可通过Theme.of(context)获取当前主题。
    • TextTheme:用于定义文本组件的样式,包括字体、颜色、大小等。可通过Theme.of(context).textTheme获取。
    • TextStyle:用于定义文本的样式,可单独设置或继承自TextTheme。
    • BoxDecoration:用于设置容器组件(如Container、BoxDecoration)的背景、边框、阴影等样式。
    • CustomPaint:用于自定义绘制图形、路径等的组件。结合CustomPainter类实现自定义绘制逻辑。
  4. 动画和交互

    • AnimationController:用于控制动画的播放、暂停、循环等操作的类。
    • Tween:用于定义动画开始值和结束值之间的插值。支持多种类型,如ColorTweenSizeTween等。
    • AnimatedWidget:用于创建隐式动画组件的基类。例如:AnimatedOpacityAnimatedPositioned等。
    • AnimatedBuilder:用于根据动画值重建子组件的组件。与AnimationControllerTween一起使用,以创建高度可定制的动画。
    • Hero:一种特殊类型的动画,用于在页面之间创建共享元素过渡效果。Hero组件包含一个唯一的tag,用于在不同页面之间识别共享元素。
    • GestureDetector:用于处理触摸事件(如点击、长按、拖动等)的组件。可以包装其他组件以为其添加交互功能。
    • InkWell:一个具有水波纹效果的触摸反馈组件。通常用于包装按钮、列表项等可交互元素。
  5. 列表和滚动视图

    • ListView:用于创建可滚动的列表视图。支持垂直和水平方向滚动。可以根据子组件的数量自动调整长度。
    • GridView:用于创建可滚动的网格视图。支持自定义每行/列的组件数量和间距。
    • CustomScrollView:一个高度可定制的滚动视图。可以包含多种类型的滚动内容,如列表、网格、悬停的app bar等。
    • Sliver:一种特殊类型的组件,用于构建CustomScrollView的子组件。常见的Sliver组件有:SliverListSliverGridSliverAppBar等。
    • RefreshIndicator:用于创建下拉刷新功能的组件。通常与滚动视图(如ListView、CustomScrollView)一起使用。

    通过了解这些布局和UI概念,你将能够在Flutter中创建各种类型的应用程序。请记住,实践是最好的学习方法,因此建议通过实际项目来熟练掌握这些概念。

客户端 UI 分析

ChatGPT 既然是Chat,那总归需要有个聊天窗口的,我们先来看下ChatGPT的界面。非常简单,总共三部分:聊天窗口、历史记录、其他。本章节我们先来专注于聊天窗口这部分。

Screenshot 2023-04-04 at 09.05.42

聊天窗口

我们来分析下如何使用Flutter的基础组件,一步步做出这个界面。

  1. 聊天消息列表:

    • 使用ListView.builder来创建一个可滚动的聊天消息列表。这种方法可以根据需要加载列表项,提高性能。
    • 对于每条消息,创建一个自定义的消息组件,包括文本、头像、时间戳等元素。可以使用ContainerRowColumn等布局组件来组织这些元素。
    • 根据消息的发送者(用户或ChatGPT)来调整消息组件的对齐方式和样式。例如,用户消息可以显示在右侧,ChatGPT消息显示在左侧。
  2. 输入框消息发送按钮

    • 使用TextField组件创建一个输入框,用户可以在其中输入消息。可以自定义输入框的样式,如边框、背景色、字体等。
    • 创建一个发送按钮,可以使用IconButton。将按钮与输入框放在同一行,可使用Row组件进行布局。
    • 为发送按钮添加点击事件。当用户点击发送按钮时,将输入框中的文本添加到消息列表,并清空输入框。
  3. 其他UI元素和功能

    • 添加一个顶部的app bar,显示聊天界面的标题或其他功能按钮(如设置、帮助等)。

通过结合这些设计元素和功能,你将能够创建一个功能完善、易于使用的聊天界面。请注意,界面设计应该保持简洁且直观,确保用户能够轻松地与ChatGPT进行交互。接下来我们来具体实现:

新建一个 chatgpt 项目

  1. 首先创建一个 Flutter 项目 flutter create chatgpt

  2. 使用 vscode 打开下项目,你会看到一下内容

    Screenshot 2023-04-04 at 09.40.32

  3. F5 运行一下项目,选择你喜欢的平台,web、windows、macOS、Android、iOS都可以,这里以macOS为例

    Screenshot 2023-04-04 at 09.47.12

  4. 能看到以上界面说明成功了,接下来我们进入正题

创建聊天窗口

  1. 新建一个widgets 文件夹,新建一个 chat_screen.dart

    Screenshot 2023-04-04 at 09.50.20

  2. 新建一个 ChatScreen class

    import 'package:flutter/material.dart';
    
    class ChatScreen extends StatelessWidget {
      const ChatScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Chat'),
          ),
          body: const Center(
            child: Text('Chat Screen'),
          ),
        );
      }
    }
    
  3. 修改 main.dart, 改为一下代码

    import 'package:chatgpt/widgets/chat_screen.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'ChatGPT',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const ChatScreen(),
        );
      }
    }
    
    
  4. F5 运行一下,看下效果

    Screenshot 2023-04-04 at 09.55.36

  5. 继续修改 build 函数

    Widget build(BuildContext context) {
      Widget build(BuildContext context) {
        return Scaffold(
         // 省略。。。
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                Expanded(
                  // 聊天消息列表
                  child: ListView.separated(
                    itemBuilder: (context, index) {
                      return Row(
                        children: [
                          const CircleAvatar(
                            child: Text('A'),
                          ),
                          const SizedBox(
                            width: 8,
                          ),
                          Text('Message $index'),
                        ],
                      );
                    },
                    itemCount: 5, // 消息数量
                    separatorBuilder: (context, index) => const SizedBox(
                      height: 8,
                    ),
                  ),
                ),
                // 输入框
                TextField(
                  decoration: InputDecoration(
                      hintText: 'Type a message', // 显示在输入框内的提示文字
                      suffixIcon: IconButton(
                        onPressed: () {
                          // 这里处理发送事件
                        },
                        icon: const Icon(
                          Icons.send,
                        ),
                      )),
                ),
              ],
            ),
          ),
        );
    

    增加了两部分,一个部分是消息列表,底部是输入框

    Screenshot 2023-04-04 at 10.26.52

    基本页面显示完成了,接下来我们来设计下数据模型来存储消息

数据模型

我们需要创建一个消息模型以存储和管理聊天消息。消息模型将包含消息的内容、发送者、时间戳等信息。以下是一个简单的消息模型示例:

  1. 首先,在项目中创建一个名为models的新文件夹,并在其中创建一个名为message.dart的文件。
  2. message.dart中,定义一个Message类,并为其添加以下属性:
    • String content:消息的文本内容。
    • bool isUser:标识消息是由用户发送还是由ChatGPT发送。true表示用户发送,false表示ChatGPT发送。
    • DateTime timestamp:消息的发送时间。
class Message {
  final String content;
  final bool isUser;
  final DateTime timestamp;

  Message({required this.content, required this.isUser, required this.timestamp});
}
  1. 为了方便地创建和管理消息列表,可以为Message类添加一些实用方法,如:
  • toJson():将Message对象转换为JSON格式。
  • fromJson(Map<String, dynamic> json):从JSON数据中创建一个Message对象。

class Message {
  final String content;
  final bool isUser;
  final DateTime timestamp;

  Message({required this.content, required this.isUser, required this.timestamp});

  Map<String, dynamic> toJson() => {
        'content': content,
        'isUser': isUser,
        'timestamp': timestamp.toIso8601String(),
      };

  factory Message.fromJson(Map<String, dynamic> json) => Message(
        content: json['content'],
        isUser: json['isUser'],
        timestamp: DateTime.parse(json['timestamp']),
      );
}

现在我们已经创建了一个简单的消息模型,接下来可以使用这个模型来存储和显示聊天消息。

组装数据到界面

  1. 初始化数据, ChatSreen 类中初始化一下数据

    
      final List<Message> messages = [
        Message(content: "Hello", isUser: true, timestamp: DateTime.now()),
        Message(content: "How are you?", isUser: false, timestamp: DateTime.now()),
        Message(
            content: "Fine,Thank you. And you?",
            isUser: true,
            timestamp: DateTime.now()),
        Message(content: "I am fine.", isUser: false, timestamp: DateTime.now()),
      ];		
    
  2. 更新下 Listview 中的数据

                            
    											// ....
                          ),
                          Text(messages[index].content),
                        ],
                      );
                    },
                    itemCount: messages.length, // 消息数量
    

    更新完成后,你会看到一下效果

    Screenshot 2023-04-04 at 10.47.34

  3. 消息列表中,用户和ChatGPT的消息区分不开,我们需要重构下消息Item的内容, 新建MessageItem 类,根据 isUser 讲头像显示不同的背景色

    
    class MessageItem extends StatelessWidget {
      const MessageItem({
        super.key,
        required this.message,
      });
    
      final Message message;
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            CircleAvatar(
              backgroundColor: message.isUser ? Colors.blue : Colors.grey,
              child: Text(
                message.isUser ? 'A' : 'GPT',
              ),
            ),
            const SizedBox(
              width: 8,
            ),
            Text(message.content),
          ],
        );
      }
    }
    
    
    // ....
          			// 聊天消息列表
                  child: ListView.separated(
                    itemBuilder: (context, index) {
                      return MessageItem(message: messages[index]);
                    },
                    itemCount: messages.length, // 消息数量
                    separatorBuilder: (context, index) => const Divider(
                      height: 16,
                    ),
                  ),
                ),
    

    Screenshot 2023-04-04 at 11.00.28

用户输入

  1. 为了后去输入框的内容,需要给 textfield绑定一个controller,我们在ChatScreen中新建一个

     final _textController = TextEditingController();
    
    //...
    
         // 输入框
                TextField(
                  controller: _textController,  //这里绑定
    
  2. 新建一个函数来处理输入框的内容

    
      _sendMessage(String content) {
        final message =
            Message(content: content, isUser: true, timestamp: DateTime.now());
        messages.add(message);
        _textController.clear();
      }
    
  3. 在按钮的点击事件中绑定此函数

                suffixIcon: IconButton(
                        onPressed: () {
                          // 这里处理发送事件
                          if (_textController.text.isNotEmpty) {
                            _sendMessage(_textController.text);
                          }
                        },
    
  4. 在输入框中输入文字,点击按钮测试下,输入框的内容被清除了,但是界面上没有任何更新。接下来就是我们后面需要做的事情,Flutter 开发中的状态管理。

状态管理和Riverpod

在前面的代码中我们完成了一个聊天界面,但是用户在输入消息之后并没有更新的界面上。其根本是我们的UI 没有知道messages更新,因此我们需要想办法让UI知道我们有数据更新了。这里就需要状态管理了。

什么是状态管理

状态管理是一个关键的概念,用于处理应用程序中数据的流动和更新。在Flutter应用程序中,状态管理是确保应用程序UI和数据保持同步的方法。状态管理的主要目的是在应用程序的不同部分共享和同步数据,同时提供良好的代码结构和可维护性。

在Flutter中,有多种状态管理方法可供选择。以下是一些常见的状态管理方法:

  1. StatefulWidget:这是Flutter中最基本的状态管理方法。有状态的组件可以存储自己的状态,并在需要时更新。这种方法适用于简单的场景,例如独立的UI元素或局部状态。
  2. InheritedWidgetInheritedModel:这些是允许状态在组件树中向下传递的特殊类型的组件。它们可以帮助您在应用程序的不同层级之间共享状态。这种方法对于较小的应用程序或有限的状态共享需求较为合适。
  3. Provider:Provider是一个依赖注入和状态管理库,它允许您在组件树中向下传递数据和状态。Provider可以监听状态变化,并在需要时重新构建关联的组件。这种方法适用于各种规模的应用程序,具有良好的可扩展性和灵活性。
  4. Riverpod:Riverpod是一个相对较新的状态管理库,它类似于Provider,但提供了更多的功能和改进。Riverpod允许您创建不可变的、可组合的和可测试的状态管理解决方案。这种方法适用于需要更高度可控和可测试性的应用程序。
  5. BLoC(Business Logic Component):BLoC是一种基于响应式编程的状态管理方法。BLoC将业务逻辑与UI分离,使您可以轻松地测试和重用代码。BLoC通常与RxDart(一种Dart的响应式编程库)一起使用,以提供强大的数据流处理能力。这种方法适用于需要处理复杂业务逻辑和大量数据流的应用程序。
  6. Redux:Redux是一种集中式状态管理库,它将应用程序的状态存储在一个单一的状态树中。Redux使用纯函数(称为reducers)来处理状态更新,使您可以轻松地跟踪和管理应用程序的状态变化。这种方法适用于需要严格的状态管理和可预测性的应用程序。

选择合适的状态管理方法取决于您的应用程序的需求、复杂性和个人喜好。不同的方法有不同的优缺点,因此在选择状态管理方法时,请务必充分了解每种方法的特点,并权衡其适用性。

在本章节中,我们选择了Riverpod 作为状态管理的解决方案

[Riverpod](New Tab (riverpod.dev))

Riverpod是一个状态管理库,用于构建Flutter应用程序。它是Provider库的改进版,由同一作者开发。Riverpod解决了Provider的一些局限性,并引入了更多功能和改进。以下是Riverpod的一些主要特点:

  1. 不可变性:Riverpod中的状态是不可变的,这意味着状态在更新时会创建一个新的对象,而不是修改现有对象。这有助于减少错误,并使状态更易于理解和跟踪。
  2. 类型安全:Riverpod在编译时提供了更强的类型安全性,有助于减少类型错误并提高代码质量。
  3. 无需BuildContext:与Provider不同,Riverpod不依赖于BuildContext来访问状态。这使得在组件之外的位置(如函数或类)访问状态变得更加容易,同时提高了可测试性。
  4. 可组合:Riverpod允许您组合不同的Provider以创建更复杂的状态管理解决方案。这有助于保持代码的模块化和可维护性。
  5. 易于测试:由于Riverpod的状态不依赖于BuildContext,您可以更轻松地编写单元测试。此外,Riverpod提供了用于模拟状态和测试的实用工具。
  6. 家族功能:Riverpod具有所谓的“家族”功能,允许您根据参数创建多个相同类型的Provider实例。这使得在使用相同逻辑但参数不同的多个组件时可以更好地管理状态。
  7. 非常灵活:Riverpod具有很高的灵活性,可以很好地适应不同的应用程序结构和需求。您可以使用Riverpod来构建简单的局部状态管理,或者构建复杂的全局状态管理解决方案。

总之,Riverpod是一个强大的状态管理库,适用于各种规模的Flutter应用程序。它提供了不可变性、类型安全性、无需BuildContext的访问、可组合性、易于测试和家族功能等多种优点。如果您正在寻找一个现代、灵活且易于使用的状态管理解决方案,Riverpod是一个值得考虑的选择。[更多相关的内容直接参考相关文章](Flutter Riverpod 全面深入解析,为什么官方推荐它? - 掘金 (juejin.cn))

使用 Riverpod来改造我们的程序

  1. pubspec.yaml中添加以下依赖,然后执行flutter pub get

    dependencies:
      # 。。。
    	flutter_hooks: ^0.18.0
      hooks_riverpod: ^2.3.2
    
  2. main.dart 中添加 ProviderScope

    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    void main() {
      runApp(const ProviderScope(child: MyApp()));
    }
    
    
  3. 修改 ChatScreen ,使其继承自HookConsumerWidget, build函数需要对应修改

    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    class ChatScreen extends HookConsumerWidget {
      ChatScreen({super.key});
    // ....
      @override
      Widget build(BuildContext context, WidgetRef ref) {
    
  4. 新建states 文件夹,然后新建message_state.dart, 来管理 message

    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    import '../models/message.dart';
    
    class MessageList extends StateNotifier<List<Message>> {
      MessageList() : super([]);
    
      void addMessage(Message message) {
        state = [...state, message];
      }
    }
    
    final messageProvider = StateNotifierProvider<MessageList, List<Message>>(
      (ref) => MessageList(),
    );
    
    
  5. 修改 ChatScreen使用messageProvider 来管理数据

      Widget build(BuildContext context, WidgetRef ref) {
        final messages = ref.watch(messageProvider);  // 获取数据
        return Scaffold(
          //....
          
    
      // 增加WidgetRef
      _sendMessage(WidgetRef ref, String content) {
        final message =
            Message(content: content, isUser: true, timestamp: DateTime.now());
        ref.read(messageProvider.notifier).addMessage(message); // 添加消息
        _textController.clear();
      }
    
                          if (_textController.text.isNotEmpty) {
                            _sendMessage(ref, _textController.text); // 增加 ref 参数
                          }
    
  6. 运行后,可以查看下效果。输入消息后,点击发送,可以直接更新到消息列表了。

    Screenshot 2023-04-04 at 13.19.19 到此我们已经完成了一个基本的聊天界面,接下来的章节我们将会对接 ChatGPT API了。

ChatGPT API 接入

在开始本章节之前,我们默认你已经注册过 openai 账号,并且已经获取到了相应的 api key。如果没有的话,请自行baidu 解决。

ChatGPT SDK vs API

ChatGPT 有两种方式接入,直接使用官方API,或者使用社区的SDK。为了简单我们直接使用openai_api - dart SDK来处理次问题。

openai_api

此SDK提供了我们客户端开发需要2个关键API (ChatCompletion 以及 Whisper),同时提供了代理以及自建API服务的支持。

开始接入

  1. 添加依赖 flutter pub add openai_api

  2. 新建一个services文件夹,并创建一个chatgpt_service.dart

    import 'package:openai_api/openai_api.dart';
    
    class ChatGPTService {
      final client = OpenaiClient(
        config: OpenaiConfig(
          apiKey: "", // 你的key
          baseUrl: "",  // 如果有自建反向代理请设置这里
          httpProxy: "",  // 代理服务地址
        ),
      );
    
      Future<ChatCompletionResponse> sendChat(String content) async {
        final request = ChatCompletionRequest(model: Model.gpt3_5Turbo, messages: [
          ChatMessage(
            content: content,
            role: ChatMessageRole.user,
          )
        ]);
        return await client.sendChatCompletion(request);
      }
    }
    
    
  3. 新建一个 injection.dart 初始化 service

    import 'package:chatgpt/services/chatgpt.dart';
    
    final chatgpt = ChatGPTService();
    
    
  4. ChatScreen 增加新方法 _requestChatGPT,在_sendMessage 调用

      _requestChatGPT(WidgetRef ref, String content) async {
        final res = await chatgpt.sendChat(content);
        final text = res.choices.first.message?.content ?? "";
        final message =
            Message(content: text, isUser: false, timestamp: DateTime.now());
        ref.read(messageProvider.notifier).addMessage(message);
      }
    
    // ..
    
      // 增加WidgetRef
      _sendMessage(WidgetRef ref, String content) {
    			//...
        _requestChatGPT(ref, content);
      }
    
    
    
  5. 运行一下,输入hello发送,看到以下结果说明正常了

    Screenshot 2023-04-04 at 14.05.08

    如果碰到如下错误信息,则是网络权限问题

    Screenshot 2023-04-04 at 14.04.03

    macOS上需要在 DebugProfile.entitlementsReleaseProfile.entitlements文件中增加配置, 重新编译即可

    		<key>com.apple.security.network.client</key>
    		<true />
    

到这里我们就完成了ChatGPT API 接入。

.env 来管理敏感配置

敏感的 apikey 参数直接配置到代码中是非常不安全的,在其他的语言中通常使用.env来管理环境变量,在Flutter中同样适用。

envied 是专门用来处理这个问题,详细使用方法请参照官方文档。

  1. 安装依赖 flutter pub add envied dev:envied_generator dev:build_runner

  2. 新建env.dart 文件

    import 'package:envied/envied.dart';
    
    part 'env.g.dart';
    
    @Envied(path: ".env")
    abstract class Env {
      @EnviedField(varName: 'OPENAI_API_KEY')
      static const apiKey = _Env.apiKey;
    
      @EnviedField(varName: 'HTTP_PROXY', defaultValue: '')
      static const httpProxy = _Env.httpProxy;
    
      @EnviedField(varName: 'BASE_URL', defaultValue: '')
      static const baseUrl = _Env.baseUrl;
    }
    
    
  3. 新建.env文件内部添加对应的环境变量

    OPENAI_API_KEY=sk-aaaaaaaaaaaa
    HTTP_PROXY=http://localhost:7890
    BASE_URL=https://openai.proxy.dev/v1
    
  4. 生成代码 flutter pub run build_runner build --delete-conflicting-outputs

    Screenshot 2023-04-04 at 14.19.48

  5. .envenv.g.dart添加到 .gitignroe 中,即可避免敏感配置提交的代码中。

消息UI优化

  1. 当消息太长试,UI 会出现异常的情况,接下来我们就来优化一下消息UI。

Screenshot 2023-04-04 at 14.27.27

​ 针对MessageItem 的修改如下

 Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        CircleAvatar(
          backgroundColor: message.isUser ? Colors.blue : Colors.blueGrey,
          child: Text(
            message.isUser ? 'A' : 'GPT',
          ),
        ),
        const SizedBox(
          width: 8,
        ),
        Flexible(
          child: Container(
            margin: const EdgeInsets.only(top: 12),
            child: Text(message.content),
          ),
        ),
      ],
    );
  }

​ 修改效果如下:

Screenshot 2023-04-04 at 14.47.42

  1. ChatGPT 同时只能相应一个请求,所以在相应还么有返回时,输入框应该无法发送消息

    • states中新建 chat_ui_state.dart

      import 'package:hooks_riverpod/hooks_riverpod.dart';
      
      class ChatUiState {
        final bool requestLoading;
        ChatUiState({
          this.requestLoading = false,
        });
      }
      
      class ChatUiStateProvider extends StateNotifier<ChatUiState> {
        ChatUiStateProvider() : super(ChatUiState());
      
        void setRequestLoading(bool requestLoading) {
          state = ChatUiState(requestLoading: requestLoading);
        }
      }
      
      final chatUiProvider = StateNotifierProvider<ChatUiStateProvider, ChatUiState>(
        (ref) => ChatUiStateProvider(),
      );
      
      
    • ChatScreen 中更新相 输入框相关的状态

        Widget build(BuildContext context, WidgetRef ref) {
          // ...
      		final chatUIState = ref.watch(chatUiProvider);
      
          //...
                TextField(
                    enabled: !chatUIState.requestLoading,
                  
      
        _requestChatGPT(WidgetRef ref, String content) async {
          ref.read(chatUiProvider.notifier).setRequestLoading(true);
          final res = await chatgpt.sendChat(content);
          ref.read(chatUiProvider.notifier).setRequestLoading(false);
          //...
      
    • 运行代码验证,在发送请求之后,输入框不可使用

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。

未完待续。。。