前言:Flutter作为一门跨端框架,桌面端的支持一直在团队的Roadmap规划中。在2022年第一个Release版本发布后,我也开始使用Flutter开发桌面应用,过程中对Flutter在桌面端支持的力度、生态社区的成熟度有深刻的体会。
当前使用Flutter做桌面开发,开发者必须会C++。如果完全依赖于官方推进,那整个项目一定会踩到各种各样的坑。
一、Flutter如何创建Windows App
1. 生成依赖
创建项目成功后,Flutter pub get
会生成Flutter相关的依赖。因为我们已经在ide的Flutter插件中配置了Flutter SDK Path,pub get指令会将必要的文件生成到项目中。
2. 定义CmakeFile
Flutter Windows目录是个很普通的C++项目;通过CMake编译项目,在windows下的CMakeLists.txt中定义了很多全局的宏,包括可执行文件的配置、运行环境等; 然后在runner下的CMakeLists.txt中链接各种依赖库,这就包含了刚才生成的Flutter文件,至此Cpp文件可以正常使用Flutter的Api。
3. C++创建Win32Window
继续往下来到源代码处,main.cpp中声明了flutter::DartProject
,传给FlutterWindow对象,FlutterWindow继承自Win32Window。
C++
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data"); // 创建DartProject
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); // 设置入口参数
FlutterWindow window(project); // 传给FlutterWindow对象
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
// 注意这里的CreateAndShow,非常重点
if (!window.CreateAndShow(L"cmake_robot", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}
window.CreateAndShow会调用Win32官方的CreateWindow接口,创建一个windows窗口,再调用OnCreate方法;
C++
// win32_window.cpp
bool Win32Window::CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size,
const bool& isToolWindow)
{
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = { static_cast<LONG>(origin.x),static_cast<LONG>(origin.y) };
// ----省略好多代码----
// WinUser.h提供的CreateWindow
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPED & ~WS_THICKFRAME | WS_MINIMIZEBOX & ~WS_VISIBLE,
newOrigin.x, newOrigin.y,
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
// 设置应用程序窗口背景颜色为透明
UpdateWindow(window);
if (!window)
{
return false;
}
return OnCreate();
}
注意这里调用的是FlutterWindow重写的OnCreate,把FlutterViewController的视图传给SetChildContent,SetChildContent中通过MoveWindow刷新视图,从而把Flutter的UI绘制上去。
C++
// flutter_windows.cpp
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow()); // 这里把flutter_controller_的视图传进去
return true;
}
C++
void Win32Window::SetChildContent(HWND content)
{
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true); // MoveWindow也是Win32的接口,传入true表示强制刷新视图
SetFocus(child_content_);
}
可以看出,整个应用窗口的创建都是C++负责的。Flutter依然是很纯粹的UI框架,通过FlutterViewController把内容更新到Win32Window中,之后就是内部管理UI视图的更新。
二、Flutter提供的能力太过单一
通过分析项目Native的代码,我们已经得出结论:Flutter只负责UI的绘制 。
1. 基本的窗口操作
然而在业务场景中,桌面应用往往需要考虑很多窗口的交互,比如设置窗口显隐、改变窗口大小等非常常见的操作,遗憾的是:Flutter并没有提供任何操作窗口的Api。
目前只能依赖于pub的开源插件:window_manager来操作窗口,实际使用过程中还是会产生小部分的兼容问题,此时往往需要开发者在C++层面自行解决 。
2. 不同的应用类型
笔者是OS开发,实际开发的应用类型是比较杂的,比如:全屏应用、悬浮工具、后台应用等;同时我们对窗口启动速度也会有要求,如果等待Flutter窗口显示完毕再做窗口属性的设置,那么用户体验将会大打折扣。
所以我们都是直接操作Win32Window的属性,应用窗口的样式/位置、单例模式等,都在C++层去解决掉,提高性能参数,同时让Flutter更纯粹 。既然它不适合干窗口的事情,那就安心做好跨端 UI 吧!
3. Pub生态成熟度
这两年pub.dev上适配Windows的插件数量越来越多,这无疑给开发者打了不少强心剂。 但实事求是,21855个插件实际上大部分都无需跟C++层打交道,比如我想获取网卡的Mac地址都找不到适合的插件。再如更新窗口名称、获取USB设备、监听hid设备插拔等普遍的需求,Flutter生态都是不支持的。因此具备编写Flutter Desktop Platform 插件,是必不可少的能力。
三、核心痛点:多窗口、底层服务
1. 多窗口
桌面应用与移动应用的最大区别绝对是:窗口化。作为炙手可热的桌面应用框架,Flutter完全不支持多窗口,这个问题也是被人诟病已久。
在官方最新的Roadmap中,也终于再次提及多窗口的支持,实际上多窗口提了4年之久,2024年有望实现吗~ 另外由于某些众所周知的原因,2023年Flutter桌面没什么大的进展,作为开发者需要自力更生。
我们在梳理Flutter机制后,发现多窗口完全就是多个Win32的窗口,每个窗口对应一个FlutterEngine,各个Engine虽然内存无法实现共享,但也能依附于主进程在多个channel中通信。由此自行开发了多窗口的插件,这一切还是离不开C++的能力。
2. 底层服务
随着Flutter的应用深入,我们开始探索Flutter更多的可能性。比如依赖Flutter engine,扩展dart在跨端服务的可行性。
在Android中,我们创建Android Service,使用FlutterEngineGroup来管理Flutter Engine,通过 DartExecutor
执行dart逻辑;
在C++中,我们依然能看到flutter_engine.h的身影,那么它能不能在Window Service中去执行特定的Dart逻辑代码呢?自然是可以的。
由此我们成功实现了Flutter在Android和Windows中,UI和服务双双跨平台,这实际上已经远远超过Flutter的技术范畴。
四、结语
Flutter Windows应用,基于C++编写的Win32Window,在原生窗口上绘制UI,原理上离不开C++的技术;同时在列举的业务场景、框架痛点上也足以说明C++能力的重要性。
作为Flutter开发者,有精力的情况下,一定要多学各个平台的框架和能力,让Flutter、更让自己走的更远!