I Am Rick (Episode 5): Which TWD Character Are You?

Building a quiz app with Flutter and Rick Grimes.

Alexandros Baramilis
18 min readFeb 16, 2020

Intro

If you haven’t been following the series so far, check out the previous apps I’ve made:

and the Github repo for the series.

If you’re having trouble installing Flutter on macOS Catalina, check out Setting up Flutter on macOS Catalina.

ALERT: Next episodes are out!

As to why I’m writing this series:

One of the best ways for me to learn something is to write about it, so I’m making this series with the notes that I take as I go through App Brewery’s Flutter Bootcamp. Publishing my notes and code also forces me to maintain a high standard and allows me to easily go back for a quick review or to update them. I’m keeping the Rick Grimes theme alive for as long as I can, because it just makes it so much more fun. Don’t worry, App Brewery’s bootcamp doesn’t have Rick in it, but I still highly recommend it if you want to learn Flutter. 😄

Which TWD (The Walking Dead) Character Are You?

For this module, App Brewery is building a boring true/false quiz app, so I’m making a cool TWD quiz app, for you to finally find out the answer to your lifelong question of: Which The Walking Dead Character Are You?

Ok, I admit it, I didn’t actually make the quiz myself. I hacked it from this boston.com quiz, using the no.1 hacker weapon: Chrome DevTools.

If you want to replicate this intricate hack:

  • Go to the quiz page, using the link above and hit option + command + J (on Mac). This will open a big sidebar with all the developer tools built into Chrome.
  • Go to the Sources section from toolbar and from the navigator on the left dig into the archive.boston.com folder until you find the (index) file.
  • This is a long file, so to save you the time you can hit command + F to search for pquiz_data. This is a JavaScript object, which is pretty much a JSON object, containing all the data you need for the quiz.

Ok that wasn’t so intricate.

Decoding JSON in Dart

We’re taking a small detour from the course here (no, the course doesn’t teach you how to hack quizzes from websites), just to get the data we need for the quiz.

Dart has tools to easily work with JSON, through the dart:convert library.

import 'dart:convert';

var jsonString = '''
{"quiz_congratulations":"","questions": ... la carte anyone?)."}}}
''';
quizData = jsonDecode(jsonString);

We simply need to import the library, copy and paste the JSON string (without the last semicolon from the JavaScript object), wrap the string with triple quotes to turn it into a Dart String (then add the Dart semicolon) and then use the jsonDecode function.

This should return a Map object. We can then easily access the elements inside like:

quizData['questions']['1']['options']['1']['text']

Visualising the JSON data

Now that we got the data, we need to make some sense of it, in order to understand how the quiz works.

It’s very helpful if we can visualise the data. Thankfully, there are plenty of free web-based JSON visualisers. One that I’ve been using is jsonviewer.stack.hu.

Just paste the JSON string (without the last semicolon from the JavaScript object) into the Text section from the top toolbar and then switch to the Viewer section.

We can see that at the top level we have the questions and the outcomes.

If we open the questions, we see that there are 10 questions inside. Each question has an index, a text and an options field with 4 options.

Each option has an index, a text and a scores field, with 7 scores inside. Each of these 7 scores corresponds to a character. The characters are codenamed from 286 to 292.

So basically, every time the user picks an answer, you add up the scores for each characters. In the end, the character with the highest score wins.

To get the result to show to the user, we need to open the outcomes field.

We can see that under outcomes are the codenames for our characters. Each codename has an id, an image url, a name, a tease (?) and a text.

And that’s how this quiz works.

Let’s make it into an app!

Making of the UI

I’m not going to tire you with all the code that we’ve covered before. If you want to see the full picture you can get the final code on GitHub.

(Btw, I downloaded GitHub Desktop and was amazed by its simplicity and ease of use! I did a good reorg of Rick’s GitHub page that would have been a pain using the web app or the command line. I highly recommend downloading it if you’re using GitHub.)

So I start with the usual StatelessWidget, MaterialApp, Scaffold, AppBar, SafeArea, Padding and inside a StatefulWidget that will hold the quiz state.

