Как бороться с нежелательной сборкой виджетов?

По разным причинам иногда метод build моих виджетов вызывается снова.

Я знаю, что это происходит из-за обновления родителя. Но это вызывает нежелательные эффекты. Типичная ситуация, когда это вызывает проблемы, - это использование FutureBuilder таким образом:

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: httpCall(),
    builder: (context, snapshot) {
      // create some layout here
    },
  );
}

В этом примере, если бы метод build был вызван снова, он инициировал бы другой HTTP-запрос. Что нежелательно.

Учитывая это, как бороться с нежелательной сборкой? Есть ли способ предотвратить вызов сборки?


person Rémi Rousselet    schedule 10.09.2018    source источник
comment
этот пост может вам помочь .. https://stackoverflow.com/questions/53223469/flutter-statelesswidget-build-called-multiple-times/55626839#55626839   -  person bunny    schedule 11.04.2019
comment
В документации поставщика вы ссылаетесь здесь, говоря, что См. Этот ответ stackoverflow, в котором более подробно объясняется, почему использование конструктор .value для создания значений нежелателен. Однако вы не упоминаете конструктор значения здесь или в своем ответе. Вы хотели связать где-нибудь еще?   -  person Suragch    schedule 18.05.2020
comment
@Suragch, это правильная ссылка. Проблема не связана с поставщиком, а проблема с конструктором .value идентична описанной здесь. То есть замените FutureBuilder на SomeProvider.value   -  person Rémi Rousselet    schedule 23.08.2020
comment
Я бы рекомендовал либо объяснять нежелательные побочные эффекты непосредственно в документации (первый вариант), либо добавить дополнительные объяснения здесь (второй вариант). Я не знаю, представляю ли я среднего пользователя провайдера или нет, но когда я прихожу сюда, я все еще не понимаю взаимосвязи между использованием .value и нежелательной сборкой виджета или build методом, который должен быть чистым.   -  person Suragch    schedule 24.08.2020
comment
Почему бы вам не попробовать назначить ключи   -  person Tanmay Kumar    schedule 22.05.2021


Ответы (5)


Метод build разработан таким образом, что он должен быть чистым / без побочных эффектов. Это связано с тем, что многие внешние факторы могут вызвать создание нового виджета, например:

  • Маршрут pop / push
  • Изменение размера экрана, обычно из-за изменения внешнего вида клавиатуры или ориентации
  • Родительский виджет воссоздал своего дочернего элемента
  • InheritedWidget, от которого зависит виджет (Class.of(context) шаблон).

Это означает, что метод build не должен не вызывать HTTP-вызов или изменять какое-либо состояние.


Как это связано с вопросом?

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

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

В случае вашего примера вы должны преобразовать свой виджет в StatefulWidget, а затем извлечь этот HTTP-вызов в initState вашего State:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<int> future;

  @override
  void initState() {
    future = Future.value(42);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        // create some layout here
      },
    );
  }
}

Я это уже знаю. Я пришел сюда, потому что очень хочу оптимизировать перестройки

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

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

Самый простой способ - использовать конструкторы dart const:

@override
Widget build(BuildContext context) {
  return const DecoratedBox(
    decoration: BoxDecoration(),
    child: Text("Hello World"),
  );
}

Благодаря этому ключевому слову const экземпляр DecoratedBox останется прежним, даже если сборка вызывалась сотни раз.

Но вы можете добиться того же результата вручную:

@override
Widget build(BuildContext context) {
  final subtree = MyWidget(
    child: Text("Hello World")
  );

  return StreamBuilder<String>(
    stream: stream,
    initialData: "Foo",
    builder: (context, snapshot) {
      return Column(
        children: <Widget>[
          Text(snapshot.data),
          subtree,
        ],
      );
    },
  );
}

В этом примере, когда StreamBuilder получает уведомление о новых значениях, subtree не будет перестраиваться, даже если StreamBuilder / Column это сделает. Это происходит потому, что из-за закрытия экземпляр MyWidget не изменился.

Этот паттерн часто используется в анимации. Типичное использование - AnimatedBuilder и все переходы, такие как AlignTransition.

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

