Записная книжка на Flutter и СУБД РЕД База Данных)
Цель
Создать простое десктопное приложение на Flutter для RedOS, выполняющее CRUD‑операции в базе данных РЕД База Даных. Программа реализует записную книжку с контактами.
Установка Flutter
-
Установите зависимости для сборки Linux‑версии Flutter:
sudo dnf install cmake ninja-build clang gtk3-devel -
В VS Code установите расширение Dart и Flutter


-
При помощи данного расширения создайте новый проект

-
После этого появится окно с ошибкой; нажмите
Download SDK
-
Выбираем директорию для установки (в моем случае я буду устанавливать в новой папке Libs) и нажимаем
Clone Flutter -
Ждем, пока Flutter установится.

-
Теперь, как и на шаге 3, снова создаём проект и видим ту же ошибку, но на этот раз нажимаем
Locate SDKи указываем путь к папкеbin.
Создание первого приложения
-
Создадим новый проект
Как мы тут видим теперь у нас появился выбор, какой вид приложения мы хотим создать, выбираем
Application
-
Указываем директорию, в которой будут располагаться файлы проекта и в конце указываем имя проекта.
После этого у вас должен инициализироваться начальный проект, который вы сразу сможeте запустить нажатием клавиши
F5
Обратите внимание, что первый запуск может занять продолжительное время, так как проект загружает все нужные ему библиотеки.
Структура Flutter-приложения
Здесь я приведу общую структуру приложения на Flutter:
lib/
- main.dart — точка входа. Здесь void main() => runApp(...). По умолчанию — демо-счётчик.
- Всё остальное приложение (виджеты, страницы, состояние, сервисы) принято держать в lib/.
test/
- Тесты. Создаётся пример widget_test.dart с проверкой счётчика.
android/
-
Нативная часть для Android (Gradle-проект).
Важно:
- app/src/main/AndroidManifest.xml — разрешения, название activity, label.
- app/build.gradle — applicationId, minSdk, targetSdk, версии.
- MainActivity.kt — точка входа Android-контейнера (обычно не требует изменений).
- Плагины с нативным кодом для Android добавляют файлы сюда.
ios/
-
Нативная часть для iOS/macOS (iOS) — Xcode-проект Runner.
Важно:
- Runner/Info.plist — имя приложения, права, схемы URL и т. п.
- Runner.xcodeproj/Runner.xcworkspace — открывать в Xcode.
- Podfile / Pods/ — CocoaPods для зависимостей плагинов.
web/
-
Заглушки для веб-сборки: index.html, web/favicon.png.
-
Настройки web-рендерера (HTML/CanvasKit) можно передавать флагами при запуске.
linux/, macos/, windows/
-
Платформенный «раннер» для desktop.
-
linux/ и windows/ используют CMake (CMakeLists.txt), macos/ — Xcode-проект.
-
Точки входа (main.cc/AppDelegate.swift) обычно не меняем, кроме интеграции плагинов/оконных настроек.
build/
- Временные артефакты сборки. Генерируется Flutter’ом. В репозиторий не коммитится.
Настройка VS Code для сборки и отладки приложения
-
В каталоге с вашим приложением создайте папку
.vscode -
Внутри папки создайте файл
launch.json— он нужен для создания конфигурации отладки{ "version": "0.2.0", "configurations": [ { "name": "Flutter – Запустить на Linux‑устройстве", "type": "dart", "request": "launch", "program": "lib/main.dart", "args": ["-d", "linux"], } ] }name- Понятное имя профиля, отображается в выпадающем списке «Run and Debug».type- Тип отладчика. Для Flutter/Dart используетсяdart(расширение Dart-Code).request- Режим запуска:launch— запустить приложение;attach— подключиться к уже запущенному процессу.
program- Входная точка приложения (путь к файлу Dart), обычноlib/main.dart. Путь задаётся относительно корня рабочего пространства.args- Аргументы командной строки для вашего Dart-приложения (то есть попадут вmain(List<String> args)), а не для инструментаflutter.
-
Теперь напишем команды для сборки приложения в бинарный файл и загрузки зависимостей, для этого создадим файл
tasks.json{ "version": "2.0.0", "tasks": [ { "label": "flutter pub get", "type": "shell", "command": "flutter pub get", "problemMatcher": [], "group": "build" }, { "label": "flutter run", "type": "shell", "command": "flutter run -d linux", "problemMatcher": [], "group": "build" } ] }-
label- человекочитаемое имя задачи. Показывается в списке задач. По этому имени можно ссылаться из других конфигов (например,preLaunchTaskвlaunch.json). -
type- тип исполнения.shell— команда будет выполнена через оболочку (bash/cmd/powershell). Есть ещё вариантprocess(запуск процесса без оболочки). -
command- строка команды, которую надо выполнить. В нашей конфигурации:flutter pub get— скачивает/обновляет зависимости проекта изpubspec.yaml.flutter run -d linux— собирает и запускает приложение на десктоп-таргете Linux.
-
problemMatcher- настройка распознавания ошибок/предупреждений в выводе задачи (регэкспы, чтобы VS Code подсветил файлы/строки).Пустой массив
[]означает: ошибки из вывода не разбираются в раздел «Проблемы» (Problems). Можно подключить свой matcher при необходимости. -
group- логическая группа задачи. Значение "build" относит задачу к группе «Сборка». Это даёт:- быстрый запуск через Ctrl+Shift+B (по умолчанию запускается задача из группы build; можно пометить как
isDefaultв объектном виде:{ "kind": "build", "isDefault": true }); - удобную категоризацию в UI.
- быстрый запуск через Ctrl+Shift+B (по умолчанию запускается задача из группы build; можно пометить как
-
Написание приложения "Телефонная книга"
-
Для начала в файле
pubspec.yamlна 36-й строке добавим библиотеки, которые будем использоватьflutter_hooks: ^0.18.5+1 fbdb: ^1.5.0 path_provider: ^2.0.0flutter_hooks
Что это: надстройка над Flutter, позволяющая писать логику состояния с «хуками» вместо ручного создания State-классов.
Когда полезно: когда хочется компактнее управлять контроллерами/таймерами/инициализацией (например, TextEditingController, AnimationController, подписки), а также вызывать эффекты при монтировании/изменениях.
fbdb
Что это: клиентская библиотека для работы с РЕД База Даных из Dart/Flutter. Позволяет создавать/подключать базу, выполнять SQL, получать результаты.
path_provider
Что это: плагин для получения «правильных» директорий на платформах (Android/iOS/macOS/Windows/Linux): где хранить документы, кэш, временные файлы и т. п.
Когда полезно: чтобы не хардкодить путь '/home/user/...', а класть БД в корректное место для ОС.
-
Удалим весь код из файла
lib/main.dart, чтобы написать приложение с нуля -
Подключим зависимости
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:fbdb/fbdb.dart'; -
Напишем функцию
main, которая будет точкой входа в программуvoid main() async { WidgetsFlutterBinding.ensureInitialized(); final db = await openDb(); runApp(MaterialApp(title: 'Телефонная книжка', home: ContactsApp(db))); }Как вы видите здесь мы используем функцию
openDbкоторой у нас нет. Здесь мы хотим создавать базу данных, если ее нет или подключаться к уже существующей. Давайте напишем и эту функцию и расположим перед функциейmain.Future<FbDb> openDb() async { const path = '/home/user/contacts.fdb'; // поправьте путь при необходимости final exists = File(path).existsSync(); final db = exists ? await FbDb.attach(database: path, user: 'sysdba', password: 'masterkey') : await FbDb.createDatabase( database: path, user: 'sysdba', password: 'masterkey'); final table = await db.selectAll( sql: r"SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'CONTACTS'", ); if (table.isEmpty) { await db.execute(sql: ''' CREATE TABLE contacts( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100), phone VARCHAR(50) ) '''); } return db; }Что происходит в этой функции:
final exists = File(path).existsSync()- проверяет наличие файла, если его нет, то мы используемFbDb.createDatabase, которая создаст базу данных, а если файл есть, то мы просто подключаемся к немуawait FbDb.attach- проверка RDB$RELATIONS узнаёт, создана ли таблица CONTACTS
- r"..." — «raw string» (удобно для SQL)
-
Теперь напишем корневой виджет — экран приложения. Он должен хранить подключение к БД и состояние экрана.
class ContactsApp extends StatefulWidget { final FbDb db; const ContactsApp(this.db, {super.key}); @override State<ContactsApp> createState() => _ContactsAppState(); }Здесь мы использовали его как
StatefulWidget, чтобы список контактов мог меняться -
Создадим состояние: контроллеры полей и список контактов
class _ContactsAppState extends State<ContactsApp> { final _name = TextEditingController(); final _phone = TextEditingController(); List<Map<String, dynamic>> _contacts = []; @override void initState() { super.initState(); _load(); }Тут мы будем держать текстовые контроллеры и данные. Также, чтобы при открытии приложения мы видели существующие записи, будем загружать их при старте в
initState().TextEditingController- это контроллеры, которые управляют вводом._contacts- массив строк из БД.Далее напишем функции этого класса.
-
Напишем сейчас функцию
_load, которая будет загружать данные из БД// Метод класса _ContactsAppState Future<void> _load() async { _contacts = await widget.db.selectAll( sql: 'SELECT id, name, phone FROM contacts ORDER BY id', ); setState(() {}); }Здесь мы тянем все контакты и обновляем UI:
selectAllвернётList<Map<String,dynamic>>setStateперерисует экран
Примечание: в РЕД База Даных без кавычек имена столбцов становятся ВЕРХНИМ РЕГИСТРОМ, поэтому в коде дальше обращение идёт как
c['NAME'],c['ID'],c['PHONE'] -
Напишем функцию
_add, которая будет добавлять новый контакт// Метод класса _ContactsAppState Future<void> _add() async { await widget.db.execute( sql: 'INSERT INTO contacts(name, phone) VALUES(?, ?)', parameters: [_name.text, _phone.text], ); _name.clear(); _phone.clear(); await _load(); }Здесь выполняем параметризованный INSERT, чистим поля, перезагружаем список.
- плейсхолдеры
?+parametersзащищают от SQL-инъекций и упрощают подстановку значений.
- плейсхолдеры
-
Напишем функцию
_updateдля редактирования контактов// Метод класса _ContactsAppState Future<void> _update(Map c) async { final name = TextEditingController(text: c['NAME']); final phone = TextEditingController(text: c['PHONE']); final ok = await showDialog<bool>( context: context, builder: (_) => AlertDialog( title: const Text('Редактирование'), content: Column(mainAxisSize: MainAxisSize.min, children: [ TextField(controller: name, decoration: const InputDecoration(labelText: 'Имя')), TextField(controller: phone, decoration: const InputDecoration(labelText: 'Телефон')), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Отмена')), TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Сохранить')), ], ), ); if (ok ?? false) { await widget.db.execute( sql: 'UPDATE contacts SET name=?, phone=? WHERE id=?', parameters: [name.text, phone.text, c['ID']], ); await _load(); } }В этой функции мы показываем диалог с предзаполненными полями. При нажатии кнопки "Сохранить" делаем
UPDATEshowDialogвозвращаетtrue/falseв зависимости от кнопки- при подтверждении выполняем
UPDATEпоid
-
Напишем функцию
_deleteдля удаления контактов// Метод класса _ContactsAppState Future<void> _delete(int id) async { await widget.db.execute( sql: 'DELETE FROM contacts WHERE id=?', parameters: [id], ); await _load(); }Здесь мы выполняем
DELETEи перезагружаем список. -
Верстка
build— UI экранПоскольку основные функции готовы, воспользуемся ими при создании визуальной оболочки нашего приложения. Она будет выглядеть так: сверху список с редактированием и удалением, снизу панель ввода и добавления.
// Метод класса _ContactsAppState @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: const Text('Контакты')), body: Column(children: [ Expanded( child: ListView.builder( itemCount: _contacts.length, itemBuilder: (_, i) { final c = _contacts[i]; return ListTile( title: Text(c['NAME']), subtitle: Text(c['PHONE']), onTap: () => _update(c), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () => _delete(c['ID']), ), ); }, ), ), Padding( padding: const EdgeInsets.all(8), child: Row(children: [ Expanded(child: TextField(controller: _name, decoration: const InputDecoration(labelText: 'Имя'))), const SizedBox(width: 8), Expanded(child: TextField(controller: _phone, decoration: const InputDecoration(labelText: 'Телефон'))), IconButton(icon: const Icon(Icons.add), onPressed: _add), ]), ), ]), );Здесь
ListView.builderпоказывает все контакты; тап по строке — редактирование, корзина — удаление. Нижняя панель — дваTextField+ кнопка+для добавления.ВНИМАНИЕ: функции
_load,_add,_update,_deleteиbuildпринадлежат классу_ContactsAppStateи должны находиться внутри него.
Готовый результат

Полный код программы
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:fbdb/fbdb.dart';
Future<FbDb> openDb() async {
const path = '/home/user/contacts.fdb'; // поправьте путь при необходимости
final exists = File(path).existsSync();
final db = exists
? await FbDb.attach(database: path, user: 'sysdba', password: 'masterkey')
: await FbDb.createDatabase(
database: path, user: 'sysdba', password: 'masterkey');
final table = await db.selectAll(
sql: r"SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'CONTACTS'",
);
if (table.isEmpty) {
await db.execute(sql: '''
CREATE TABLE contacts(
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(100),
phone VARCHAR(50)
)
''');
}
return db;
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final db = await openDb();
runApp(MaterialApp(title: 'Записная книжка', home: ContactsApp(db)));
}
class ContactsApp extends StatefulWidget {
final FbDb db;
const ContactsApp(this.db, {super.key});
@override
State<ContactsApp> createState() => _ContactsAppState();
}
class _ContactsAppState extends State<ContactsApp> {
final _name = TextEditingController();
final _phone = TextEditingController();
List<Map<String, dynamic>> _contacts = [];
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
_contacts = await widget.db.selectAll(
sql: 'SELECT id, name, phone FROM contacts ORDER BY id',
);
setState(() {});
}
Future<void> _add() async {
await widget.db.execute(
sql: 'INSERT INTO contacts(name, phone) VALUES(?, ?)',
parameters: [_name.text, _phone.text],
);
_name.clear();
_phone.clear();
await _load();
}
Future<void> _update(Map c) async {
final name = TextEditingController(text: c['NAME']);
final phone = TextEditingController(text: c['PHONE']);
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Редактирование'),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(controller: name, decoration: const InputDecoration(labelText: 'Имя')),
TextField(controller: phone, decoration: const InputDecoration(labelText: 'Телефон')),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Отмена')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Сохранить')),
],
),
);
if (ok ?? false) {
await widget.db.execute(
sql: 'UPDATE contacts SET name=?, phone=? WHERE id=?',
parameters: [name.text, phone.text, c['ID']],
);
await _load();
}
}
Future<void> _delete(int id) async {
await widget.db.execute(
sql: 'DELETE FROM contacts WHERE id=?',
parameters: [id],
);
await _load();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Контакты')),
body: Column(children: [
Expanded(
child: ListView.builder(
itemCount: _contacts.length,
itemBuilder: (_, i) {
final c = _contacts[i];
return ListTile(
title: Text(c['NAME']),
subtitle: Text(c['PHONE']),
onTap: () => _update(c),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _delete(c['ID']),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(children: [
Expanded(child: TextField(controller: _name, decoration: const InputDecoration(labelText: 'Имя'))),
const SizedBox(width: 8),
Expanded(child: TextField(controller: _phone, decoration: const InputDecoration(labelText: 'Телефон'))),
IconButton(icon: const Icon(Icons.add), onPressed: _add),
]),
),
]),
);
}
Справочная информация
Дата последнего изменения: 14.09.2026
Если вы нашли ошибку, пожалуйста, выделите текст и нажмите Ctrl+Enter.