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

Как сделать адаптивный дизайн?

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

Size screenSize = MediaQuery.of(context).size;
Orientation orientation = MediaQuery.of(context).orientation;

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

ResponsiveBuilder(
    builder: (context, sizingInformation) {
      // Здесь проверяется информация о размерах и возвращается пользовательский интерфейс
          if (sizingInformation.deviceScreenType == DeviceScreenType.desktop) {
          return Container(color:Colors.blue);
        }

        if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) {
          return Container(color:Colors.red);
        }

        if (sizingInformation.deviceScreenType == DeviceScreenType.watch) {
          return Container(color:Colors.yellow);
        }

        return Container(color:Colors.purple);
      },
    },
  );
}

Или же предоставляются некоторые заранее заданные типы экранов:

ScreenTypeLayout.builder(
  mobile: (BuildContext context) => Container(color:Colors.blue),
  tablet: (BuildContext context) => Container(color:Colors.yellow),
  desktop: (BuildContext context) => Container(color:Colors.red),
  watch: (BuildContext context) => Container(color:Colors.purple),
);

(Приведенные выше примеры кода взяты из пакета responsive_builder).

Другой интерфейс, по примеру MaterialStateProperty

При написании адаптивного кода нужно добиться хорошей гибкости. Иногда гибким надо сделать лишь одно значение (например, целое число crossAxisCount в GridView). А иногда нужны совершенно разные виды дизайна пользовательского интерфейса для разных экранов (например, TabBarView для мобильных устройств и Row для настольных компьютеров). Поэтому нужно сделать адаптивным либо целое число,либо виджет, не записывая снова и снова операторы if/switch. Кроме того, люди расходятся во мнениях относительно того, где устанавливать контрольные точки. Поэтому нужно предоставить им самим свободно определять собственные контрольные точки.

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

property?.resolve(_states)

А что если аналогичный интерфейс задействовать для целей адаптивности?

Состоянием теперь будет некая целевая конфигурация экрана, называющаяся в нашем случае ScreenScope:

class ScreenScope {
  final double minWidth;
  final double maxWidth;
  final double minHeight;
  final double maxHeight;
  final Orientation? orientation;
  ...
}

У нас уже имеются заранее заданные параметры этой конфигурации:

mobileScreenScope (0px - 480px width)
tabletScreenScope (480px - 840px width)
desktopScreenScope (840px -  width)

mobilePortraitScreenScope (0px - 480px width, portrait)
tabletPortraitScreenScope (480px - 840px width, portrait)
desktopPortraitScreenScope (840px - width, portrait)

mobileLandscapeScreenScope (0px - 840px width, landscape)
tabletLandscapeScreenScope (840px - 1200px width, landscape)
desktopLandscapeScreenScope (1200px - width, landscape)
Конфигурации экрана ​с двумя контрольными точками и различными ориентациями

Когда используется только одна контрольная точка:

smallScreenScope (0px - 600px width)
bigScreenScope (600px -  width)

smallPortraitScreenScope (0px - 600px width, portrait)
bigPortraitScreenScope (600px - width, portrait)

smallPortraitScreenScope (0px - 1000px width, landscape)
bigPortraitScreenScope (1000px - width, landscape)

То объявляется адаптивный экземпляр, а фактическое значение получается вызовом в нем метода resolve:

GridView.count(
  crossAxisSpacing: 10,
  mainAxisSpacing: 10,
  crossAxisCount: Responsive({
        mobileScreenScope: 2,
        tabletScreenScope: 4,
        desktopScreenScope: 6
      }).resolve(context)!,
  children: List.generate(
      30,
      (index) =>
          Container(color: Colors.green, child: Text("SOME TEXT"))),
);
Адаптивная сетка

А вот что получается, когда нужно использовать разные виджеты для разных экранов:

Widget widget = Responsive({
    mobileScreenScope: mobileWidget,
    tabletScreenScope: tabletWidget,
    desktopScreenScope: desktopWidget,
  }).resolve(context);

Чтобы сэкономить вам время, мы сделали вспомогательный виджет ScreenBuilder:

ScreenBuilder(
  mobile: mobile,
  tablet: tablet,
  desktop: desktop,
  );

ScreenBuilder.builder(
  mobileBuilder: mobileBuilder,
  tabletBuilder: tabletBuilder,
  desktopBuilder: desktopBuilder,
  )

В его синтаксисе используется WidgetBuilder, чтобы иметь доступ к BuildContext.

Адаптивный WidgetBuilder

Заключение

Вот и все о пакете responsive_property. Цель проста: обеспечить гибкость адаптивного дизайна, избавляя от необходимости написания операторов if.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Wenkai Fan: Responsive design at different levels in Flutter

Предыдущая статьяПарадокс надежности ИИ
Следующая статья22 сниппета на Python для повседневных задач