In the StatefulWidget’s build method I have a Column that holds three children. Two Texts for the question title and text and another Column that holds all the answer options.

Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Question 1/10',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 32.0,
),
),
Text(
'What is your walker slaying weapon of choice?',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 26.0,
),
),
Column(
children: <Widget>[
answerButton(option: 1, text: 'Katana'),
answerButton(option: 2, text: 'Revolver'),
answerButton(option: 3, text: 'Crossbow'),
answerButton(option: 4, text: 'Hammer'),
],
)
],
);

To avoid repetition for each answer button I made a method called answerButton that returns a Padding widget that has a RaisedButton widget as a child.

Padding answerButton({int option, String text}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text(
text,
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 26.0,
color: Colors.white,
),
),
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
color: Colors.red[800],
onPressed: () {
print('Button $option pressed.');
},
),
);
}

We pass the button number to the method so that we know which button was pressed, as well as the text to display for the answer.

So if the user taps on the first button, the console will log ‘Button 1 pressed.’

I’m using hardcoded values for now just to work on the UI first and then I’ll update the code to change the values dynamically.

This produces this UI:

The font is Zombie Queen Regular from the Zombie Queen Font Family by Juha Korhonen. I know, they’re called Walkers, not Zombies, but it was a nice font!

Some Android Studio tips

TODO

In Android Studio we can create TODO comments like this:

//TODO: Write your todo here

and there is a section called TODO in the bottom toolbar where you can find all the todos in one place.

I usually keep a separate to-do list with all the things I need to do for my project, but this is another way of doing it.

It’s also useful if you wanna jump to something else before finishing a part and you want to leave a note so you don’t forget to come back to it.

VCS (Version Control System)

Another helpful tool in Android Studio. At the top toolbar you can find VCS and inside is Local History where you can see all the changes you’ve made and also revert to different points in time.

Object Oriented Programming (OOP)

The biggest concept introduced in this module is Object Oriented Programming and its four pillars: Abstraction, Encapsulation, Inheritance and Polymorphism.

Because it’s an important topic, I’m going to make a quick note here. If it’s not enough and these concepts are not very clear for you, do watch Angela’s class, she does a great job at explaining them with fun and lively examples.

Abstraction

Abstraction is when you separate your code into many small modules, each one doing a very specific job.

This is in contrast to having a single code file doing everything.

Notable advantages are:

  • much more readable code (for you and other people) especially in complex projects
  • reduced complexity, or at least complexity that human brains can grasp
  • avoidance of spaghetti code creating headaches and obscure bugs that require whole expeditions with complex sketched maps and to find
  • maintaining and upgrading the app becomes much easier: you can swap or change parts instead of messing with the whole beast
  • code reusability: if you write a component that does a specific task, you can reuse it easily in other parts of the codebase, or even in different codebases, making your life as a developer much easier

Abstraction in practice

Abstraction is commonly achieved through classes, which are like the blueprints to create the objects in OOP.

To create a Class in Android Studio right click on the lib folder, then New, Dart File and name it question.

Then you declare the Class like:

class Question {
String text;
}

and you can give it properties like text.

An object is an instance of a class. The class is the blueprint to create the object. You can have many different objects/instances of the same class.

You can also write a constructor for the class, which is a method that will get called when the object is created. It’s a good place to initialise the properties with their initial values, etc.

The constructor in Dart is simply a method that has the same name as the class.

class Question {
String text;
Question() {
text = 'My first text';
}
}

You can also pass parameters to the constructor.

class Question {
String text;
Question({String initialText}) {
text = initialText;
}
}

Using the curly braces forces us to explicitly specify the parameter name when calling the method like:

Question(initialText: 'My first text');

If we don’t use the curly braces we can do:

Question('My first text');

which is simpler, but might get confusing and lead to accidental errors if you have many parameters. I prefer to use the curly braces.

If we want to have the same parameter name as the property name, we can use the this keyword to differentiate between the two.

class Question {
String text;
Question({String text}) {
this.text = text;
}
}

In this case, text refers to the parameter and this.text refers to the property.

If we have many properties, Dart also has some syntactic sugar to simplify this repetitive process of initialisation. Instead of writing all the constructor code with the assignments etc. we can just write:

class Question {
String text;
Question({this.text});
}

and Dart will take care of the constructing and initialising of the properties.

We can also do it without the curly braces if we don’t want to explicitly name the parameters when we’re creating a new object.

