Flutter: How to stop state from changing UI

Issue

I have some local JSON data that I’ve parsed into my app. The data contains a few arrays and math questions (building a quiz app), so based on the math there are chapters which I’ve displayed in a ListView builder, when clicking on one of those it opens a new screen and passes on specific data for that chapter based on the index of the chapter.

In my quiz screen math questions in an array are brought from the JSON data depending on what chapter it is, I have the questions shuffled so that they appear in a random order, issue is when a user tries to answer a question in a provided TextField the keyboard opens as intended but the state of the UI changes which also changes the currently displayed question. How can I stop this?

To see the problem I have in the app here’s a video link: https://youtu.be/n_VeT26I8NE

The JSON data has been parsed from a different screen and set in these variables:

List quiz = widget.newData["quiz"];
//Shuffle questions
var randomQuiz = (quiz..shuffle()).first;

If you prefer to read the Dart code from GitHub: https://github.com/BotsheloRamela/classio/blob/main/quiz_screen.dart

Some of the JSON data:

[
  {
    "chapter": "Exponents & Surds",
    "introduction": {
    "description": "A number's exponent determines how many times to multiply it. 
    Exponents can also be called powers or indices. In mathematics, a surd is a value 
    that cannot be further simplified into a whole number or integer. They are 
    irrational numbers.",
        "videoID": ["568dGLFTom8", "XZRQhkii0h0"]
    },
    "quiz": [
        {
         "question": "Simplify the follwoing.",
         "sum": "2a^2.3a^2.b^0",
         "answer": "6a^5",
         "explaination": ""
        },
        {
         "question": "Rewrite the following with prime bases, your answer should be a fraction.",
         "sum": "16.8^{-4}",
         "answer": "1/256",
         "explaination": ""
        },
        {
         "question": "Solve the following, your answer should be a fraction.",
         "sum": "\\frac{(-3a^3 b)^2}{a^5 b^3}",
         "answer": "9a/b",
         "explaination": ""
        },
        {
         "question": "Rewrite the following with prime bases and simplify, your answer should be a fraction.",
         "sum": "(\\frac{2x^3}{8y^{-4}})^{-3}",
         "answer": "\\frac{64}{x^9 y^12}",
         "explaination": ""
        }
    ]
    
 },

 {
  "chapter": "Equations & Inequalities",
  "introduction": {
  "description": "Equations and inequalities are both mathematical sentences made up of two expressions related to one another. The symbol = indicates that two expressions are regarded as equal within an equation. While in an inequality, two terms are not necessarily equal, which is indicated by the following symbols: >, <, ≤ or ≥.",
  "videoID": ["hJ-_OoCHTks"]
    },
    "quiz": [
        {
         "question": "Show that the following trinomial is a perfect square.",
         "sum": "x^2+2x+1",
         "answer": "(x+1)^2",
         "explaination": ""
        },
        {
         "question": "Show that the following trinomial is a perfect square.",
         "sum": "x^2-6x+9",
         "answer": "(x-3)^2",
         "explaination": ""
        },
        {
         "question": "Show that the following trinomial is a perfect square.",
         "sum": "x^2+8x+16",
         "answer": "(x+4)^2",
         "explaination": ""
        }
    ]
 },
]

Dart code:

import 'package:classio_students/bottom_nav.dart';
import 'package:classio_students/colors.dart';
import 'package:classio_students/screens/learn/maths_formula_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:page_transition/page_transition.dart';

// ignore: must_be_immutable
class Gr11MathQuiz extends StatefulWidget {
var newData;
Gr11MathQuiz({Key? key, required this.newData}) : super(key: key);

@override
_Gr11MathQuizState createState() => _Gr11MathQuizState();
}

class _Gr11MathQuizState extends State<Gr11MathQuiz> {
final TextEditingController _answerController = TextEditingController();
@override
void initState() {
super.initState();
//SystemChannels.textInput.invokeMethod('TextInput.show');
}

@override
Widget build(BuildContext context) {
//Creates a list from the JSON data & shuffles the data quiz in a random order everytime the state changes
List quiz = widget.newData["quiz"];
var randomQuiz = (quiz..shuffle()).first;
//Check answer function
checkAnswer() async {
  if (_answerController.text.toString() !=
      randomQuiz['answer'].toString()) {
    Fluttertoast.showToast(
        msg: 'Are you sure? Take a look at your answer.',
        gravity: ToastGravity.TOP,
        backgroundColor: Colors.orangeAccent,
        textColor: Colors.white);
  } else {
    Fluttertoast.showToast(
        msg: 'Nice!',
        gravity: ToastGravity.TOP,
        backgroundColor: Colors.green,
        textColor: Colors.white);
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {});
      _answerController.clear();
    });
  }
}

