# Как улучшить производительность вашего Flutter приложения ![](https://habrastorage.org/webt/76/nr/ut/76nrutrkfl7c49c7ifdu9qgb134.png) Есть много вопросов, связанных с тем, как мы можем улучшить производительность нашего Flutter приложения. Необходимо сразу уточнить, что Flutter производительный по умолчанию, но мы должны избегать некоторых ошибок при написании кода, чтобы приложение работало хорошо и быстро. Ниже я подготовил для вас ряд советов и подсказок, как можно писать, чтобы не приходилось постоянно обращаться к инструментам профилирования. ## Не выносите виджеты в методы класса Когда у нас есть сложное представление, чтобы реализовать в один виджет, то обычно мы разделяем его на виджеты поменьше, которые помещаем в методы класса. В следующем примере представлен виджет, содержащий заголовок, основной контент и "подвал" *(footer)*. ```java class MyHomePage extends StatelessWidget { Widget _buildHeaderWidget() { final size = 40.0; return Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( backgroundColor: Colors.grey[700], child: FlutterLogo( size: size, ), radius: size, ), ); } Widget _buildMainWidget(BuildContext context) { return Expanded( child: Container( color: Colors.grey[700], child: Center( child: Text( 'Hello Flutter', style: Theme.of(context).textTheme.display1, ), ), ), ); } Widget _buildFooterWidget() { return Padding( padding: const EdgeInsets.all(8.0), child: Text('This is the footer '), ); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(15.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildHeaderWidget(), _buildMainWidget(context), _buildFooterWidget(), ], ), ), ); } } ``` ![](https://habrastorage.org/webt/e5/fo/gl/e5fogldinutl4jybj3cpd5m5wju.gif) То, что мы увидели выше, – это антипаттерн. Почему так? Всё потому, что, когда мы вносим изменения и обновляем `MyHomePage` виджет, то виджеты, которые у нас вынесены методах, также обновляются, даже если в этом нет никакой необходимости. Чтобы не было лишних вычислений, мы можем переделать эти методы в `StatelessWidget` следующим образом. ```java class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(15.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ HeaderWidget(), MainWidget(), FooterWidget(), ], ), ), ); } } class HeaderWidget extends StatelessWidget { @override Widget build(BuildContext context) { final size = 40.0; return Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( backgroundColor: Colors.grey[700], child: FlutterLogo( size: size, ), radius: size, ), ); } } class MainWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( child: Container( color: Colors.grey[700], child: Center( child: Text( 'Hello Flutter', style: Theme.of(context).textTheme.display1, ), ), ), ); } } class FooterWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: Text('This is the footer '), ); } } ``` У `Stateful`/`Stateless` виджетов есть специальный механизм "кэширования", учитывающий ключ, тип виджета и его атрибуты, который позволяет не перестраивать виджет без необходимости. Кроме того, это помогает нам инкапсулировать и рефакторировать наши виджеты. ([Разделяй и властвуй](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm)) И было бы неплохо добавить `const` в наши виджеты. Позже мы увидим, почему это важно. ## Избегайте обновления всех виджетов Это обычная ошибка, которую многие совершают, когда начинают использовать Flutter и впервые сталкиваются с необходимостью обновить `StatefulWidget` с помощью `setState`. Следующий пример - это представление, содержащее квадрат в центре и `FloatingActionButton` кнопку, при каждом нажатии на которую вызывается изменение цвета. О, а еще на странице также есть виджет с фоновым изображением. Кроме того, мы добавим несколько `print` операторов внутри `build` метода каждого виджета, чтобы посмотреть, как он работает. ```java class _MyHomePageState extends State { Color _currentColor = Colors.grey; Random _random = new Random(); void _onPressed() { int randomNumber = _random.nextInt(30); setState(() { _currentColor = Colors.primaries[randomNumber % Colors.primaries.length]; }); } @override Widget build(BuildContext context) { print('building `MyHomePage`'); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, child: Icon(Icons.colorize), ), body: Stack( children: [ Positioned.fill( child: BackgroundWidget(), ), Center( child: Container( height: 150, width: 150, color: _currentColor, ), ), ], ), ); } } class BackgroundWidget extends StatelessWidget { @override Widget build(BuildContext context) { print('building `BackgroundWidget`'); return Image.network( 'https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569_960_720.jpg', fit: BoxFit.cover, ); } } ``` ![](https://habrastorage.org/webt/yk/0k/-h/yk0k-heypf5gclu8em7abi1jp2y.gif) После клика по кнопке в консоли мы увидим два вывода ``` flutter: building `MyHomePage` flutter: building `BackgroundWidget` ``` Каждый раз, когда мы нажимаем кнопку, мы обновляем весь экран: `Scaffold`, `BackgroundWidget` и, наконец, то, что и хотели обновить, – квадрат-`Container`. Как мы уже выяснили выше, перестраивать виджеты без необходимости – нехорошая практика. Обновляем только то, что нам нужно. Многие знают, что это можно сделать с помощью различных пакетов управления состоянием: [flutter_bloc](https://pub.dev/packages/flutter_bloc), [mobx](https://pub.dev/packages/mobx), [provider](https://pub.dev/packages/provider) и т. д. Но мало кто знает, что также это можно сделать с помощью классов, которые Flutter уже предлагает из коробки, без каких-либо сторонных библиотек. Давайте, рассмотрим тот же пример, но обновленный с помощью `ValueNotifier`. ```java class _MyHomePageState extends State { final _colorNotifier = ValueNotifier(Colors.grey); Random _random = new Random(); void _onPressed() { int randomNumber = _random.nextInt(30); _colorNotifier.value = Colors.primaries[randomNumber % Colors.primaries.length]; } @override void dispose() { _colorNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print('building `MyHomePage`'); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, child: Icon(Icons.colorize), ), body: Stack( children: [ Positioned.fill( child: BackgroundWidget(), ), Center( child: ValueListenableBuilder( valueListenable: _colorNotifier, builder: (_, value, __) => Container( height: 150, width: 150, color: value, ), ), ), ], ), ); } } ``` Кликнем по кнопке и... ничего не увидим в консоли. Это работает, как и ожидалось, мы просто обновляем только тот виджет, который нам нужен *(прим. подробнее про [ValueListenableBuilder и ValueNotifier](https://www.youtube.com/watch?v=s-ZG-jS5QHQ))*. Но есть еще один интересный виджет, который мы также можем использовать в этом случае, если мы хотим дополнительно разделить бизнес-логику и представление (но, возможно, добавив немного логики в него) и обрабатывать больше данных в вне виджета *(в уведомителе)*. Снова тот же пример, но уже с `ChangeNotifier`. ```java //------ ChangeNotifier class ----// class MyColorNotifier extends ChangeNotifier { Color myColor = Colors.grey; Random _random = new Random(); void changeColor() { int randomNumber = _random.nextInt(30); myColor = Colors.primaries[randomNumber % Colors.primaries.length]; notifyListeners(); } } //------ State class ----// class _MyHomePageState extends State { final _colorNotifier = MyColorNotifier(); void _onPressed() { _colorNotifier.changeColor(); } @override void dispose() { _colorNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print('building `MyHomePage`'); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, child: Icon(Icons.colorize), ), body: Stack( children: [ Positioned.fill( child: BackgroundWidget(), ), Center( child: AnimatedBuilder( animation: _colorNotifier, builder: (_, __) => Container( height: 150, width: 150, color: _colorNotifier.myColor, ), ), ), ], ), ); } } ``` Самое прекрасное, что и в этом случае у нас нет ненужных обновлений. ## Используйте `const` Рекомендуется использовать ключевое слово `const` для значений, которые возможно инициализировать во время компиляции, а также при вызове конструктора виджета (если он поддерживает `const`, конечно), что позволяет работать с одним и тем же каноническим экземпляром, тем самым избегая повторных вычислений *(прим. подробнее про [работу const(https://habr.com/ru/post/501804/))*. Давайте ещё раз используем наш пример с `setState`, но в этот раз мы добавим счетчик, который будет увеличивать значение каждый раз на 1, когда мы нажимаем кнопку. ```java class _MyHomePageState extends State { int _counter = 0; void _onPressed() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { print('building `MyHomePage`'); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, child: Icon(Icons.colorize), ), body: Stack( children: [ Positioned.fill( child: BackgroundWidget(), ), Center( child: Text( _counter.toString(), style: Theme.of(context).textTheme.display4.apply( color: Colors.white, fontWeightDelta: 2, ), )), ], ), ); } } class BackgroundWidget extends StatelessWidget { @override Widget build(BuildContext context) { print('building `BackgroundWidget`'); return Image.network( 'https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569_960_720.jpg', fit: BoxFit.cover, ); } } ``` ![](https://habrastorage.org/webt/yl/yf/kg/ylyfkggjsh4yge2qbnsofptsjdu.gif) У нас снова 2 вывода в консоли, один из которых относится к основнову виджету, а другой – к `BackgroundWidget`. Каждый раз, когда мы нажимаем кнопку, мы видим, что дочерний виджет также перестраивается, хотя его содержимое никак не меняется. ``` flutter: building `MyHomePage` flutter: building `BackgroundWidget` ``` А сейчас добавим `const` при работе с виджетом `BackgroundWidget`: ```java class _MyHomePageState extends State { int _counter = 0; void _onPressed() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { print('building `MyHomePage`'); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, child: Icon(Icons.colorize), ), body: Stack( children: [ Positioned.fill( child: const BackgroundWidget(), ), Center( child: Text( _counter.toString(), style: Theme.of(context).textTheme.display4.apply( color: Colors.white, fontWeightDelta: 2, ), )), ], ), ); } } class BackgroundWidget extends StatelessWidget { const BackgroundWidget(); @override Widget build(BuildContext context) { print('building `BackgroundWidget`'); return Image.network( 'https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569_960_720.jpg', fit: BoxFit.cover, ); } } ``` Теперь после клика по кнопке мы видим вывод только для основного виджета (конечно, если использовать те подходы, что я описал выше, избавиться можно и от этого вывода) и избегаем перестроения виджета, вызванного с `const`. ## Используйте `itemExtent` для `ListView` при больших списках Иногда, когда у нас есть очень длинный список, и мы хотим быстро переместиться по нему, например, в самый конец, очень важно использовать `itemExtent`. Давайте рассмотрим простой пример. У нас есть список из 10 тысяч элементов. При нажатии на кнопку мы перейдем к последнему элементу. В этом примере мы не будем использовать `itemExtent` и позволим элементам списка самим определить свой размер. ```java class MyHomePage extends StatelessWidget { final widgets = List.generate( 10000, (index) => Container( height: 200.0, color: Colors.primaries[index % Colors.primaries.length], child: ListTile( title: Text('Index: $index'), ), ), ); final _scrollController = ScrollController(); void _onPressed() async { _scrollController.jumpTo( _scrollController.position.maxScrollExtent, ); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, splashColor: Colors.red, child: Icon(Icons.slow_motion_video), ), body: ListView( controller: _scrollController, children: widgets, ), ); } } ``` ![](https://habrastorage.org/webt/of/fi/h9/offih9w4zrrrkbtj8q1dbh-sys8.gif) Как можно увидеть на анимации выше, переход происходит очень долго (~10 секунд). Так получается из-за того, что дочерние элементы сами определяют свой размер. Это даже блокирует UI! Чтобы избежать этого, мы должны использовать свойство `itemExtent`, благодаря которому при прокрутке не совершается лишней работы по расчету позиции скролла, так как размеры элементов заранее известны. ```java class MyHomePage extends StatelessWidget { final widgets = List.generate( 10000, (index) => Container( color: Colors.primaries[index % Colors.primaries.length], child: ListTile( title: Text('Index: $index'), ), ), ); final _scrollController = ScrollController(); void _onPressed() async { _scrollController.jumpTo( _scrollController.position.maxScrollExtent, ); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, splashColor: Colors.red, child: Icon(Icons.slow_motion_video), ), body: ListView( controller: _scrollController, children: widgets, itemExtent: 200, ), ); } } ``` ![](https://habrastorage.org/webt/pk/le/rt/pklertikfqc-kab5nr4bskx3dke.gif) С этим небольшим изменением мы мгновенно переходим в самый низ без каких-либо задержек. ## Избегайте ненужных перестроений виджетов внутри `AnimatedBuilder` Часто мы хотим добавить анимацию к нашим виджетам. В таких случаях обычно мы просто добавляем слушателя *(addListener)* для нашего `AnimationController` и вызываем `setState`. Но, как мы видели в самом начале, так работает не лучшим образом. Вместо этого мы будем использовать виджет `AnimatedBuilder` для обновления только того виджета, который мы хотим анимировать. Давайте создадим экран, который содержит виджет в центре со значением счетчика и кнопку, при нажатии на которую виджет вращается на 360 градусов. ```java class _MyHomePageState extends State with SingleTickerProviderStateMixin { AnimationController _controller; int counter = 0; void _onPressed() { setState(() { counter++; }); _controller.forward(from: 0.0); } @override void initState() { _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 600)); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _onPressed, splashColor: Colors.red, child: Icon(Icons.slow_motion_video), ), body: AnimatedBuilder( animation: _controller, builder: (_, child) => Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateY(360 * _controller.value * (pi / 180.0)), child: CounterWidget( counter: counter, ), ), ), ); } } class CounterWidget extends StatelessWidget { final int counter; const CounterWidget({Key key, this.counter}) : super(key: key); @override Widget build(BuildContext context) { print('building `CounterWidget`'); return Center( child: Text( counter.toString(), style: Theme.of(context).textTheme.display4.apply(fontWeightDelta: 3), ), ); } } ```