class Question {
String text;
Question(this.text);
}

If you have a class with many properties, it’s a bit repetitive to type out every property again in the constructor, but thankfully there’s a handy shortcut for that in Android Studio. If you just type the class name without any properties inside the parentheses, like Question(); , the compiler will show an error:

If you tap on ‘Add final field formal parameters’, it will add all of them automatically for you. 😁

More on constructors here.

It’s good practice to create classes in different files.

To use the classes in different files, we simply need to import them at the top.

import 'question.dart';

Then, creating an object is simple.

Question q1 = Question(text: ‘What?’);

To access class properties we use dot notation.

q1.text

Encapsulation

Another important element of OOP is encapsulation.

Through abstraction, you hopefully now have many different modules doing specific tasks.

What you don’t want now is for them to mess with each other’s jobs.

You want to hide their internal workings and interfaces so you don’t accidentally mess up with them from other parts of code.

Encapsulation in practice

Encapsulation is achieved commonly through private variables.

In Dart, we make a variable private by adding an underscore in front of it.

String _text;

Now _text is not accessible outside the class, neither for reading nor for writing.

If we want to be able to read the value outside the class, we can add a getter method.

class Question {
String _text;
Question(this.text); String getText() {
return _text;
}
}

There are many more tools to achieve more granular encapsulation, but we’re keeping it simple here.

Inheritance

Sometimes you want to keep some of the functionality you wrote for a class and make it more specialised in different subclasses.

Instead of writing all the code from scratch in each subclass, you can inherit all the functionality from a common superclass.

Inheritance in practice

In Dart, you can make a class inherit from another class, using the extends keyword.

class Car {
int wheels = 4;
void drive() {
print('Im driving!');
}
}
class ElectricCar extends Car {
int batteryLevel = 100;
void charging() {
print('Im charging!');
}
}

Now the ElectricCar subclass has access to all of its parent’s properties and methods, as well as its own.

Polymorphism

Sometimes a subclass might want to keep some of its parents functionality, but also do some things differently.

Polymorphism allows for that flexibility.

You might want to use the same method, but write it differently. Or use the same method and add some extra functionality at the end.

Polymorphism in practice

In Dart, polymorphism is achieved through the @override keyword.

class Car {
int wheels = 4;
void drive() {
print('Im driving!');
}
}
class SelfDrivingCar extends Car {
int destination = 'Athens';
@override
void drive() {
print('Im self-driving!');
}
}

Here we override the parent method, so instead of printing ‘Im driving!’, we print ‘Im self-driving!’.

If we want to keep the parent’s functionality but add something to it, we can use the super keyword (stands for superclass).

class Car {
int wheels = 4;
void drive() {
print('Im driving!');
}
}
class ElectricCar extends Car {
int batteryLevel = 100;

void charging() {
print('Im charging!');
}
@override
void drive() {
super.drive();
print('Im also charging at the same time using regenerative braking!');
}
}

So in this case if we call the drive method on the subclass, it will call the parent’s drive method first, printing ‘Im driving!’ and then execute the rest of the code in the subclass’ drive method, printing ‘Im also charging at the same time using regenerative braking!’.

4 pillars of OOP summary

  • Abstraction: Each class has a specific job.
  • Encapsulation: Classes shouldn’t mess with each other’s jobs.
  • Inheritance: Subclasses can extend their superclasses’ functionality.
  • Polymorphism: Subclasses can override their superclasses’ functionality.

Creating the TWDCharacterQuiz class

Putting what we learned in practice, I made a class called TWDCharacterQuiz in a separate TWDCharacterQuiz.dart file that will deal the quiz data, while main.dart remains more focused on the UI.

Try to go through it and see if you understand it without any explanations and read the breakdown after.

