Флаттер: где добавить слушателей в StatelessWidget?

Если бы я использовал StatefulWidget, то я бы слушал поток, например, внутри метода initState. Где бы я сделал эквивалент в StatelessWidget (хотел бы использовать Bloc с потоками для управления состоянием)? Я мог бы сделать это в методе сборки, но так как они повторяются, я задавался вопросом, существует ли более эффективный способ, чем проверка существующих слушателей, как показано ниже. Я знаю, что это избыточный и бесполезный пример, но он просто показывает проблему.

    import "package:rxdart/rxdart.dart";

    import 'package:flutter/material.dart';

    final counter = BehaviorSubject<int>();
    final notifier = ValueNotifier<int>(0);

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

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        if (!counter.hasListener)
          counter.listen((value) => notifier.value += value);  

        return MaterialApp(
          home: Scaffold(
            body: Center(
              child:FlatButton(
                onPressed: () => counter.add(1),
                child: ValueListenableBuilder(
                  valueListenable: notifier,
                  builder: (context, value, child) => Text(
                    value.toString()
                  ),
                ),
              )
            ),
          )
        );
      }
    }

Ответ 1

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

Виджет без состояния никогда не меняется.

ОБНОВЛЕНИЕ: Я думаю, что это проблема понимания концепций управления состоянием Flutter. Этот новый рекомендуемый способ командой Flutter должен устранить некоторые недоразумения.

Ответ 2

Не существует чистого способа прослушивания StatelessWidget прослушиваемого/потока. Вам всегда понадобится StatefulWidget.

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

Типичными примерами для этого шаблона являются виджеты, такие как ValueListenableBuilder, StreamBuilder или AnimatedBuilder. Но можно сделать то же самое и для прослушивания.

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

class Foo extends StatelessWidget {
  Foo({Key key, this.counter}): super(key: key);

  final ValueListenable<int> counter;

  @override
  Widget build(BuildContext context) {
    return ValueListenableListener(
      valueListenable: counter,
      onChange: (value) {
        // TODO: do something
      },
      child: Something(),
    );
  }
}

Где ValueListenableListener реализован следующим образом:

class ValueListenableListener<T> extends StatefulWidget {
  const ValueListenableListener(
      {Key key, this.valueListenable, this.onChange, this.child})
      : super(key: key);

  final ValueListenable<T> valueListenable;
  final ValueChanged<T> onChange;
  final Widget child;

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

class _ValueListenableListenerState extends State<ValueListenableListener> {
  @override
  void initState() {
    super.initState();
    widget.valueListenable?.addListener(_listener);
    _listener();
  }

  @override
  void didUpdateWidget(ValueListenableListener oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.valueListenable != widget.valueListenable) {
      oldWidget.valueListenable?.removeListener(_listener);
      widget.valueListenable?.addListener(_listener);
      _listener();
    }
  }

  @override
  void dispose() {
    widget.valueListenable?.removeListener(_listener);
    super.dispose();
  }

  void _listener() {
    widget.onChange?.call(widget.valueListenable.value);
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

Ответ 3

просто добавляя к тому, что @Willy уже упоминало, я еще не сталкивался с кодом, который слушает поток внутри StatelessWidget, но я думаю, что это анти-паттерн, которого вам следует избегать.

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

import "package:rxdart/rxdart.dart";

import 'package:flutter/material.dart';

final counter = BehaviorSubject<int>();
final notifier = ValueNotifier<int>(0);

void main() => runApp(ValueListenableBuilder(
                  valueListenable: notifier,
                  builder: (context, value, child) => MyApp(value),
                ));

class MyApp extends StatelessWidget {
final int value;
MyApp(this.value);
  @override
  Widget build(BuildContext context) {
    if (!counter.hasListener)
      counter.listen((value) => notifier.value += value);  

    return MaterialApp(
      home: Scaffold(
        body: Center(
          child:FlatButton(
            onPressed: () => counter.add(1),
            child: Text(
                value.toString()
              ),
          )
        ),
      )
    );
  }
}

Ответ 4

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

Относительно предыдущего ответа: нет никаких проблем в использовании StreamBuilders внутри ваших StatelessWidgets, так как сам StreamBuilder является виджетом, который выходит из StatefulWidget и будет заботиться о своем собственном состоянии и корректно распоряжаться самостоятельно.

Ответ 5

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

class ExampleWidget extends StatelessWidget {
  bool _initialized = false;

  @override
  Widget build(BuildContext context) {
    if (!_initialized) {
      _initialized = true;
      // Add listeners here only once
    }

    return Container();
  }
}

Но вы не должны! Фактически, ваша IDE выдаст вам предупреждение, потому что это не тот способ, которым нужно использовать виджет без @immutable состояния, поскольку он помечен как @immutable. Если вам нужно использовать методы жизненного цикла (например, initState()), вы должны сделать это виджетом с initState() состояния. Там нет ничего сложного.

Ответ 6

Попробуйте bloc_provider (не путать с пакетом провайдера)

Я пробовал bloc, bloc_provider и провайдера. Я нашел пакет bloc_provider, наиболее подходящий для дизайна моего приложения

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

Я использую комбинацию унаследованного виджета и bloc_provider

РЕДАКТИРОВАТЬ

ПРИМЕР с использованием пакета bloc_prvider

class FormReadyStateTransmitter extends Bloc<bool, bool>
{
    @override
    bool get initialState => false;

    @override
    Stream<bool> mapEventToState(bool event) async*
    {
        yield event;
    }
}


class FormController
{
    final FormReadyStateTransmitter _readyStateTransmitter = FormReadyStateTransmitter() ;

    Widget build()
    {
        return BlocProviderTree
        (
            blocProviders:
            [
                BlocProvider<FormReadyStateTransmitter>( bloc: _readyStateTransmitter ),
            ],

            child: FormWidget( button: MyDynamicButton() )
        );
    }


    void onFormReady()
    {
        _readyStateTransmitter.dispatch(true);
    }
}


class MyDynamicButton extends StatelessWidget
{
    @override
    Widget build(BuildContext context)
    {
        final FormReadyStateTransmitter blockReadyState = BlocProvider.of<FormReadyStateTransmitter>(  context );

        return BlocBuilder
        (
            bloc: blockReadyState,
            builder: (context, bool ifReady  )
            {
                return Text( ifReady.toString() );
            }
        );
    }
}

дочерний виджет будет перестраивать (только если состояние будет изменено) при каждом вызове

 _readyStateTransmitter.dispatch(bool);