Fit Grid

Сетка, которая сама подбирает размер и количество плиток под доступное пространство, минимизируя пустые зазоры.

Описание

Пакет предоставляет виджет FitGrid, который сам подбирает количество столбцов и строк, размер плиток и их позиции — без ручного указания crossAxisCount, как в обычном GridView. Раскладка пересчитывается на layout-фазе (CustomMultiChildLayout + MultiChildLayoutDelegate), поэтому ресайз области не вызывает rebuild дерева виджетов — только relayout.

Основные возможности

  • Автоматический подбор количества столбцов/строк и размера плиток под доступное пространство
  • Минимизация пустого пространства: если элементов меньше вместимости страницы — приоритет отдаётся заполнению области, если страница заполнена целиком — максимизации размера плитки
  • Расчёт раскладки на layout-фазе, без rebuild при ресайзе
  • Настраиваемое выравнивание сетки через AlignmentDirectional — учитывает направление текста (RTL/LTR)
  • Отдельное выравнивание неполной последней строки
  • Поддержка отступов между плитками (itemPadding) и от границ области (gridPadding)
  • Поддержка пропорций плиток (aspectRatio)
  • Настраиваемый список пресетов размеров плиток (presets) — свой набор форм-факторов вместо встроенного
  • Встроенная пагинация: maxItemsPerPage, currentPage и уведомления через pageInfoNotifier
  • Утилита расчёта maxItemsPerPage по ширине области с дефолтными брейкпоинтами под mobile/tablet и desktop/web
  • Без сторонних зависимостей — только Flutter SDK

Установка

Пакет опубликован на pub.dev. Добавьте зависимость в pubspec.yaml:

dependencies:
  fit_grid: ^1.0.0

Затем выполните:

flutter pub get

Использование

Базовый пример

import 'package:fit_grid/fit_grid.dart';

FitGrid(
  children: [
    Card(child: Text('Карточка 1')),
    Card(child: Text('Карточка 2')),
    Card(child: Text('Карточка 3')),
    // ... больше карточек
  ],
  maxItemsPerPage: 9,
  alignment: AlignmentDirectional.center,
)

Расширенный пример с настройками

FitGrid(
  children: _buildCards(),
  maxItemsPerPage: 12,
  alignment: AlignmentDirectional.topStart,
  itemPadding: Size(16, 16),  // Отступы между плитками
  gridPadding: EdgeInsets.all(24),  // Отступы от границ
  aspectRatio: 16 / 9,  // Пропорции плитки (опционально)
)

Выравнивание и RTL

alignment типизирован как AlignmentDirectional, поэтому start/end автоматически зеркалятся под направление текста из ближайшего Directionality (например, для арабской/ивритской локали). Если нужно жёстко закрепить сторону независимо от locale — используйте AlignmentDirectional.topLeft/topRight и т.д. напрямую вместо topStart/topEnd.

Пример с пагинацией

final ValueNotifier<FitGridPageInfo> pageInfoNotifier = ValueNotifier<FitGridPageInfo>(
  const FitGridPageInfo(currentPage: 1, totalPages: 1),
);

// Слушаем изменения информации о страницах
ValueListenableBuilder<FitGridPageInfo>(
  valueListenable: pageInfoNotifier,
  builder: (context, pageInfo, child) {
    return Text('Страница ${pageInfo.currentPage} из ${pageInfo.totalPages}');
  },
)

// Используем в FitGrid
FitGrid(
  children: _buildCards(),
  maxItemsPerPage: 12,
  alignment: AlignmentDirectional.center,
  pageInfoNotifier: pageInfoNotifier,  // Уведомления о страницах
  currentPage: 1,  // Текущая страница (опционально)
)

Расчёт maxItemsPerPage по ширине (брейкпоинты)

Если вы хотите автоматически подбирать maxItemsPerPage в зависимости от ширины области, можно использовать дефолтные брейкпоинты пакета:

