您的当前位置:首页正文

flutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈

2024-10-17 来源:个人技术集锦

全新研发 flutter3+dart3+photo_view 跨多端 仿微信App 界面聊天 Flutter3-Chat

flutter3-chat 基于最新跨全平台技术 flutter3+dart3+material-design+shared_preferences+easy_refresh 构建的仿微信APP界面聊天实例项目。实现 发送图文表情消息/gif大图、长按仿微信语音操作面板、图片预览、红包及朋友圈 等功能。

技术架构

  • 编辑器:Vscode
  • 框架技术:Flutter3.16.5+Dart3.2.3
  • UI组件库:material-design3
  • 弹窗组件:showDialog/SimpleDialog/showModalBottomSheet/AlertDialog
  • 图片预览:photo_view^0.14.0
  • 本地缓存:shared_preferences^2.2.2
  • 下拉刷新:easy_refresh^3.3.4
  • toast提示:toast^0.3.0
  • 网址预览组件:url_launcher^6.2.4

Flutter3.x开发跨平台项目,性能有了大幅度提升,官方支持编译到android/ios/macos/windows/linux/web等多平台,未来可期!

项目构建目录

通过 flutter create app_project 命令即可快速创建一个跨平台初始化项目。

通过命令创建项目后,项目结构就如上图所示。

需要注意:flutter项目基于dart语音开发,需要首先配置Dart SDK和Flutter SDK开发环境,大家可以去官网查看配置文档。

另外使用VScode编辑器开发项目,可自行安装Flutter / Dart扩展插件。

目前flutter3-chat聊天app已经同步到我的原创作品集。

由于flutter3支持编译到windows,大家可以开发初期在windows上面调试,后期release apk到手机上。

通过如下命令即可运行到windows平台

flutter run -d windows

运行后默认窗口大小为1280x720,可以修改 windows/runner/main.cpp 文件里面的窗口尺寸。

同样,可以通过 flutter run -d chrome 命令运行到web上预览。

假如在没有真机的情况下,我们可以选择模拟器调试。目前市面上有很多类型模拟器,他们使用adb连接时都会有不同的默认端口,下面列出了一些常用的模拟器及端口号。通过adb connect连接上指定模拟器之后,执行flutter run命令即可运行项目到模拟器上面。

flutter3实现圆角文本框及渐变按钮

Container(  height: 40.0,  margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 30.0),  decoration: BoxDecoration(    color: Colors.white,    border: Border.all(color: const Color(0xffdddddd)),    borderRadius: BorderRadius.circular(15.0),  ),  child: Row(    children: [      Expanded(        child: TextField(          keyboardType: TextInputType.phone,          controller: fieldController,          decoration: InputDecoration(            hintText: '输入手机号',            suffixIcon: Visibility(              visible: authObj['tel'].isNotEmpty,              child: InkWell(                hoverColor: Colors.transparent,                highlightColor: Colors.transparent,                splashColor: Colors.transparent,                onTap: handleClear,                child: const Icon(Icons.clear, size: 16.0,),              )            ),            contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12.0),            border: const OutlineInputBorder(borderSide: BorderSide.none),          ),          onChanged: (value) {            setState(() {              authObj['tel'] = value;            });          },        ),      )    ],  ),),

按钮渐变则是通过Container组件的decotaion里面的 gradient 属性设置渐变效果。

