Язык программирования Dart был изначально разработан как перспективная замена JavaScript в веб-приложениях (с поддержкой асинхронности, потоков, классической поддержки ООП и возможностью использования строгой типизации), но, к сожалению, в этом качестве он так и не достиг значительных успехов. Однако в дальнейшем компилятор Dart был доработан для других целевых платформ и наибольшего успеха достиг в сочетании с фреймворком Flutter как инструмент разработки высокопроизводительных мобильных приложений, создаваемых на основе реактивной модели. Но нужно отметить, что наряду с возможностями компиляции в целевые платформы Android и iOS (а также, разумеется, Web), Dart также может использоваться для создания приложений для операционных систем Windows, Linux и MacOS, что в сочетании с возможностями фреймворка Flutter и оптимизированных платформенных реализаций Flutter Engine и Embedder, представило новый путь к созданию нативных приложений с графическим интерфейсом. В этой статье мы рассмотрим возможности и особенности реализации desktop-приложений на Flutter и разберемся с механизмами интеграции внешних библиотек.

Проект на языке Dart представляет из себя один или несколько исходных файлов с расширением .dart (с явным указанием импорта используемых компонентов), файл описания проекта pubspec.yaml (описывает метаданные для компилятора, а также зависимости и используемые ресурсы), а также платформенные компоненты, которые могут быть интегрированы в финальный исполняемый артефакт на этапе сборки проекта. Во время сборки и компиляции исходные тексты проекта и подключенных компонентов, а также платформенные компоненты (при использовании плагинов) объединяются в единый исполняемый образ, который может выполняться как внутри специальной среды выполнения, так и являться полностью автономным исполняемым файлом. Любое приложение, даже если ничего не импортировано явно, интегрирует пакет dart:core, который содержит методы для работы со строками, коллекциями объектов, датой-временем, регулярными выражениями и сетевыми адресами, потоками и асинхронностью, что позволяет использовать базовую функциональность одинаковым образом, независимо от целевой платформы.

Компиляция проекта может происходить в одном из нескольких режимов и выполняется командой dart compile <subcommand>:

  • js – компиляция исходных кодов в код на JavaScript для запуска в браузере (например, используется в Flutter for Web или при разработке сайтов с использованием React или AngularDart, либо без использования фреймворка). При компиляции выполняется tree-shaking, в результате которого из кода удаляются все неиспользуемые функции и их зависимости.

  • jit-snapshot – создание промежуточного кода для выполнения на конкретной архитектуре (в дальнейшем может быть запущен через команду dart <name>.jit), выполняет тестовый прогон для сохранения состояния памяти и результата just-in-time компиляции для возможности быстрого повторного выполнения.

  • aot-snapshot – создание двоичного кода для текущей архитектуры, не включает в себя реализацию среды выполнения. Для запуска снимка можно использовать команду dartaotruntime <name>aot.

  • kernel – создание переносимого представления исходного кода (может быть запущено на любой поддерживаемой платформе), может быть в дальнейшем запущено через команду dart <name>.dill

  • exe – компиляция в выполняемый файл (включает в себя двоичный код, реализующий логику приложения, а также среду выполнения и связанные библиотеки, необходимые для работы приложения).

Для примера мы создадим простое приложение для вывода информации о зарегистрированных расходах и последовательно будем его дорабатывать и превратим в конечном итоге в полноценное приложение с графическим интерфейсом на Flutter. В качестве целевой платформы мы будем рассматривать Linux, но похожим образом может быть создано приложение и для Windows / MacOS (отличия будут только в способах подключения библиотек и алгоритме сборки финальных распространяемых артефактов). Начнем с простого консольного приложения, которое будет получать информацию о расходах из текстового файла.

import 'dart:io';
import 'dart:convert';

Stream<double> expenses() {
  return File("expenses.csv").openRead().map(utf8.decode).transform(LineSplitter()).map((l) => double.tryParse(l));
}