final height = MediaQuery.of(context).size.height;
const shape = StadiumBorder();
return Scaffold(
    resizeToAvoidBottomInset: false,
    appBar: AppBar(
      title: const Text('Gr11 Maths Quiz',
          style: TextStyle(
              color: titleColor,
              letterSpacing: 1,
              fontSize: 20,
              fontWeight: FontWeight.bold)),
      leading: IconButton(
          onPressed: () => Navigator.pop(context),
          icon: const Icon(
            Icons.arrow_back,
            color: Colors.black,
            size: 30,
          )),
      actions: [
        IconButton(
            onPressed: () => Navigator.pushReplacement(
                context,
                MaterialPageRoute(
                    builder: (context) => const CustomNavBar())),
            icon: const Icon(
              Icons.home,
              color: Colors.black,
              size: 30,
            ))
      ],
      backgroundColor: Colors.white,
      centerTitle: true,
      elevation: 0,
    ),
    body: SizedBox(
      height: height,
      child: Flexible(
        child: Padding(
          padding: const EdgeInsets.all(13),
          child: Column(
            children: [
              Text(
                randomQuiz['question'].toString(),
                style: const TextStyle(color: Colors.black, fontSize: 17),
              ),
              const SizedBox(
                height: 30,
              ),
              //Sum
              Center(
                child: Math.tex(
                  randomQuiz['sum'].toString(),
                  mathStyle: MathStyle.display,
                  textStyle: const TextStyle(fontSize: 25),
                ),
              ),
              const SizedBox(
                height: 60,
              ),
              //Answer text
              const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  'Answer:',
                  style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.black,
                      fontSize: 20),
                ),
              ),
              const SizedBox(
                height: 10,
              ),
              //Answer textfield & check btn
              Row(
                children: [
                  //Textfield
                  Expanded(
                    child: TextField(
                      controller: _answerController,
                      style: const TextStyle(color: Colors.black),
                      decoration: InputDecoration(
                          hintText: 'Answer',
                          hintStyle: const TextStyle(color: Colors.grey),
                          labelStyle: const TextStyle(color: Colors.grey),
                          enabledBorder: OutlineInputBorder(
                              borderSide:
                                  const BorderSide(color: Colors.grey),
                              borderRadius: BorderRadius.circular(13)),
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(13),
                          ),
                          focusedBorder: OutlineInputBorder(
                              borderSide:
                                  const BorderSide(color: submitBtn),
                              borderRadius: BorderRadius.circular(13))),
                    ),
                  ),
                  const SizedBox(
                    width: 15,
                  ),
                  //Button
                  Container(
                    decoration: BoxDecoration(
                        color: titleColor,
                        borderRadius: BorderRadius.circular(10)),
                    child: ElevatedButton(
                      onPressed: () {
                        checkAnswer();
                      },
                      child: const Text(
                        "Check",
                        style: TextStyle(fontSize: 16),
                      ),
                      style: ElevatedButton.styleFrom(
                          primary: Colors.transparent,
                          shadowColor: Colors.transparent,
                          padding: const EdgeInsets.symmetric(
                              vertical: 19, horizontal: 20),
                          //minimumSize: const Size(100.0, 5.0),
                          textStyle: const TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 23)),
                    ),
                  ),
                ],
              ),
              const SizedBox(
                height: 20,
              ),
              //View Math symbols dialog
              Align(
                alignment: Alignment.centerLeft,
                child: Container(
                  decoration: BoxDecoration(
                      color: Colors.grey.shade400,
                      borderRadius: BorderRadius.circular(10)),
                  child: ElevatedButton(
                    onPressed: () {
                      showDialog(
                          context: context,
                          builder: (context) {
                            return const MathFormulaDialog();
                          });
                    },
                    child: const Text(
                      "How should I type my answer?",
                      style: TextStyle(fontSize: 16, color: Colors.black),
                    ),
                    style: ElevatedButton.styleFrom(
                        primary: Colors.transparent,
                        shadowColor: Colors.transparent,
                        padding: const EdgeInsets.symmetric(
                            vertical: 19, horizontal: 20),
                        //minimumSize: const Size(100.0, 5.0),
                        textStyle: const TextStyle(
                            fontWeight: FontWeight.bold, fontSize: 23)),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    ));
 }
}

Solution

Thanks to @Namini40, I moved the variables to initState() from the build function, the checkAnswer() function remained in the build method and I changed a couple things. If a user gets the answer right rather than using setState() I instead used Navigator.pushReplacement() to navigate to the very same page but will display a different question.

New code: https://github.com/BotsheloRamela/classio/blob/main/quiz_screen.dart

Answered By – Botshelo Ramela

Answer Checked By – Katrina (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *