Why everything is being reinitialized every second after adding periodic Timer in Flutter?

649 Views Asked by At

I'm trying to build a quiz app. When any user starts to participate in a quiz a timer starts and don't know why everything is being reinitialized every second. The boolean parameter 'answered' is being set as false every second. As a result the participant can answer the same question multiple times which leads to wrong result and not suppose to happen. Here's a snippet-

class MathQuizPlay extends StatefulWidget {
  final String quizId;
  MathQuizPlay({Key key, this.quizId}) : super(key: key);

  @override
  _MathQuizPlayState createState() => _MathQuizPlayState(this.quizId);
}

int total = 0;
int _correct = 0;
int _incorrect = 0;
int _notAttempted = 0;
int timer;
String showtimer;

class _MathQuizPlayState extends State<MathQuizPlay> {
  var quizId;
  _MathQuizPlayState(this.quizId);
  QuerySnapshot questionSnapshot;
  bool isLoading = true;

  getQuestionData(String quizId) async {
    return await Firestore.instance
        .collection('math')
        .document(quizId)
        .collection('QNA')
        .getDocuments();
  }

  @override
  void initState() {
    getQuestionData(quizId).then((value) {
      questionSnapshot = value;
      setState(() {
        total = questionSnapshot.documents.length;
        _correct = 0;
        _incorrect = 0;
        _notAttempted = questionSnapshot.documents.length;
        isLoading = false;
        timer = total * 15;
        showtimer = timer.toString();
      });
    });

    starttimer();
    super.initState();
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }

  Questions getQuestionModelFromDatasnapshot(
      DocumentSnapshot questionSnapshot) {
    final Questions questionModel = Questions(
        question: questionSnapshot.data['question'],
        option1: questionSnapshot.data['option1'],
        option2: questionSnapshot.data['option2'],
        option3: questionSnapshot.data['option3'],
        option4: questionSnapshot.data['option4'],
        correctOption: questionSnapshot.data['correctOption'],
        answered: false);

    return questionModel;
  }

  void starttimer() async {
    const onesec = Duration(seconds: 1);
    Timer.periodic(onesec, (Timer t) {
      setState(() {
        if (timer < 1) {
          t.cancel();
          Navigator.pushReplacement(
              context,
              MaterialPageRoute(
                  builder: (context) => Result(
                        correct: _correct,
                        incorrect: _incorrect,
                        total: total,
                        notattempt: _notAttempted,
                        collection: 'math',
                        quizId: quizId,
                      )));
        } else {
          timer = timer - 1;
        }
        showtimer = timer.toString();
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.teal[300],
        title: Text("Questions",
            style: TextStyle(
              color: Colors.white,
            )),
        elevation: 5.0,
        centerTitle: true,
      ),
      body: isLoading
          ? Container(
              child: Center(child: CircularProgressIndicator()),
            )
          : SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SizedBox(
                    height: 10,
                  ),
                  Center(
                      child: Container(
                          height: 60,
                          width: 60,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(36),
                            border: Border.all(
                              width: 2.0,
                              color: Colors.red.withOpacity(0.8),
                            ),
                          ),
                          child: Center(
                              child: Text(
                            showtimer,
                            style: TextStyle(
                                fontWeight: FontWeight.w500,
                                fontSize: 19.0,
                                color: Colors.red.withOpacity(0.8)),
                          )))),
                  SizedBox(
                    height: 10,
                  ),
                  Center(child: Text('Tap on the option to select answer')),
                  SizedBox(
                    height: 10,
                  ),
                  questionSnapshot.documents == null
                      ? Container(
                          child: Center(
                            child: Text("No Data"),
                          ),
                        )
                      : ListView.builder(
                          itemCount: questionSnapshot.documents.length,
                          shrinkWrap: true,
                          physics: ClampingScrollPhysics(),
                          itemBuilder: (context, index) {
                            return Padding(
                              padding: const EdgeInsets.symmetric(
                                  vertical: 15.0, horizontal: 25),
                              child: QuizPlayTile(
                                questionModel: getQuestionModelFromDatasnapshot(
                                    questionSnapshot.documents[index]),
                                index: index,
                              ),
                            );
                          }),
                  SizedBox(
                    height: 30,
                  ),
                  Center(
                    child: RaisedButton(
                        padding:
                            EdgeInsets.symmetric(vertical: 18, horizontal: 60),
                        color: Colors.teal[300],
                        textColor: Colors.white,
                        shape: RoundedRectangleBorder(
                            borderRadius: new BorderRadius.circular(8.0)),
                        child: Text(
                          'Submit',
                          style: TextStyle(fontSize: 16),
                        ),
                        elevation: 7.0,
                        onPressed: () {
                          Navigator.pushReplacement(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => Result(
                                        correct: _correct,
                                        incorrect: _incorrect,
                                        total: total,
                                        notattempt: _notAttempted,
                                        collection: 'math',
                                        quizId: quizId,
                                      )));
                        }),
                  ),
                  SizedBox(
                    height: 50,
                  )
                ],
              ),
            ),
    );
  }
}


2

There are 2 best solutions below

0
On

I couldn't solve the problem using ChangeNotifierProvider either. But luckily I found a package which completely solved my issue. So I used that package instead of periodic Timer for setting the time. Here's the update-

import 'package:circular_countdown_timer/circular_countdown_timer.dart';

