Привет, Хабр! Меня зовут Екатерина, я Flutter-разработчик в компании Friflex. Мы создаем мобильные приложения и сайты для бизнеса.

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

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

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

Сторонние библиотеки под любую проблему

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

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

dependencies:
  flutter:
    sdk: flutter

  # Для получения данных о погоде
  http: ^1.2.2
  # Для иконок погоды
  weather_icons: ^3.0.0

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

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

Как сделать лучше?

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

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

dependencies:
  flutter:
    sdk: flutter
  # Для получения данных о погоде
  http: ^1.2.2

flutter:
  assets:
    - assets/icons/

Итог: мы подстраиваем инструменты под приложение, а не создаем приложение под инструменты.

Вынесение виджетов в методы вместо создания отдельных классов

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

Пример виджета
class ExampleScreen extends StatelessWidget {
  const ExampleScreen({super.key, required this.onPressed});

  final Function() onPressed;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            GestureDetector(
              onTap: onPressed,
              child: const Card(
                color: Colors.red,
                child: Text('Компонент'),
              ),
            ),
            Column(
              children: [
                GestureDetector(
                  onTap: onPressed,
                  child: const Card(
                    color: Colors.amber,
                    child: Text('Компонент'),
                  ),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    GestureDetector(
                      onTap: onPressed,
                      child: const Card(
                        color: Colors.blue,
                        child: Text('Компонент'),
                      ),
                    ),
                    GestureDetector(
                      onTap: onPressed,
                      child: const Card(
                        color: Colors.green,
                        child: Text('Компонент'),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

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

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

Вспомогательный метод
class ExampleScreen extends StatelessWidget {
  const ExampleScreen({super.key, required this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _buildComponent(Colors.red),
            Column(
              children: [
                _buildComponent(Colors.amber),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    _buildComponent(Colors.blue),
                    _buildComponent(Colors.green),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildComponent(Color color) {
    return GestureDetector(
      onTap: onPressed,
      child: Card(
        color: color,
        child: const Text('Компонент'),
      ),
    );
  }
}

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

Но здесь кроется подвох: если наш компонент каким-либо образом обновится, например, поменяет цвет после нажатия, то вместе с ним перерисуется весь экран. Ненужное обновление элементов UI может заметно сказаться на производительности приложения, поэтому такой подход не рекомендуется.

Как сделать лучше?

Создать новый виджет под компонент, чтобы фреймворк знал, какую именно часть UI следует перерисовать.

Новый виджет под компонент
class ExampleScreen extends StatelessWidget {
  const ExampleScreen({super.key, required this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _ExampleComponent(
              color: Colors.red,
              onPressed: onPressed,
            ),
            Column(
              children: [
                _ExampleComponent(
                  color: Colors.amber,
                  onPressed: onPressed,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    _ExampleComponent(
                      color: Colors.blue,
                      onPressed: onPressed,
                    ),
                    _ExampleComponent(
                      color: Colors.green,
                      onPressed: onPressed,
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _ExampleComponent extends StatelessWidget {
  const _ExampleComponent({
    required this.color,
    required this.onPressed,
  });

  final Color color;

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onPressed,
      child: Card(
        color: color,
        child: const Text('Компонент'),
      ),
    );
  }
}

Инструменты, от которых больше проблем, чем пользы

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

Пример 1: shrinkWrap: true

Если вы сталкивались с проблемой неограниченного размера ListView, то могли наткнуться на советы установить виджет shrinkWrap: true.

@override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
        child: Column(
          children: [
            ListView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: 10000,
              itemBuilder: (_, __) => const Text('Hello World!'),
            ),
          ],
        ),
      );

  }

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

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

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

Как сделать лучше?

Есть несколько способов, как сохранить ленивую инициализацию. При использовании ListView внутри Flex-виджетов самым простым решением будет использовать Expanded или Flexible.

@override
  Widget build(BuildContext context) {
    return Column(
        children: [
          Flexible(
            child: ListView.builder(
              itemCount: 10000,
              itemBuilder: (_, __) => const Text('Hello World!'),
            ),
          ),
        ],
      );

  }

Кроме того, если проблема возникла из-за использования ListView внутри другого ScrollView (к примеру, ListView внутри SingleChildScrollView), то поможет замена этих виджетов на CustomScrollView и SliverList.

Пример 2: Злоупотребление GlobalKey  

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

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

Пример
class RandomColorsScreen extends StatefulWidget {
  const RandomColorsScreen({super.key});

  @override
  State<RandomColorsScreen> createState() => _RandomColorsScreenState();
}

class _RandomColorsScreenState extends State<RandomColorsScreen> {
  List<Widget> coloredBoxes = List.generate(
    10,
    (_) => const RandomColoredBox(),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 32),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ...coloredBoxes,
              ElevatedButton(
                onPressed: () => setState(() => coloredBoxes.shuffle()),
                child: const Text('Перемешать'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class RandomColoredBox extends StatefulWidget {
  const RandomColoredBox({super.key});

  @override
  State<RandomColoredBox> createState() => _RandomColoredBoxState();
}

class _RandomColoredBoxState extends State<RandomColoredBox> {
  final color = Colors.white
      .withGreen(Random().nextInt(255))
      .withBlue(Random().nextInt(255))
      .withRed(Random().nextInt(255));

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 50,
      width: 50,
      color: color,
    );
  }
}

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

Чтобы элементы списка всё же перемешались следует добавить ключ к ним. Часто GlobalKey может стать первым выбором из-за простоты его использования.

List<Widget> coloredBoxes = List.generate(
    10,
    (_) => RandomColoredBox(
      key: GlobalKey(),
    ),
  );

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

Как сделать лучше?

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

List<Widget> coloredBoxes = List.generate(
    10,
    (_) => RandomColoredBox(
      key: UniqueKey(),
    ),
  );

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

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

Полезные ссылки:

Комментарии (1)


  1. mrDevGo
    05.11.2024 11:23

    Спасибо, интересная статья. Особенно для новичков. 
    Также хотелось бы добавить про такой антитренд, когда разработчики повсеместно внедряют стейт-менеджеры (Bloc, Riverpod и другие). Хотя было бы проще и понятнее использовать ChangeNotifier/ValueNotifier/InheritedWidget.