Flutter: Where to add listeners in StatelessWidget?

15,855

Solution 1

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;
  }
}

Solution 2

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.

Solution 3

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.

Solution 4

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);
        });
      }
    }
Share:
15,855

Related videos on Youtube

footurist
Author by

footurist

Updated on July 05, 2020

Comments

  • footurist
    footurist almost 4 years

    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()
                      ),
                    ),
                  )
                ),
              )
            );
          }
        }
    
    • Simon Mourier
      Simon Mourier almost 5 years
      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.
  • footurist
    footurist about 5 years
    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.
  • 最白目
    最白目 almost 5 years
    Nice to see one of the flutter overlords joins the discussion. As always, many thanks for the great infos!
  • funder7
    funder7 about 3 years
    I don't see the point here, once initialized is set to true, the widget will not get rendered again! initialized is always true