Язык программирования 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: https://github.com/dzolotov/flutter-linux
Как протестировать приложение с информацией из сети? Об этом расскажу уже завтра на бесплатном открытом уроке. В рамках урока мы разберемся как создать тесты для сетевых приложений на Flutter и проверим работу простого клиента для отображения мероприятий из публичного API на всех уровнях (модульные тесты, тесты виджетов, интеграционные тесты). Созданные тесты будут интегрированы в единый сценарий сборки в конвейере CI.
Регистрация на бесплатный урок.