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


144

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

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

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

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

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



6
В документации поставщика вы ссылаетесь здесь, говоря: «См. Этот ответ stackoverflow, в котором более подробно объясняется, почему использование конструктора .value для создания значений нежелательно». Однако вы не упоминаете конструктор значения здесь или в своем ответе. Вы хотели добавить ссылку где-нибудь еще?
Сурагч

Ответы:


230

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

  • Маршрут поп / толчок
  • Изменение размера экрана, обычно из-за изменения внешнего вида клавиатуры или ориентации
  • Родительский виджет воссоздал своего дочернего элемента
  • 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 намеренно не восстанавливает детей. Это означает, что вы можете кэшировать части своего дерева виджетов, чтобы предотвратить ненужные перестроения.

Самый простой способ - использовать 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в поле своего класса, хотя это менее рекомендуется, поскольку это нарушает функцию горячей перезагрузки.


2
Не могли бы вы объяснить, почему сохранение subtreeв поле класса прерывает горячую перезагрузку?
Мишель Файнштейн

4
Проблема, с которой я столкнулся, StreamBuilderзаключается в том, что при появлении клавиатуры экран меняется, поэтому маршруты необходимо перестраивать. Итак StreamBuilder, перестраивается, создается новый StreamBuilderи подписывается на stream. Когда a StreamBuilderподписывается на a stream, snapshot.connectionStateстановится, ConnectionState.waitingчто заставляет мой код возвращать a CircularProgressIndicator, а затем snapshot.connectionStateизменяется, когда есть данные, и мой код возвращает другой виджет, который заставляет экран мерцать разными вещами.
Мишель Файнштейн

1
Я решил сделать StatefulWidget, подписаться streamна initState()и установить currentWidgetс, setState()поскольку он streamотправляет новые данные, передавая currentWidgetих build()методу. Есть ли лучшее решение?
Мишель Файнштейн

1
Я немного запутался. Вы отвечаете на свой вопрос, но по содержанию это не похоже.
sgon00

8
Утверждение, что сборка не должна вызывать HTTP-метод, полностью опровергает очень практический пример FutureBuilder.
TheGeekZn

6

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

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

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

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

  • После вызова initState
  • После вызова didUpdateWidget
  • когда вызывается setState () .
  • когда клавиатура открыта
  • когда ориентация экрана изменилась
  • Родительский виджет создается, затем дочерний виджет также перестраивается

0

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,
  ),
);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.