Как я создал приложение для расчёта строительных материалов за 3 месяца
Три месяца назад я работал консультантом в магазине стройматериалов. Каждый день одна и та же картина: приходит человек с замерами на мятой бумажке, говорит «мне нужен ламинат на комнату», и начинается цирк с калькулятором.
Умножаем длину на ширину, делим на площадь упаковки, округляем вверх, добавляем «ну, возьмите пару пачек про запас». Всё. Это весь расчёт, который получает клиент в 2026 году.
При этом никто не спрашивает, как он собирается укладывать — прямо или по диагонали. Никто не учитывает, что у двери будет подрезка, а у батареи — ещё одна. Про раппорт рисунка вообще молчу, это для большинства продавцов тёмный лес.
Результат предсказуемый. Человек либо приезжает через неделю докупать (а там уже другая партия, и оттенок отличается), либо у него полгаража забито остатками, которые никогда не пригодятся. При ценах на ремонт в несколько миллионов такие «погрешности» обходятся в десятки тысяч рублей.
И вот что меня зацепило: строительство — это же точная наука. Есть ГОСТы, есть СНиПы, есть нормы расхода на каждый материал. Всё давно посчитано и задокументировано. Почему тогда нет нормального инструмента, который берёт эти данные и выдаёт точный расчёт?
Онлайн-калькуляторы на сайтах магазинов умеют только площадь умножить на цену. Профессиональные программы вроде Гранд-Сметы стоят как чугунный мост и требуют недельного обучения. Западные приложения считают в футах и дюймах, что для российского прораба примерно как китайская грамота.
Так родилась идея «Мастерка» — мобильного калькулятора, который будет точным как инженерный справочник и простым как обычный калькулятор на телефоне.
Почему Flutter, а не React Native
С выбором технологии я провозился дольше, чем хотелось бы признавать.
React Native я знал хорошо, там богатая экосистема, можно быстро стартовать. Но за годы работы с ним я упёрся в одну фундаментальную проблему: мост между JavaScript и нативным кодом. Для приложения, где десятки полей ввода и каждое изменение должно мгновенно пересчитывать результат с валидацией — это узкое место.
Нативная разработка на Kotlin и Swift дала бы максимальную производительность, но означала бы писать всё дважды. Для одного разработчика это растягивание проекта на полгода минимум, а то и на год.
Flutter я раньше не трогал, Dart был для меня новым языком. Но два момента перевесили всё остальное. Во-первых, он компилируется напрямую в нативный ARM-код, без всяких мостов и прослоек. Во-вторых, горячая перезагрузка превращает разработку интерфейса в диалог с приложением — меняешь код и сразу видишь результат, без ожидания компиляции.
На освоение Dart ушла примерно неделя. Синтаксис похож на смесь Java и JavaScript, ничего космического.
Финальный стек получился таким:
YAML# Архитектура и состояние
flutter_riverpod: ^2.6.1 # Реактивное управление состоянием
isar_community: ^3.2.0 # Быстрая оффлайн БД (community fork)
# Пользовательский интерфейс
google_fonts: ^6.1.0 # Типографика Material You
fl_chart: ^0.69.0 # Визуализация данных
# Аналитика и мониторинг
firebase_core: ^3.3.0
firebase_analytics: ^11.2.1
firebase_crashlytics: ^4.0.4
# Генерация документов
pdf: ^3.11.3 # Экспорт результатов
printing: ^5.14.2
# Интернационализация
intl: ^0.20.2 # Поддержка 7 языков
Отдельная история с Isar: оригинальную библиотеку автор забросил, но сообщество сделало форк и продолжает развивать. Пришлось перейти на isar_community. Хороший урок про хрупкость open-source — даже отличные решения могут исчезнуть, если за ними не стоит активное комьюнити.
Почему я выбрал Clean Architecture для калькулятора
Когда начинаешь проект, всегда есть соблазн сделать попроще. Один файл, StatefulWidget, прямые обращения к базе — и вперёд. Для прототипа на три калькулятора это работает отлично.
Но я с самого начала понимал, что калькуляторов будет много. Десятки. И каждый со своей логикой, своими формулами, своими особенностями материалов.
Clean Architecture по Роберту Мартину — это когда бизнес-логика отделена от интерфейса и от базы данных. Звучит как оверинжиниринг для «простого калькулятора», и первые две недели я сомневался, правильно ли делаю.
Сомнения развеялись через месяц, когда пришлось полностью переделывать систему локализации. Благодаря разделению слоёв изменения затронули только интерфейс, вся логика расчётов осталась нетронутой. Если бы код был монолитным, я бы переписывал половину приложения.
Ещё один момент, который окупился: добавление нового калькулятора. Первые прототипы я писал по несколько часов каждый. К пятидесятому калькулятору процесс занимал полчаса, потому что вся инфраструктура уже была готова.
Как не сойти с ума, когда калькуляторов 54
Самый сложный вопрос проекта: как сделать так, чтобы калькулятор ламината и калькулятор бетонного фундамента работали по единой логике, но при этом оставались достаточно гибкими для своих особенностей?
Любой строительный расчёт проходит одинаковые этапы. Сначала проверяем входные данные — площадь не может быть отрицательной, толщина стяжки не может быть нулевой. Потом применяем формулы и нормативы. Добавляем запас на отходы и подрезку. Считаем комплектующие — если нужен клей, то сколько банок. Формируем итоговый список покупок.
Первый подход был наивным: скопировать эту логику в каждый калькулятор. Когда калькуляторов стало пятнадцать, я нашёл ошибку в расчёте запаса — не учитывалось округление до целых упаковок. Пришлось править в пятнадцати файлах. После этого родилась идея базового класса, от которого наследуются все калькуляторы.
Но этого оказалось мало. Калькуляторы отличаются не только формулами, но и интерфейсом. Для ламината нужны поля «длина комнаты», «ширина комнаты», «размер доски». Для бетона — «объём», «марка», «добавки». Рисовать формы вручную для каждого калькулятора означало повторить ту же ошибку на уровне интерфейса.
Решение нашлось в декларативном подходе. Вместо того чтобы писать код формы, я описываю калькулятор как структуру данных:
dartfinal laminateCalculator = CalculatorDefinition(
id: 'laminate_flooring',
category: MaterialCategory.flooring,
fields: [
InputField(
id: 'room_length',
type: FieldType.decimal,
label: 'calculator.laminate.roomLength',
unit: 'м',
minValue: 0.1,
maxValue: 100.0,
defaultValue: 5.0,
),
InputField(
id: 'room_width',
type: FieldType.decimal,
label: 'calculator.laminate.roomWidth',
unit: 'м',
minValue: 0.1,
maxValue: 100.0,
defaultValue: 4.0,
),
InputField(
id: 'plank_length',
type: FieldType.decimal,
label: 'calculator.laminate.plankLength',
unit: 'м',
minValue: 0.1,
maxValue: 3.0,
defaultValue: 1.38,
),
SelectField(
id: 'laying_pattern',
type: FieldType.select,
label: 'calculator.laminate.layingPattern',
options: [
Option(value: 'straight', label: 'Прямая'),
Option(value: 'diagonal', label: 'Диагональная'),
],
defaultValue: 'straight',
),
],
calculator: LaminateCalculationEngine(),
);
Теперь движок автоматически строит форму по этому описанию, валидирует данные на основе minValue/maxValue, подставляет локализованные строки по ключам.
Это открыло неожиданные возможности. Поиск по калькуляторам заработал автоматически — просто ищем по меткам в описаниях. Версионирование стало тривиальным — можно создать улучшенную версию калькулятора, не ломая старую. Теоретически, можно даже сделать редактор калькуляторов для людей без технического бэкграунда.
Неделя на локализацию, которую я мог сделать за день
Это история о техническом долге и о том, как он прилетает в самый неподходящий момент.
На третьей неделе разработки у меня работало двадцать калькуляторов. Всё было прекрасно, кроме одного: весь текст был захардкожен на русском прямо в коде:
dart// ❌ Так делать не надо
Text('Введите площадь помещения в квадратных метрах')
showDialog(
context: context,
child: AlertDialog(
title: Text('Ошибка'),
content: Text('Площадь должна быть больше нуля'),
),
)
Когда я решил добавить английский язык, начался ад. Пришлось пройтись по сотням файлов, вытащить каждую строку, создать для неё ключ в файле локализации, заменить хардкод на вызов локализации:
dart// ✅ Правильный подход
Text(AppLocalizations.of(context)!.inputAreaHint)
showDialog(
context: context,
child: AlertDialog(
title: Text(AppLocalizations.of(context)!.errorTitle),
content: Text(AppLocalizations.of(context)!.errorAreaPositive),
),
)
Неделя работы, которая не добавила ни одной новой фичи. Если бы я с первого дня использовал систему локализации, каждая новая строка добавлялась бы в нужное место автоматически. Вместо недели потратил бы в сумме часа два-три, размазанных по всему проекту.
Когда тесты нашли ошибку, которая могла стоить денег
После тридцати калькуляторов я решил, что пора писать нормальные тесты. Не потому что люблю тесты, а потому что уже начал путаться, что где работает.
Первый же прогон показал проблему. Калькулятор тёплого пола выдавал 17 квадратных метров нагревательной плёнки для комнаты, где по моим ручным расчётам должно было быть 14. Расхождение почти на 20 процентов.
Полез разбираться. Оказалось, я неправильно применял запас на отходы:
dart// ❌ Неправильно: запас добавляется к площади
final totalArea = roomArea * 0.7; // 70% полезной площади
final withMargin = totalArea * 1.1; // +10% запас
final packages = (withMargin / packageArea).ceil();
А правильно так:
dart// ✅ Правильно: запас добавляется к количеству единиц
final totalArea = roomArea * 0.7;
final unitsNeeded = totalArea / unitArea;
final withMargin = unitsNeeded * 1.1; // Запас на уровне единиц
final packages = withMargin.ceil();
Разница кажется мелкой, но на практике это означает, что человек по моему расчёту купил бы лишнего на несколько тысяч рублей. Для одного пользователя неприятно, для тысячи — уже репутационная катастрофа.
После этого я написал тесты для всех калькуляторов, включая граничные случаи:
dartgroup('Laminate Calculator Tests', () {
test('должен корректно рассчитывать для маленькой комнаты', () {
final result = calculator.calculate({
'room_length': 2.0,
'room_width': 2.0,
'plank_length': 1.38,
'plank_width': 0.193,
'laying_pattern': 'straight',
});
expect(result.totalArea, closeTo(4.0, 0.01));
expect(result.packagesNeeded, greaterThanOrEqualTo(2));
});
test('должен добавлять больше запаса для диагональной укладки', () {
final straightResult = calculator.calculate({
'laying_pattern': 'straight',
// ... остальные параметры
});
final diagonalResult = calculator.calculate({
'laying_pattern': 'diagonal',
// ... те же параметры
});
expect(diagonalResult.packagesNeeded,
greaterThan(straightResult.packagesNeeded));
});
test('должен обрабатывать нестандартные пропорции комнаты', () {
final result = calculator.calculate({
'room_length': 10.0,
'room_width': 1.5, // Узкий коридор
// ...
});
expect(result.isValid, isTrue);
});
});
Финальное покрытие — 127 тысяч строк тестов при 106 тысячах строк продуктового кода. Да, тестов больше чем самого приложения. Для критичной логики это нормально.
Почему главный экран тормозил на полторы секунды
Ближе к релизу всплыла неожиданная проблема. Главный экран со списком всех калькуляторов открывался с заметной задержкой — примерно полторы секунды. На хорошем телефоне терпимо, на бюджетном раздражает.
Полез в профайлер. Оказалось, при каждом открытии экрана приложение заново перебирало все 54 калькулятора, строило индексы для поиска и фильтрации, формировало списки по категориям. Каждый раз с нуля.
Решение стандартное: кэширование и ленивая инициализация:
dart// ❌ До оптимизации: пересоздаётся каждый раз
class CalculatorRegistry {
List<CalculatorDefinition> getAllCalculators() {
return [
laminateCalculator,
tileCalculator,
// ... все 54 калькулятора
];
}
List<CalculatorDefinition> searchCalculators(String query) {
return getAllCalculators()
.where((calc) => calc.matchesQuery(query))
.toList();
}
}
// ✅ После оптимизации: кэшируется при первом обращении
class CalculatorRegistry {
List<CalculatorDefinition>? _cachedCalculators;
Map<String, List<CalculatorDefinition>>? _categoryIndex;
List<CalculatorDefinition> getAllCalculators() {
_cachedCalculators ??= [
laminateCalculator,
tileCalculator,
// ...
];
return _cachedCalculators!;
}
List<CalculatorDefinition> searchCalculators(String query) {
// Используем закэшированный список
return getAllCalculators()
.where((calc) => calc.matchesQuery(query))
.toList();
}
List<CalculatorDefinition> getByCategory(MaterialCategory category) {
_categoryIndex ??= _buildCategoryIndex();
return _categoryIndex![category.name] ?? [];
}
Map<String, List<CalculatorDefinition>> _buildCategoryIndex() {
final index = <String, List<CalculatorDefinition>>{};
for (final calc in getAllCalculators()) {
index.putIfAbsent(calc.category.name, () => []).add(calc);
}
return index;
}
}
После оптимизации время загрузки упало до 150 миллисекунд. В десять раз быстрее.
Что получилось в сухих цифрах
Финальная статистика проекта:
Размер приложения: около 28 мегабайт для современных телефонов на ARM64. Для старых 32-битных устройств чуть меньше, около 25 мегабайт.
Кодовая база:
- 106 261 строка продуктового кода в 428 файлах
- 127 311 строк тестов в 401 файле
- Всего 237 694 строки на Dart
- Средний размер файла около 248 строк
Функциональность: 54 калькулятора, разбитых на категории:
- Фундамент — 2 калькулятора
- Внутренняя отделка — 42 калькулятора (стены, полы, потолки, перегородки, утепление, ванные комнаты, смеси, окна и двери)
- Наружная отделка — 6 калькуляторов
- Инженерные работы — 4 калькулятора
Локализация: пока только русский.
Публикация в RuStore
Выбор площадки для публикации в 2026 году для российского разработчика не особо богатый. Google Play фактически закрыт, App Store требует юрлицо и долларовый счёт. Остаётся RuStore.
Сборка Flutter-приложения под Android — процесс отлаженный:
Bash# Очистка предыдущих артефактов
flutter clean && flutter pub get
# Кодогенерация для Isar (БД)
flutter pub run build_runner build --delete-conflicting-outputs
# Сборка релизного APK с разделением по архитектурам
flutter build apk --release --split-per-abi
На выходе три файла под разные архитектуры процессоров:
- app-armeabi-v7a-release.apk (~25 MB) — 32-битный ARM для старых телефонов
- app-arm64-v8a-release.apk (~28 MB) — 64-битный ARM для современных устройств
- app-x8664-release.apk (~30 MB) — x86 для эмуляторов
Отдельная работа — подготовка материалов для магазина. Описание на 4000 символов с учётом поисковых запросов («калькулятор», «стройматериалы», «ремонт», «расчёт»), восемь скриншотов, иконка приложения. Тут важно не переборщить с текстом на картинках — агрегаторы это не любят и понижают в выдаче.
Модерация заняла два рабочих дня, прошла без замечаний. Проверяли работоспособность на тестовых устройствах, соответствие описанию, отсутствие запрещённого контента.
Что дальше?
Сейчас «Мастерок» — это инструмент. Полезный, работающий, решающий конкретную задачу. Но архитектура позволяет развивать его в нескольких направлениях.
Горизонтально — добавлять узкоспециализированные калькуляторы. Вентиляция, умный дом, специфика конкретных брендов материалов, региональные нормы для разных климатических зон.
Вертикально — интегрироваться с магазинами. Посчитал материалы, нажал кнопку, получил готовую корзину в Леруа или Петровиче с актуальными ценами и наличием. Это уже не калькулятор, а полноценный сервис.
В сторону платформы — веб-версия на Next. js, совместные сметы для бригад, маркетплейс калькуляторов от сообщества.
В сторону умных технологий — распознавание чертежей через камеру, рекомендации на основе истории пользователя, консультант в формате чата.
Каждое направление требует ресурсов и несёт свои риски. Пока я выбрал тактику развития по запросам пользователей: собираю обратную связь, смотрю что реально нужно, и двигаюсь в ту сторону.
Что я понял за эти три месяца
Архитектура важнее скорости на старте. Неделя на проектирование чистой архитектуры окупилась многократно при масштабировании с десяти калькуляторов до пятидесяти четырёх. Если бы я начал с монолитного кода, рефакторинг занял бы месяцы.
Технический долг неизбежен, но его можно контролировать. Локализация, оптимизация, миграция на форк базы данных — всё это последствия быстрых решений на старте. Полностью избежать невозможно, потому что в начале проекта не знаешь всех требований. Но можно минимизировать, если задавать себе вопрос «а что будет, когда этого станет в десять раз больше?»
Тестирование — обязательный этап разработки. 127 тысяч строк тестов кажутся избыточными, пока не находишь ошибку в формуле, которая заставила бы реальных людей переплачивать за материалы.
Проект начинался с простого желания: сделать точный калькулятор стройматериалов. Три месяца и 238 тысяч строк кода спустя стало понятно, что «простота» для пользователя и «простота» разработки — это противоположные вещи.
Пользователь открывает приложение, вводит три числа, нажимает кнопку и получает точный список покупок. Тридцать секунд, никакой магии. А за этими тридцатью секундами — многоуровневая система валидации, российские ГОСТы и нормативы, учёт специфики каждого материала, расчёт комплектующих, сохранение истории.
Превратить сложную инженерную задачу в простой пользовательский опыт — вот что на самом деле означает «сделать калькулятор».
Если кого-нибудь заинтересовало такое приложение, буду рад фидбеку.
Источник: chatgpt.com









5 комментариев
Добавить комментарий
1. Я правильно понимаю, что опыта отделочных работ нет, только продажи? Найми профессионального отделочника в консультанты. Есть масса не очевидных моментов по расходу материалов.
2. Сделай он-лаин версию. Потребители скорее будут искать сайт с калькуляторами, чем приложение. Плюс он-лаин калькулятор послужит основой для тематического портала.
з.ы. А вообще в отделке есть универсальная формула и по ней 2 * 2 = 4,6 ))))))
Оффлайн версия очень полезна людям, которые находятся за городом или если проблемы с интернетом.
1. Купили ламинат все подсчитали, но уже когда начали класть выяснилось что в одной комнате класть нужно не вдоль, а поперек.
2. Заказывали обои, потом оказалось что в одной из комнат вместо обычной покраски нужно покраску под обои. А это особый тип обоев.
3. Кафель с рисунком (отдельные плитки с рисунком) вещь в себе. Математике не поддаётся.
4. «Фартук» на кухне тоже вещь в себе
короче считай-не считай, а +- 10% еще заложи.
Добавить комментарий