Container(  margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),  decoration: BoxDecoration(    borderRadius: BorderRadius.circular(15.0),    // 自定义按钮渐变色    gradient: const LinearGradient(      begin: Alignment.topLeft,      end: Alignment.bottomRight,      colors: [        Color(0xFF0091EA), Color(0xFF07C160)      ],    )  ),  child: SizedBox(    width: double.infinity,    height: 45.0,    child: FilledButton(      style: ButtonStyle(        backgroundColor: MaterialStateProperty.all(Colors.transparent),        shadowColor: MaterialStateProperty.all(Colors.transparent),        shape: MaterialStatePropertyAll(          RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0))        )      ),      onPressed: handleSubmit,      child: const Text('登录', style: TextStyle(fontSize: 18.0),),    ),  )),

flutter实现60s倒计时发送验证码功能。

Timer? timer;String vcodeText = '获取验证码';bool disabled = false;int time = 60;// 60s倒计时void handleVcode() {  if(authObj['tel'] == '') {    snackbar('手机号不能为空');  }else if(!Utils.checkTel(authObj['tel'])) {    snackbar('手机号格式不正确');  }else {    setState(() {      disabled = true;    });    startTimer();  }}startTimer() {  timer = Timer.periodic(const Duration(seconds: 1), (timer) {    setState(() {      if(time > 0) {        vcodeText = '获取验证码(${time--})';      }else {        vcodeText = '获取验证码';        time = 60;        disabled = false;        timer.cancel();      }    });  });  snackbar('短信验证码已发送,请注意查收', color: Colors.green);}

Flutter3沉浸式渐变状态导航栏

要实现如上图渐变AppBar也非常简单,只需要配置AppBar提供的可伸缩灵活区域属性 flexibleSpace 配合gradient即可快速实现渐变导航栏。

AppBar(  title: Text('Flutter3-Chat'),  flexibleSpace: Container(    decoration: const BoxDecoration(      gradient: LinearGradient(        begin: Alignment.topLeft,        end: Alignment.bottomRight,        colors: [          Color(0xFF0091EA), Color(0xFF07C160)        ],      )    ),  )),

Flutter3字体图标/自定义badge

flutter内置了丰富的字体图标,通过图标组件 Icon(Icons.add) 引入即可使用。

另外还支持通过自定义IconData方式自定义图标,如使用阿里iconfont图表库图标。

Icon(IconData( 0xe666 , fontFamily: ' iconfont ' ), size: 18.0 )

把下载的字体文件放到assets目录,

pubspec.yaml 中引入字体文件。

class FStyle {  // 自定义iconfont图标  static iconfont(int codePoint, {double size = 16.0, Color? color}) {    return Icon(      IconData(codePoint, fontFamily: 'iconfont', matchTextDirection: true),      size: size,      color: color,    );  }  // 自定义Badge红点  static badge(int count, {    Color color = Colors.redAccent,    bool isdot = false,    double height = 18.0,    double width = 18.0  }) {    final num = count > 99 ? '99+' : count;    return Container(      alignment: Alignment.center,      height: isdot ? height / 2 : height,      width: isdot ? width / 2 : width,      decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(100.00)),      child: isdot ? null : Text('$num', style: const TextStyle(color: Colors.white, fontSize: 12.0)),    );  }}

FStyle.badge(23)FStyle.badge(2, color: Colors.pink, height: 10.0, width: 10.0)FStyle.badge(0, isdot: true)

Flutter仿微信PopupMenu下拉菜单/下拉刷新

通过flutter提供的 PopupMenuButton 组件实现下拉菜单功能。

PopupMenuButton(  icon: FStyle.iconfont(0xe62d, size: 17.0),  offset: const Offset(0, 50.0),  tooltip: '',  color: const Color(0xFF353535),  itemBuilder: (BuildContext context) {    return <PopupMenuItem>[      popupMenuItem(0xe666, '发起群聊', 0),      popupMenuItem(0xe75c, '添加朋友', 1),      popupMenuItem(0xe603, '扫一扫', 2),      popupMenuItem(0xe6ab, '收付款', 3),    ];  },  onSelected: (value) {    switch(value) {      case 0:        print('发起群聊');        break;      case 1:        Navigator.pushNamed(context, '/addfriends');        break;      case 2:        print('扫一扫');        break;      case 3:        print('收付款');        break;    }  },)
// 下拉菜单项static popupMenuItem(int codePoint, String title, value) {  return PopupMenuItem(    value: value,    child: Row(      mainAxisAlignment: MainAxisAlignment.start,      children: [        const SizedBox(width: 10.0,),        FStyle.iconfont(codePoint, size: 21.0, color: Colors.white),        const SizedBox(width: 10.0,),        Text(title, style: const TextStyle(fontSize: 16.0, color: Colors.white),),      ],    ),  );}

如上图:下拉刷新、上拉加载更多是通过 easy_refresh 组件实现功能。

EasyRefresh(  // 下拉加载提示  header: const ClassicHeader(    // showMessage: false,  ),  // 加载更多提示  footer: ClassicFooter(),  // 下拉刷新逻辑  onRefresh: () async {    // ...下拉逻辑    await Future.delayed(const Duration(seconds: 2));  },  // 上拉加载逻辑  onLoad: () async {    // ...  },  child: ListView.builder(    itemCount: chatList.length,    itemBuilder: (context, index) {      return Ink(        // ...      );    },  ),)

如上图:弹窗功能均是自定义AlertDialog实现效果。通过 无限制容器UnconstrainedBox配合SizedBox组件实现自定义窗口大小。

// 关于弹窗void aboutAlertDialog(BuildContext context) {  showDialog(    context: context,    builder: (context) {      return UnconstrainedBox(        constrainedAxis: Axis.vertical,        child: SizedBox(          width: 320.0,          child: AlertDialog(            contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),            backgroundColor: Colors.white,            surfaceTintColor: Colors.white,            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),            content: Padding(              padding: const EdgeInsets.symmetric(horizontal: 10.0),              child: Column(                mainAxisSize: MainAxisSize.min,                children: [                  Image.asset('assets/images/logo.png', width: 90.0, height: 90.0, fit: BoxFit.cover,),                  const SizedBox(height: 10.0),                  const Text('Flutter3-WChat', style: TextStyle(color: Color(0xFF0091EA), fontSize: 22.0),),                  const SizedBox(height: 5.0),                  const Text('基于flutter3+dart3开发跨平台仿微信App聊天实例。', style: TextStyle(color: Colors.black45),),                  const SizedBox(height: 20.0),                  Text('©2024/01 Andy   Q: 282310962', style: TextStyle(color: Colors.grey[400], fontSize: 12.0),),                ],              ),            ),          ),        ),      );    }  );}// 二维码名片弹窗void qrcodeAlertDialog(BuildContext context) {  showDialog(    context: context,    builder: (context) {      return UnconstrainedBox(        constrainedAxis: Axis.vertical,        child: SizedBox(          width: 320.0,          child: AlertDialog(            contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),            backgroundColor: const Color(0xFF07C160),            surfaceTintColor: const Color(0xFF07C160),            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.0)),            content: Padding(              padding: const EdgeInsets.symmetric(horizontal: 10.0),              child: Column(                mainAxisSize: MainAxisSize.min,                children: [                  Image.asset('assets/images/qrcode.png', width: 250.0, fit: BoxFit.cover,),                  const SizedBox(height: 15.0),                  const Text('扫一扫,加我公众号', style: TextStyle(color: Colors.white60, fontSize: 14.0,),),                ],              ),            ),          ),        ),      );    }  );}// 退出登录弹窗void logoutAlertDialog(BuildContext context) {  showDialog(    context: context,    builder: (context) {      return AlertDialog(        content: const Text('确定要退出登录吗?', style: TextStyle(fontSize: 16.0),),        backgroundColor: Colors.white,        surfaceTintColor: Colors.white,        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),        elevation: 2.0,        actionsPadding: const EdgeInsets.all(15.0),        actions: [          TextButton(            onPressed: () {Navigator.of(context).pop();},            child: const Text('取消', style: TextStyle(color: Colors.black54),)          ),          TextButton(            onPressed: handleLogout,            child: const Text('退出登录', style: TextStyle(color: Colors.red),)          ),        ],      );    }  );}

flutter实现微信朋友圈九宫格

GroupZone(images: item['images']),GroupZone(  images: uploadList,  album: true,  onChoose: () async {    Toast.show('选择手机相册图片', duration: 2, gravity: 1);  },),

// 创建可点击预览图片createImage(BuildContext context, String img, int key) {  return GestureDetector(    child: Hero(      tag: img, // 放大缩小动画效果标识      child: img == '+' ?       Container(color: Colors.transparent, child: const Icon(Icons.add, size: 30.0, color: Colors.black45),)      :      Image.asset(        img,        width: width,        fit: BoxFit.contain,      ),    ),    onTap: () {      // 选择图片      if(img == '+') {        onChoose!();      }else {        Navigator.of(context).push(FadeRoute(route: ImageViewer(          images: album ? imgList!.sublist(0, imgList!.length - 1) : imgList,          index: key,        )));      }    },  );}

使用 photo_view 插件实现预览大图功能,支持预览单张及多张大图。

import 'package:flutter/material.dart';import 'package:photo_view/photo_view.dart';import 'package:photo_view/photo_view_gallery.dart';class ImageViewer extends StatefulWidget {  const ImageViewer({    super.key,    this.images,    this.index = 0,  });  final List? images; // 预览图列表  final int index; // 当前预览图索引  @override  State<ImageViewer> createState() => _ImageViewerState();}class _ImageViewerState extends State<ImageViewer> {  int currentIndex = 0;  @override  void initState() {    super.initState();    currentIndex = widget.index;  }  @override  Widget build(BuildContext context) {    var imgCount = widget.images?.length;    return Scaffold(      body: Stack(        children: [          Positioned(            top: 0,            left: 0,            bottom: 0,            right: 0,            child: GestureDetector(              child: imgCount == 1 ? PhotoView(                imageProvider: AssetImage(widget.images![0]),                backgroundDecoration: const BoxDecoration(                  color: Colors.black,                ),                minScale: PhotoViewComputedScale.contained,                maxScale: PhotoViewComputedScale.covered * 2,                heroAttributes: PhotoViewHeroAttributes(tag: widget.images![0]),                enableRotation: true,              )              :              PhotoViewGallery.builder(                itemCount: widget.images?.length,                builder: (context, index) {                  return PhotoViewGalleryPageOptions(                    imageProvider: AssetImage(widget.images![index]),                    minScale: PhotoViewComputedScale.contained,                    maxScale: PhotoViewComputedScale.covered * 2,                    heroAttributes: PhotoViewHeroAttributes(tag: widget.images![index]),                  );                },                scrollPhysics: const BouncingScrollPhysics(),                backgroundDecoration: const BoxDecoration(                  color: Colors.black,                ),                pageController: PageController(initialPage: widget.index),                enableRotation: true,                onPageChanged: (index) {                  setState(() {                    currentIndex = index;                  });                },              ),              onTap: () {                Navigator.of(context).pop();              },            ),          ),          // 图片索引index          Positioned(            top: MediaQuery.of(context).padding.top + 15,            width: MediaQuery.of(context).size.width,            child: Center(              child: Visibility(                visible: imgCount! > 1 ? true : false,                child: Text('${currentIndex+1} / ${widget.images?.length}', style: const TextStyle(color: Colors.white)),              )            ),          ),        ],      ),    );  }}

flutter3聊天模块

文本框TextField设置maxLines: null即可实现多行文本输入,支持图文emoj混排,网址连接识别等功能。

// 输入框Offstage(  offstage: voiceBtnEnable,  child: TextField(    decoration: const InputDecoration(      isDense: true,      hoverColor: Colors.transparent,      contentPadding: EdgeInsets.all(8.0),      border: OutlineInputBorder(borderSide: BorderSide.none),    ),    style: const TextStyle(fontSize: 16.0,),    maxLines: null,    controller: editorController,    focusNode: editorFocusNode,    cursorColor: const Color(0xFF07C160),    onChanged: (value) {},  ),),

支持仿微信语音按住说话,左滑取消发送、右滑转换语音功能。

// 语音Offstage(  offstage: !voiceBtnEnable,  child: GestureDetector(    child: Container(      decoration: BoxDecoration(        color: Colors.white,        borderRadius: BorderRadius.circular(5),      ),      alignment: Alignment.center,      height: 40.0,      width: double.infinity,      child: Text(voiceTypeMap[voiceType], style: const TextStyle(fontSize: 15.0),),    ),    onPanStart: (details) {      setState(() {        voiceType = 1;        voicePanelEnable = true;      });    },    onPanUpdate: (details) {      Offset pos = details.globalPosition;      double swipeY = MediaQuery.of(context).size.height - 120;      double swipeX = MediaQuery.of(context).size.width / 2 + 50;      setState(() {        if(pos.dy >= swipeY) {          voiceType = 1; // 松开发送        }else if (pos.dy < swipeY && pos.dx < swipeX) {          voiceType = 2; // 左滑松开取消        }else if (pos.dy < swipeY && pos.dx >= swipeX) {          voiceType = 3; // 右滑语音转文字        }      });    },    onPanEnd: (details) {      // print('停止录音');      setState(() {        switch(voiceType) {          case 1:            Toast.show('发送录音文件', duration: 1, gravity: 1);            voicePanelEnable = false;            break;          case 2:            Toast.show('取消发送', duration: 1, gravity: 1);            voicePanelEnable = false;            break;          case 3:            Toast.show('语音转文字', duration: 1, gravity: 1);            voicePanelEnable = true;            voiceToTransfer = true;            break;        }        voiceType = 0;      });    },  ),),

按住录音显示面板

// 录音主体(按住说话/松开取消/语音转文本)Visibility(  visible: voicePanelEnable,  child: Material(    color: const Color(0xDD1B1B1B),    child: Stack(      children: [        // 取消发送+语音转文字        Positioned(          bottom: 120,          left: 30,          right: 30,          child: Visibility(            visible: !voiceToTransfer,            child: Column(              children: [                // 语音动画层                Stack(                  children: [                    Container(                      height: 70.0,                      margin: const EdgeInsets.symmetric(horizontal: 50.0),                      decoration: BoxDecoration(                        color: Colors.white,                        borderRadius: BorderRadius.circular(15.0),                      ),                      child: Row(                        mainAxisAlignment: MainAxisAlignment.center,                        children: [                          Image.asset('assets/images/voice_record.gif', height: 30.0,)                        ],                      ),                    ),                    Positioned(                      right: (MediaQuery.of(context).size.width - 60) / 2,                      bottom: 1,                      child: RotatedBox(                        quarterTurns: 0,                        child: CustomPaint(painter: ArrowShape(arrowColor: Colors.white, arrowSize: 10.0)),                      )                    ),                  ],                ),                const SizedBox(height: 50.0,),                // 操作项                Row(                  mainAxisAlignment: MainAxisAlignment.spaceBetween,                  children: [                    // 取消发送                    Container(                      height: 60.0,                      width: 60.0,                      decoration: BoxDecoration(                        borderRadius: BorderRadius.circular(50.0),                        color: voiceType == 2 ? Colors.red : Colors.black38,                      ),                      child: const Icon(Icons.close, color: Colors.white54,),                    ),                    // 语音转文字                    Container(                      height: 60.0,                      width: 60.0,                      decoration: BoxDecoration(                        borderRadius: BorderRadius.circular(50.0),                        color: voiceType == 3 ? Colors.green : Colors.black38,                      ),                      child: const Icon(Icons.translate, color: Colors.white54,),                    ),                  ],                ),              ],            ),          ),        ),        // 语音转文字(识别结果状态)        Positioned(          bottom: 120,          left: 30,          right: 30,          child: Visibility(            visible: voiceToTransfer,            child: Column(              children: [                // 提示结果                Stack(                  children: [                    Container(                      height: 100.0,                      decoration: BoxDecoration(                        color: Colors.red,                        borderRadius: BorderRadius.circular(15.0),                      ),                      child: const Row(                        mainAxisAlignment: MainAxisAlignment.center,                        children: [                          Icon(Icons.info, color: Colors.white,),                          Text('未识别到文字。', style: TextStyle(color: Colors.white),),                        ],                      ),                    ),                    Positioned(                      right: 35.0,                      bottom: 1,                      child: RotatedBox(                        quarterTurns: 0,                        child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)),                      )                    ),                  ],                ),                const SizedBox(height: 50.0,),                // 操作项                Row(                  mainAxisAlignment: MainAxisAlignment.spaceBetween,                  children: [                    GestureDetector(                      child: Container(                        height: 60.0,                        width: 60.0,                        decoration: const BoxDecoration(                          color: Colors.transparent,                        ),                        child: const Column(                          mainAxisAlignment: MainAxisAlignment.center,                          children: [                            Icon(Icons.undo, color: Colors.white54,),                            Text('取消', style: TextStyle(color: Colors.white70),)                          ],                        ),                      ),                      onTap: () {                        setState(() {                          voicePanelEnable = false;                          voiceToTransfer = false;                        });                      },                    ),                    GestureDetector(                      child: Container(                        height: 60.0,                        width: 100.0,                        decoration: const BoxDecoration(                          color: Colors.transparent,                        ),                        child: const Column(                          mainAxisAlignment: MainAxisAlignment.center,                          children: [                            Icon(Icons.graphic_eq_rounded, color: Colors.white54,),                            Text('发送原语音', style: TextStyle(color: Colors.white70),)                          ],                        ),                      ),                      onTap: () {},                    ),                    GestureDetector(                      child: Container(                        height: 60.0,                        width: 60.0,                        decoration: BoxDecoration(                          borderRadius: BorderRadius.circular(50.0),                          color: Colors.white12,                        ),                        child: const Icon(Icons.check, color: Colors.white12,),                      ),                      onTap: () {},                    ),                  ],                ),              ],            ),          ),        ),        // 提示文字(操作状态)        Positioned(          bottom: 120,          left: 0,          width: MediaQuery.of(context).size.width,          child: Visibility(            visible: !voiceToTransfer,            child: Align(              child: Text(voiceTypeMap[voiceType], style: const TextStyle(color: Colors.white70),),            ),          ),        ),        // 背景        Align(          alignment: Alignment.bottomCenter,          child: Visibility(            visible: !voiceToTransfer,            child: Image.asset('assets/images/voice_record_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill),          ),        ),        // 背景图标        Positioned(          bottom: 25,          left: 0,          width: MediaQuery.of(context).size.width,          child: Visibility(            visible: !voiceToTransfer,            child: const Align(              child: Icon(Icons.graphic_eq_rounded, color: Colors.black54,),            ),          ),        ),      ],    ),  ),)

flutter3绘制箭头

聊天模块消息及各种箭头展示,通过flutter提供的画板功能绘制箭头。

// 绘制气泡箭头class ArrowShape extends CustomPainter {  ArrowShape({    required this.arrowColor,    this.arrowSize = 7,  });  final Color arrowColor; // 箭头颜色  final double arrowSize; // 箭头大小  @override  void paint(Canvas canvas, Size size) {    var paint = Paint()..color = arrowColor;    var path = Path();    path.lineTo(-arrowSize, 0);    path.lineTo(0, arrowSize);    path.lineTo(arrowSize, 0);    canvas.drawPath(path, paint);  }  @override  bool shouldRepaint(CustomPainter oldDelegate) {    return false;  }}

Okay,以上就是Flutter3+Dart3开发全平台聊天App实例的一些知识分享,希望对大家有所帮助哈~~?

最后附上两个最新实战项目

uni-app+vue3+pinia2仿抖音直播商城:

electron27+react18 hooks仿macos桌面系统:

显示全文