Flutter Linux应用初探

4 阅读6分钟

距离我上一篇文章,足足过去一年!!!
断更是艰难的过程,日常斥责自己没有作品。除了工作的忙碌、技术栈重心的变化外,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)

image.png 总的来说,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上溯源其提交版本,很快的解决了这个问题。

image.png

写在后面

Linux App在国内的应用场景是比较少的,但随着接下来设备国产化的战略继续推进,我相信Flutter Linux会有进一步的需求。但是从生态上来看,不会C++的团队,在Flutter For Linux的道路上,是会遇到比较多的困难的。
Anyway,在国内鸿蒙化、国产化;世界范围AI编程、AOSP停止维护的大背景下,衷心希望Flutter桌面端越来越好吧~