void main() {
  print("Expenses for period:");
  expenses().listen((a) {
    print("*$a");
  });
}

Создадим файл с величинами расходов (expenses.csv). Затем выполним компиляцию в исполняемый файл и проверим корректность работы скомпилированного приложения:

dart compile exe expenses.dart
./expenses.exe
*100.1
*10.99
*100.5

Заменим извлечение строк из файла на получение информации через сеть, для этого будем использовать возможности пакета http (поддерживается как на мобильных платформах, так и для Web и desktop-приложений).

import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;

Stream<double> expenses() async* {
  final client = http.Client();
  final expenses = await client.get(Uri.parse('https://raw.githubusercontent.com/dzolotov/flutter-linux/main/expenses.csv'));
  for (final v in LineSplitter().convert(expenses.body).map((l) => double.tryParse(l))) {
    yield v;
  };
}

void main() async {
  print("Expenses for month:");
  (await expenses()).listen((a) {
    print("*$a");
  });
}

Мы должны увидеть в консоли список строк со значениями расходов, извлеченных из указанного сетевого расположения.

Следующим этапом добавим поддержку графического интерфейса в нативном приложении. На этапе мы попробуем реализовать поддержку графического интерфейса без использования Flutter, это возможно через связывание разрабатываемого приложения с библиотеками GTK. Dart представляет возможность обращаться к внешним загружаемым библиотекам (so/dll) через поддержку Foreign Function Interface (пакет dart:ffi). FFI представляет набор классов для описания типов данных C и указателей, а также способы определения внешних функций (NativeFunction), управления памятью (Allocator) и предоставляет механизмы для вызова функций Dart из внешней библиотеки на C (NativeApi). Также возможно загружать динамическую библиотеку (.so / .dll) и использовать экспортированные символы через конструкторы класса DynamicLibrary.

Для подключения библиотеки GTK мы будем использовать экспериментальные биндинги из проекта https://github.com/Kleak/gtk

apt-get install llvm-dev libclang1 libclang-cpp-dev clang-dev libclang1-dev 
dart pub get
dart pub run ffigen:setup -I/usr/lib/llvm-13/include -L/usr/lib/llvm-13/lib
dart compile exe example/counter.dart

после успешной сборки можно запустить example/counter.exe и получить gtk-вариант приложения со счетчиком.

Hidden text

Если возникает ошибка при запуске, нужно создать символическую ссылку на gtk (sudo ln -s /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 /usr/lib/x86_64-linux-gnu/libgtk-3.so), либо изменить путь к динамической библиотеки в gtk/lib/src/init.dart.

По аналогии можно было бы создать и наше приложения учета финансов, но в действительности задача не выглядит очень простой. Как минимум нужно будет реализовать подключения к функциям динамической библиотеки, поскольку в существующих биндингах их набор ограничен. Так, например, для создания контейнера необходимо создать определения типов и использовать механизмы для обнаружения экспорта динамической библиотеки, например так:

typedef gtk_container_new_func = Pointer<NativeGtkContainer> Function();
typedef GtkContainerNew = Pointer<NativeGtkContainer> Function();
Pointer<NativeGtkContainer> gtkContainerNew() {
  final f = gtk.lookupFunction<gtk_container_new_func, GtkContainerNew>('gtk_container_new');
  return f();
}

Для создания графических приложений более удобным способом будет применение фреймворка Flutter, который использует Dart как основную технологию разработки и предоставляет удобные механизмы связывания с графическими библиотеками для нативных платформ (на Linux используется GTK). При этом фреймворк реализует функциональность визуальной компоновки и отслеживания изменений дерева виджетов и позволяет создавать приложения в реактивном стиле с возможностью декларативного связывания конфигурации интерфейса и состояния (которое может быть связано с виджетами, либо храниться отдельно и распространяться с использованием подписки на изменения).

