Let's Build Destini a Story Based Quiz App
It's been many days since my last post and I am sorry that I was stuck with procrastination and also having a problem with the internet (Let's stop here about my problems ๐).
Introduction
Today we are going build a Story-based quiz app by that I mean the quiz question is going to differ depending on users answers( Just like Bandersnatch a Netflix movie).
Things we are going to learn by building this app:
- Container decoration
- Raised Button
- Visibility
- Background Image
Coding Part
This app going to almost similar to the last built Quiz app but little difference in logic which will help us in recalling the things we have learnt previously so let's get started.
Let's create the base setup
By now you might have learnt that we can create the base set up by running a single flutter command.
flutter create Destini
By running above command we are going to get the default app code which we don't require so let's delete the content of main.dart
file and also as we are not going to add any test cases so let's just delete the test folder.
Also, let's provide our app with an icon by updating the icons folder for both Android and iOS.
You can get the complete code from this repo
Let's create the Questioner page
So in the page where we are going to display the question along with two choices to the user so we can say that we going to need one Text
widget and 2 Button for the choices also let's add a beautiful background image.
So let make the app with the dark theme which is some much easier with flutter we just have to use the theme
parameter in the MaterialApp
widget.
void main() => runApp(Destini());
class Destini extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StoryPage(),
theme: ThemeData.dark(),
);
}
}
So you can have a blank if you run the app so next add our background image to the app.
For to add local image first we have to add the image to the project and next, we have to mention the added image folder as an asset in the pubsec.yaml
which holds all the flutter dependency.
name: destini
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to the pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- images/
By mentioning the only folder name we don't have to add all the images added to the project.
Next, we use the BoxDecoration
widget to add the background image to the app which quite a hassle but worth it as we can add our wanted background image to our app background ๐
.
class _StoryPageState extends State<StoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
),
);
}
}
By now you also might have this:
Next, let's get ourselves a place to display the question:
class _StoryPageState extends State<StoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 15.0),
constraints: BoxConstraints.expand(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Center(
child: Text(
'Question will go here',
style: TextStyle(fontSize: 25.0),
),
),
),
],
),
),
);
}
}
As we later going to add the buttons below the questions lets wrap our Text
widget inside the Column
widget.
And to make question text size a little bigger let's add some font-size TextStyle
of 25 pixels. Also, add some padding in between the question and the screen edges so our app doesn't get stuck to the edge of the screen ๐
.
Now it's time to add buttons to the app.
class _StoryPageState extends State<StoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 15.0),
constraints: BoxConstraints.expand(),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 12,
child: Center(
child: Text(
'Question will be displayed here',
style: TextStyle(fontSize: 25.0),
),
),
),
Expanded(
flex: 2,
child: RaisedButton(
color: Colors.red, // background
textColor: Colors.white, // foreground
onPressed: () {},
child: Text(
'Choice 1',
style: TextStyle(fontSize: 20.0),
),
),
),
SizedBox(
height: 20.0,
),
Expanded(
flex: 2,
child: Visibility(
child: RaisedButton(
color: Colors.blue, // background
textColor: Colors.white, // foreground
onPressed: () {},
child: Text(
'Choice 2',
style: TextStyle(fontSize: 20.0),
),
),
),
),
],
),
),
),
);
}
}
So by this, we have 2 buttons to our app named choice 1
and choice 2
. we are using the RaisedButton
widget for our as the FlatButton
widget will soon be deprecated from the flutter.
In RaisedButton
to give a colour to the app we are going to use the color
parameter and to give the colour to the text displayed on the button we use the textcolor
. For our app to make a distinction between the buttons let's make one button blue and another as red.
Also to make Question Text
widget acquire more space in the display we are going to use the flex widget which is same as the flex in CSS which tells us about the portion of the display to be used by the widget.
By this we can say we are done with the design part and now our app looks like this:
Let's build the Brain of the App
So as we are done with the looks of now let's give it some brain so it works as we wanted it to work.
First, let's store some question and answers we are going to give to the user. So to keep the code clean let's create one more file for the brain code.
First, let's create a class which will be work as the blueprint for the quiz brain object which we are going to use to store the questions and answers.
class Story {
String storyTitle;
String choice1;
String choice2;
Story({this.storyTitle, this.choice1, this.choice2});
}
By this class, we can say that we have 3 parameters one for the question and 2 for each choice we are going to give to the user. Next to the constructor which is used to initialize the object and also let's make it named parameters so we can know which parameter is passing ๐.
Next, let's create one more file where we are going to use the previously created class and the functionality we are going to implement in our app and let's name it story_brain
since it will be working as the brain of the app๐.
class StoryBrain {
List<Story> _storyData = [
Story(
storyTitle:
'Your car has blown a tire on a winding road in the middle of nowhere with no cell phone reception. You decide to hitchhike. A rusty pickup truck rumbles to a stop next to you. A man with a wide-brimmed hat with soulless eyes opens the passenger door for you and asks: "Need a ride, boy?".',
choice1: 'I\'ll hop in. Thanks for the help!',
choice2: 'Better ask him if he\'s a murderer first.'),
Story(
storyTitle: 'He nods slowly, unphased by the question.',
choice1: 'At least he\'s honest. I\'ll climb in.',
choice2: 'Wait, I know how to change a tire.'),
Story(
storyTitle:
'As you begin to drive, the stranger starts talking about his relationship with his mother. He gets angrier and angrier by the minute. He asks you to open the glovebox. Inside you find a bloody knife, two severed fingers, and a cassette tape of Elton John. He reaches for the glove box.',
choice1: 'I love Elton John! Hand him the cassette tape.',
choice2: 'It\'s him or me! You take the knife and stab him.'),
Story(
storyTitle:
'What? Such a cop-out! Did you know traffic accidents are the second leading cause of accidental death for most adult age groups?',
choice1: 'Restart',
choice2: ''),
Story(
storyTitle:
'As you smash through the guardrail and careen towards the jagged rocks below you reflect on the dubious wisdom of stabbing someone while they are driving a car you are in.',
choice1: 'Restart',
choice2: ''),
Story(
storyTitle:
'You bond with the murderer while crooning verses of "Can you feel the love tonight". He drops you off at the next town. Before you go he asks you if you know any good places to dump bodies. Your reply: "Try the pier".',
choice1: 'Restart',
choice2: '')
];
}
These are the question we are going to give to the user. Now let's create a method which will be passing the question to our Question Text
widget and the choices to the Choice
Text widget in our Choice Buttons
.
class StoryBrain {
int _storyNumber = 0;
String getStory() {
return _storyData[_storyNumber].storyTitle;
}
String getChoice1() {
return _storyData[_storyNumber].choice1;
}
String getChoice2() {
return _storyData[_storyNumber].choice2;
}
So now just have to use these methods to get the question and choices.
class _StoryPageState extends State<StoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 15.0),
constraints: BoxConstraints.expand(),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 12,
child: Center(
child: Text(
storyBrain.getStory(), //This is the call for the method we created above to get the questions
style: TextStyle(fontSize: 25.0),
),
),
),
Expanded(
flex: 2,
child: RaisedButton(
color: Colors.red, // background
textColor: Colors.white, // foreground
onPressed: () {},
child: Text(
storyBrain.getChoice1(),//This is the call for the method we created above to get the choice 1
style: TextStyle(fontSize: 20.0),
),
),
),
SizedBox(
height: 20.0,
),
Expanded(
flex: 2,
child: Visibility(
child: RaisedButton(
color: Colors.blue, // background
textColor: Colors.white, // foreground
onPressed: () {},
child: Text(
storyBrain.getChoice2(),//This is the call for the method we created above to get the choice 2
style: TextStyle(fontSize: 20.0),
),
),
),
),
],
),
),
),
);
}
}
So by now you also might get the question and answers displayed in the place of the placeholder text we had earlier.
Now let's try to get the next question when the user clicks on one of the choices gives. So for that let's create methods which provide us next question based on user answer.
Here is the logic for the next question based on the answer:
class StoryBrain {
String getStory() {
return _storyData[_storyNumber].storyTitle;
}
String getChoice1() {
return _storyData[_storyNumber].choice1;
}
String getChoice2() {
return _storyData[_storyNumber].choice2;
}
int _storyNumber = 0;
void nextStory({int choiceNumber}) {
if (_storyNumber == 0 && choiceNumber == 1) {
_storyNumber = 2;
} else if (_storyNumber == 0 && choiceNumber == 2) {
_storyNumber = 1;
} else if (_storyNumber == 1 && choiceNumber == 1) {
_storyNumber = 2;
} else if (_storyNumber == 2 && choiceNumber == 1) {
_storyNumber = 5;
} else if (_storyNumber == 2 && choiceNumber == 2) {
_storyNumber = 4;
} else if (_storyNumber == 1 && choiceNumber == 2) {
_storyNumber = 3;
} else {
restart();
}
}
void restart() {
_storyNumber = 0;
}
}
Yeah I know it's hard coding but let's work with it in future let's come up solution or some algorithm to handle this ๐.
Also, I have added one more method reset the story number to 0 so we won't be having an out of bounds exception
.
So let's use this nextStory method inside the choice buttons using the setState method which triggers build when there are some changes.
class _StoryPageState extends State<StoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 15.0),
constraints: BoxConstraints.expand(),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 12,
child: Center(
child: Text(
storyBrain.getStory(),
style: TextStyle(fontSize: 25.0),
),
),
),
Expanded(
flex: 2,
child: RaisedButton(
color: Colors.red, // background
textColor: Colors.white, // foreground
onPressed: () {
setState(() {
storyBrain.nextStory(choiceNumber: 1);//Get's the next question and choices if the choice is 1st one
});
},
child: Text(
storyBrain.getChoice1(),
style: TextStyle(fontSize: 20.0),
),
),
),
SizedBox(
height: 20.0,
),
Expanded(
flex: 2,
child: RaisedButton(
color: Colors.blue, // background
textColor: Colors.white, // foreground
onPressed: () {
setState(() {
storyBrain.nextStory(choiceNumber: 2);//Get's the next question and choices if the choice is 2nd one
});
},
child: Text(
storyBrain.getChoice2(),
style: TextStyle(fontSize: 20.0),
),
),
),
],
),
),
),
);
}
}
So one final thing pending as you can see right at the end we have an empty button so let's try to make it invisible at the end so only the restart button is displayed to the user.
For that let's create a method which checks if it's the conclusion to the story in story_brain class.
bool buttonShouldBeVisible() {
if (_storyNumber == 0 || _storyNumber == 1 || _storyNumber == 2) {
return true;
} else {
return false;
}
}
So the methods pass boolean based on the question number. So let's use the visibility
widget helps use in hiding or displaying a widget.
Expanded(
flex: 2,
child: Visibility(
visible: storyBrain.buttonShouldBeVisible(),
child: RaisedButton(
color: Colors.blue, // background
textColor: Colors.white, // foreground
onPressed: () {
setState(() {
storyBrain.nextStory(choiceNumber: 2);
});
},
child: Text(
storyBrain.getChoice2(),
style: TextStyle(fontSize: 20.0),
),
),
),
),
So Using the Visbility
widget works based visibility parameter is set to true or false if the it is false then the RaisedButton
is set to invisible.
Summary of things learned with challenges faced
So by building this Story-based quiz app we got to learn about using the background image to the app. Then adding the Question and choices for that question where we had to give the question lot more space compared to the choices which were solved by the use of the flex option in the Expanded
widget. Later built the brain of the app which handles the question bank and methods such as getting the question and choices and also restarting the question at the end of the story and handling the visibility of the choice button. At last, we had to get the next question based on user choice.
Thank you
First, let me thank you if you have completely read this post and congratulations if you are also got yourselves your own story based quiz app.
As usual, if you like the post leave a like and comment. And if youfind any mistakes and suggestion for me you should definitely leave a comment for me it would be helpful for me as I am new to this.
In the next post, I am going to a BMI calculator app with new things to be learned in a flutter. till then
do widzenia ๐.