1

Context :

Say I have a custom DisplayElapsedTime Widget which is Stateful and holds a Text Widget. With a Timer, every 1sec, the text value is updated with the current elapsed time given by a Stopwatch.

And now, say I have a page which is Stateless and has the DisplayElapsedTime Widget as a child.

What I would like to do :

  1. On the click of a "Start" button in my page, I would like to start the DisplayElapsedTime (which means starting the Stopwatch and the Timer).

  2. From my page, I would also like to have access to the elapsed time value of the Stopwatch "whenever I want".

Why I am having a hard time :

So far (see: I use Stateless Widget alongside BLoC almost everytime. Am I wrong?), I have almost always worked with Stateless Widget alongside the pattern BLoC and never used Stateful. Currently, having extremely long and complex Widgets, I am starting to sense the "limits" of not using the better of the two worlds. But I don't quite fully understand how the Widgets should be interacting between one another.

I really cannot find the solution to my problem anywhere (or am really bad at searching). However, surely, I cannot be the first person to want to have "control" over a Stateful Widget from a Stateless Widget, right ?

Thank you so much in advance for any help.

George Lee
  • 814
  • 18
  • 34
Myrmillion
  • 78
  • 6

1 Answers1

1

If I understand your question correctly, let me try to explain this using the most familiar app of all time, the beginning counter app.

This snippet contains a single StatefulWidget that controls its ability to rebuild using its setState method _incrementCounter. So, the value is incremented and the widget is rebuilt whenever the StatefulWidget calls the setState method inside itself.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> 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>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

A StatefulWidget can fully rebuild itself, and when doing so, it may also rebuild all its children downstream of it in the widget tree (but not always, as const widgets are not rebuilt). To get another widget to rebuild a parent widget (upstream of it in the widget tree), you need to have that StatefulWidget's setState function. This can be done using a callback function. A callback function is made by the parent widget and passed to a child widget. So, in the following example, I have made a StatelessWidget with a button, which controls its parent widget because it calls its parent's callback function; notice that I give:

ExampleStlessWidget(counter: _counter, fx: _incrementCounter),

and not:

ExampleStlessWidget(counter: _counter, fx: _incrementCounter()),

Passing _incrementCounter() with the parenthesis calls it at the moment it is passed, while _incrementCounter allows it to be called downstream in the widget tree.

Use the callback function in the child widget by calling it anywhere (notice the parentheses).

onPressed: () {
                fx();
              },

Here is the new code

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> 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: ExampleStlessWidget(counter: _counter, fx: _incrementCounter),
    );
  }
}

class ExampleStlessWidget extends StatelessWidget {
  const ExampleStlessWidget({
    super.key,
    required int counter,
    required this.fx,
  }) : _counter = counter;

  final int _counter;
  final Function fx;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ],
          ),
        ),
        ElevatedButton(
          onPressed: () {
            fx();
          },
          child: const Text('Click me'),
        ),
      ],
    );
  }
}

A bloc involves an inherited widget, which allows for monitoring the state throughout the widget tree and rebuilds widgets depending on that state. So, a bloc doesn't need a StatefulWidget to change UI. It would help if you did not look at one tool's ability to rebuild widgets as bad or good. It would be best to look at StatefulWidgets and BLoC as different tools for different jobs.

I hope this helps. Happy coding.

George Lee
  • 814
  • 18
  • 34
  • Thank you very much for taking the time to help me ! I will try what you offered and come back to you as soon as possible ! – Myrmillion Nov 20 '22 at 08:45
  • 2
    If you were to post a viable single-code snippet with what you are trying, we may be able to help you out further. – George Lee Nov 20 '22 at 09:09
  • Yes I realize it is difficult to totally understand what I'm asking without seeing some code. I will try to post it and add further indications regarding what I'm looking for ! Could you just indicate to me what would be the most correct way to do that on Stackoverflow ? Should I "answer to my question", create a new post, or something else ? – Myrmillion Nov 21 '22 at 15:46
  • 2
    i think you could edit your original post by adding more to that post and then leave a message that you have updated the original post. – George Lee Nov 22 '22 at 10:29
  • Really useful answer. It deserves to be the part of official flutter tutorial. – Yuriy N. May 16 '23 at 22:47