51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

原创分享!flutter3+getx桌面端聊天Exe

继上一篇文章获得了很多小伙伴的点赞,这次来主要是对完结的新项目做一些小分享,希望有更多的开发者看到,能给他们有些帮助! [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 ``` 如果有需要,也可以来我工房看看,粉丝有福利哟!

赞(11)
未经允许不得转载:工具盒子 » 原创分享!flutter3+getx桌面端聊天Exe