person Rémi Rousselet    schedule 10.09.2018
comment
Не могли бы вы объяснить, почему сохранение subtree в поле класса прерывает горячую перезагрузку? - person Michel Feinstein; 14.03.2019
comment
Проблема, с которой я столкнулся с StreamBuilder, заключается в том, что при появлении клавиатуры экран меняется, поэтому маршруты необходимо перестраивать. Итак, StreamBuilder перестраивается и создается новый StreamBuilder, который подписывается на stream. Когда StreamBuilder подписывается на stream, snapshot.connectionState становится ConnectionState.waiting, что заставляет мой код возвращать CircularProgressIndicator, а затем snapshot.connectionState изменяется, когда есть данные, и мой код вернет другой виджет, что заставляет экран мерцать разными вещами. - person Michel Feinstein; 14.03.2019
comment
Единственное решение - поместить StreamBuilder во внешнюю переменную? - person Michel Feinstein; 14.03.2019
comment
Я решил сделать StatefulWidget, подписаться на stream на initState() и установить currentWidget на setState(), поскольку stream отправляет новые данные, передавая currentWidget методу build(). Есть ли лучшее решение? - person Michel Feinstein; 14.03.2019
comment
Я немного запутался. Вы отвечаете на свой вопрос, однако по содержанию это не похоже. - person sgon00; 15.03.2019
comment
@mFeinstein, вы можете использовать шаблон Bloc, чтобы раскрыть поток как объект поведения, кэшируя повторное использование последнего значения для следующей подписки - person Vairavan; 18.03.2019
comment
sgon00 Я спрашивал, но через некоторое время я сам нашел ответ, просто не уверен, что он лучший. @Vairavan проблема в том, что еще не отправлено значение и нет начального значения для BehaviorSubject, которое имеет смысл. Например, если вы хотите контролировать, вошел ли пользователь в систему или нет, покажите экран входа в систему, если пользователя нет, главный экран, если есть пользователь, и экран загрузки, если мы все еще загружаем информацию, если есть пользователь или нет, что и делает информация snapshot.connectionState. - person Michel Feinstein; 19.03.2019
comment
@Vairavan, но даже с BevaiourSubject StreamBuilder запускал бы единственный ожидающий кадр при перестроении виджета. Застрял на этом, любая помощь приветствуется. - person Brixto; 20.03.2019
comment
@Brixto Можете ли вы поделиться проектом через github? - person Vairavan; 20.03.2019
comment
Утверждение, что сборка не должна вызывать HTTP-метод, полностью опровергает очень практический пример FutureBuilder. - person TheGeekZn; 17.06.2019
comment
Природа виджетов с отслеживанием состояния заключается в том, что управление состояниями и HTTP-вызовы являются асинхронными, поэтому могут иметь несколько состояний. Или вы создаете перенос своего http-вызова в какой-то кеш, чтобы избежать вызова при будущей перестройке ... или используйте initState () для хранения будущего значения ... вы все равно будете использовать FutureBuilder, если вы хочу управлять самим собой Future и позже вызвать setState (). Это очень хороший момент, чтобы избежать ненужных перестроек и отделить логику от пользовательского интерфейса. - person roipeker; 29.06.2019
comment
Хорошо, но допустим, у меня есть 3 вкладки. Если я перейду с первой вкладки на третью, вызывается второй метод построения вкладки, за которым следует удаление. Но если сборка ожидала ответа (скажем, ChangeNotifierProvider) от модели, она выдаст ошибку, когда вызывается notifyListeners () и ответ поступает на вторую вкладку (после удаления использовалась «модель». После того, как вы вызвали dispose () для «модели», ее больше нельзя использовать.) - person Dpedrinha; 14.09.2019
comment
Так что, если вы хотите, чтобы при ошибке отображалось диалоговое окно, а затем кнопка в диалоговом окне, которая перенаправляет на другую страницу? Как ты можешь это сделать? - person Chrillewoodz; 20.12.2019
comment
Любой, кто читает вышеупомянутое решение. Также следует увидеть здесь лучший подход (разобраться с обновлением FutureBuilder): medium. ru / saugo360 / - person sagar suri; 27.04.2020
comment
Прочитал статью выше, точно не лучше. Это альтернатива, и я бы сказал, просто вводит ненужную сложность: использование AsyncMemoizer. И что произойдет, если ваша функция Future будет более сложной? Вам нужно создать AsyncMemoizer для каждой функции, которую вы вызываете в функции, генерирующей будущее. - person Ben Butterworth; 21.05.2020
comment
@MichelFeinstein У меня такая же проблема, и это меня раздражает. Я решил создать BehaviorSubjectBuilder поверх RxDart BehaviorSubject, который для этой цели представляет собой поток, последнее значение которого вы можете получить. Он просто обертывает StreamBuilder, но initialData имеет значение subject.value, а поток - subject.stream. Я думаю, что вы можете сделать его еще лучше, заставив встроенный в поток виджет принимать ключ, чтобы он не заменялся без надобности точной копией, но это просто оптимизация. - person Ruben; 22.09.2020
comment
пожалуйста, обновите этот ответ нулевой безопасностью - person Vit Amin; 28.05.2021

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

1) Создайте дочерний класс Statefull для отдельной небольшой части пользовательского интерфейса

2) Используйте библиотеку Provider, чтобы с ее помощью вы могли остановить нежелательный вызов метода сборки

В эти ниже вызов метода построения ситуации

  • После вызова initState
  • После вызова didUpdateWidget
  • когда вызывается setState ().
  • когда клавиатура открыта
  • при изменении ориентации экрана
  • Родительский виджет создается, затем дочерний виджет также перестраивается
