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

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

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

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

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

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

7 ответов

Решение

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

  • Route 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
      },
    );
  }
}

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

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

Самый простой способ - использовать дартс 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 и все * переходы такие как AlignTransition,

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

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

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

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

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

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

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,
  ),
);

Один из самых простых способов избежать нежелательных повторных сборок , которые обычно вызываются вызовом для обновления только определенного виджета, а не обновления всей страницы, - это вырезать эту часть вашего кода и обернуть ее как независимый Widget в другой Statefulучебный класс.
Например, в следующем коде метод родительской страницы вызывается снова и снова нажатием кнопки 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)
    );
  }
}

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

  • Маршрут поп / толчок

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

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

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

Вы можете сделать что-то вроде этого:

        class Example extends StatefulWidget {
      @override
      _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
      Future<int> future;
    
      @override
      void initState() {
        future = httpCall();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder(
          future: future,
          builder: (context, snapshot) {
            // create some layout here
          },
        );
      }
    
    
     void refresh(){
      setState((){
       future = httpCall();
       });
    }

  }

ПРОСТОЙ СПОСОБ : используйте флаг, чтобы предотвратить двойной вызов, например. :ValueNotifier...

Сосредоточиться наbooleanсо значением:true | falseвexactly time, чтобы избежать звонкаbuild()метод дважды.

См. существующий пример кода ниже:

Другие вопросы по тегам