import 'dart:convert';
import 'dart:math';
class TWDCharacterQuiz {
Map _quizData;
int _questionNumber = 1;
int _totalQuestions = 10;
List<int> _characterScores = [0, 0, 0, 0, 0, 0, 0]; int _winnerIndex; TWDCharacterQuiz() {
var jsonString = '''
{"quiz_congratulations":"", ... a la carte anyone?)."}}}
''';
_quizData = jsonDecode(jsonString);
}
int getTotalNumberOfQuestions() {
return _totalQuestions;
}
int getQuestionNumber() {
return _questionNumber;
}
String getQuestionText() {
return _quizData['questions']['$_questionNumber']['text'];
}
String getAnswerText({int option}) {
return _quizData['questions']['$_questionNumber']['options']
['$option']['text'];
}
void nextQuestion() {
_questionNumber++;
}
void updateScores({int optionPicked}) {
for (int i = 0; i < 7; i++) {
_characterScores[i] += int.parse(_quizData['questions']
['$_questionNumber']['options']['$optionPicked']
['scores']['${286 + i}']);
}
}
bool isFinished() {
if (_questionNumber == _totalQuestions) {
return true;
} else {
return false;
}
}
void calculateResults() {
int maxScore = _characterScores.reduce(max);
_winnerIndex = _characterScores.indexOf(maxScore);
}
String getWinnerImageURL() {
return _quizData['outcomes']['${286 + _winnerIndex}']['image'];
}
String getWinnerName() {
return _quizData['outcomes']['${286 + _winnerIndex}']['name'];
}
String getWinnerText() {
return _quizData['outcomes']['${286 + _winnerIndex}']['text'];
}
void reset() {
_questionNumber = 1;
_characterScores = [0, 0, 0, 0, 0, 0, 0];
}
}

I’ll break it down bit by bit now.

Map _quizData; will hold the data for the quiz as we decode it from JSON in the constructor TWDCharacterQuiz() as we did in the beginning of the episode.

int _questionNumber = 1; will hold the current question number and is initialised to 1 and int _totalQuestions = 10; will hold the total number of questions and is fixed to 10.

List<int> _characterScores = [0, 0, 0, 0, 0, 0, 0]; will hold the sums of each character’s score as we progress through the quiz and int _winnerIndex; will hold the index of the character with the highest score at the end.

All the properties are made private and only readable through the getter methods.

getTotalNumberOfQuestions() and getQuestionNumber() are pretty clear.

In getAnswerText({int option}) we pass it the option for which answer we want the text for, and then we drill through the Map object to find it:

String getAnswerText({int option}) {
return _quizData['questions']['$_questionNumber']['options']
['$option']['text'];
}

In nextQuestion() we increment the _questionNumber by 1.

In updateScores({int optionPicked}) we pass it the answer option that the user selected (1 to 4) and then we update the scores.

void updateScores({int optionPicked}) {
for (int i = 0; i < 7; i++) {
_characterScores[i] += int.parse(_quizData['questions']
['$_questionNumber']['options']['$optionPicked']
['scores']['${286 + i}']);
}
}

We use a for loop to iterate from 0 to 6 (7 times in total) and for each score field (286 to 292) we add the score to the corresponding element in _characterScores .

The a += b operator is equivalent to writing a = a + b .

The int.parse(aString); function converts a String to an int.

The ${} notation is the string interpolation if we have operation to do within the curly braces. So '${286 + i}' adds 0–6 to 286, giving us the 286–292 numbers and then converts it to a string so we can use it as an index inside our Map object.

The bool isFinished() method, checks if the _questionNumber is equal to the _totalQuestions and if yes returns true, otherwise returns false.

calculateResults() is called when the quiz is finished to calculate the results.

void calculateResults() {
int maxScore = _characterScores.reduce(max);
_winnerIndex = _characterScores.indexOf(maxScore);
}

Using the reduce method of our _characterScores List object, we pass it the max function from the dart:math library.

The reduce method: ‘Reduces a collection to a single value by iteratively combining elements of the collection using the provided function.’

This means that it starts with the first element, compares it to the next one using the provided function (max function) and if its greater it keeps it, otherwise it discards it. Then it moves to the next element, etc, and this way it goes through all the elements in the List, finding you the max element.

Now that we have the highest score, we use the indexOf method to find the index of the maxScore and assign it to _winnerIndex.

With getWinnerImageURL() , getWinnerName() , getWinnerText() , we drill down through the outcomes field of our Map object to retrieve the winner info. We add 286 to the _winnerIndex so that it matches with the character’s codename.

Finally, in the reset() method, we reset the game quiz state to its initial state, which is 1 for the _questionNumber and [0, 0, 0, 0, 0, 0, 0] for _characterScores .

