Flutter: Where to add listeners in StatelessWidget?
Asked Answered
R

5

25

If I were using a StatefulWidget, then I would be listening to a Stream for example inside the initState method. Where would I do the equivalent in a StatelessWidget (like to use Bloc with streams for state management)? I could do it in the build method but since these are repetitively I wondered if there is a more efficient way than checking for existent listeners like below. I know that this is a redundant and useless example but it's just to show the problem.

    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()
                  ),
                ),
              )
            ),
          )
        );
      }
    }
Repentant answered 21/2, 2019 at 1:36 Comment(1)
A stateless widget is immutable, so why would it have to notify anything of any change, since no change is supposed to occur within its tree? If you consider official documentation as being the "best practice", just follow it and use StatefulWidget, provider/consumer, InheritedWidget, etc. StatufulWidget is like 4 lines more code, plus you just have to move your build code into the associated State<T> class.Cargill
S
19

There is no clean way to have a StatelessWidget listen to a Listenable/Stream. You will always need a StatefulWidget.

On the other hand, you can use composition to write that StatefulWidget just once, and be done with it.

Common examples for that pattern are widgets such as ValueListenableBuilder, StreamBuilder, or AnimatedBuilder. But it is possible to do the same thing, for listening too.

You'd use it this way:

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

Where ValueListenableListener is implemented this way:

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;
  }
}
Scheider answered 20/6, 2019 at 11:5 Comment(1)
Nice to see one of the flutter overlords joins the discussion. As always, many thanks for the great infos!Tautologize
A
11

You shouldn't. Not handling variables that might have their values modified is the very purpose of a Stateless widget:

A stateless widget never changes.

UPDATE: I think this is a problem of understanding Flutter's state management concepts. This new recommended way by the Flutter team should clear some confusions.

Anaplasty answered 1/3, 2019 at 16:15 Comment(1)
Thank you for your answer. What's wrong with using StreamBuilders in StatelessWidgets? I know a "stateless" widget is supposed to not have its state changed, but this way you write way less code, avoiding StatefulWidgets. I can't see how that would lead to problems and I've seen a lot of people do that.Repentant
A
2

You could do something like this:

class ExampleWidget extends StatelessWidget {
  bool _initialized = false;

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

    return Container();
  }
}

But you shouldn't! In fact, your IDE will give you a warning, because this is not the way to go with Stateless widget as it is marked as @immutable. If you need to use lifecycle methods (like initState()) you should make it a Stateful widget. There's no big deal.

Agulhas answered 17/6, 2019 at 16:30 Comment(1)
I don't see the point here, once initialized is set to true, the widget will not get rendered again! initialized is always trueBim
D
2

This is achievable with flutter_bloc package. The code to be run in initstate can be added inside BlocListener on whatever state you want.

BlocProvider(
    create: (BuildContext context) =>
        CategoryBlock()..add(LoadCategories()),
    child: BlocListener<CategoryBlock, CategoryStates>(
        listener: (context, state) {
         
         
          //Example to add a listener for listview

          if (state is LoadCategoriesSuccess) {
        itemPositionsListener.itemPositions.addListener(() {
          print(itemPositionsListener.itemPositions.value);
        });
      }
    }
Dybbuk answered 5/7, 2020 at 8:47 Comment(0)
M
0

You could have your streams being instantiated in a StatefulWidget and then passed down to your StatelessWidgets as an option, so the parent widget would only have a role of controlling the lifecycle of the stream while the child would be using the stream to update the view.

Regarding the earlier answer: There's no problem in using StreamBuilders inside your StatelessWidgets since the StreamBuilder itself is a a Widget that extends from StatefulWidget and will take care of it's own state and dispose correctly on its own.

Mayence answered 17/6, 2019 at 5:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.