flutter3-getx-chat电脑端聊天EXE程序|flutter3仿微信桌面端

举报
Andy Yan 发表于 2024/03/04 10:16:36 2024/03/04
【摘要】 基于flutter3+dart3+getx+bitsdojo_window+file_picker+media_kit等技术开发桌面端仿微信聊天exe实战项目FlutterWinChat。实现了聊天消息、通讯录、收藏、朋友圈、短视频、我的等页面模块。

年前有给大家分享一个flutter3.x手机端IM聊天项目。春节期间花了半个多月又爆肝开发了一个flutter3.x桌面windows端聊天项目FlutterWindowChat。

https://bbs.huaweicloud.com/blogs/421766

036360截图20240229225602857.png

360截图20240229213745505.png

技术框架

  • 编辑器:vscode
  • 窗口管理: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

p3.gif

p6.gif

项目结构目录

360截图20240229213248223.png

在开发过程中也遇到了一些问题,不过好在最后都解决了。

目前flutter在桌面windows端的项目比较少,想着这个项目还不错,就来做一些知识分享了。

整个项目窗口管理采用的bitsdojo_window插件,不过window_manager这个窗口管理插件也不错,不过相对重量级一些。

https://pub-web.flutter-io.cn/packages/bitsdojo_window

https://pub-web.flutter-io.cn/packages/window_manager

p7.gif

通过flutter create app_proj创建一个新项目。
使用flutter run -d windows来运行到window桌面。

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<void> 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();
    }
  });
}

p11.gif

flutter3路由/状态配置

采用 getx 作为路由和状态管理。将 MaterialApp 替换为GetMaterialApp 组件。

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<String, Widget> 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<GetPage> patchRoute = routeMap.entries.map((e) => GetPage(
  name: e.key, // 路由名称
  page: () => e.value, // 路由页面
  transition: Transition.noTransition, // 跳转路由动画
  middlewares: [AuthMiddleware()], // 路由中间件
)).toList();

final List<GetPage> 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窗口自定义最大化/最小化/关闭

 

@override
Widget 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,
      ),
    ],
  );
}

// 最小化
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),)
          ),
        ],
      );
    }
  );
}

flutter3公共模板

class Layout extends StatefulWidget {
  const Layout({
    super.key,
    this.activitybar = const Activitybar(),
    this.sidebar,
    this.workbench,
    this.showSidebar = true,
  });

  final Widget? activitybar; // 左侧操作栏
  final Widget? sidebar; // 侧边栏
  final Widget? workbench; // 右侧工作面板
  final bool showSidebar; // 是否显示侧边栏

  @override
  State<Layout> createState() => _LayoutState();
}
return Scaffold(
  backgroundColor: Colors.grey[100],
  body: Flex(
    direction: Axis.horizontal,
    children: [
      // 左侧操作栏
      MoveWindow(
        child: widget.activitybar,
        onDoubleTap: () => {},
      ),
      // 侧边栏
      Visibility(
        visible: widget.showSidebar,
        child: SizedBox(
          width: 270.0,
          child: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: [
                  Color(0xFFEEEBE7), Color(0xFFEEEEEE)
                ]
              ),
            ),
            child: widget.sidebar,
          ),
        ),
      ),
      // 主体容器
      Expanded(
        child: Column(
          children: [
            WindowTitleBarBox(
              child: Row(
                children: [
                  Expanded(
                    child: MoveWindow(),
                  ),
                  // 右上角操作按钮组
                  Winbtn(
                    leading: Row(
                      children: [
                        IconButton(onPressed: () {}, icon: const Icon(Icons.auto_fix_high), iconSize: 14.0,),
                        IconButton(
                          onPressed: () {
                            setState(() {
                              winTopMost = !winTopMost;
                            });
                          },
                          tooltip: winTopMost ? '取消置顶' : '置顶',
                          icon: const Icon(Icons.push_pin_outlined),
                          iconSize: 14.0,
                          highlightColor: Colors.transparent, // 点击水波纹颜色
                          isSelected: winTopMost ? true : false, // 是否选中
                          style: ButtonStyle(
                            visualDensity: VisualDensity.compact,
                            backgroundColor: MaterialStateProperty.all(winTopMost ? Colors.grey[300] : Colors.transparent),
                            shape: MaterialStatePropertyAll(
                              RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0))
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            // 右侧工作面板
            Expanded(
              child: Container(
                child: widget.workbench,
              ),
            ),
          ],
        ),
      ),
    ],
  ),
);

p12.gif

通过 setState 方法无法更新当前的dialog状态

showDialog本质上是是一个新路由页面。所以,你使用当前页面的 setState 方法当然是没法更新dialog中内容。如何更新dialog中的内容呢?答案是使用StatefulBuilder

late StateSetter setEmojState;


showDialog(
  context: context,
  barrierColor: Colors.transparent,
  builder: (context) {
    // 解决flutter通过 setState 方法无法更新当前的dialog状态
    // dialog是一个路由页面,本质跟你当前主页面是一样的。在Flutter中它是一个新的路由。所以,你使用当前页面的 setState 方法当然是没法更新dialog中内容。
    // 如何更新dialog中的内容呢?答案是使用StatefulBuilder。
    return StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {
        setEmojState = setState;
        return Container(
          ...
        );
      },
    );
  },
);
作者:xiaoyan2015
链接:https://juejin.cn/post/7341533476674207755
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。