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

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

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

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

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

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

Ответ 1

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

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

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


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

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

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

В случае вашего примера вы бы преобразовали свой виджет в StatefulWidget затем initState этот 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
      },
    );
  }
}

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

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

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

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

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

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

@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 и все * Transition, такие как AlignTransition.

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

Ответ 2

https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html

Будущее должно быть получено ранее, например, во время State.initState, State.didUpdateConfig или State.didChangeDependencies. Его нельзя создавать во время вызова метода State.build или StatelessWidget.build при создании FutureBuilder. Если будущее создается одновременно с FutureBuilder, то каждый раз, когда родитель FutureBuilder перестраивается, асинхронная задача перезапускается.

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

Закрытие работает как задумано.

Пожалуйста, подумайте о том, чтобы задавать вопросы поддержки по одному из других каналов, перечисленных по адресу http://flutter.io/support.