2024-03-12
分类:开源工具
阅读(280) 评论(0)
继上一篇文章获得了很多小伙伴的点赞,这次来主要是对完结的新项目做一些小分享,希望有更多的开发者看到,能给他们有些帮助!
[Flutter3聊天项目完结,赞爆了!](http://mp.weixin.qq.com/s?__biz=MzI1MDQ5MTY4NQ==&mid=2247485753&idx=1&sn=9774fe33a3ffae092bd030aa667e00b6&chksm=e9802f73def7a665639ca90f16cd4f8276367b2ffb141ddb231be08b4b081eda5958198a0629&scene=21#wechat_redirect)
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/d689d6e4f0e544338201c4d219fd1ea1.jpg)
**Flutter-WinChat** 项目主要基于flutter3.x+bitsdojo_window+getx等技术架构开发,目前实现了聊天模块、联系人、收藏、朋友圈、小视频、我的等页面。
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/c37d920165284753badbee57a81f8d74.jpg)
**技术栈**
* 开发工具:VScode
* 技术框架:Flutter3.16.5+Dart3.2.3
* 窗口管理:bitsdojo_window: \^0.1.6
* 系统托盘管理:system_tray: \^2.0.3
* 路由/状态管理:get: \^4.6.6
* 存储服务:get_storage: \^2.1.1
* 图片预览插件:photo_view: \^0.14.0
* 网址插件:url_launcher: \^6.2.4
* 视频套件:media_kit: \^1.1.10+1
* 文件选择器:file_picker: \^6.1.1
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/be53d36767574849a3b1e8ca29d1a76e.jpg)
**完整项目结构**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/8f7147a621084590867c6aacc0613df8.jpg)
开发之初需要自己去看官方文档配置好flutter sdk和dart sdk开发环境。
\*
\*
\*
```
https://flutter.dev/https://flutter.cn/https://www.dartcn.com/
```
通过如下命令快速创建和运行一个项目。
\*
\*
\*
\*
```
// 创建一个flutter新项目flutter create flutter_winchat// 运行到window桌面端flutter run -d windows
```
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/9d097df6eac848499514742c67832717.jpg)
项目中用到了官方的Icons图标库。
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/7f2ae27cdbf1498a9b55c3d5704d6ad2.jpg)\*
```
https://api.flutter-io.cn/flutter/material/Icons-class.html
```
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/2ddde0ec2efa450b86ee312377ce06a6.jpg)
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/4e7c7f82a27e4b49aed019b22921a4ed.jpg)
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/16894a5f832846d89f28a244148e2dfd.jpg)
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/5683a675d9654adea7dad0b9951bda0b.jpg)
项目整体采用 **bitsdojo_window** 插件进行窗口管理器。该插件去掉系统导航条,支持自定义窗口初始尺寸及操作按钮(最大化/最小化/关闭)。
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/6042107d4a184a7faa6e6a9886210e3c.jpg)\*
```
https://pub-web.flutter-io.cn/packages/bitsdojo_window
```
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/1cef7ac6013d4ee59a77487cd310c8ce.jpg)
另外还引入了flutter桌面端系统托盘图标 **system_tray** 插件,生成简单的任务栏托盘。
\*
```
https://pub-web.flutter-io.cn/packages/system_tray
```
**主页面main.dart配置**\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
import 'dart:io';import 'package:flutter/material.dart';import 'package:bitsdojo_window/bitsdojo_window.dart';import 'package:get/get.dart';import 'package:get_storage/get_storage.dart';import 'package:media_kit/media_kit.dart';import 'package:system_tray/system_tray.dart';
import 'utils/index.dart';
// 引入公共样式import 'styles/index.dart';
// 引入公共布局模板import 'layouts/index.dart';
// 引入路由配置import 'router/index.dart';
void main() async { // 初始化get_storage存储类 await GetStorage.init();
// 初始化media_kit视频套件 WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized();
initSystemTray();
runApp(const MyApp());
// 初始化bitsdojo_window窗口 doWhenWindowReady(() { appWindow.size = const Size(850, 620); appWindow.minSize = const Size(700, 500); appWindow.alignment = Alignment.center; appWindow.title = 'Flutter3-WinChat'; appWindow.show(); });}
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return GetMaterialApp( title: 'FLUTTER3 WINCHAT', debugShowCheckedModeBanner: false, theme: ThemeData( primaryColor: FStyle.primaryColor, useMaterial3: true, // 修正windows端字体粗细不一致 fontFamily: Platform.isWindows ? 'Microsoft YaHei' : null, ), home: const Layout(), // 初始路由 initialRoute: Utils.isLogin() ? '/index' :'/login', // 路由页面 getPages: routes, onInit: () {}, onReady: () {}, ); }}
// 创建系统托盘图标Future initSystemTray() async { String trayIco = 'assets/images/tray.ico'; SystemTray systemTray = SystemTray();
// 初始化系统托盘 await systemTray.initSystemTray( title: 'system-tray', iconPath: trayIco, );
// 右键菜单 final Menu menu = Menu(); await menu.buildFrom([ MenuItemLabel(label: 'show', onClicked: (menuItem) => appWindow.show()), MenuItemLabel(label: 'hide', onClicked: (menuItem) => appWindow.hide()), MenuItemLabel(label: 'close', onClicked: (menuItem) => appWindow.close()), ]); await systemTray.setContextMenu(menu);
// 右键事件 systemTray.registerSystemTrayEventHandler((eventName) { debugPrint('eventName: $eventName'); if (eventName == kSystemTrayEventClick) { Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu(); } else if (eventName == kSystemTrayEventRightClick) { Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show(); } });}
```
**flutter路由/状态管理**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/99c14e0e833347868ee3a7152c774dbf.jpg)
采用**GetX**来管理路由及状态。
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
import 'package:flutter/material.dart';import 'package:get/get.dart';
// 引入工具类import '../utils/index.dart';
/* 引入路由页面 */import '../views/auth/login.dart';import '../views/auth/register.dart';// 首页import '../views/index/index.dart';// 通讯录import '../views/contact/index.dart';import '../views/contact/addfriends.dart';import '../views/contact/newfriends.dart';import '../views/contact/uinfo.dart';// 收藏import '../views/favor/index.dart';// 我的import '../views/my/index.dart';import '../views/my/setting.dart';import '../views/my/recharge.dart';import '../views/my/wallet.dart';// 朋友圈import '../views/fzone/index.dart';import '../views/fzone/publish.dart';// 短视频import '../views/fvideo/index.dart';// 聊天import '../views/chat/group-chat/chat.dart';
// 路由地址集合final Map routeMap = { '/index': const Index(), '/contact': const Contact(), '/addfriends': const AddFriends(), '/newfriends': const NewFriends(), '/uinfo': const Uinfo(), '/favor': const Favor(), '/my': const My(), '/setting': const Setting(), '/recharge': const Recharge(), '/wallet': const Wallet(), '/fzone': const Fzone(), '/publish': const PublishFzone(), '/fvideo': const Fvideo(), '/chat': const Chat(),};
final List patchRoute = routeMap.entries.map((e) => GetPage( name: e.key, // 路由名称 page: () => e.value, // 路由页面 transition: Transition.noTransition, // 跳转路由动画 middlewares: [AuthMiddleware()], // 路由中间件)).toList();
final List routes = [ GetPage(name: '/login', page: () => const Login()), GetPage(name: '/register', page: () => const Register()), ...patchRoute,];
```
通过Getx提供了**middlewares**中间件进行路由拦截。
\*
\*
\*
\*
\*
\*
\*
```
// 路由拦截class AuthMiddleware extends GetMiddleware { @override RouteSettings? redirect(String? route) { return Utils.isLogin() ? null : const RouteSettings(name: '/login'); }}
```
**flutter自定义系统最大化/最小化/关闭**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/416c46a4220e44e6adb92931f32bf3a7.jpg)
通过 bitsdojo_window 插件来自定义系统右上角按钮。
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/228e4af8e57e446392bdb59b4247afc2.jpg)
该插件提供了四个常用的操作按钮功能。
\*
\*
\*
\*
```
MinimizeWindowButton // 最小化RestoreWindowButton // 恢复MaximizeWindowButton // 最大化CloseWindowButton // 关闭
```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
```
@overrideWidget build(BuildContext context){ return Row( children: [ Container( child: widget.leading, ), Visibility( visible: widget.minimizable, child: MouseRegion( cursor: SystemMouseCursors.click, child: SizedBox( width: 32.0, height: 36.0, child: MinimizeWindowButton(colors: buttonColors, onPressed: handleMinimize,), ) ), ), Visibility( visible: widget.maximizable, child: MouseRegion( cursor: SystemMouseCursors.click, child: SizedBox( width: 32.0, height: 36.0, child: isMaximized ? RestoreWindowButton(colors: buttonColors, onPressed: handleMaxRestore,) : MaximizeWindowButton(colors: buttonColors, onPressed: handleMaxRestore,), ), ), ), Visibility( visible: widget.closable, child: MouseRegion( cursor: SystemMouseCursors.click, child: SizedBox( width: 32.0, height: 36.0, child: CloseWindowButton(colors: closeButtonColors, onPressed: handleExit,), ), ), ), Container( child: widget.trailing, ), ], );}
```
通过自定义**leading** 可在按钮组前面插入前置组件。
**最小化/最大化/关闭事件**
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
// 最小化void handleMinimize() { appWindow.minimize();}// 设置最大化/恢复void handleMaxRestore() { appWindow.maximizeOrRestore();}// 关闭void handleExit() { 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(3.0)), elevation: 3.0, actionsPadding: const EdgeInsets.all(15.0), actions: [ TextButton( onPressed: () { Get.back(); appWindow.close(); }, child: const Text('退出', style: TextStyle(color: Colors.red),) ), TextButton( onPressed: () { Get.back(); appWindow.hide(); }, child: const Text('最小化至托盘', style: TextStyle(color: Colors.deepPurple),) ), ], ); } );}
```
桌面端需要展示不同的鼠标手势,通过**MouseRegion**组件即可设置不同的鼠标样式。
**监听窗口尺寸变化**
桌面端开发需要监测窗口尺寸改变,通过**WidgetsBindingObserver** 类监测窗口变化。
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
class _WinbtnState extends State with WidgetsBindingObserver { // 是否最大化 bool isMaximized = false;
@override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); }
@override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); }
// 监听窗口尺寸变化 @override void didChangeMetrics() { super.didChangeMetrics(); WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { isMaximized = appWindow.isMaximized; }); }); }
// ...}
```
**flutter实现微信侧边栏**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/16eebeebd7be4983ab011789a19f60c5.jpg)
如上图:如何实现类似微信侧边栏Tab切换呢?
可以使用 **NavigationRail** 组件实现功能。该组件支持自定义头部和尾部组件。
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/df96aa3a272848f5a7aee0e909fe27e2.jpg)\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
@overrideWidget build(BuildContext context) { return Container( width: 54.0, decoration: const BoxDecoration( color: Color(0xFF2E2E2E), ), child: NavigationRail( backgroundColor: Colors.transparent, labelType: NavigationRailLabelType.none, // all 显示图标+标签 selected 只显示激活图标+标签 none 不显示标签 indicatorColor: Colors.transparent, // 去掉选中椭圆背景 indicatorShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0.0), ), unselectedIconTheme: const IconThemeData(color: Color(0xFF979797), size: 24.0), selectedIconTheme: const IconThemeData(color: Color(0xFF07C160), size: 24.0,), unselectedLabelTextStyle: const TextStyle(color: Color(0xFF979797),), selectedLabelTextStyle: const TextStyle(color: Color(0xFF07C160),), // 头部(图像) leading: GestureDetector( onPanStart: (details) => {}, child: Container( margin: const EdgeInsets.only(top: 30.0, bottom: 10.0), child: InkWell( child: Image.asset('assets/images/avatar/uimg1.jpg', height: 36.0, width: 36.0,), onTapDown: (TapDownDetails details) { cardDX = details.globalPosition.dx; cardDY = details.globalPosition.dy; }, onTap: () { showCardDialog(context); }, ), ), ), // 尾部(链接) trailing: Expanded( child: Container( margin: const EdgeInsets.only(bottom: 10.0), child: GestureDetector( onPanStart: (details) => {}, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton(icon: Icon(Icons.info_outline, color: Color(0xFF979797), size: 24.0), onPressed:(){showAboutDialog(context);}), PopupMenuButton( icon: const Icon(Icons.menu, color: Color(0xFF979797), size: 24.0,), offset: const Offset(54.0, 0.0), tooltip: '', color: const Color(0xFF353535), surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), padding: EdgeInsets.zero, itemBuilder: (BuildContext context) { return [ popupMenuItem('我的私密空间', 0), popupMenuItem('锁定', 1), popupMenuItem('意见反馈', 2), popupMenuItem('设置', 3), ]; }, onSelected: (value) { switch(value) { case 0: Get.toNamed('/my'); break; case 3: Get.toNamed('/setting'); break; } }, ), ], ), ), ), ), selectedIndex: tabCur, destinations: [ ...tabNavs ], onDestinationSelected: (index) { setState(() { tabCur = index; if(tabRoute[index] != null && tabRoute[index]?['path'] != null) { Get.toNamed(tabRoute[index]['path']); } }); }, ), );}
```
**flutter小视频模块**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/f04916e6dc73497ea43fa11471f4f62a.jpg)
项目中加入了短视频功能,支持简单的垂直滑动切换,底部mini播放进度条。
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
\*
```
// flutter视频模板 Q:282310962
Container( width: MediaQuery.of(context).size.height * 9 / 16, decoration: const BoxDecoration( color: Colors.black, ), child: Stack( children: [ // 垂直滚动区域 PageView( // 自定义滚动行为(支持桌面端滑动、去掉滚动条槽) scrollBehavior: SwiperScrollBehavior().copyWith(scrollbars: false), scrollDirection: Axis.vertical, controller: pageController, onPageChanged: (index) { controller.player.pause(); }, children: [ Stack( children: [ // 视频区域 Positioned( top: 0, left: 0, right: 0, bottom: 0, child: GestureDetector( child: Stack( children: [ // 短视频插件 Video( controller: controller, fit: BoxFit.cover, // 无控制条 controls: NoVideoControls, ), // 播放/暂停按钮 Center( child: IconButton( onPressed: () { controller.player.playOrPause(); }, icon: StreamBuilder( stream: controller.player.stream.playing, builder: (context, playing) { return Visibility( visible: playing.data == false, child: Icon( playing.data == true ? Icons.pause : Icons.play_arrow_rounded, color: Colors.white70, size: 50, ), ); }, ), ), ), ], ), onTap: () { controller.player.playOrPause(); }, ), ), // 右侧操作栏 Positioned( bottom: 70.0, right: 10.0, child: Column( children: [ // ... ], ), ), // 底部区域 Positioned( bottom: 30.0, left: 15.0, right: 80.0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ... ], ), ), // 播放进度条 Positioned( bottom: 15.0, left: 15.0, right: 15.0, child: Container( // ... ), ), ], ), Container( color: Colors.black, child: const Center(child: Text('1', style: TextStyle(color: Colors.white, fontSize: 60),),) ), Container( color: Colors.black, child: const Center(child: Text('2', style: TextStyle(color: Colors.white, fontSize: 60),),) ), Container( color: Colors.black, child: const Center(child: Text('3', style: TextStyle(color: Colors.white, fontSize: 60),),) ), ], ), // 固定菜单 Align( alignment: Alignment.topCenter, child: DefaultTabController( length: 3, child: TabBar( tabs: const [ Tab(text: '推荐'), Tab(text: '关注'), Tab(text: '同城'), ], tabAlignment: TabAlignment.center, overlayColor: MaterialStateProperty.all(Colors.transparent), unselectedLabelColor: Colors.white70, labelColor: const Color(0xff0091ea), indicatorColor: const Color(0xff0091ea), indicatorSize: TabBarIndicatorSize.label, dividerHeight: 0, indicatorPadding: const EdgeInsets.all(5), ), ), ), ], ),),
```
由于聊天模块功能点比较多,限于篇幅,就分享一点状态更新知识。
**解决flutter通过 setState 方法无法更新当前的dialog状态?**
![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/a0d836903fa34783b46db934bfa490b9.jpg)
**showDialog** 本质上是另一个路由页面,它的性质跟你当前主页面是一样的。在Flutter中它是一个新的路由。所以,你使用当前页面的 setState 方法当然是没法更新dialog中内容。如何更新dialog中的内容呢?答案是使用**StatefulBuilder**。
\*
\*
```
// 定义页面StateSetterlate StateSetter setEmojState;
```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
```
showDialog( context: context, barrierColor: Colors.transparent, builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { setEmojState = setState; return Container( ... ); }, ); },);
```
这样页面就可以使用setEmojState代替setState来更新状态了。
Ok,以上就是flutter3+getx开发桌面端聊天项目的一些知识分享,希望对大家有所帮助哈\~\~![](https://img1.51tbox.com/static/2024-03-11/col/2a146cd576f5fd3f695aa45bbefa1e95/7b0ffaf221c54cbfa8d1a4d2d3d81f9c.png.jpg)
\*
```
https://gf.bilibili.com/item/detail/1105374011
```
如果有需要,也可以来我工房看看,粉丝有福利哟!
众生皆苦,唯有自渡!