Поскольку на текущий момент поддержка Linux и MacOS находится в стадии эксперимента, ее необходимо явным образом разрешать. Для настройки дополнительных целевых платформ будем использовать команду flutter config --enable-linux-desktop (или flutter config --enable-macos-desktop). Для корректной сборки также необходимо установить зависимости для компиляции:

sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev

Теперь создадим новый проект приложения flutter create –t app expenses

Перейдем в каталог проекта и убедимся, что среди каталогов есть linux (или macos, в зависимости от выбранной целевой платформы). Точкой входа в приложение на Flutter, как и для любого приложения на Dart, является функция main, по умолчанию расположенная в файле lib/main.dart.

Запуск приложения начинается с вызова функции runApp (экспортирован из пакета material/widgets.dart, либо платформенных material.dart / cupertino.dart), которому передается экземпляр корневого виджета. В большинстве случаях для корневого используются виджеты MaterialApp (или CupertionApp для iOS), которые создают необходимый контекст приложения, регистрируют навигацию и тему оформления, а также отвечают за корректную локализацию и иные аспекты взаимодействия с платформой.

Запустим наше приложение: flutter run –d linux.Результатом выполнения будет демонстрационное приложение в стиле Material Design с кнопкой и счетчиком нажатий (запущенное в виде отдельного окна). Как можно увидеть, заголовок окна повторяет название приложения, что не всегда совпадает с ожиданиями. Кроме того нет возможности изменить размеры окна при запуске. Давайте исправим это и добавим в наше приложение плагин window_manager, для этого необходимо в секцию dependencies в pubspec.yaml вписать название плагина и его версию (window_manager: ^0.2.1) и установить необходимые зависимости (flutter pub get)

Теперь мы можем изменить заголовок и конфигурацию окна до его создания (до запуска runApp). Для этого необходимо убедиться, что необходимый контекст выполнения был инициализирован и окно отображено:

import 'package:window_manager/window_manager.dart';
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await windowManager.ensureInitialized();
  windowManager.waitUntilReadyToShow().then((_) async {
    // Hide window title bar
    await windowManager.setTitleBarStyle(TitleBarStyle.normal);
    await windowManager.setTitle("Expenses Tracker");
    await windowManager.setSize(Size(400, 400));
    await windowManager.center();
    await windowManager.show();
  });
  runApp(const MyApp());
}

Также можно подписаться на события жизненного цикла окна для отслеживания закрытия, уменьшения и увеличения размера, потери и возвращении фокуса, для этого к состоянию корневого виджета нужно добавить mixin WindowListener.

class _ExpensesState extends State<Expenses> with WindowListener {
...
}

Реализуем вывод полученных данных из сети в виде списка в окне и добавим кнопку для регистрации нового значения расхода. Для этого заменим тип результата и будем создавать ожидаемое значение (Future), вместо потока, чтобы можно было идентифицировать состояние ожидания (пока идет загрузка).

Future<Iterable<double>> expenses() async {
  final client = http.Client();
  final expenses = await client.get(Uri.parse('https://raw.githubusercontent.com/dzolotov/flutter-linux/main/expenses.csv'));
  return LineSplitter().convert(expenses.body).map((l) => (double.tryParse(l) ?? 0.0));
}

class _ExtensesState extends State<Expenses> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FutureBuilder<Iterable<double>>(future: expenses(), builder: (context, snapshot) {
          if (snapshot.hasData) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center, 
              children: snapshot.requireData.map(
                (d) => Text(d.toString())).toList()
            	);
          } else if (snapshot.hasError) {
            return const Text('Error');
          } else {
            return const CircularProgressIndicator();
          }
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Add expenses',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Разработка графических приложений для Desktop ничем принципиально не отличается от создания мобильных приложений на Flutter (нужно только убедиться, что используемые плагины поддерживают конкретную целевую платформу). Важно задать граничные значения для размера окна (через плагины window_manager или window_size), чтобы сохранять верстку, а также использовать возможности определения размеров контейнеров (например, через LayoutBuilder) для создания адаптивной верстки. Дополнительно можно отключить отображение всплывающих подсказок при наведении, для этого часть дерева может быть обернута в виджет TooltipVisibility со значением visible в false.

