Flutter Linux应用初探
距离我上一篇文章,足足过去一年!!!
断更是艰难的过程,日常斥责自己没有作品。除了工作的忙碌、技术栈重心的变化外,AI的崛起带来技术交流平台的低迷,也是让我疲于更新的原因之一
近期重新投入Flutter技术,适配了Linux平台,才让我重新燃起奋笔疾书的欲望。Flutter for Linux在社区中的文章是非常之少的,期待这篇文章能给大家带来一些思考~
原理浅层分析
此次我是对旧项目进行Linux平台的适配,这个项目在Android和Windows平台已经顺利发布
运行两年。因此这里省去创建运行项目的说明。
两年前创建的项目,期间跟随Flutter版本升级到3.22。在Linux平台的首次运行,竟然一次就顺利跑起来了。这让我十分的欣喜,而后不断思考:为何Flutter在Linux能如此的顺利运行?
1. 应用载体
Flutter Linux的载体是一个典型的GtkApplication
。在main主入口,创建了MyApplication实例并运行应用程序。
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
在my_application.h中,使用G_DECLARE_FINAL_TYPE宏定义了MyApplication
的类型继承自GtkApplication
。
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication)
创建了在my_application后,自然就会按顺序的执行GtkApplication的生命周期。
应用程序的主要生命周期包含以下几个关键阶段:
- 启动(Startup)
-
激活(Activate)
:这是最重要的阶段。
主要完成:创建GTK窗口、设置窗口属性(大小、透明度等)、创建Flutter视图、注册Flutter插件... - 关闭(Shutdown)
总的来说,Flutter在Linux下的运行完全是依赖于GTK框架,通过以下步骤实现:
- 创建GTK应用程序
- 设置窗口和显示属性
- 初始化Flutter引擎
- 创建Flutter视图
- 处理生命周期事件和消息
2. engine挂载
Flutter的engine和view是怎么跟GtkApplication关联上的呢?核心代码都在GApplication::activate
的钩子中。
- 创建一个FlDartProject
g_autoptr(FlDartProject) project = fl_dart_project_new();
- 通过
fl_dart_project_set_dart_entrypoint_arguments
把启动参数,设置到Flutter层
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
- 创建FlView,并且作为GTK_WIDGET添加到容器GTK_CONTAINER中
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
- 注册Flutter插件
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
整个过程与GTKWinodw是比较脱离的,跟Android FlutterActivity、Windows FlutterWindow
的实现思路一模一样。
这也证明Flutter是个很纯粹的跨平台UI框架,脱离原生框架的束缚。所以3年前的项目,Linux端一次运行成功也就不足为奇了~
重点Tips
1. Flutter版本
Flutter的更新迭代是非常快的,并且桌面的支持也力不从心,所以对于一个新的平台来说,在开始适配的时候,一定要升级到最新版本,一定要用最新!!!
2. 设置窗口属性
Flutter是跨平台的UI,那么窗口的属性自然就无法快速去操作,比如:设置无标题栏、设置大小、居中等。
这里我们也不推荐在Flutter层面使用window_manager去操作,从性能和显示的实时效果出发,就应该在c++层处理完成
以下代码,为Flutter应用设置了依据分辨率适配大小、居中、隐藏标题栏、设置透明底等。
// 获取屏幕分辨率
gboolean GetScreenRect(gint *width, gint *height) {
GdkDisplay *display = gdk_display_get_default();
if (display) {
GdkMonitor *monitor = gdk_display_get_primary_monitor(display);
if (monitor) {
GdkRectangle geometry;
gdk_monitor_get_geometry(monitor, &geometry);
*width = geometry.width;
*height = geometry.height;
return TRUE;
}
}
return FALSE;
}
// 获取DPI
gint GetDpi() {
GdkScreen *screen = gdk_screen_get_default();
if (screen) {
return gdk_screen_get_resolution(screen);
}
return 96; // 默认DPI
}
static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr,
gpointer user_data)
{
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
cairo_paint(cr);
return FALSE;
}
static void transparent_setup(GtkWidget *win)
{
GdkScreen *screen;
GdkVisual *visual;
gtk_widget_set_app_paintable(win, TRUE);
screen = gdk_screen_get_default();
visual = gdk_screen_get_rgba_visual(screen);
if (visual != NULL && gdk_screen_is_composited(screen)) {
gtk_widget_set_visual(win, visual);
g_signal_connect(G_OBJECT(win), "draw", G_CALLBACK(on_draw_event), NULL);
}
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "SystemUpgradeMain");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "SystemUpgradeMain");
}
// 设置窗口透明
transparent_setup(GTK_WIDGET(window));
// 隐藏标题栏
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
// 设置窗口居中
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
// 获取缩放因子
double scale_factor;
gint screenWidth, screenHeight;
auto default_resolution = 1.0 * 1920 / 1080;
if (GetScreenRect(&screenWidth, &screenHeight)) {
auto current_resolution = 1.0 * screenWidth / screenHeight;
if (current_resolution > default_resolution) {
scale_factor = 1.0 * screenHeight / 1080;
} else {
scale_factor = 1.0 * screenWidth / 1920;
}
} else {
gint dpi = GetDpi();
scale_factor = dpi / 96.0;
}
std::cout << "scale_factor: " << scale_factor << std::endl;
// 设置窗口大小
gtk_window_set_default_size(window, 1172*scale_factor, 731*scale_factor);
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
GdkRGBA background_color;
gdk_rgba_parse(&background_color, "#ffffff");
fl_view_set_background_color(view, &background_color);
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
3. 查看Flutter for Linux源码
Flutter Linux的相关文章,全网都非常少见,其原因跟Flutter在Linux的投入,Linux系统下Flutter的应用生态都有所关系。好在官方的源代码文档,还是比较完整的:Flutter Linux源码。
在给Linux窗口设置透明背景时,我们就遇到了不少坑。
- 在Flutter 3.27之前,Flutter官方是没有提供透明窗口的方法的,FlutterView默认是黑色的。因此即便我们通过
cairo_paint
把GTKWindow绘制成透明的,上层的FlutterView依然不透明。
static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr,
gpointer user_data)
{
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
cairo_paint(cr);
return FALSE;
}
static void transparent_setup(GtkWidget *win)
{
GdkScreen *screen;
GdkVisual *visual;
gtk_widget_set_app_paintable(win, TRUE);
screen = gdk_screen_get_default();
visual = gdk_screen_get_rgba_visual(screen);
if (visual != NULL && gdk_screen_is_composited(screen)) {
gtk_widget_set_visual(win, visual);
g_signal_connect(G_OBJECT(win), "draw", G_CALLBACK(on_draw_event), NULL);
}
}
- 于是我们通过搜索源码文档,很快定位到了相关的api,再到github上溯源其提交版本,很快的解决了这个问题。
写在后面
Linux App在国内的应用场景是比较少的,但随着接下来设备国产化的战略继续推进,我相信Flutter Linux会有进一步的需求。但是从生态上来看,不会C++的团队,在Flutter For Linux的道路上,是会遇到比较多的困难的。
Anyway,在国内鸿蒙化、国产化;世界范围AI编程、AOSP停止维护的大背景下,衷心希望Flutter桌面端越来越好吧~