class PhyQuizPlay extends StatefulWidget {
  final String quizId;
  PhyQuizPlay({Key key, this.quizId}) : super(key: key);

  @override
  _PhyQuizPlayState createState() => _PhyQuizPlayState(this.quizId);
}

int total = 0;
int _correct = 0;
int _incorrect = 0;
int _notAttempted = 0;
int timer;

class _PhyQuizPlayState extends State<PhyQuizPlay> {
  var quizId;
  _PhyQuizPlayState(this.quizId);
  QuerySnapshot questionSnapshot;
  bool isLoading = true;

  getQuestionData(String quizId) async {
    return await Firestore.instance
        .collection('physics')
        .document(quizId)
        .collection('QNA')
        .getDocuments();
  }

  @override
  void initState() {
    getQuestionData(quizId).then((value) {
      questionSnapshot = value;
      setState(() {
        total = questionSnapshot.documents.length;
        _correct = 0;
        _incorrect = 0;
        _notAttempted = total;
        isLoading = false;
        timer = total * 15;
      });
    });
    super.initState();
  }

  Questions getQuestionModelFromDatasnapshot(
      DocumentSnapshot questionSnapshot) {
    final Questions questionModel = Questions(
        question: questionSnapshot.data['question'],
        option1: questionSnapshot.data['option1'],
        option2: questionSnapshot.data['option2'],
        option3: questionSnapshot.data['option3'],
        option4: questionSnapshot.data['option4'],
        correctOption: questionSnapshot.data['correctOption'],
        answered: false);

    return questionModel;
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.teal[300],
        title: Text("Questions",
            style: TextStyle(
              color: Colors.white,
            )),
        elevation: 5.0,
        centerTitle: true,
      ),
      body: isLoading
          ? Container(
              child: Center(child: CircularProgressIndicator()),
            )
          : SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SizedBox(
                    height: 10,
                  ),
                  Center(
                      child: CircularCountDownTimer(
                    width: 80,
                    height: 80,
                    duration: timer,
                    fillColor: Colors.red,
                    color: Colors.white38,
                    isReverse: true,
                    onComplete: () {
                      Navigator.pushReplacement(
                          context,
                          MaterialPageRoute(
                              builder: (context) => Result(
                                    correct: _correct,
                                    incorrect: _incorrect,
                                    total: total,
                                    notattempt: _notAttempted,
                                    collection: 'physics',
                                    quizId: quizId,
                                  )));
                    },
                  )),
                  SizedBox(
                    height: 10,
                  ),
                  Center(child: Text('Tap on the option to select answer')),
                  SizedBox(
                    height: 10,
                  ),
                  questionSnapshot.documents == null
                      ? Center(
                          child: CircularProgressIndicator(),
                        )
                      : ListView.builder(
                          itemCount: questionSnapshot.documents.length,
                          shrinkWrap: true,
                          physics: ClampingScrollPhysics(),
                          itemBuilder: (context, index) {
                            return Padding(
                              padding: const EdgeInsets.symmetric(
                                  vertical: 15.0, horizontal: 25),
                              child: QuizPlayTile(
                                questionModel: getQuestionModelFromDatasnapshot(
                                    questionSnapshot.documents[index]),
                                index: index,
                              ),
                            );
                          }),
                  SizedBox(
                    height: 30,
                  ),
                  Center(
                    child: RaisedButton(
                        padding:
                            EdgeInsets.symmetric(vertical: 18, horizontal: 60),
                        color: Colors.teal[300],
                        textColor: Colors.white,
                        shape: RoundedRectangleBorder(
                            borderRadius: new BorderRadius.circular(8.0)),
                        child: Text(
                          'Submit',
                          style: TextStyle(fontSize: 16),
                        ),
                        elevation: 7.0,
                        onPressed: () {
                          Navigator.pushReplacement(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => Result(
                                        correct: _correct,
                                        incorrect: _incorrect,
                                        total: total,
                                        notattempt: _notAttempted,
                                        collection: 'physics',
                                        quizId: quizId,
                                      )));
                        }),
                  ),
                  SizedBox(
                    height: 50,
                  )
                ],
              ),
            ),
    );
  }
} 

0
On

ISSUE :

As you are calling setState everytime your timer completes a second .... it refreshes the your screen after every second. Hence the build function is re-called after every second rebuilding each any every line of the code inside that function. Now the main issue is that the function : 'getQuestionModelFromDatasnapshot' will always provide a new 'questionModel' with a value of the 'answered' parameter as false every second the screen gets refreshed.

SOLUTION :

Now I have two solutions for you:

  1. Good programming practice : My guess is that you call setState to update the timer value in your UI after every seconds. Now her you have to realize that calling setState for just a small change is bad as it will refresh all the other widgets will be recreated too which is unnecessary. The good way is to use ChangeNotifierProvider which will listen to update in the timer and then wrap the UI Text showing the timer value with a Consumer. Hence whenever the timer value is updated .... It will only update the UI Text with timer value.

  2. In case you want a quick solution : Instead of calling 'getQuestionModelFromDatasnapshot' method in the build function what you can do is intialize the whole ListView in the initstate as a variable.... like : ListView x = /* Your listview code containing that fucntion */ ..... Doing this ...... the widget wont be rebuilt every second.

The code is a bit messy ....... I will strongly prefer to either re-write the code properly or use the ChangeNotifierProvider as I said in the first option.