В чем разница между функциями и классами для создания виджетов?

Я понял, что можно создавать виджеты, используя простые функции вместо создания подклассов StatelessWidget. Пример будет такой:

Widget function({ String title, VoidCallback callback }) {
  return GestureDetector(
    onTap: callback,
    child: // some widget
  );
}

Это интересно, потому что требует гораздо меньше кода, чем полноценный класс. Пример:

class SomeWidget extends StatelessWidget {
  final VoidCallback callback;
  final String title;

  const SomeWidget({Key key, this.callback, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
      return GestureDetector(
        onTap: callback,
        child: // some widget
      );
  }
}

Итак, мне было интересно: есть ли разница, кроме синтаксиса между функциями и классами для создания виджетов? И это хорошая практика для использования функций?

Ответ 1

TL; DR: никогда не используйте функции над классами для создания многократно используемого дерева виджетов. Вместо этого всегда извлекайте их в StatelessWidget.


Существует огромная разница между использованием функций вместо классов, а именно: фреймворк не знает функций, но может видеть классы.

Рассмотрим следующую функцию "виджет":

Widget functionWidget({ Widget child}) {
  return Container(child: child);
}

использовал этот способ:

functionWidget(
  child: functionWidget(),
);

И это эквивалент класса:

class ClassWidget extends StatelessWidget {
  final Widget child;

  const ClassWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: child,
    );
  }
}

используется так:

new ClassWidget(
  child: new ClassWidget(),
);

На бумаге оба, кажется, делают одно и то же: создайте 2 Container, один вложенный в другой. Но реальность немного другая.

В случае функций сгенерированное дерево виджетов выглядит так:

Container
  Container

В то время как с классами, дерево виджетов:

ClassWidget
  Container
    ClassWidget
      Container

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

  1. Классы:

    • разрешить оптимизацию производительности (конструктор const, оператор == переопределение, более детальная перестройка)
    • иметь горячую перезагрузку
    • интегрированы в инспектор виджетов (debugFillProperties)
    • можно определить ключи
    • можно использовать контекстный API
    • убедитесь, что все виджеты используются одинаково (всегда конструктор)
    • убедитесь, что переключение между двумя различными макетами корректно избавляет от ресурсов (функции могут повторно использовать некоторое предыдущее состояние)
  2. Функции:

    • у меня меньше кода (и даже там, я сделал генератор кода, чтобы сделать классы такими же маленькими, как функции: functions_widget)
    • ?

Вывод должен быть уже достаточно ясным:

Не используйте функции для создания виджетов.

Ответ 2

Когда вы вызываете виджет Flutter, убедитесь, что вы используете ключевое слово const. Например, const MyListWidget();

Ответ 3

Я изучал эту проблему в течение последних 2 дней. Я пришел к следующему выводу: можно разбивать части приложения на функции. Это просто идеально, что эти функции возвращают StatelessWidget, так Оптимизации могут быть сделаны, например, делая StatelessWidget const, поэтому он не перестраивать, если он не должен. Например, этот кусок кода является абсолютно допустимым:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    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:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            const MyWidgetClass(key: const Key('const')),
            MyWidgetClass(key: Key('non-const')),
            _buildSomeWidgets(_counter),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget _buildSomeWidgets(int val) {
    print('${DateTime.now()} Rebuild _buildSomeWidgets');
    return const MyWidgetClass(key: Key('function'));

    // This is bad, because it would rebuild this every time
    // return Container(
    //   child: Text("hi"),
    // );
  }
}

class MyWidgetClass extends StatelessWidget {
  const MyWidgetClass({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('${DateTime.now()} Rebuild MyWidgetClass $key');

    return Container(
      child: Text("hi"),
    );
  }
}

Использование функции там прекрасно, так как она возвращает const StatelessWidget. Пожалуйста, поправьте меня, если я ошибаюсь.