skip to Main Content

I need to implement a stopwatch that is controlled by a FloatingActionButton. However, even when I declare the stopwatch class statically, I am unable to access the SetState() member function at all, which makes it impossible to update the start/stop status of the stopwatch externally.

Minimal example:

import 'package:flutter/material.dart';
import 'dart:async';

void main() {
  runApp(
    app(),
  );
}

class StopWatch extends StatefulWidget {
  const StopWatch({super.key});

  @override
  State<StopWatch> createState() => _StopWatchState();
}

class _StopWatchState extends State<StopWatch> {
  static final Stopwatch _stopwatch = Stopwatch();
  late Timer _timer;
  String _result = "0";

  void Start() {
    _timer = Timer.periodic(
      const Duration(milliseconds: 1),
      (Timer t) {
        setState(
          () {
            _result = _stopwatch.elapsed.inSeconds.toString();
          },
        );
      },
    );
    _stopwatch.start();
  }

  void Stop() {
    _timer.cancel();
    _stopwatch.stop();
  }

  @override
  Widget build(BuildContext context) {
    return Text(_result);
  }
}

class app extends StatelessWidget {
  app({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SingleChildScrollView(
        child: Scaffold(
          body: StopWatch(),
          floatingActionButton: FloatingActionButton.small(
            onPressed: () {},
          ),
        ),
      ),
    );
  }
}

I’ve tried looking at event listeners, but they don’t seem to be the correct tool for the job.

The current state of the app makes it unfeasible to abstract out the clock logic, and only use the stateful widget to update the text.

I’ve also attempted to look for a way to include the FloatingActionButton within the StopWatch class, but there doesn’t seem to be a way to do that such that it’s placed within the outermost ‘scope’ of the heirarchy, i.e. the MaterialApp, let alone put it within the Scaffold class.

2

Answers


  1. Actually, you need a state management tool to properly handle this. But, well, you can do it without depending on other state management tools but just setState and using a Key to identify your object.

    If that’s what you’re looking for, check the following code:

    import 'package:flutter/material.dart';
    import 'dart:async';
    
    void main() {
      runApp(App());
    }
    
    class StopwatchScreen extends StatefulWidget {
      const StopwatchScreen({super.key});
    
      @override
      State<StopwatchScreen> createState() => _StopwatchScreenState();
    }
    
    class _StopwatchScreenState extends State<StopwatchScreen> {
      static final Stopwatch _stopwatch = Stopwatch();
      late Timer _timer;
      String _result = "0";
    
      void start() {
        _timer = Timer.periodic(
          const Duration(milliseconds: 1),
          (Timer t) {
            setState(
              () {
                _result = _stopwatch.elapsed.inSeconds.toString();
              },
            );
          },
        );
        _stopwatch.start();
      }
    
      void stop() {
        _timer.cancel();
        _stopwatch.stop();
      }
    
      bool isRunning() {
        return _stopwatch.isRunning;
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text(_result),
        );
      }
    }
    
    class App extends StatelessWidget {
      App({super.key});
    
      final stopwatchKey = GlobalKey<_StopwatchScreenState>();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: StopwatchScreen(
              key: stopwatchKey,
            ),
            floatingActionButton: FloatingActionButton.small(
              child: const Icon(Icons.play_arrow),
              onPressed: () {
                final state = stopwatchKey.currentState;
                if (state == null) return;
                if (state.isRunning()) {
                  state.stop();
                } else {
                  state.start();
                }
              },
            ),
          ),
        );
      }
    }
    

    Note that I’ve added a isRunning method to expose the state of the Timer – whether the stop watch is currently running or not.

    But, the main thing here is the usage of GlobalKey to access the current state of the child widget.

    Login or Signup to reply.
  2. setState should NOT be accessed from outside a widget itself, and it’s part of the hidden state class of StatefulWidget’s.

    You could take a similar approach as what it is done with TextField and have a controller which allows you to listen and modify the internal state of your timer.

    class StopWatchController extends ValueNotifier<int> {
      StopWatchController() : super(0);
    
      /// The underlying [Timer].
      Timer? _timer;
    
      /// Flag to check a timer is running.
      bool get running => _timer != null;
    
      void start() {
        value = 0;
        _timer = Timer.periodic(
          const Duration(milliseconds: 1),
          (_) => value = value + 1,
        );
      }
    
      void stop() {
        value = 0;
        _timer?.cancel();
        _timer = null;
      }
    
      @override
      void dispose() {
        _timer?.cancel();
        super.dispose();
      }
    }
    

    Then any widget can listen to changes emitted by that controller:

    class App extends StatefulWidget {
      const App({super.key});
    
      @override
      State<App> createState() => _AppState();
    }
    
    class _AppState extends State<App> {
      final StopWatchController _controller = StopWatchController();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Stopwath Demo',
          home: Scaffold(
            body: StopWatch(controller: _controller),
            floatingActionButton: FloatingActionButton.small(
              child: ListenableBuilder(
                listenable: _controller,
                builder: (ctx, _) => Icon(_controller.running ? Icons.stop : Icons.play_arrow),
              ),
              onPressed: () {
                if (_controller.running) {
                  _controller.stop();
                } else {
                  _controller.start();
                }
              },
            ),
          ),
        );
      }
    }
    
    class StopWatch extends StatelessWidget {
      const StopWatch({super.key, required this.controller});
    
      final StopWatchController controller;
    
      @override
      Widget build(BuildContext context) {
        return ListenableBuilder(
          listenable: controller,
          builder: (ctx, _) {
            return Center(
              child: Text(controller.value.toString()),
            );
          },
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search