Важным аспектом разработки приложений, ориентированных на запуск в Windows/Linux/MacOS является регистрация комбинаций клавиш и их связывание с действиями. Одним из вариантов может быть виджет RawKeyboardListener (определяет события нажатия и отпускания клавиши) или более высокоуровневый FocusableActionDetector, который связывает между собой LogicalKeySet и Intent (в shortcuts), а также Intent и функцию (в actions).

Также для desktop-приложений возможно получать доступ к нативным API операционной системы через ffi (аналогично тому, как ранее мы рассматривали подключение к gtk), для большинства задач существуют готовые плагины (например, win32 для доступа к Win32 API, win32_registry для получения доступа к реестру Windows, win32_gamepad для подключения к геймпаду, posix для доступа к POSIX API на всех операционных системах и др.)

Частый сценарий в desktop-приложениях – необходимость отправить информацию (например, отчет о расходах) на печать или в PDF-документ. Здесь может быть полезной библиотека printing, которая работает на всех платформах и может создавать форматированные PDF-документы. Документация и примеры использования библиотеки могут быть найдены на официальной странице.

Последний вопрос, который мы разберем сегодня – сборка приложения в устанавливаемый артефакт. Алгоритм сборки зависит от выбранной платформы и пошагово описан в официальной документации, мы рассмотрим только сборку приложения в snap для установки на Linux с использованием snapd.

Для сборки snap будет необходимо установить инструментальную поддержку:

snap install snapcraft —classic
snap install multipass —classic

Опционально можно установить поддержку сборку с использованием контейнеризации на основе lxd:

snap install lxd
sudo lxd init
(оставим все ответы по умолчанию)

Создадим файл описания приложения snapcraft.yml

name: expenses
version: 0.0.1
summary: Expenses Tracker
description: Take control on your expenses!

confinement: strict
base: core18
grade: stable

slots:
  dbus-expenses: 
    interface: dbus
    bus: session
    name: tech.dzolotov.expenses
    
apps:
  expenses:
    command: expenses
    extensions: [flutter-master] # здесь можно поставить экспериментальную ветку
    plugs:
    - network
    slots:
      - dbus-expenses
parts:
  expenses:
    source: .
    plugin: flutter
    flutter-target: lib/main.dart	# файл, содержащий точку входа (функцию main)

Создадим файл с описанием ярлыка в файле /snap/gui/expenses.desktop

[Desktop Entry]
Name=Expenses
Comment=Take control on your expenses
Exec=expenses 
Icon=${SNAP}/meta/gui/expenses.png
Terminal=false
Type=Application
Categories=Education; 

И также нужно добавить пиктограмму (в том же расположении expenses.png)

Теперь можно выполнить сборку:

snapcraft (для использования виртуальной машины через multipass) или snapcraft —lxd (для использования контейнеризации lxd)

Разработанное приложение может быть загружено (для этого необходимо зарегистрироваться на snapcraft.io, затем войти в учетную запись snapcraft login, зарегистрировать приложение snapcraft register и загрузить snap-файл через snapcraft upload —release=track expenses.snap)

Локально установить приложение можно из созданного snap-файла:

sudo snap install expenses_0.0.1_amd64.snap --dangerous

После чего можно его запустить через /snap/bin/expenses (или через созданный ярлык, зарегистрированный в графической оболочке Linux).

Таким образом мы разработали простой прототип, который может быть доработан с использованием всех доступных возможностей Flutter Framework и библиотек, доступных на pub.dev, что предоставляет качественно новые возможности создания адаптивных пользовательских интерфейсов (с использованием реактивной модели), которое также может использовать существующие библиотеки и компоненты бизнес-логики.

Все исходные тексты приложения размещены в GitHub: ссылка на GitHub.


