В этой статье речь пойдет о том, как создать Flutter-приложение, которое умеет адаптироваться к разным экранам и ориентациям. Статья будет полезна как начинающим, так и опытным пользователям Flutter. Первые найдут шаблон для изучения, вторые — еще один взгляд на этот вопрос.
Постановка задачи или проблемы адаптивной верстки
«Делай то, что тебе нравится». Flutter
Звучит как духовно обогащенный мотиватор, но это — реальный взгляд разработчиков фреймворка на эту проблему. У Flutter нет одного решения, «прибитого гвоздями», здесь у разработчика полная свобода и возможность выбирать способ решения этих проблем (заодно и собрать грабли по пути).
На данный момент Flutter поддерживает мобильные платформы (Android, iOS), Web, редко используется для desktop. Это значит, что приложение должно поддерживать широкий диапазон разрешений экранов устройств и ориентации. Также мобильное устройство (если оно не квадратное) может быть повернуто пользователем в портретную или ландшафтную ориентацию. Пользователи мобильных устройств любят и умеют это делать во время работы приложения, чтоб рассмотреть подробнее содержимое экрана. Так что, чтобы не разочаровывать пользователя, мы должны позаботиться о проблеме поворота экрана во время работы приложения.
И при всем этом приложению желательно еще и работать, отображая информацию о своей жизнедеятельности, несмотря на характеристики и параметры устройства, на которое его занесло, и на то, какие действия с ним может совершать пользователь.
«Смешать, но не взбалтывать». Проблема мультиплатформенного дизайна
Редко встретишь дизайнера, который понимает проблемы мультиплатформенного дизайна. И задача дизайнера Flutter-приложения — приготовить такой коктейль из элементов мобильного дизайна, который не вызывает отторжения у пользователя.
Ведь нужно как-то совместить эти элементы с привычным UX на платформах, на которых их раньше не было. Например, FAB (floating action button in material design) и другие элементы и подходы дизайна, привычные на мобильных платформах, на вебе и десктопе немного в новинку, и наоборот — приемы, привычные для веба, смотрятся диковато на мобилках.
Обычно обновления дизайна происходят итерационно. Так, дизайнер вряд ли сразу выдаст окончательный вариант под все ориентации и разрешения. Поэтому нужно предусмотреть возможность добавления и изменения, не трогая то, что уже работает.
Как это уже работает в Android
Проблема и способы ее решения известны. На Android в свое время это называлось отзывчивый (responsive) дизайн. По этому поводу даже есть даже целые гайдлайны: Responsive UI - Layout (правда сейчас уже в архиве), Support different screen sizes.
Как это выглядит на практике для разработчика? В самом начале существует один макет, к которому по мере необходимости добавляем макеты для разных по разрешению экрана устройств, а затем обе ориентации для каждого типа устройств. Далее эти макеты выбираются в зависимости от того, на каком устройстве (с каким разрешением) и в какой ориентации они работают.
Для экранов с большой площадью обычно используют макеты с большим количеством информации, которую можно отобразить. Хороший пример — master/detail flow из шаблонов новых приложений в Android Studio. Это классика того, как рекомендуется работать с экранами большой площади in android way.
Долго ли сказка говорится, начнем писать код. Для этого используем модную нынче методологию BDD.
Реализация
Перед тем как писать код, опишем сценарии:
В пределах одного макета на разных устройствах с разной плотностью пикселей должно выполнятся масштабирование контента (как при использовании dpi указания размеров).
Я хочу иметь возможность добавлять макеты в любом порядке (сделали макет для landscape — отлично, используем его везде, сделали для portrait — отлично, автоматически подставляем его для портретной ориентации).
Я хочу иметь возможность добавлять макеты как в Android Studio для landscape, для portrait, для более широких экранов, для более узких.
Добавление новых макетов не должно влиять на существующие макеты.
Цепочка вызовов для созданного макета не должна без моего ведома изменяться со временем.
Я хочу, чтобы логика выбора макета не изменялась, ее не нужно было изменять при добавлении/удалении макетов.
Логика выбора должна быть одинаковой и предсказуемой.
Код примера, рассматриваемого в статье и пакет, его реализующий, находится по ссылкам:
пакет: https://pub.dev/packages/sizer_mod
пример в папке example: https://github.com/NickZt/sizer_mod/tree/master/example
Инициализация
Начнем с инициализации пакета. Вызов нашей MaterialApp оборачиваем в вызов инициализации виджета подстройки размеров виджетов под dpi экранов:
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return OrientationBuilder(
builder: (context, orientation) {
SizerUtil().init(constraints, orientation);
return MaterialApp(
Этот код находится в example lib\main.dart
Шаг 1. Настройка виджетов под изменяемое разрешение экрана
Мы хотим отображать виджеты на разных экранах наиболее близко к задуманному при первоначальном дизайне. Эта часть заимствована из библиотеки sizer и работает так же как в ней.
Также можно использовать следующий метод, основанный на SizerUtil.orientation свойстве для более вариабельной настройки параметров виджета. Пример:
appBar: AppBar(
title: SizerUtil.orientation == Orientation.portrait
? const Text('portrait')
: const Text('landscape'),
),
На AppBar выдается заголовок с названием текущей ориентации. Этот код находится в example lib\screens\home_screen.dart
Проверим соответствие сценариям.
Этот способ выполняет наше пожелание из пункта 1 списка Перед тем как писать код, опишем сценарии: «1. В пределах одного макета на разных устройствах с разной плотностью пикселей должно выполнятся масштабирование контента (как при использовании dpi указания размеров)».
Шаг 2. Используем специализированный виджет для работы с разными разрешениями экрана и ориентацией
Поддержка изменения ориентации и переключения виджетов под разные размеры (разрешения) экрана реализована с использованием виджета ResponsiveWidget. В его поля мы подставляем макет (виджет) для каждой пары разрешения/ориентации. В начале у него есть одно обязательное поле (по аналогии с default xml in android) landscapeLargeScreen. Дополнительные виджеты под другие значения разрешения/ориентации опциональны, т.е их можно добавить по мере появления:
landscapeMediumScreen
landscapeSmallScreen
portraitMediumScreen
portraitSmallScreen
portraitLargeScreen
В приведенном ниже примере с помощью виджета WelcomePage создается страница в ориентации landscape и строится макет с использованием прокрутки контента в вертикальной плоскости, а для ориентации portrait используется такой же виджет с таким же набором страниц, только контент прокручивается в горизонтальной плоскости:
body: ResponsiveWidget(
landscapeLargeScreen: WelcomePage(
pageIndex: 0,
scrollDirection: Axis.vertical,
children: listOfPages,
),
portraitLargeScreen: WelcomePage(
pageIndex: 0,
scrollDirection: Axis.horizontal,
children: listOfPages,
),
),
Для визуалов:
https://github.com/NickZt/sizer_mod/raw/master/example/images/portrait_land_mob.gif
https://github.com/NickZt/sizer_mod/raw/master/example/images/portrait_mob.gif
Мы получили экран, который имеет разные макеты для портретной и альбомной ориентации. Когда устройство меняет ориентацию, мы перестраиваем наш макет.
Как мы обнаруживаем изменения ориентации? Мы смотрим на отношение ширины к длине страницы. И в зависимости от этого переключаем макеты.
Проверим соответствие сценариям
Это выполняет наши пожелания, указанные со 2 по 7 пункт из Перед тем как писать код, опишем сценарии:
2. Я хочу иметь возможность добавлять макеты в любом порядке (сделали макет для landscape — отлично, используем его везде, сделали для portrait — отлично, автоматически подставляем его для портретной ориентации).
3. Я хочу иметь возможность добавлять макеты как в Android Studio для landscape, для portrait, для более широких экранов, для более узких.
4. Добавление новых макетов не должно влиять на существующие макеты.
5. Цепочка вызовов для созданного макета не должна без моего ведома изменяться со временем.
6. Я хочу, чтобы логика выбора макета не изменялась и ее не нужно было изменять при добавлении/удалении макетов.
7. Логика выбора должна быть одинаковой и предсказуемой.
Выводы
По рецепту из моей статьи можно приготовить приложение, в котором размер и ориентация экрана на всех 3-х платформах будут работать одинаково. Надеюсь, этот способ будет полезен и в вашем приложении.
Буду рад общению по теме (и не по теме тоже :)) в комментариях.
К статье добавил опрос, результаты которого помогут мне выбрать тип приложения, который я возьму в качестве примера для второй части статьи. В ней я собираюсь более подробно остановиться на виджетах, помогающих сделать приложение отзывчивым. А также хочу рассказать о некоторых нюансах работы с ними в сравнении с элементами дизайна, принятыми в Android. Также хочу рассмотреть способы автоматического импорта дизайна из Figma.
Код примера, рассматриваемого в статье, и пакет, его реализующий, находятся здесь:
пакет: https://pub.dev/packages/sizer_mod
Git репозиторий: https://github.com/NickZt/sizer_mod
пример — в папке example: https://github.com/NickZt/sizer_mod/tree/master/example
Полезные ссылки
Support different screen sizes
Floating action button in material design
denis-isaev
Для тех, кто не хочет тащить лишнюю зависимость, аналогичное можно реализовать парой виджетов. Бонусом будет легче под себя похачить (дополнить новыми комбинациями, поменять размеры и т.п.)
responsive_builder.dart
screen_type_layout.dart
И пример использования:
nikita_dol
Я хотел бы написать ишьюс на GitHub, но там нельзя, поэтому здесь:
Используйте WidgetBuilder
Который отличается 1 параметром — это ещё один повод задуматься над удобным API и пунктом 4 из этого списка
Nick_Maverick Автор
Спасибо за замечание, учту. И заодно включил issues
anonymous
Так в Flutter 2, который недавно вышел уже нормальная поддержка веб-приложений. Not a beta channel. Или я не прав?
Nick_Maverick Автор
точно. поправил. Спасибо. У меня просто включен beta-channel так что я особо и не заметил это событие )