Разработка Flutter-приложения для учета аренды и ЖКХ

Курс обучит созданию мобильного приложения на Flutter для арендодателей, опираясь на опыт существующих решений вроде ЧекаЧек [appadvice.com](https://appadvice.com/app/d1-87-d0-b5-d0-ba-d0-b0-d1-87-d0-b5-d0-ba-d1-83-d1-87-d0-b5-d1-82-d0-b0-d1-80-d0-b5-d0-bd-d0-b4-d1-8b-d0-b8-d0-b6-d0-ba-d1-83/6471586789) и Planus [github.com](https://github.com/pkkamil/Planus). Вы научитесь вести базу квартиросъемщиков, фиксировать показания счетчиков и рассчитывать платежи по справочнику тарифов.

1. Проектирование архитектуры и базы данных для учета аренды и ЖКХ

Проектирование архитектуры и базы данных для учета аренды и ЖКХ

Любое надежное мобильное приложение начинается с фундамента — грамотно спроектированной архитектуры и структуры хранения данных. Приложение для учета аренды и жилищно-коммунальных услуг (ЖКХ) работает с финансовыми показателями, историей платежей и тарифами. Ошибка в проектировании на начальном этапе приведет к тому, что при изменении стоимости киловатта электроэнергии у пользователей «поедет» вся история предыдущих расчетов.

Архитектура программного обеспечения — это совокупность важнейших решений об организации системы, выборе структурных элементов и их интерфейсов. В контексте мобильной разработки на фреймворке Flutter правильная архитектура позволяет разделить визуальную часть (кнопки, списки) и бизнес-логику (расчеты, сохранение в базу).

Выбор архитектурного паттерна

Для масштабируемых проектов в экосистеме Flutter стандартом де-факто является Clean Architecture (Чистая архитектура), часто в связке со стейт-менеджером BLoC (Business Logic Component). Этот подход делит приложение на независимые слои.

> Архитектура определяет структуру, взаимосвязь между компонентами и их взаимодействия. Четко разработанная архитектура не только упрощает процесс разработки, но и обеспечивает масштабируемость, гибкость и удобство сопровождения кода. > > Архитектура Flutter проекта простым языком

В нашем приложении мы выделим три основных слоя:

  • Слой данных (Data Layer): отвечает за работу с базой данных (БД) и внешними API. Здесь находятся SQL-запросы и модели данных.
  • Доменный слой (Domain Layer): содержит чистую бизнес-логику. Например, именно здесь находится правило: «сумма к оплате за воду равна разнице показаний счетчика, умноженной на текущий тариф».
  • Слой представления (Presentation Layer): экраны, виджеты и контроллеры состояний (BLoC), которые реагируют на действия пользователя.
  • !Схема слоев Чистой архитектуры в приложении

    Использование паттерна Репозиторий (Repository) позволяет скрыть от бизнес-логики то, откуда именно берутся данные. Доменный слой просто запрашивает getTariffs(), а репозиторий сам решает, достать их из локальной базы данных устройства или скачать с сервера.

    Проектирование базы данных

    Поскольку приложение должно вести строгий учет финансов и показаний, нам требуется реляционная база данных. В мобильной разработке для локального хранения чаще всего используется SQLite. Реляционная модель позволяет жестко связать арендаторов, их счетчики и справочник тарифов.

    Сравним подходы к хранению данных, чтобы обосновать наш выбор:

    | Характеристика | Реляционные БД (SQLite) | NoSQL БД (Hive, SharedPreferences) | | :--- | :--- | :--- | | Структура данных | Строгие таблицы со связями | Гибкие документы (JSON) | | Сложные запросы | Поддерживаются (JOIN, агрегация) | Ограничены или отсутствуют | | Целостность данных | Высокая (внешние ключи) | Зависит от кода приложения | | Применение в нашем проекте | Идеально для связи тарифов, показаний и платежей | Подходит только для сохранения настроек темы (светлая/темная) |

    Основные сущности и таблицы

    Для реализации требований необходимо спроектировать несколько взаимосвязанных таблиц. Каждая таблица представляет собой отдельную сущность.

    1. Таблица арендаторов (Tenants) Хранит базовую информацию о квартиросъемщиках. * id — уникальный идентификатор; * name — ФИО или название; * apartment_number — номер квартиры или комнаты; * base_rent — фиксированная сумма арендной платы.

    2. Справочник тарифов (Tariffs) Вынесение тарифов в отдельный справочник — критически важное архитектурное решение. Тарифы меняются (обычно раз в год), и если мы будем хранить стоимость киловатта прямо в таблице платежей, мы не сможем гибко управлять историей. * id — идентификатор тарифа; * service_type — тип услуги (электричество, холодная вода, горячая вода); * rate — стоимость за единицу (например, за 1 кВт·ч или 1 куб. м); * effective_date — дата, с которой тариф вступает в силу.

    3. Показания счетчиков (MeterReadings) Фиксирует данные приборов учета на определенную дату. * id — идентификатор записи; * tenant_id — ссылка на арендатора (внешний ключ); * service_type — тип услуги; * value — показание счетчика; * reading_date — дата снятия показаний.

    4. Платежи (Payments) Объединяет все начисления и фактические оплаты. * id — идентификатор платежа; * tenant_id — ссылка на арендатора; * amount — сумма; * payment_type — тип (аренда, электричество, вода); * status — статус (оплачено, ожидается, просрочено); * created_at — дата формирования счета.

    Математика расчетов ЖКХ

    Бизнес-логика приложения опирается на точные математические расчеты. Когда пользователь вводит новые показания счетчика, приложение должно найти предыдущее показание, вычислить разницу и умножить ее на актуальный тариф.

    Формула расчета стоимости потребленного ресурса выглядит следующим образом:

    Где: * — итоговая стоимость к оплате (Cost); * — текущие (новые) показания счетчика; * — предыдущие показания счетчика; * — ставка тарифа из справочника, актуальная на дату снятия показаний.

    Рассмотрим на конкретном примере с числами. Допустим, в прошлом месяце показания счетчика электроэнергии составляли 1540 кВт·ч. В текущем месяце квартиросъемщик прислал фото счетчика с цифрой 1690 кВт·ч. В справочнике тарифов указано, что стоимость 1 кВт·ч составляет 5 рублей.

    Сначала находим объем потребления: 1690 - 1540 = 150 кВт·ч. Затем умножаем объем на тариф: 150 × 5 = 750 рублей. Именно эта сумма должна быть записана в таблицу Payments со статусом «ожидается».

    !Интерактивный калькулятор расчета стоимости услуг ЖКХ по показаниям счетчиков

    Связь базы данных и интерфейса

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

    Сначала пользователь открывает экран «Детали арендатора». Виджет отправляет событие LoadTenantDetails в контроллер состояния (BLoC). Контроллер обращается к доменному слою (Use Case), который запрашивает данные у Репозитория.

    Репозиторий выполняет SQL-запрос к локальной базе данных SQLite. Например, чтобы получить историю платежей конкретного человека, выполняется запрос с использованием оператора WHERE tenant_id = 1. База данных возвращает сырые строки, которые Репозиторий преобразует в удобные объекты языка Dart (модели данных). Эти объекты передаются обратно в контроллер, который обновляет состояние экрана, и пользователь видит красивый список платежей.

    Разделение тарифов и показаний счетчиков позволяет реализовать сложный, но необходимый функционал: перерасчет. Если выяснится, что тариф изменился задним числом, приложение сможет пройтись по таблице MeterReadings, взять разницу показаний и умножить на новое значение из таблицы Tariffs, автоматически обновив суммы в таблице Payments.

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

    2. Создание пользовательского интерфейса на Flutter для управления квартиросъемщиками

    Создание пользовательского интерфейса на Flutter для управления квартиросъемщиками

    Спроектированная база данных и выстроенная архитектура — это невидимый фундамент приложения. Однако для конечного пользователя — арендодателя или управляющего недвижимостью — приложением является исключительно его визуальная часть. Пользовательский интерфейс (User Interface, UI) служит мостом между сложной логикой расчетов тарифов ЖКХ и человеком, которому нужно быстро внести показания счетчиков и выставить счет.

    Фреймворк Flutter использует декларативный подход к созданию интерфейсов. Это означает, что разработчик описывает, как должен выглядеть экран при определенном состоянии данных, а фреймворк сам перерисовывает нужные элементы при изменении этого состояния. В контексте нашего приложения для учета аренды это позволяет мгновенно обновлять суммы долга на экране, как только в базу данных добавляется новый платеж.

    Принципы проектирования интерфейса учетных систем

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

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

    При разработке экранов управления квартиросъемщиками необходимо опираться на три базовых принципа:

  • Визуальная иерархия. Самая важная информация (имя арендатора, номер квартиры, статус оплаты) должна считываться за доли секунды. Второстепенная информация (дата заключения договора, история старых показаний) скрывается внутри карточки.
  • Защита от ошибок ввода. Поскольку приложение работает с деньгами и тарифами, интерфейс должен блокировать ввод некорректных данных на этапе набора текста.
  • Адаптивность. Управляющий может использовать приложение как на смартфоне, стоя в коридоре перед счетчиком, так и на планшете, сидя за рабочим столом.
  • Структура экранов модуля арендаторов

    Для эффективного управления базой квартиросъемщиков нам потребуется создать логичную навигационную цепочку. В Flutter каждый экран представляет собой виджет, обычно обернутый в Scaffold — базовый каркас, предоставляющий место для верхней панели (AppBar), основного контента и плавающей кнопки действия (FloatingActionButton).

    Мы разделим модуль управления на три основных экрана:

    * Экран списка (Tenants List): Сводная панель со всеми объектами недвижимости и их жильцами. * Экран деталей (Tenant Details): Подробная информация по конкретному человеку, включая графики потребления воды и электричества. * Экран формы (Tenant Form): Интерфейс для добавления нового жильца или редактирования существующих данных.

    !Схема навигации между экранами: от общего списка к деталям арендатора и форме редактирования

    Реализация списка квартиросъемщиков

    Главный экран — это первое, что видит пользователь. Если у арендодателя 5 квартир, список можно вывести любым способом. Но если в управлении находится 100 объектов, неправильный выбор виджета приведет к зависанию приложения.

    Во Flutter для отображения длинных списков используется виджет ListView.builder. В отличие от обычного ListView, который загружает в память все элементы сразу, builder создает виджеты только для тех элементов, которые в данный момент видны на экране смартфона. Как только карточка арендатора уходит за пределы видимости при прокрутке, она уничтожается, освобождая память.

    Сравним основные виджеты для построения списков во Flutter:

    | Виджет | Особенность рендеринга | Применение в нашем проекте | | :--- | :--- | :--- | | Column | Рендерит все дочерние элементы сразу. Не имеет встроенной прокрутки. | Подходит для коротких форм ввода (3-5 полей). | | ListView | Рендерит все элементы сразу. Поддерживает прокрутку. | Идеально для статических меню настроек. | | ListView.builder | Ленивый рендеринг (создает элементы по мере прокрутки). | Основной выбор для списка арендаторов и истории платежей. | | GridView.builder | Ленивый рендеринг в виде сетки (колонок и строк). | Полезно для планшетной версии приложения. |

    Каждый элемент списка мы оформим с помощью виджета ListTile. Он предоставляет готовый шаблон с местом для иконки (аватарки), заголовка (ФИО арендатора), подзаголовка (номер квартиры) и конечного виджета (сумма долга).

    Для улучшения пользовательского опыта (UX) мы добавим цветовую индикацию. Если статус последнего платежа в таблице Payments отмечен как «просрочено», сумма долга будет окрашена в красный цвет. Если все оплачено — в нейтральный серый или зеленый. Это позволяет арендодателю мгновенно оценить финансовую ситуацию без необходимости открывать каждую карточку.

    Формы ввода и строгая валидация

    Самый уязвимый этап работы любого приложения — это ввод данных пользователем. В нашем случае арендодатель должен вводить показания счетчиков и суммы арендной платы.

    Во Flutter для создания форм используется виджет Form, внутри которого размещаются текстовые поля TextFormField. Ключевая особенность TextFormField — встроенная поддержка валидации (проверки) данных.

    Рассмотрим пример ввода показаний счетчика электроэнергии. В предыдущей статье мы определили формулу расчета стоимости:

    Где — итоговая стоимость, — новые показания, — предыдущие показания, а — тариф.

    Математика и логика диктуют жесткое правило: счетчик не может крутиться назад (за исключением случаев замены или сброса, что обрабатывается отдельным бизнес-процессом). Следовательно, новые показания всегда должны быть больше или равны старым: .

    Если в прошлом месяце показания были 1540 кВт·ч, а пользователь случайно вводит 1500, приложение не должно отправлять эти данные в базу. Функция-валидатор внутри TextFormField перехватит ввод, сравнит его с и мгновенно выведет красную надпись под полем: «Новые показания не могут быть меньше предыдущих».

    !Интерактивная форма ввода показаний счетчика с мгновенной проверкой на ошибки и расчетом разницы

    Кроме логической валидации, необходимо настроить правильную клавиатуру. Для полей ввода ФИО используется стандартная текстовая клавиатура. Но для ввода тарифов, сумм и показаний счетчиков необходимо принудительно вызывать цифровую клавиатуру. Во Flutter это делается параметром keyboardType: TextInputType.numberWithOptions(decimal: true). Это исключает возможность случайного ввода букв вместо цифр и ускоряет процесс работы.

    Связь интерфейса с состоянием приложения

    Интерфейс не существует в вакууме. Он должен реагировать на процессы, происходящие в доменном слое и слое данных (базе SQLite).

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

    Для управления такими состояниями во Flutter часто используется паттерн BLoC (Business Logic Component) и виджет BlocBuilder. Этот виджет слушает изменения состояния и перерисовывает только ту часть экрана, которая зависит от данных.

    Типичный жизненный цикл экрана выглядит так:

  • Состояние Loading: BlocBuilder возвращает виджет с крутящимся индикатором загрузки.
  • Состояние Loaded: База данных вернула список арендаторов. BlocBuilder получает эти данные и передает их в ListView.builder, который отрисовывает карточки на экране.
  • Состояние Error: Если произошла ошибка (например, повреждение файла базы данных), BlocBuilder возвращает виджет с текстом ошибки и кнопкой «Повторить».
  • Состояние Empty: Если база данных пуста (пользователь только установил приложение), выводится красивая иллюстрация и большая кнопка «Добавить первого арендатора».
  • Обработка пустого состояния (Empty State) критически важна для новых пользователей. Пустой белый экран вызывает растерянность. Приложение должно подсказывать следующий шаг.

    Интерактивность и обратная связь

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

    Во Flutter для кратковременных уведомлений используется SnackBar — всплывающая панель внизу экрана. Например, после нажатия кнопки «Сохранить платеж», экран формы закрывается, пользователь возвращается к списку арендаторов, и снизу на 3 секунды появляется зеленая плашка: «Платеж на сумму 15 000 руб. успешно добавлен».

    Также важно учитывать обработку долгих нажатий. В списке арендаторов долгое удержание пальца на карточке жильца может вызывать контекстное меню с быстрыми действиями: «Позвонить», «Написать в мессенджер» или «Выставить счет за месяц». Это сокращает количество кликов для выполнения рутинных задач.

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

    3. Разработка модуля фиксации показаний счетчиков воды и электричества

    Разработка модуля фиксации показаний счетчиков воды и электричества

    Управление недвижимостью неразрывно связано с учетом потребляемых ресурсов. После того как мы спроектировали базу данных и создали базовый пользовательский интерфейс для управления списком арендаторов, необходимо реализовать один из самых критичных бизнес-процессов приложения — сбор и обработку показаний приборов учета.

    Модуль фиксации показаний выполняет роль связующего звена между физическим миром (цифрами на табло счетчика) и финансовым учетом (выставлением счетов квартиросъемщику). Ошибка на этом этапе, будь то опечатка пользователя или неверный расчет внутри приложения, неизбежно приводит к финансовым разногласиям.

    Моделирование данных для учета ресурсов

    В основе модуля лежит строгая структура данных. Во Flutter-приложении, использующем принципы чистой архитектуры, мы должны определить сущность (Entity) для показаний счетчика. Эта модель будет использоваться для передачи данных между пользовательским интерфейсом и локальной базой данных SQLite.

    Рассмотрим базовую структуру класса на языке Dart:

    Обратите внимание на геттер consumed. Бизнес-логика вычисления разницы между текущими и предыдущими показаниями инкапсулирована внутри самой модели. Это гарантирует, что в любой части приложения (на экране деталей, в квитанции или в отчете) объем потребленного ресурса будет рассчитываться единообразно.

    Интеграция со справочником тарифов

    Ключевая архитектурная особенность нашего приложения — вынесение тарифов в отдельный независимый справочник. Тарифы на услуги ЖКХ регулярно индексируются государством или управляющими компаниями. Если жестко «зашить» стоимость киловатт-часа в код приложения или привязать ее напрямую к профилю арендатора, система потеряет гибкость.

    > Справочник тарифов — это таблица в базе данных, хранящая историю изменения цен. Каждая запись содержит тип ресурса, стоимость единицы измерения и период действия (дату начала и дату окончания).

    Сравним подходы к хранению тарифов:

    | Подход | Преимущества | Недостатки | Применение | | :--- | :--- | :--- | :--- | | Жесткое кодирование (Hardcode) | Максимальная простота реализации. | Невозможно обновить цену без выпуска новой версии приложения. | Прототипы и студенческие проекты. | | Хранение в профиле арендатора | Легко настроить индивидуальные условия. | При изменении тарифа ЖКХ придется вручную обновлять профили всех 100 жильцов. | Специфические коммерческие договоры. | | Отдельный справочник с историей | Централизованное обновление, сохранение истории расчетов за прошлые годы. | Требует более сложных SQL-запросов для поиска актуальной цены на нужную дату. | Профессиональные учетные системы. |

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

    Проектирование интерфейса ввода показаний

    Интерфейс модуля должен быть максимально аскетичным и сфокусированным на одной задаче — вводе цифр. Для реализации формы во Flutter используется виджет Form, объединяющий несколько полей TextFormField.

    Для минимизации ошибок ввода необходимо настроить правильный тип клавиатуры. Использование keyboardType: TextInputType.numberWithOptions(decimal: true) принудительно открывает цифровую клавиатуру с возможностью ввода десятичной точки. Это отсекает целый класс ошибок, связанных со случайным вводом буквенных символов.

    !Блок-схема процесса обработки показаний счетчика: Ввод данных -> Валидация -> Запрос тарифа -> Расчет стоимости -> Сохранение в БД

    Кроме того, интерфейс должен автоматически подставлять предыдущие показания. Арендодатель не должен вспоминать или искать в блокноте, какие цифры он вводил месяц назад. Приложение делает запрос к базе данных, находит последнюю запись для данного счетчика и отображает ее в поле previousValue, делая его доступным только для чтения (readOnly: true).

    Математика расчетов и строгая валидация

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

    Для проверки этого условия во Flutter используется свойство validator виджета TextFormField. Это функция, которая принимает введенную строку, преобразует ее в число и сравнивает с предыдущим значением.

    ``dart String? validateReading(String? value, double previousReading) { if (value == null || value.isEmpty) { return 'Пожалуйста, введите показания'; } final currentReading = double.tryParse(value); if (currentReading == null) { return 'Некорректный формат числа'; } if (currentReading < previousReading) { return 'Новые показания не могут быть меньше CR_{new}R_{old}TR_{new} \geq R_{old}$.

  • Поле «Предыдущие показания» сбрасывается в 0 (или позволяет ввести начальные показания нового счетчика, если он был установлен не с нулевой отметки).
  • В базу данных сохраняется специальная метка (флаг isReplaced: true), чтобы в будущем алгоритмы построения графиков потребления корректно обрабатывали этот разрыв в данных.
  • Сохранение данных и обновление состояния

    Завершающий этап работы модуля — сохранение проверенных и рассчитанных данных. При нажатии кнопки «Сохранить» приложение формирует объект MeterReading и передает его в слой данных для записи в SQLite.

    Одновременно с этим необходимо обновить финансовый баланс арендатора. Рассчитанная сумма (в нашем примере 750 руб.) должна быть добавлена к общей сумме долга квартиросъемщика. Это требует выполнения транзакции в базе данных: либо обе операции (сохранение показаний и обновление долга) выполняются успешно, либо обе отменяются. Это гарантирует консистентность (целостность) финансовых данных.

    После успешного сохранения интерфейс должен отреагировать. Экран ввода закрывается (используется метод Navigator.pop(context)), а на предыдущем экране появляется всплывающее уведомление SnackBar`, подтверждающее успешное добавление данных. Благодаря использованию реактивного управления состоянием (например, BLoC), список показаний и сумма долга на главном экране обновятся автоматически, без необходимости ручной перезагрузки страницы.

    4. Реализация справочника тарифов и алгоритмов расчета арендных платежей

    Реализация справочника тарифов и алгоритмов расчета арендных платежей

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

    Главная задача текущего этапа — научить Flutter-приложение автоматически рассчитывать итоговую сумму к оплате, опираясь на базовую арендную ставку, потребленные коммунальные ресурсы и актуальные тарифы.

    Архитектура справочника тарифов

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

    Для решения этой проблемы применяется паттерн Temporal Database (темпоральная база данных), где каждая запись имеет срок жизни.

    Рассмотрим структуру модели Tariff на языке Dart:

    Ключевой элемент здесь — поле endDate, которое может быть null (отсутствовать). Если endDate равно null, это означает, что данный тариф является актуальным на сегодняшний день и действует бессрочно, пока не будет введен новый.

    > Темпоральная модель данных гарантирует иммутабельность (неизменяемость) истории. При повышении цен старая запись тарифа не удаляется, а лишь получает дату окончания (endDate), после чего создается новая запись с новой датой начала (startDate).

    Алгоритм поиска актуального тарифа

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

    Для этого в слое Repository реализуется метод, который выполняет SQL-запрос с фильтрацией по дате. Логика запроса выглядит следующим образом:

  • Выбрать все тарифы для заданного типа ресурса (например, ResourceType.electricity).
  • Оставить только те, у которых startDate меньше или равна дате расчета.
  • Из оставшихся выбрать те, у которых endDate больше даты расчета ИЛИ endDate является NULL.
  • Во Flutter с использованием пакета sqflite этот запрос выглядит так:

    Математика расчета коммунальных платежей

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

    Базовая формула расчета стоимости потребленного ресурса:

    Где: * — итоговая сумма за коммунальную услугу. * — текущие показания прибора учета. * — предыдущие показания прибора учета. * — стоимость единицы ресурса из справочника тарифов на дату расчета.

    Пример расчета: Арендатор потребил холодную воду. Предыдущие показания () составляли 120 куб.м, новые () — 135 куб.м. Приложение обращается к справочнику и находит, что на момент расчета тариф () составляет 45,50 руб. за куб.м.

    Расчет: (135 - 120) × 45,50 = 15 × 45,50 = 682,50 руб.

    !Интерактивный калькулятор коммунальных платежей

    Обработка изменения тарифа внутри месяца

    В профессиональных системах учета ЖКХ существует сложный краевой случай: что делать, если тариф изменился ровно в середине расчетного месяца (например, 15 числа), а показания снимаются только в конце месяца (30 числа)?

    В таком случае применяется пропорциональное разделение потребления (сплит-биллинг). Общий объем потребленного ресурса делится на две части пропорционально количеству дней действия каждого тарифа.

    | Период | Дни | Тариф | Доля потребления | Формула | | :--- | :--- | :--- | :--- | :--- | | 1 - 14 число | 14 дней | Старый (40 руб.) | 14 / 30 = 46.6% | | | 15 - 30 число | 16 дней | Новый (45 руб.) | 16 / 30 = 53.4% | |

    Для базовой версии Flutter-приложения такую сложную логику можно опустить, принимая за истину тариф, действующий на дату снятия показаний. Однако при масштабировании продукта этот алгоритм становится обязательным.

    Алгоритм расчета базовой арендной платы

    Помимо счетчиков, арендатор оплачивает фиксированную стоимость проживания. Если жилец прожил полный месяц, расчет тривиален — выставляется полная сумма по договору. Но часто возникают ситуации неполного месяца: заезд, выезд или досрочное расторжение договора.

    Для таких случаев применяется формула пропорционального начисления (prorated rent):

    Где: * — сумма арендной платы за неполный месяц. * — полная ежемесячная стоимость аренды по договору. * — общее количество дней в текущем календарном месяце (28, 29, 30 или 31). * — фактическое количество дней проживания арендатора в этом месяце.

    Пример расчета: Ежемесячная арендная плата () составляет 45 000 руб. Арендатор заехал 11 сентября. В сентябре 30 дней (). Количество дней проживания с 11 по 30 сентября включительно составляет 20 дней ().

    Расчет: 45 000 / 30 × 20 = 1 500 × 20 = 30 000 руб.

    Формирование единого счета (Квитанции)

    Заключительный этап — агрегация всех рассчитанных сумм в единый объект Invoice (Счет/Квитанция). Этот объект будет использоваться для отображения данных на экране и сохранения истории начислений в базу данных.

    !Схема работы биллингового движка: сбор данных, расчет и формирование квитанции

    Во Flutter для управления процессом сборки счета идеально подходит паттерн BLoC (Business Logic Component). Мы создаем InvoiceBloc, который реагирует на событие GenerateInvoiceEvent.

    Процесс внутри BLoC выглядит так:

  • Сбор данных: BLoC параллельно запрашивает данные о договоре аренды, последние показания счетчиков и актуальные тарифы через соответствующие репозитории.
  • Вычисление: Применяются математические алгоритмы для аренды и ЖКХ.
  • Агрегация: Создается объект Invoice, содержащий список позиций (строк квитанции) и итоговую сумму.
  • Обновление UI: BLoC выдает состояние InvoiceGeneratedState, и интерфейс отрисовывает красивую карточку с детализацией платежа.
  • Разделение baseRentAmount и utilitiesAmount внутри модели критически важно для аналитики. Арендодатель должен четко понимать, какая часть его дохода является чистой прибылью от сдачи недвижимости, а какая — транзитными деньгами, которые он должен перевести ресурсоснабжающим организациям (водоканалу, энергосбыту).

    Реализация надежного справочника тарифов и строгих математических алгоритмов расчета превращает простое приложение-записную книжку в полноценную биллинговую систему, способную автоматизировать рутину и исключить человеческий фактор при финансовых расчетах.

    5. Тестирование приложения, генерация отчетов и подготовка к релизу

    Тестирование приложения, генерация отчетов и подготовка к релизу

    Разработка архитектуры, создание пользовательского интерфейса и реализация сложных математических алгоритмов биллинга — это фундамент приложения для учета аренды и ЖКХ. Однако перед тем как передать продукт конечным пользователям (арендодателям), необходимо убедиться в абсолютной надежности финансовых расчетов, предоставить возможность выгрузки документов и подготовить проект к публикации в магазинах приложений.

    Стратегия тестирования финансовой логики

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

    Для предотвращения подобных ситуаций применяется Unit-тестирование (модульное тестирование). Этот вид тестов проверяет изолированные участки кода — функции и методы — без запуска пользовательского интерфейса или подключения к реальной базе данных.

    > Модульные тесты должны покрывать все возможные краевые случаи: отрицательные значения, нулевое потребление, смену тарифа в середине месяца и високосные годы при расчете пропорциональной аренды.

    Во Flutter для написания модульных тестов используется стандартный пакет test. Структура качественного теста строится по паттерну AAA (Arrange, Act, Assert):

  • Arrange (Подготовка): Инициализация входных данных (создание моковых показаний счетчиков и тарифов).
  • Act (Действие): Вызов тестируемой функции расчета.
  • Assert (Проверка): Сравнение полученного результата с ожидаемым эталоном.
  • Пример модульного теста для проверки расчета стоимости воды:

    Конкретный пример: если арендатор потребил 15 кубометров воды по тарифу 45,50 руб., итоговая сумма должна составить ровно 682,50 руб. Если кто-то из разработчиков случайно изменит формулу в бизнес-логике, тест немедленно упадет, сигнализируя об ошибке до того, как код попадет в релиз.

    Визуальное тестирование интерфейсов

    Помимо логики, важно гарантировать, что пользовательский интерфейс отображается корректно на экранах разных размеров. Верстка карточки арендатора или формы ввода показаний не должна «поехать» после добавления новых функций.

    Для автоматизации проверки UI во Flutter применяются Golden-тесты (золотые тесты).

    Golden-тест — это методология визуального тестирования, при которой фреймворк рендерит виджет в памяти, делает его попиксельный скриншот и сравнивает с заранее сохраненным эталонным изображением («золотым стандартом»).

    | Тип тестирования | Что проверяет | Скорость выполнения | Требует ли запуска эмулятора | | :--- | :--- | :--- | :--- | | Unit-тесты | Изолированную бизнес-логику и математику | Очень высокая (миллисекунды) | Нет | | Widget-тесты | Взаимодействие компонентов (нажатия кнопок) | Высокая (секунды) | Нет | | Golden-тесты | Попиксельное совпадение UI с эталоном | Средняя | Нет (рендеринг в памяти) | | Integration-тесты | Работу всего приложения целиком | Низкая (минуты) | Да (реальное устройство) |

    Если в карточке счета за ЖКХ шрифт итоговой суммы случайно изменится с жирного на обычный, или отступ съедет на 2 пикселя, Golden-тест выдаст ошибку и сгенерирует изображение с подсвеченной разницей (diff). Это позволяет разработчикам и QA-инженерам не тратить часы на ручное прокликивание всех экранов приложения на разных устройствах.

    Генерация квитанций в формате PDF

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

    Во Flutter для этих целей используется библиотека pdf. Ее уникальность заключается в том, что она предоставляет собственную систему виджетов, синтаксически очень похожую на стандартные виджеты Flutter (Column, Row, Text), но адаптированную для отрисовки векторной графики в документе.

    Процесс формирования отчета состоит из следующих шагов:

  • Создание виртуального документа.
  • Добавление страницы с заданными отступами и форматом (например, A4).
  • Отрисовка шапки (реквизиты сторон, период оплаты).
  • Формирование таблицы начислений (базовая аренда, электричество, вода).
  • Подведение итогов.
  • Пример структуры данных для генерации: Базовая аренда: 45 000 руб. Электричество: 150 кВт × 5,00 руб. = 750 руб. Вода: 10 куб.м × 50,00 руб. = 500 руб. Итого к оплате: руб.

    Сгенерированный файл сохраняется во временную директорию устройства с помощью пакета path_provider, после чего вызывается нативный диалог «Поделиться» (Share) через пакет share_plus. Таким образом, арендодатель в два клика формирует профессиональный финансовый документ и отправляет его арендатору в Telegram или WhatsApp.

    Подготовка к релизу и безопасность

    Когда код протестирован, а функционал отчетов работает безупречно, наступает этап подготовки приложения к публикации в Google Play и App Store.

    Релизная сборка кардинально отличается от отладочной (debug). В ней отключены инструменты профилирования, удалены логи, а сам код оптимизирован для максимальной производительности.

    Обфускация кода

    Поскольку приложение работает с финансовыми данными (пусть и локальными), важно защитить его от реверс-инжиниринга. Обфускация — это процесс запутывания исходного кода. Имена классов, переменных и функций заменяются на бессмысленные символы (например, класс TenantRepository превращается в a, а метод calculateRent в b).

    Во Flutter обфускация включается специальными флагами при сборке: flutter build apk --obfuscate --split-debug-info=/<project-name>/debug-info

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

    Цифровые подписи

    Ни один магазин приложений не примет сборку без криптографической подписи. Подпись гарантирует, что приложение было создано конкретным разработчиком и не было изменено третьими лицами после компиляции.

    Для Android создается файл хранилища ключей (Keystore), содержащий приватный ключ. Для iOS используются сертификаты разработчика и профили обеспечения (Provisioning Profiles), генерируемые в панели Apple Developer.

    > Утеря файла Keystore для Android означает полную невозможность выпустить обновление для уже опубликованного приложения. Google Play не позволит загрузить APK, подписанный другим ключом.

    Автоматизация через CI/CD

    Ручная сборка приложения, прогон тестов и загрузка в магазины — процесс долгий и подверженный человеческому фактору. В профессиональной разработке применяется подход CI/CD (Continuous Integration / Continuous Deployment — Непрерывная интеграция и доставка).

    !Схема автоматизированного конвейера CI/CD для сборки и публикации Flutter-приложения

    Как только разработчик отправляет новый код в репозиторий (например, на GitHub), облачный сервер автоматически запускает конвейер задач:

  • Загрузка исходного кода и установка зависимостей.
  • Запуск статического анализатора (linter) для проверки стиля кода.
  • Выполнение всех Unit и Golden тестов.
  • В случае успеха — компиляция релизных сборок (APK/AAB для Android, IPA для iOS).
  • Автоматическая отправка сборок в TestFlight (iOS) и Google Play Console (Android) для бета-тестирования.
  • Если на этапе 3 хотя бы один тест падает (например, сломалась формула расчета тарифа), конвейер останавливается, и бракованная сборка не попадает к пользователям.

    Внедрение строгих процессов тестирования, автоматизированной генерации документов и настроенного CI/CD конвейера превращает набор кода в надежный, коммерчески успешный IT-продукт, готовый к масштабированию и выходу на рынок.