LayoutBuilder(
  builder: (context, constraints) {
    final FitGridPlatformGroup platformGroup = FitGridPlatformGroup.desktopAndWeb;
    final int maxItemsPerPage = FitGridItemsPerPageCalculator.calculateForPlatformGroup(
      width: constraints.maxWidth,
      platformGroup: platformGroup,
    );
    return FitGrid(
      children: _buildCards(),
      maxItemsPerPage: maxItemsPerPage,
      alignment: AlignmentDirectional.center,
    );
  },
)

FitGridItemsPerPageBreakpoint

FitGridItemsPerPageBreakpoint — правило вида «диапазон ширины → рекомендуемое maxItemsPerPage».

  • minWidth: минимальная ширина (включительно)
  • maxWidth: максимальная ширина (включительно), можно использовать double.infinity
  • maxItemsPerPage: рекомендуемое значение maxItemsPerPage для диапазона

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

LayoutBuilder(
  builder: (context, constraints) {
    final List<FitGridItemsPerPageBreakpoint> breakpoints =
        <FitGridItemsPerPageBreakpoint>[
      const FitGridItemsPerPageBreakpoint(
        minWidth: 0,
        maxWidth: 599,
        maxItemsPerPage: 8,
      ),
      const FitGridItemsPerPageBreakpoint(
        minWidth: 600,
        maxWidth: 1023,
        maxItemsPerPage: 12,
      ),
      const FitGridItemsPerPageBreakpoint(
        minWidth: 1024,
        maxWidth: double.infinity,
        maxItemsPerPage: 16,
      ),
    ];
    final int maxItemsPerPage = FitGridItemsPerPageCalculator.calculateForBreakpoints(
      width: constraints.maxWidth,
      breakpoints: breakpoints,
    );
    return FitGrid(
      children: _buildCards(),
      maxItemsPerPage: maxItemsPerPage,
      alignment: AlignmentDirectional.center,
    );
  },
)

Пресеты размеров плиток

FitGrid перебирает список пресетов и выбирает тот, что даёт максимальное заполнение области (presets, по умолчанию — встроенный tileSizePresets). В debug-режиме выбранный пресет выводится в консоль.

Пресет — это не только пропорция, но и потолок абсолютного размера плитки: если элементов на странице ровно maxItemsPerPage (страница заполнена целиком), плитка не может превысить preset.size. Если встроенные пресеты вам не подходят (например, нужны более крупные плитки на широких экранах), передайте свой список:

FitGrid(
  children: _buildCards(),
  maxItemsPerPage: 9,
  alignment: AlignmentDirectional.center,
  presets: const [
    TileSizePreset('wide', Size(900, 400)),
    TileSizePreset('square', Size(500, 500)),
  ],
)

API

FitGrid

Основной виджет пакета.

Параметры

Параметр Тип Обязательный По умолчанию Описание
children List<Widget> ✅ - Список виджетов-карточек для отображения
maxItemsPerPage int ✅ - Максимальное количество элементов на странице
alignment AlignmentDirectional ✅ - Выравнивание сетки внутри области (учитывает RTL/LTR)
itemPadding Size ❌ Size(8, 8) Отступы между плитками (width = горизонтальный, height = вертикальный)
gridPadding EdgeInsets ❌ EdgeInsets.zero Внешние отступы сетки от границ области
aspectRatio double? ❌ null Пропорции плитки (width / height). Если null, используется 1:1
pageInfoNotifier ValueNotifier<FitGridPageInfo>? ❌ null ValueNotifier для уведомления о текущей странице и общем количестве страниц
currentPage int? ❌ null Текущая страница (начинается с 1). Если не указан, используется значение из pageInfoNotifier.value.currentPage, по умолчанию 1
presets List<TileSizePreset> ❌ tileSizePresets Список пресетов размеров плитки, из которых выбирается оптимальный

Выравнивание

Поддерживаются все стандартные варианты AlignmentDirectional:

  • AlignmentDirectional.topStart, topCenter, topEnd
  • AlignmentDirectional.centerStart, center, centerEnd
  • AlignmentDirectional.bottomStart, bottomCenter, bottomEnd

а также нейтральные к направлению варианты (topLeft/topRight и т.д.), если зеркалирование под RTL не нужно.

findOptimalPreset

Функция для автоматического выбора оптимального пресета размера плиток.