Updating the UI dynamically with data from TWDCharacterQuiz

Now we just need to make a few modifications to main.dart so we can make use of the data that we have in TWDCharacterQuiz.

First of all, don’t forget to import the file at the top and instantiate a new TWDCharacterQuiz object. I named the object quiz for simplicity.

import 'TWDCharacterQuiz.dart';TWDCharacterQuiz quiz = TWDCharacterQuiz();

Now, in the Column that we had before, inside the build method of the StatefulWidget, we replace the hardcoded values with calls to the getter methods of our quiz object.

Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Question ${quiz.getQuestionNumber()}/${quiz.getTotalNumberOfQuestions()}',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 32.0,
),
),
Text(
quiz.getQuestionText(),
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 26.0,
),
),
Column(
children: <Widget>[
answerButton(option: 1), // removed text: 'Katana'
answerButton(option: 2), // removed text: 'Revolver'
answerButton(option: 3), // removed text: 'Crossbow'
answerButton(option: 4), // removed text: 'Hammer'
],
)
],
);

In the answerButton method we don’t need the text parameter anymore since we’re getting our answer text through the quiz object, passing it the answer option (1–4).

Padding answerButton({int option}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text(
quiz.getAnswerText(option: option),
style: TextStyle(
fontFamily: 'Zombie Queen',
fontSize: 26.0,
color: Colors.white,
),
),
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
color: Colors.red[800],
onPressed: () {
setState(() {
quiz.updateScores(optionPicked: option);
if (quiz.isFinished()) {
quiz.calculateResults();
_showResult();
} else {
quiz.nextQuestion();
}
});

},
),
);
}

The biggest change is in the onPressed callback function of the RaisedButton widget. Here is where we react to the user’s input of tapping one of the answer buttons.

We wrap everything inside the setState() function because we are updating the state of the app and we want the changes reflected in the UI.

Then we call the quiz.updateScores(optionPicked: option) method of the quiz object to update the scores and check if the quiz is finished through the quiz.isFinished() method. If it’s finished, we calculate the final results using the quiz.calculateResults() method and display the result to the user by calling the _showResult() method. If it’s not finished yet we move on to the next question by calling quiz.nextQuestion().

Displaying the result using the AlertDialog class

The final missing piece is this _showResult() method that we called at the end of the quiz.

Here it is. Same as before, I’ll break it down after.

Future<void> _showResult() async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.red[800],
title: Text(
quiz.getWinnerName(),
textAlign: TextAlign.center,
style: TextStyle(...),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.network(quiz.getWinnerImageURL()),
SizedBox(height: 30.0),
Text(
quiz.getWinnerText(),
style: TextStyle(...),
),
],
),
actions: <Widget>[
FlatButton(
child: Text(
'Retake Quiz',
textAlign: TextAlign.center,
style: TextStyle(...),
),
onPressed: () {
setState(() {
quiz.reset();
});
Navigator.of(context).pop();
},
),
],
);
},
);
}

This method overlays an AlertDialog widget on top of the current UI.

The barrierDismissible: false property means that the user cannot dismiss the alert by tapping anywhere on the screen. He has to tap the action button to dismiss it.

We can style the alert however we want, setting the backgroundColor for example.

We can set a title, using the quiz.getWinnerName() method.

We can also set the content. Here I added a Column with three children:

  • An Image that is fetched from the network using the image url that we get by calling Image.network(quiz.getWinnerImageURL())
  • A SizedBox just to add some padding between the Image and the Text.
  • And a Text that displays the text that we get from calling quiz.getWinnerText()

I also set mainAxisSize: MainAxisSize.min so that the Column doesn’t get any longer that it needs to.

Finally, we can set the action of the alert through the FlatButton. We just want to reset the quiz when the user taps on ‘Retake Quiz’.

In the onPressed callback function we call setState() again because we want to update the UI with the new state and inside call quiz.reset() to reset the quiz to its initial state.

This is the result:

Beautiful, isn’t it?

I actually got Michonne btw. I guess choosing Katana on the first question gave me away 😂.

If you want to take the challenge and build this app, share your result in the comments, I’d love to see it!

Here’s the final code again.

Till next time!

ALERT: Next episodes are out!

--

--