Есть много вопросов, связанных с тем, как мы можем улучшить производительность нашего Flutter приложения. Необходимо сразу уточнить, что Flutter производительный по умолчанию, но мы должны избегать некоторых ошибок при написании кода, чтобы приложение работало хорошо и быстро. Ниже я подготовил для вас ряд советов и подсказок, как можно писать, чтобы не приходилось постоянно обращаться к инструментам профилирования.
Когда у нас есть сложное представление, чтобы реализовать в один виджет, то обычно мы разделяем его на виджеты поменьше, которые помещаем в методы класса.
В следующем примере представлен виджет, содержащий заголовок, основной контент и "подвал" (footer).
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(),
],
),
),
);
}
}То, что мы увидели выше, – это антипаттерн. Почему так? Всё потому, что, когда мы вносим изменения и обновляем MyHomePage виджет, то виджеты, которые у нас вынесены методах, также обновляются, даже если в этом нет никакой необходимости.
Чтобы не было лишних вычислений, мы можем переделать эти методы в StatelessWidget следующим образом.
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 виджетов есть специальный механизм "кэширования", учитывающий ключ, тип виджета и его атрибуты, который позволяет не перестраивать виджет без необходимости. Кроме того, это помогает нам инкапсулировать и рефакторировать наши виджеты. (Разделяй и властвуй)
И было бы неплохо добавить const в наши виджеты. Позже мы увидим, почему это важно.
Это обычная ошибка, которую многие совершают, когда начинают использовать Flutter и впервые сталкиваются с необходимостью обновить StatefulWidget с помощью setState.
Следующий пример - это представление, содержащее квадрат в центре и FloatingActionButton кнопку, при каждом нажатии на которую вызывается изменение цвета. О, а еще на странице также есть виджет с фоновым изображением. Кроме того, мы добавим несколько print операторов внутри build метода каждого виджета, чтобы посмотреть, как он работает.
class _MyHomePageState extends State<MyHomePage> {
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,
);
}
}После клика по кнопке в консоли мы увидим два вывода
flutter: building `MyHomePage`
flutter: building `BackgroundWidget`
Каждый раз, когда мы нажимаем кнопку, мы обновляем весь экран: Scaffold, BackgroundWidget и, наконец, то, что и хотели обновить, – квадрат-Container.
Как мы уже выяснили выше, перестраивать виджеты без необходимости – нехорошая практика. Обновляем только то, что нам нужно. Многие знают, что это можно сделать с помощью различных пакетов управления состоянием: flutter_bloc, mobx, provider и т. д. Но мало кто знает, что также это можно сделать с помощью классов, которые Flutter уже предлагает из коробки, без каких-либо сторонных библиотек.
Давайте, рассмотрим тот же пример, но обновленный с помощью ValueNotifier.
class _MyHomePageState extends State<MyHomePage> {
final _colorNotifier = ValueNotifier<Color>(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).
Но есть еще один интересный виджет, который мы также можем использовать в этом случае, если мы хотим дополнительно разделить бизнес-логику и представление (но, возможно, добавив немного логики в него) и обрабатывать больше данных в вне виджета (в уведомителе).
Снова тот же пример, но уже с ChangeNotifier.
//------ 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<MyHomePage> {
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(https://habr.com/ru/post/501804/)).
Давайте ещё раз используем наш пример с setState, но в этот раз мы добавим счетчик, который будет увеличивать значение каждый раз на 1, когда мы нажимаем кнопку.
class _MyHomePageState extends State<MyHomePage> {
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,
);
}
}

