I have created a screen in flutter that shows a question and displays an array of possible answers as buttons. The questions and answers are inside an AnimatedSwitcher widget, so once an answer button is clicked, the next question and answers should be displayed. Unfortunately, the AnimatedSwitcher widget only works when the button is outside its child widget. This is not the behaviour is want, since I want the answer and buttons to both be part of the animation.
Is there a way to do this or possibly a better widget to use? I'd be thankful for your help!
import 'package:flutter/material.dart';
int index = 0;
final widgets = [
QuestionWidget(
question:
Question("What is your favorite color?", ["red", "blue", "green"]),
key: const Key('1')),
QuestionWidget(
question: Question("How do you do today?", ["great", "not so well"]),
key: const Key('2')),
QuestionWidget(
question: Question("Do you like Flutter", ["yes", "no"]),
key: const Key('3')),
];
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
body: Center(
child: SizedBox(
width: size.width * 0.7,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) =>
SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: const Offset(0.0, 0.0))
.animate(animation),
child: FadeTransition(
opacity: animation, child: child)),
child: widgets[index]),
],
))));
}
}
class QuestionWidget extends StatefulWidget {
final Question question;
const QuestionWidget({
required this.question,
Key? key,
}) : super(key: key);
@override
State<QuestionWidget> createState() => _QuestionWidgetState();
}
class _QuestionWidgetState extends State<QuestionWidget> {
@override
Widget build(BuildContext context) {
return Column(children: [
Text(
widget.question.questionText,
style: const TextStyle(
fontSize: 25,
),
),
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
spacing: 5.0,
children: [
for (var i in widget.question.answers)
ElevatedButton(
child: Text(i.toString()),
onPressed: () {
final isLastIndex = index == widgets.length - 1;
setState(() => index = isLastIndex ? 0 : index 1);
})
],
)
]);
}
}
class Question {
String questionText;
List<String> answers;
Question(this.questionText, this.answers);
}
CodePudding user response:
Storing mutable state in global variables is not a valid approach in Flutter.
One of the main rules in Flutter developement is: state goes down, events go up.
In your case, it seems that the Test
widget should be responsible for defining the index of the current question, so you need to make it a part of its State
. The Question
widget shouldn't care about what to do when the right answer is selected, it should only know how to detect such a event and who to notify about it.
Putting it all together:
Test
should store the current question indexTest
should select whichQuestion
to display at the given momentQuestion
should notifyTest
when the right answer is selectedTest
should change the current index in response to the event above.
In your case, notifying about the event can be nothing more than just calling a callback provided in a constructor argument.
In code:
class TestState extends State<Test> {
int _index = 0;
@override
Widget build(BuildContext context) {
...
QuestionWidget(
key: Key(index.toString()),
question: questions[index],
onCorrectAnswer: () => setState(() => index )),
),
...
}
}
class QuestionWidget extends StatelessWidget {
final void Function() onCorrectAnswer;
@override
Widget build(BuildContext context) {
...
ElevatedButton(
onPressed: () => onCorrectAnswer(),
),
...
}
}
I highly recommend reading Flutter docs' take on state management
CodePudding user response:
With the help of mfkw1's answer, I came up with this solution. I struggled a little with this which was because I did not see that the QuestionWidget
was turned into a StatelessWidget
.
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
int index = 0;
next() {
final isLastIndex = index == 2;
setState(() => index = isLastIndex ? 0 : index 1);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final widgets = [
QuestionWidget(
question: Question(
"What is your favorite color?", ["red", "blue", "green"]),
key: const Key("1"),
onAnswer: () => next()),
QuestionWidget(
question: Question("How do you do today?", ["great", "not so well"]),
key: const Key("2"),
onAnswer: () => next()),
QuestionWidget(
question: Question("Do you like Flutter?", ["yes", "no"]),
key: const Key("3"),
onAnswer: () => next()),
];
var size = MediaQuery.of(context).size;
return Scaffold(
body: Center(
child: SizedBox(
width: size.width * 0.7,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) =>
SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: const Offset(0.0, 0.0))
.animate(animation),
child: FadeTransition(
opacity: animation, child: child)),
child: widgets[index]),
],
))));
}
}
class QuestionWidget extends StatelessWidget {
final Question question;
final Function() onAnswer;
const QuestionWidget({
required this.question,
required this.onAnswer,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(children: [
Text(
question.questionText,
style: const TextStyle(
fontSize: 25,
),
),
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
spacing: 5.0,
children: [
for (var i in question.answers)
ElevatedButton(
child: Text(i.toString()),
onPressed: () {
onAnswer();
})
],
)
]);
}
}
class Question {
String questionText;
List<String> answers;
Question(this.questionText, this.answers);
}