年前有给大家分享一款flutter3.x+dart3手机端聊天App实例。春节期间就又捣鼓了flutter3桌面端开发实践项目。
https://cloud.tencent.com/developer/article/2388052
flutter3_winchat 一款基于flutter3+getx+bitsdojo_window
架构的客户端聊天实例项目。
该项目周期不过个半月,开发中遇到了很多的技术问题,不过都最后解决了。
通过这个项目旨在探究Flutter在桌面端技术实践,通过下面的一些技术分享让更多的开发者能参与推动flutter客户端的发展。
// 创建flutter新项目模板
flutter create flutter_winchat
// 运行flutter项目到window桌面
flutter run -d windows
窗口管理使用的是bitsdojo_window
插件,不过window_manager
这个窗口管理插件也不错,功能更加的丰富重量级一些。
https://pub-web.flutter-io.cn/packages/bitsdojo_window
https://pub-web.flutter-io.cn/packages/window_manager
flutter构建的项目结构目录非常清晰,一目了然。
项目整体采用左中右三列布局,UI结构和微信客户端类似。
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,
),
),
],
),
),
],
),
);
使用flutter内置的 NavigationRail
导航组件实现Tabs功能。该组件支持自定义头部和尾部组件。
@override
Widget 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>[
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']);
}
});
},
),
);
}
整个项目采用Getx
作为路由及状态管理插件。
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,
),
home: const Layout(),
// 初始路由
initialRoute: Utils.isLogin() ? '/index' :'/login',
// 路由页面
getPages: routes,
);
}
}
新建一个router路由管理目录。
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,
];
// 路由拦截
class AuthMiddleware extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
return Utils.isLogin() ? null : const RouteSettings(name: '/login');
}
}
Getx提供了middlewares中间件进行路由拦截。
// 路由拦截
class AuthMiddleware extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
return Utils.isLogin() ? null : const RouteSettings(name: '/login');
}
}
如下图:通过bitsdojo_window
组件实现自定义窗口最小化/最大化/关闭按钮功能。
该插件引入会自动去掉系统导航条,支持自定义窗口尺寸,拖拽及最大化/最小化/关闭功能。
@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),)
),
],
);
}
);
}
最大化后不能及时监听窗口大小变化,可以通过flutter内置的WidgetsBindingObserver
来监测窗口变化。
class _WinbtnState extends State<Winbtn> 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小视频播放模块 Q:282310962
Container(
width: MediaQuery.of(context).size.height * 9 / 16,
decoration: const BoxDecoration(
color: Colors.black,
),
child: Stack(
children: [
// Swiper垂直滚动区域
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: [
// ...
],
),
),
// 播放mini进度条
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),),)
),
],
),
// 固定tab菜单
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),
),
),
),
],
),
),
Ok,限于篇幅,基于flutter3.x开发桌面端聊天项目的知识分享就到这里。希望对大家有所帮助~~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。