Why Flutter timer speed up after restart?

173 views Asked by At

I have a periodic timer and i'm trying to call some methods when it started. But the problem is when i press the inkwell a few times in a row and wait the program 2 seconds for restart the timer. The timer speeds up..

I'm trying to solve it for a while but i doesn't work. I hope it was clear for you to understand. Thanks.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:wordgame/controller.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Material App', home: MyWidget());
  }
}

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

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    startTimer();

    super.initState();
  }

  late Duration duration;
  Timer? timer;

  void startTimer() {
    duration = Duration(seconds: 5);
    myController.selectMainWord();
    myController.selectValueWords();
    myController.selectedIndex = -1;

    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (duration.inSeconds > 0) {
        duration -= Duration(seconds: 1);
        print(duration.inSeconds.toString());
      } else {
        duration = Duration(seconds: 5);
      }

      setState(() {});
    });
  }

  cancelTimer() {
    timer?.cancel();
    timer = null;
  }

  MyController myController = MyController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          Container(
            child: Text(
              myController.mainWord,
              style: TextStyle(fontSize: 36),
            ),
          ),
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                  maxCrossAxisExtent: 200,
                  childAspectRatio: 3 / 2,
                  crossAxisSpacing: 20,
                  mainAxisSpacing: 20),
              itemCount: myController.valueWordList.length,
              itemBuilder: (BuildContext context, int index) {
                return InkWell(
                  onTap: () async {
                    myController.selectedIndex = index;

                    setState(() {
                      timer?.cancel();
                    });
                    await Future.delayed(Duration(seconds: 2));
                    startTimer();
                  },
                  child: Container(
                    color: myController.isRightWord(index) &&
                            myController.selectedIndex == index
                        ? Colors.green
                        : Colors.blue,
                    alignment: Alignment.center,
                    child: Text(
                      myController.valueWordList[index],
                      style: TextStyle(fontSize: 36),
                    ),
                  ),
                );
              },
            ),
          ),
          Container(
            child: Text(duration.inSeconds.toString()),
          )
        ],
      ),
    );
  }
}

1

There are 1 answers

0
Nour Salman On

When tapping, you are indeed canceling the timer, but you cannot stop the previous onTap callback execution, which will call startTimer creating a new timer internally. This will lead to multiple timers kicking off, resulting in the sped-up count-down.

Since we cannot interrupt an async opertaion, we need to cancel it before it even starts, and here comes the debounce pattern for the rescue:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:wordgame/controller.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Material App', home: MyWidget());
  }
}

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

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    startTimer();
    super.initState();
  }

  //Avoids the early "late initialization" error
  Duration duration = const Duration(seconds: 5);
  Timer? _debouncedTimer;
  Timer? _timer;

  void startTimer() {
    // Show first value (5) before going down
    if (mounted) {
      setState(() {
        duration = Duration(seconds: 5);
      });
    }
    myController.selectMainWord();
    myController.selectValueWords();
    myController.selectedIndex = -1;

    // Good to cancel timers before restart anyways
    _debouncedTimer?.cancel();
    _debouncedTimer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (duration.inSeconds > 0) {
        duration -= Duration(seconds: 1);
        print(duration.inSeconds.toString());
      } else {
        duration = Duration(seconds: 5);
      }

      // Safer to check for mounted
      if (mounted) {
        setState(() {});
      }
    });
  }

  void restartTimer() {
    _timer?.cancel();
    _debouncedTimer?.cancel();
    _timer = Timer(
      const Duration(seconds: 2),
      () => startTimer(),
    );
  }

  MyController myController = MyController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          Container(
            child: Text(
              myController.mainWord,
              style: TextStyle(fontSize: 36),
            ),
          ),
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                  maxCrossAxisExtent: 200,
                  childAspectRatio: 3 / 2,
                  crossAxisSpacing: 20,
                  mainAxisSpacing: 20),
              itemCount: myController.valueWordList.length,
              itemBuilder: (BuildContext context, int index) {
                return InkWell(
                  onTap: () async {
                    myController.selectedIndex = index;
                    restartTimer();
                  },
                  child: Container(
                    color: myController.isRightWord(index) &&
                            myController.selectedIndex == index
                        ? Colors.green
                        : Colors.blue,
                    alignment: Alignment.center,
                    child: Text(
                      index.toString(),
                      style: TextStyle(fontSize: 36),
                    ),
                  ),
                );
              },
            ),
          ),
          Container(
            child: Text(duration.inSeconds.toString()),
          )
        ],
      ),
    );
  }
}