TileSizePreset findOptimalPreset({
  required Size layoutSize,
  required int itemCount,
  required int maxItems,
  required Size itemPadding,
  required Alignment alignment,
  required EdgeInsets gridPadding,
  double? aspectRatio,
  List<TileSizePreset> presets = tileSizePresets,
})

TileSizePreset

Класс для представления предустановленного размера плитки.

class TileSizePreset {
  final String name;  // Название пресета
  final Size? size;   // Размер плитки (null означает максимальный размер)
  double? get aspectRatio;  // Соотношение сторон
}

tileSizePresets

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

  • 377x200
  • 332x266
  • 432x366
  • 409x234
  • 351x304
  • 604x351
  • 615x309

FitGridItemsPerPageCalculator

Класс для расчёта количества элементов на странице по ширине области.

Методы

  • calculateForPlatformGroup - рассчитывает maxItemsPerPage по ширине области с использованием дефолтных брейкпоинтов для указанной группы платформ
  • calculateForBreakpoints - рассчитывает maxItemsPerPage по ширине области на основе произвольного списка брейкпоинтов

FitGridDefaultBreakpoints

Класс с дефолтными правилами (брейкпоинтами) для расчёта количества элементов на странице.

Константы

  • mobileAndTabletBreakpoints - брейкпоинты для мобильных устройств и планшетов
  • desktopAndWebBreakpoints - брейкпоинты для десктопа и веба

FitGridItemsPerPageBreakpoint

Класс для представления правила брейкпоинта.

class FitGridItemsPerPageBreakpoint {
  final double minWidth;  // Минимальная ширина (включительно)
  final double maxWidth;  // Максимальная ширина (включительно), можно использовать double.infinity
  final int maxItemsPerPage;  // Рекомендуемое значение maxItemsPerPage для диапазона
}

FitGridPageInfo

Класс для хранения информации о текущей странице и общем количестве страниц.

class FitGridPageInfo {
  final int currentPage;  // Текущая страница (начинается с 1)
  final int totalPages;   // Общее количество страниц
}

FitGridLayoutDelegate

MultiChildLayoutDelegate, на котором построен FitGrid. Экспортируется для случаев, когда нужно встроить ту же раскладку в свой CustomMultiChildLayout напрямую.

TileLayoutCalculator

Калькулятор оптимальной сетки: перебирает варианты количества столбцов/строк, считает размер плитки с учётом пропорций, отступов и потолка пресета, а также смещение сетки для выравнивания. Экспортируется для кастомных сценариев расчёта вне FitGrid.

Примеры

Полный рабочий пример доступен в папке example/. Запустите его командой:

cd example
flutter run

Как это работает

  1. Расчёт оптимальной сетки: алгоритм перебирает все возможные комбинации строк и столбцов для заданного количества элементов. Если элементов меньше maxItemsPerPage, приоритет отдаётся заполнению области; если страница заполнена целиком — максимизируется площадь плитки (минимизируется пустое пространство).

  2. Выбор пресета: для каждого пресета из presets прогоняется расчёт сетки, и выбирается тот, что даёт наибольший коэффициент заполнения области. Пресет задаёт как пропорцию плитки, так и потолок её абсолютного размера.

  3. Расчёт размера плиток: для выбранной конфигурации сетки и пресета рассчитывается финальный размер плитки с учётом доступного пространства, отступов и заданных пропорций (aspectRatio переопределяет пропорцию пресета).

  4. Позиционирование: плитки размещаются с учётом выбранного AlignmentDirectional (резолвится в Alignment через Directionality контекста), включая отдельное выравнивание неполной последней строки.

  5. Пересчёт на layout-фазе: всё вышеперечисленное происходит в FitGridLayoutDelegate.performLayout, а не в build(), поэтому ресайз области не перестраивает дерево виджетов — только relayout.

Тесты

Модульные и виджет-тесты покрывают алгоритм раскладки, брейкпоинты, подбор пресета, кастомные пресеты, RTL-выравнивание и пагинацию:

flutter test

Требования

  • Flutter SDK: >=3.41.9
  • Dart SDK: >=3.6.0 <4.0.0

Лицензия

MIT, см. LICENSE.