Как протестировать приложение с информацией из сети? Об этом расскажу уже завтра на бесплатном открытом уроке. В рамках урока мы разберемся как создать тесты для сетевых приложений на Flutter и проверим работу простого клиента для отображения мероприятий из публичного API на всех уровнях (модульные тесты, тесты виджетов, интеграционные тесты). Созданные тесты будут интегрированы в единый сценарий сборки в конвейере CI.

Регистрация на бесплатный урок.

Комментарии (18)


  1. addewyd
    05.04.2022 18:52
    +3

    Сходу попробовал expenses. И сходу не вышло.

    1. Пришлось сначала сделать dart create examle
    2. Потом dart pub add http
    3. Заменить на <double?>
    Тогда сработало (до сего момента с dart/flutter не сталкивался)

    4. Поток не закрывается, ждёт чего-то…


  1. HemulGM
    06.04.2022 08:49
    +3

    Какие, по-вашему, в данном случае преимущества у Dart+Flutter перед другими языками и их фреймворками, позволяющими создавать кроссплатформенные GUI приложения?


    1. danial72
      06.04.2022 13:52

      Субъективная позиция
      Более-менее нормальная типизация.
      Компонентный подход.
      Большая библиотека плагинов, которые работают работают одинаково на разных осях.
      Очень удобное асинхронное апи.


      1. HemulGM
        06.04.2022 13:59
        +3

        Не скажу за все языки, но такого добра в общем-то у некоторых хватает. Как по отдельности так и все пункты сразу.

        Могу привести в пример Delphi:
        ● Типизация? Есть
        ● Компонентный подход? Есть
        ● Большая библиотека кроссплатформенных плагинов? Есть
        очень удобное асинхронное апи? Тут есть, конечно, нюансы. async/await нет, но асинхронность там работает очень хорошо (TThread, ITask, IFuture и т.д.)

        При этом есть и другие преимущества, но и, естественно, недостатки


        1. Arman_kz
          06.04.2022 15:35
          +3

          Эххх дедьфисты вы ещё живы ....


          1. HemulGM
            07.04.2022 07:21
            +2

            Живее всех живых))


      1. staticmain
        06.04.2022 16:44
        +1

        Qt


    1. proninyaroslav
      06.04.2022 20:15
      +1

      Из реально используемой кроссплатформы сейчас только веб-фреймворки, .NET и Flutter (возможно Kotlin + Compose в будущем). Qt в мобильном сегменте это исчезающий вид, а остальные фреймворки скорее экзотика. А что выбрать уже каждый решает для себя, исходя из личных знаний, сложности приложения, скорости выполнения задачи. Ну, к примеру, преимущество Flutter в том что он даёт похожий на веб-фреймворки декларативный подход к разработке, но с большим performance готового приложения.


      1. HemulGM
        06.04.2022 20:36
        +2

        Как я уже сказал чуть выше, Delphi позволяет создавать нативные кроссплатформенные приложения. Используется фреймворк FMX (FireMonkey). Код исполняется нативно, а интерфейс может быть как нативный, так и не нативный, на выбор. Нативный интерфейс, логично, ограничен ОС и не поддается полной стилизации, не нативный более гибкий с огромными возможностями стилизации (сопоставимо с CSS стилизацией). При чем стиль любого контрола можно создавать визуально, без применения кода (в том числе и анимацию и реакции). При этом для некоторых ОС можно добавлять или убирать часть функционала, ограничивая код директивами, а стили можно создавать как отдельно, так и общий для всех платформ. Стилизация использует не стандартный подход, что позволяет применять любые стили любым контролам. Т.е. можно стиль кнопки применить к "метке" label или даже таблице. Ограничений нет. Но это уже тонкости.

        Приложение, созданное на основе FMX собирается на Windows (кроме ARM пока), Linux (за исключением некоторых дистрибутивов, в которых есть проблемы с Gtk, но их всего несколько и не столь популярных), MacOS, Android и iOS. Т.е. покрывает все основные ОС.

        Однако, из-за достаточно низкой популярности, об это мало кто знает


        1. proninyaroslav
          06.04.2022 20:43

          Это понятно что есть и другие решения, но популярные можно пересчитать на пальцах. А у решений на Delphi есть ещё клеймо устаревшего/академического языка, приток новых разработчиков намного меньше C# или JS.


        1. bzq
          07.04.2022 09:42

          А напишите статью, как сделать мобильное приложение на Дельфи. Должно быть интересно.


          1. HemulGM
            07.04.2022 10:32
            +1

            На самом деле, нет ни каких особенностей или ухищрений, чтобы создать мобильное приложение на Делфи.

            Для этого используется штатная среда разработки (которая всегда и была) - RAD Studio. Проект мы создаем почти как всегда, за исключением того, что выбираем не Windows VCL Application, а Multi-Device Application. А дальше всё как обычно: окно, кнопочки, поля. Есть конечно множество тонкостей и изменений в контролах, т.к. они позволяют намного больше, чем VCL. В частности привязки (Align), слои и так далее.

            По умолчанию выбрана сборка под Windows - Debug, но для сборки под другую платформу достаточно выбрать требуемую платформу в дереве проекта и снова нажать сборку. При этом, если телефон (или эмулятор) подключен к компу, то в верхнем меню он отобразится (там список), чтоб можно было запустить приложение и отлаживать.

            При разработке интерфейса есть возможность настроить его отдельно под разные виды экранов, в том числе разный DPI.

            Если хватит духу, возможно, напишу статью


            1. jtraub
              07.04.2022 11:17

              Было бы интересно почитать.

              А как дела у Lazarus (вроде бы так опенсорная RAD называлась в начале 2000) обстоят сейчас, не знаете?


              1. HemulGM
                07.04.2022 12:07

                Напрямую с Лазарусом не работаю. Но работаю со многими, кто использует только эту среду для написания на Delphi/FPC. Большинство из них используют Linux и разрабатывают для него, либо для него и Windows.

                Сама среда, к моему удивлению, не потерпела визуальных изменений с момент её выхода. И на первый взгляд не внушает доверия, однако FPC по своей ветке развития ушел достаточно далеко. Например, добавлены дженерики (не совместимые с Делфи) и, возможно, много чего ещё. Платформ, на которые позволяет собирать эта среда очень велико. Ну и сама среда может работать на всех основных ОС.

                В общем, всё что могу сказать, это то, что идет развитие и среды и FPC. Свежий апдейт вышел 5 января этого года.


            1. bzq
              07.04.2022 11:21
              +2

              Напишите. Тут дело даже не в сложностях разработки, а в самом факте. Просто покажите как выглядит этот процесс, а заодно много окружающих узнают о самой такой возможности. Я вот впервые слышу, что в дельфи можно под мобильную платформу разрабатывать. Не то чтобы это была великая тайна, мне просто в голову не приходило поинтересоваться.


        1. aywan
          07.04.2022 10:05

          Что насчёт цены Delphi, а также возможность нанять разработчиков?


          1. HemulGM
            07.04.2022 10:38

            Сейчас вообще с покупкой проблемы, в связи с последними, так сказать, событиями. Цены там своеобразные, но зато не подписочная система. Лицензия дает полное право получать обновления в рамках мажорной версии. А обновление мажорной версии стоит на 2/3 дешевле (примерно). Какие цены будут после текущих событий, надеюсь, узнаем в будущем.

            Но, есть и бесплатная версия - Community Edition, которая имеет ограничение лицензии аналогичной MS VS и отсутствует сборка под Linux. В остальном ограничений нет.


  1. avdosev
    06.04.2022 11:21

    К списку пакетов я бы еще добавил:
    Чтоб сделать "оконный" экспериенс поприятнее для пользователя
    https://pub.dev/packages/bitsdojo_window
    https://pub.dev/packages/flutter_acrylic
    https://pub.dev/packages/fluent_ui


    Для нотификации
    https://pub.dev/packages/dbus