person Sanjayrajsinh    schedule 28.01.2020
comment
первая точка мешает последнему. Создайте дочерний класс Statefull для отдельной небольшой части пользовательского интерфейса с родительским виджетом, затем дочерний виджет также перестроится - person Ezzabuzaid; 13.12.2020
comment
Нет, позвольте мне привести пример, 1-й: если у вас есть экран формы регистрации и создайте небольшой дочерний интерфейс для получения BDay, поэтому, когда вы перестраиваете виджет BDay, весь экран формы регистрации не перестраивается Но если вы перестраиваете родительский экран, тогда весь дочерний элемент также перестраивается - person Sanjayrajsinh; 14.12.2020
comment
если кто-то все еще задается вопросом, @Sanjayrajsinh означает, что вы должны создавать небольшие отдельные виджеты с отслеживанием состояния, потому что обновление состояния в них не повлияет на родителя. Если у вас, например, огромные виджеты, каждый setState () будет обновлять все - person eja; 20.12.2020

У Flutter тоже есть ValueListenableBuilder<T> class . Это позволяет вам перестроить только некоторые из виджетов, необходимых для ваших целей, и пропустить дорогостоящие виджеты.

вы можете увидеть здесь документы ValueListenableBuilder flutter docs
или просто пример кода ниже:

  return Scaffold(
  appBar: AppBar(
    title: Text(widget.title)
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        ValueListenableBuilder(
          builder: (BuildContext context, int value, Widget child) {
            // This builder will only get called when the _counter
            // is updated.
            return Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                Text('$value'),
                child,
              ],
            );
          },
          valueListenable: _counter,
          // The child parameter is most helpful if the child is
          // expensive to build and does not depend on the value from
          // the notifier.
          child: goodJob,
        )
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.plus_one),
    onPressed: () => _counter.value += 1,
  ),
);
person Taba    schedule 27.07.2020

Один из самых простых способов избежать нежелательных повторных сборок, которые обычно вызываются вызовом setState() для обновления только определенного виджета, а не обновления всей страницы, - это вырезать эту часть кода и обернуть это как независимый Widget в другом Stateful классе.
Например, в следующем коде метод Build родительской страницы вызывается снова и снова нажатием кнопки FAB:

import 'package:flutter/material.dart';

void main() {
  runApp(TestApp());
}

class TestApp extends StatefulWidget {

  @override
  _TestAppState createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {

  int c = 0;

  @override
  Widget build(BuildContext context) {

    print('build is called');

    return MaterialApp(home: Scaffold(
      appBar: AppBar(
        title: Text('my test app'),
      ),
      body: Center(child:Text('this is a test page')),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          setState(() {
            c++;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.wb_incandescent_outlined, color: (c % 2) == 0 ? Colors.white : Colors.black)
      )
    ));
  }
}

Но если вы отделите виджет FloatingActionButton в другом классе с его собственным жизненным циклом, метод setState() не приведет к повторному запуску метода Build родительского класса:

import 'package:flutter/material.dart';
import 'package:flutter_app_mohsen/widgets/my_widget.dart';

void main() {
  runApp(TestApp());
}

class TestApp extends StatefulWidget {

  @override
  _TestAppState createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {

  int c = 0;

  @override
  Widget build(BuildContext context) {

    print('build is called');

    return MaterialApp(home: Scaffold(
      appBar: AppBar(
        title: Text('my test app'),
      ),
      body: Center(child:Text('this is a test page')),
      floatingActionButton: MyWidget(number: c)
    ));
  }
}

и класс MyWidget:

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {

  int number;
  MyWidget({this.number});

  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
        onPressed: (){
          setState(() {
            widget.number++;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.wb_incandescent_outlined, color: (widget.number % 2) == 0 ? Colors.white : Colors.black)
    );
  }
}

person Mohsen Emami    schedule 24.02.2021
comment
Это сработало для меня, спасибо - person Apps 247; 02.06.2021

Я просто хочу поделиться своим опытом сборки нежелательных виджетов в основном из-за контекста, но я нашел способ, который очень эффективен для

  • Маршрут pop / push

Поэтому вам нужно использовать Navigator.pushReplacement (), чтобы контекст предыдущей страницы не имел отношения к следующей странице.

  1. Используйте Navigator.pushReplacement () для перехода с первой страницы на вторую.
  2. На второй странице нам снова нужно использовать Navigator.pushReplacement (). В appBar мы добавляем -
    leading: IconButton(
            icon: Icon(Icons.arrow_back),
            onPressed: () {
              Navigator.pushReplacement(
                context,
                RightToLeft(page: MyHomePage()),
              );
            },
          )
    

Таким образом мы можем оптимизировать наше приложение

person Rohan    schedule 16.04.2021