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

Практический курс по изучению Flutter с нуля до готового продукта. Вы создадите приложение для управления арендной платой, учета показаний счетчиков и расчета тарифов, опираясь на архитектурные решения open-source проектов, таких как Tenant Ledger [github.com](https://github.com/iamthetwodigiter/Tenant_Ledger) и Electricity Counter [github.com](https://github.com/jelazi/electricity_counter_bloc).

1. Основы Flutter и проектирование UI приложения для арендодателя

Основы Flutter и проектирование UI приложения для арендодателя

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

Flutter — это кроссплатформенный фреймворк от компании Google, использующий язык программирования Dart. Его главное преимущество заключается в том, что он позволяет создать единую кодовую базу, которая будет работать как на устройствах iOS, так и на Android, сохраняя при этом высокую производительность и плавность анимаций.

Декларативный подход и концепция виджетов

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

В отличие от императивного подхода, где разработчик вручную указывает системе, как изменить элемент на экране (например, «найди текстовое поле и поменяй его цвет на красный»), Flutter использует декларативный UI.

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

Математически эту концепцию можно выразить простой формулой:

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

!Схема дерева виджетов и потока данных

Проектирование структуры приложения арендодателя

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

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

  • Главный экран (Дашборд): сводная информация за текущий месяц. Общая сумма ожидаемой прибыли, количество должников и график поступлений.
  • Список объектов и жильцов: перечень квартир с указанием текущих арендаторов, их контактных данных и статуса оплаты базовой аренды.
  • Учет коммунальных услуг: экран для ввода показаний счетчиков (электричество, холодная и горячая вода) по каждому объекту.
  • Справочник тарифов: раздел настроек, где задается стоимость одного киловатта или кубометра.
  • Для реализации навигации между этими экранами во Flutter часто используется виджет BottomNavigationBar — нижняя панель с иконками, позволяющая быстро переключаться между основными разделами.

    Базовые виджеты для построения интерфейса

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

    * Scaffold: «строительные леса» экрана. Предоставляет стандартную структуру с верхней панелью (AppBar), телом экрана и нижней панелью навигации. * Column и Row: виджеты для выстраивания дочерних элементов в колонку (по вертикали) или в строку (по горизонтали). * ListView: виджет для создания прокручиваемых списков. Идеально подходит для отображения десятков квартирантов. * Card: визуальный контейнер с закругленными углами и тенью. Отлично подходит для оформления информации об отдельном жильце. * TextField: поле для ввода текста или чисел. Будет использоваться для ввода показаний счетчиков.

    !Интерактивный конструктор карточки жильца

    Управление состоянием: Stateless и Stateful виджеты

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

    | Тип виджета | Описание | Пример использования в нашем приложении | | :--- | :--- | :--- | | StatelessWidget | Виджет без состояния. Его внешний вид задается один раз при создании и не меняется. | Статическая иконка счетчика, заголовок экрана «Справочник тарифов», разделительная линия. | | StatefulWidget | Виджет с состоянием. Может перерисовываться в ответ на действия пользователя или получение новых данных. | Чекбокс «Оплата получена», поле ввода показаний воды, счетчик общей суммы долга. |

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

    Рассмотрим пример кода, описывающего простую карточку жильца с использованием StatelessWidget. Обратите внимание на вложенность элементов:

    ```dart import 'package:flutter/material.dart';

    class TenantCard extends StatelessWidget { final String name; final double rentAmount;

    const TenantCard({Key? key, required this.name, required this.rentAmount}) : super(key: key);

    @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.all(8.0), child: ListTile( leading: const Icon(Icons.person), title: Text(name), subtitle: Text('Аренда: PV_{current}V_{previous}T(1650 - 1500) \times 5 = 750TotalRent\sum_{i=1}^{n} P_inV_{current} < V_{previous}$ (текущие показания меньше предыдущих), интерфейс должен подсветить поле красным цветом и заблокировать кнопку сохранения, так как счетчик не может крутиться в обратную сторону (за исключением случаев замены счетчика, что потребует отдельного бизнес-процесса в приложении).

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

    2. Управление состоянием: работа со справочниками тарифов и данными жильцов

    Управление состоянием: работа со справочниками тарифов и данными жильцов

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

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

    Локальное и глобальное состояние

    В контексте разработки мобильных приложений все данные можно разделить на две большие категории в зависимости от их жизненного цикла и области видимости.

    Локальное состояние (Ephemeral State) — это данные, которые нужны только одному конкретному виджету и теряют свою актуальность при переходе на другой экран.

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

    Для управления локальным состоянием идеально подходит встроенный метод setState(), который мы упоминали ранее. Он работает быстро и не требует подключения сторонних библиотек.

    Глобальное состояние (App State) — это данные, которые должны быть доступны из разных частей приложения. Если изменение данных на одном экране должно мгновенно отразиться на другом, мы имеем дело с глобальным состоянием.

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

    Проблема передачи данных по дереву виджетов

    Если попытаться передать глобальные данные (например, тариф на воду) от главного экрана к экрану конкретного счетчика через конструкторы виджетов, мы столкнемся с архитектурной проблемой, известной как Prop Drilling (бурение свойств).

    Представьте иерархию: Главный экран Список квартир Карточка квартиры Экран счетчиков Виджет расчета воды.

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

    !Схема передачи данных: Prop Drilling против централизованного хранилища

    Чтобы избежать этой проблемы, используются специализированные менеджеры состояний. Они создают независимое «хранилище» данных, к которому может обратиться любой виджет напрямую, минуя иерархию дерева.

    Инструменты управления состоянием во Flutter

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

  • Provider — один из самых популярных и рекомендуемых подходов для приложений малого и среднего размера. Он работает как обертка над встроенными механизмами Flutter, позволяя легко внедрять зависимости и подписывать виджеты на изменения данных.
  • BLoC (Business Logic Component) и его облегченная версия Cubit — мощный инструмент для крупных проектов. Он строго разделяет интерфейс и бизнес-логику, используя потоки данных (Streams).
  • Riverpod — современная эволюция Provider, решающая его архитектурные ограничения и обеспечивающая безопасность на этапе компиляции.
  • Для нашего приложения по учету жильцов мы рассмотрим логику работы на базе паттерна, схожего с Provider, так как он наиболее нагляден для понимания связи между данными и UI.

    Проектирование справочника тарифов

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

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

    Ключевой метод здесь — notifyListeners(). Как только арендодатель меняет тариф в настройках и вызывает метод updateElectricityRate, система рассылает сигнал всем виджетам, которые «слушают» этот класс. Виджеты расчетов мгновенно перерисовываются с новыми цифрами.

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

    !Интерактивный симулятор реактивности: изменение тарифа и перерасчет

    Управление списком жильцов и расчетами

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

    Математическая модель расчета долга конкретного жильца за месяц опирается на данные из двух разных источников состояния (данные жильца и глобальные тарифы):

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

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

    Рассмотрим пример логики добавления нового показания счетчика:

  • Пользователь вводит число 1520 в поле «Текущие показания воды» для квартиры №1.
  • UI вызывает метод addWaterReading(apartmentId: 1, reading: 1520) в классе состояния.
  • Класс состояния находит нужную квартиру, проверяет, что 1520 больше предыдущего показания.
  • Если проверка пройдена, класс состояния обновляет данные квартиры, пересчитывает формулу с использованием актуального и вызывает notifyListeners().
  • Экран карточки квартиры получает сигнал и обновляет текст с итоговой суммой долга.
  • Оптимизация производительности при перерисовке

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

    Для оптимизации производительности во Flutter применяются точечные подписки. Например, при использовании пакета Provider применяется виджет Consumer или метод context.select(). Они позволяют указать фреймворку: «Перерисуй только этот конкретный текстовый виджет с суммой, когда изменится тариф, а остальной экран не трогай».

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

    3. Локальная база данных: хранение показаний счетчиков воды и электричества

    Локальная база данных: хранение показаний счетчиков воды и электричества

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

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

    Выбор инструмента: почему SQLite?

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

    Для сложных структур данных применяются полноценные базы данных. В экосистеме Flutter стандартом де-факто для реляционных баз данных является плагин sqflite, который предоставляет доступ к встроенному в мобильные операционные системы движку SQLite.

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

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

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

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

    Для нашего приложения потребуются как минимум две основные таблицы:

  • Таблица жильцов (tenants)
  • Таблица показаний счетчиков (meter_readings)
  • Связь между ними будет осуществляться через внешний ключ (Foreign Key). Это означает, что каждая запись о показаниях счетчика будет содержать идентификатор жильца, к которому она относится.

    !Схема реляционной базы данных: таблица Tenants связана с таблицей Meter_Readings связью один-ко-многим

    Рассмотрим структуру таблицы meter_readings более подробно:

    | Имя столбца | Тип данных (SQL) | Описание | | :--- | :--- | :--- | | id | INTEGER | Уникальный идентификатор записи (Primary Key), генерируется автоматически | | tenant_id | INTEGER | Ссылка на ID жильца из таблицы tenants (Foreign Key) | | reading_date | TEXT | Дата снятия показаний в формате ISO 8601 (например, 2023-10-01) | | water_value | REAL | Показания счетчика воды (могут содержать дробные значения) | | electricity_value | REAL | Показания счетчика электроэнергии |

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

    Инициализация базы данных во Flutter

    Для работы с SQLite во Flutter используется пакет sqflite в связке с пакетом path (для правильного определения пути к файлу базы данных на iOS и Android).

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

    Обратите внимание на конструкцию ON DELETE CASCADE. Это правило означает, что если арендодатель удалит профиль жильца из базы, все связанные с ним показания счетчиков будут удалены автоматически. Это предотвращает появление «осиротевших» записей, которые занимают память и могут вызвать ошибки в приложении.

    Реализация CRUD-операций

    Аббревиатура CRUD описывает четыре базовые функции работы с данными: Create (создание), Read (чтение), Update (обновление) и Delete (удаление). Рассмотрим, как они применяются к показаниям счетчиков.

    Создание (Create)

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

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

    Чтение и математика расчетов (Read)

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

    Запрос к базе данных будет выглядеть так: мы просим найти все записи для конкретного tenant_id, отсортировать их по дате по убыванию и взять только первые две.

    Получив эти данные, мы применяем математическую формулу расчета потребления:

    Где: * — объем потребленного ресурса (Consumption). * — текущее показание счетчика. * — предыдущее показание счетчика.

    Пример из практики: арендодатель запрашивает данные для квартиры №1. База данных возвращает две записи. Текущее показание воды () равно 450.5 кубометров. Предыдущее () было 442.0 кубометров.

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

    Обновление и Удаление (Update, Delete)

    Люди склонны ошибаться. Арендодатель может случайно ввести 1500 вместо 150. Для исправления таких ситуаций необходимы методы обновления и удаления.

    Метод update находит нужную строку по её уникальному id и заменяет значения в столбцах. Метод delete полностью стирать строку. В интерфейсе приложения эти функции обычно прячутся за долгим нажатием на запись в истории показаний или иконкой корзины.

    Паттерн Репозиторий: связываем базу данных и UI

    Прямой вызов методов базы данных из виджетов (кнопок и текстовых полей) считается плохой практикой. Это нарушает принцип единственной ответственности: интерфейс должен заниматься только отрисовкой, а не SQL-запросами.

    Для решения этой проблемы применяется паттерн Репозиторий (Repository). Создается отдельный класс-прослойка, который общается с базой данных и передает готовые объекты в менеджер состояний (например, в Provider или BLoC).

    Когда пользователь нажимает кнопку «Сохранить»:

  • Виджет передает введенные числа в менеджер состояний.
  • Менеджер состояний вызывает метод Репозитория.
  • Репозиторий формирует SQL-запрос и сохраняет данные в SQLite.
  • Репозиторий запрашивает обновленный список показаний и возвращает его менеджеру состояний.
  • Менеджер состояний вызывает notifyListeners(), и интерфейс обновляется, показывая новую запись в истории.
  • Такая архитектура делает код предсказуемым, легко тестируемым и позволяет в будущем безболезненно заменить локальную базу данных SQLite на облачное решение (например, Firebase), не переписывая при этом ни строчки кода в пользовательском интерфейсе.

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

    4. Бизнес-логика: расчет ежемесячной платы и коммунальных услуг по тарифам

    Бизнес-логика: расчет ежемесячной платы и коммунальных услуг по тарифам

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

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

    Анатомия ежемесячного платежа

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

  • Фиксированная часть: базовая арендная плата за помещение. Эта сумма не меняется из месяца в месяц и прописывается в договоре.
  • Переменная часть: оплата потребленных ресурсов (коммунальные услуги). В нашем случае это холодная/горячая вода и электроэнергия. Эта сумма зависит от фактического расхода и текущих тарифов.
  • Математическая модель расчета выглядит следующим образом:

    Итоговая сумма = Арендная плата + (Расход воды × Тариф на воду) + (Расход электричества × Тариф на электричество)

    Пример из практики: базовая аренда квартиры составляет 30 000 руб. В текущем месяце жилец потратил 5 кубометров воды (тариф 50 руб. за куб) и 100 киловатт-часов электроэнергии (тариф 5 руб. за кВт⋅ч).

    Расчет переменной части: (5 × 50) + (100 × 5) = 250 + 500 = 750 руб. Итоговая сумма к оплате: 30 000 + 750 = 30 750 руб.

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

    Изоляция бизнес-логики: паттерн Service

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

    Согласно принципам Clean Architecture (чистой архитектуры), бизнес-логика должна быть полностью изолирована от пользовательского интерфейса (UI). Для этого в Dart создаются отдельные классы-сервисы.

    Создадим класс BillingService, который будет принимать сырые данные, производить вычисления и возвращать готовый объект квитанции.

    Шаг 1: Создание моделей данных

    Сначала определим структуру данных, которую будет возвращать наш сервис. Нам нужен класс MonthlyBill (Ежемесячный счет), который будет содержать всю детализацию, чтобы UI мог красиво отобразить чек.

    Шаг 2: Реализация логики расчета расхода

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

    Расход = Текущее показание - Предыдущее показание

    Однако в реальном мире счетчики имеют свойство «обнуляться» (переходить через ноль), когда достигают своего максимального значения. Например, старый счетчик электроэнергии с четырьмя циферблатами после значения 9999 покажет 0000. Если текущее показание 0002, а предыдущее 9998, простая разница даст отрицательное число (-9996), что сломает всю экономику приложения.

    Бизнес-логика должна учитывать такие краевые случаи (edge cases).

    Пример с переходом через ноль: максимальное значение счетчика 10000. Предыдущее показание 9998, текущее 0002. Расход = (10000 - 9998) + 2 = 2 + 2 = 4 единицы.

    Шаг 3: Сборка итогового сервиса

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

    !Схема потока данных при расчете квитанции

    Интеграция бизнес-логики с UI и State Management

    Имея готовый BillingService, мы можем легко интегрировать его в наш пользовательский интерфейс. Процесс взаимодействия слоев приложения выглядит так:

  • Пользователь открывает карточку жильца и нажимает «Рассчитать платеж».
  • Виджет обращается к менеджеру состояний (например, Provider или Riverpod) и запрашивает актуальные тарифы.
  • Виджет обращается к репозиторию базы данных и запрашивает два последних показания счетчиков для этого жильца.
  • Собранные данные передаются в BillingService.generateBill().
  • Сервис мгновенно возвращает объект MonthlyBill.
  • Виджет берет данные из MonthlyBill и отрисовывает красивую квитанцию на экране.
  • Такое разделение обязанностей делает код предсказуемым. Если завтра государство изменит правила расчета коммунальных услуг (например, введет социальную норму потребления, после которой тариф удваивается), вам не придется трогать ни базу данных, ни интерфейс. Вы измените только математику внутри BillingService.

    Обработка ошибок и валидация данных

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

    Перед тем как передавать данные в математические формулы, их необходимо провалидировать (проверить на корректность).

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

    Если данные не проходят валидацию, бизнес-логика должна выбросить исключение (Exception), которое интерфейс перехватит и покажет пользователю понятное сообщение об ошибке, например: «Текущие показания воды меньше предыдущих. Проверьте правильность ввода».

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

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

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

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

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

    Пирамида тестирования во Flutter

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

    Во Flutter применяется классическая концепция Testing Pyramid (пирамиды тестирования), которая делит проверки на три уровня:

    | Уровень тестирования | Что проверяет | Скорость выполнения | Зависимости | | :--- | :--- | :--- | :--- | | Unit-тесты (Модульные) | Отдельные функции и классы (бизнес-логику) | Миллисекунды | Нет (изолированно) | | Widget-тесты (Компонентные) | Отрисовку UI и взаимодействие с виджетами | Секунды | Виртуальное дерево виджетов | | Integration-тесты (Интеграционные) | Весь путь пользователя (от запуска до результата) | Минуты | Реальное устройство или эмулятор |

    Чем выше уровень, тем медленнее выполняются тесты и тем сложнее их поддерживать. Поэтому основу пирамиды (около 70-80% всех проверок) должны составлять быстрые модульные тесты.

    Модульное тестирование финансового ядра

    Встроенный пакет flutter_test предоставляет мощный инструментарий для проверки Dart-кода. Рассмотрим процесс тестирования на примере нашего сервиса BillingService, который отвечает за расчет квитанций.

    Особое внимание следует уделить краевым случаям (edge cases). В прошлой статье мы реализовали логику обработки ситуации, когда счетчик электроэнергии переходит через свой максимум и обнуляется. Напишем тест для этого сценария, используя паттерн AAA (Arrange, Act, Assert — Подготовка, Действие, Проверка).

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

    > Разработка через тестирование (TDD) — это методика, при которой сначала пишется тест, покрывающий желаемое изменение, а затем — код, который позволит пройти этот тест. > > temofeev.ru

    Для оценки качества тестирования используется метрика покрытия кода (Code Coverage). Она рассчитывается по формуле:

    Где — количество строк кода, выполненных во время тестов, а — общее количество строк в проекте. Для критически важных модулей, таких как расчет денег, покрытие должно стремиться к 100%.

    Изоляция зависимостей: Mock-объекты

    При тестировании виджетов или сервисов, которые обращаются к базе данных SQLite, возникает проблема: тесты не должны изменять реальные данные на устройстве. Для решения этой задачи применяются Mock-объекты (имитации).

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

    Сборка релизного приложения

    Когда код написан и покрыт тестами, приложение необходимо скомпилировать. В режиме разработки (при нажатии кнопки Run в IDE) Flutter использует JIT-компиляцию (Just-In-Time). Это позволяет применять функцию Hot Reload — мгновенно видеть изменения на экране без пересборки. Однако JIT-сборка работает медленно и занимает много места в памяти.

    Для публикации используется AOT-компиляция (Ahead-Of-Time). Весь Dart-код переводится в оптимизированный машинный код целевой платформы (ARM для мобильных устройств). Из приложения удаляются инструменты отладки, что делает его быстрым и компактным.

    Сборка для Android

    Для платформы Android существует два основных формата релизных файлов:

  • APK (Android Package Kit): Традиционный формат. Содержит скомпилированный код и ресурсы для всех возможных архитектур процессоров. Подходит для прямой установки на устройство (например, отправки файла арендодателю через мессенджер).
  • AAB (Android App Bundle): Современный формат для публикации в Google Play. Магазин сам генерирует оптимизированные APK-файлы под конкретное устройство пользователя, что значительно уменьшает вес скачиваемого приложения.
  • Команды для сборки в терминале:

    Сборка для iOS

    Сборка под устройства Apple требует наличия компьютера с macOS и установленного Xcode. Результатом сборки является файл формата IPA (iOS App Store Package).

    Важнейшим этапом сборки для обеих платформ является подписание кода (Code Signing). Разработчик использует криптографический ключ (Keystore для Android, Provisioning Profile для iOS), чтобы доказать свое авторство. Без цифровой подписи операционная система откажется устанавливать приложение из соображений безопасности.

    Автоматизация доставки: CI/CD

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

    CI/CD — это набор скриптов, которые выполняются на удаленном сервере каждый раз, когда разработчик отправляет новый код в репозиторий (например, на GitHub или GitLab).

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

    Типичный конвейер (pipeline) для нашего приложения арендодателя выглядит так:

  • Linting (Анализ кода): Сервер проверяет код на соответствие стандартам форматирования языка Dart.
  • Testing (Тестирование): Автоматически запускаются все Unit- и Widget-тесты. Если хотя бы один тест падает (например, сломалась формула расчета долга), конвейер останавливается, и сборка отменяется.
  • Building (Сборка): Сервер выполняет AOT-компиляцию, создавая файлы AAB и IPA.
  • Distribution (Дистрибуция): Готовые файлы автоматически отправляются тестировщикам или в магазины приложений.
  • > В современных условиях автоматизация деплоя на такие платформы, как Google Play, TestFlight и Significa, становится необходимостью. Это позволяет настроить автоматическую отправку приложений в магазины, чтобы вы могли сосредоточиться на разработке, а не на рутинных задачах. > > habr.com

    Для внутренней дистрибуции (когда приложение нужно передать ограниченному кругу лиц, например, только знакомым арендодателям) часто используются сервисы вроде Firebase App Distribution или Significa. Они позволяют тестировщикам скачивать новые версии приложения по прямой ссылке или QR-коду, минуя долгие проверки модераторов официальных магазинов.

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