Flutter Riverpod Quiz App Tutorial | Apps From ScratchFeb 17, 2021
Source Code: https://github.com/MarcusNg/flutter_riverpod_quiz
Users are presented with a question with four possible answers from a random quiz. If the correct answer is chosen, then a green border and green checkmark icon appear around the answer. If the incorrect answer is chosen, then a red border and red X icon appear around the selected answer. The correct answer to the question is also revealed.
The Next Question button moves users to the next question. When users finish their quiz, they can view their quiz results. Tapping on New Quiz will load up a new quiz for our users.
The first thing we’re going to do is create a new flutter project called flutter_riverpod_quiz. Inside of our pubspec.yaml file, let’s add the necessary dependencies.
dio is for sending http requests and allows us to fetch data from the quiz API.
enum_to_string lets us convert enum values to strings.
equatable makes it super easy to compare objects in Dart.
html_character_entities is for decoding the html character entities that are sometimes present in the strings returned from the API.
meta is for dart annotations.
The API we’re retrieving quizzes from is the Open Trivia Database.
We can generate a URL by going to the API and choosing the number of questions, category, difficulty, and type.
When we go to the generated url, we get back a JSON that has a response code and a results list. We want to focus on an individual quiz question in this results list in order to create our Question Model schema.
In a new folder called models, let’s add a file called question_model.dart.
Question Model contains a
class Question that
extends Equatable. If we look at the JSON, all of the values are Strings except for
incorrect_answers, which is a
List<Strings>. Let’s parse the question information we want: category, difficulty, question, correct answer, and all answers. Now answers is going to be a
List<Strings> containing the correct answer and the incorrect answers.
Let’s generate our constructor and make all of these parameters
@required. Add our Equatable
props so we’re able to compare Questions objects with one another. And lastly we’ll add a factory constructor called
fromMap that takes
If map is
null, then return
null, but if it’s not
null, then we can return a Question. By referencing the JSON blob, we’re able to easily create the Question because we know what data the API gives us.
The double question mark is a null aware operator that makes sure if the expression on the left evaluates to
null, we assign an empty string or empty list depending on the situation. This is so we prevent calling any methods on
null, which would result in an error.
Answers is a shuffled list of all possible answers.
And now we’re all done with our Question Model.
Base Quiz Repository and Difficulty Enum
Now make a new folder called repositories with a quiz folder. Here we’ll make two files: base_quiz_repository and quiz_repository.
Base quiz repository is an abstract class that contains the signatures of any methods quiz repository needs to implement. Since this quiz app is a simple example, we only need to have one method called
getQuestions returns a
Future<List<Questions>> and takes in three parameters: the number of questions, the category Id, and a difficulty enum.
The number of questions is how many questions we want the API request to return.
The category id is the integer id of a category defined by Open Trivia Database. We can view all the different categories and their corresponding ids by going to the API Documentation, and going to the Category Lookup endpoint.
At the moment, there’s 24 different categories with IDs ranging from 9 to 32, inclusive.
The Difficulty enum is for defining the question difficulty. We’ll make this enum in an enums folder that has difficulty.dart. The enum has four values: any, easy, medium, and hard.
Quiz Repository and Failure Model
Inside quiz_repository, we have a
QuizRepository that extends BaseQuizRepository.
QuizRepository has a dependency on
Reader from Riverpod.
Reader allows the QuizRepository to read other providers in the app. We’re going to use this to access our
dioProvider to make HTTP requests. We’ll define our dioProvider at the top of this file.
Next we can implement
getQuestions. In a try catch block, we should define the query parameters for our GET request.
type is 'multiple', so we only get multiple choice questions.
amount is the number of questions.
category is the category id.
We only want to pass in
difficulty as a query parameter if it is not equal to
We can make the request to the API by accessing our
dioProvider with read and passing in the endpoint with the query parameters.
If the response is successful, we convert
Map<String, dynamic>, grab the list of results, and then return the
List<Question>. If the request is not successful or the results list is empty, we return an empty list.
For our catch block, we’re going to handle
SocketExceptions. We’ll throw our own model called
Failure, which we’ll define in our models folder.
Failure is a class that contains a String message.
To access this QuizRepository anywhere in the app, we should declare
quizRepositoryProvider, providing our QuizRepository and passing in
ref.read as the Reader.
Let’s create a folder called controllers that has a folder called quiz.
Inside we’ll make two files: quiz_state and quiz_controller
The UI will interact with the Quiz Controller to modify our Quiz State. Based on the Quiz State, our UI will react accordingly and render the appropriate widgets. If you’re familiar with Bloc, this is very similar to using a cubit where the quiz controller is our cubit.
We can breakdown the UI of our app to figure out what values our quiz state needs and how our quiz controller should modify the quiz state.
This screen has four different scenarios:
- The user has not chosen an answer (initial)
- The user has selected a correct answer (correct)
- The user has selected an incorrect answer (incorrect)
- And the user has completed the quiz (complete)
We’ll keep track of this with an enumeration called QuizStatus.
To know which answer the user selects, we need a String that stores the selected answer.
And finally we need to remember which questions the user gets right and wrong so we can show the results at the end of the quiz. In our app, we just show the number of correct questions, but we’re going to keep track of both correct and incorrect answers in case you want to expand on the project and show the actual questions to the user.
I added a getter named answered which returns a boolean based on if the quiz status is currently incorrect or correct. This will clean up our UI code as we’ll have to check for this a couple of times.
QuizState has a factory constructor called
initial that returns a default state when the quiz is first loaded. Remember to add a
copyWith method to easily modify values in quiz state.
Let’s move onto our QuizController. QuizController extends a StateNotifier with type QuizState and the super constructor returns the initial quiz state.
We have three methods to modify our quiz state.
submitAnswer for when a user taps on an answer. We check if the current state is answered and return, to prevent users from submitting answers multiple times. Our state is modified using copyWith based on if the answer is correct or incorrect.
nextQuestion for when a user taps the NextQuestion or SeeResults button. The selectedAnswer is set back to an empty string and the status is updated depending on if we are at the last question or not.
reset for resetting the quiz when the user taps on New Quiz.
At the top of this file, let’s provide this QuizController to our app using a
StateNotifierProvider. Tacking on
autoDispose will make sure the state of our provider is destroyed when it’s no longer used. This would be more beneficial if we had an app with navigation between multiple screens, but I’ll leave this in anyway.
main.dart and Quiz UI
We’re finally ready to start working on the UI of our app. I’ll be creating all of the UI inside of main.dart, but feel free to refactor each widget into its own file for better organization. I’m going to remove the counter example and import all of the necessary files and packages.
To access any provider in our app, we have to wrap our
ProviderScope. We’ll set
debugShowCheckedModeBanner to false, the
Colors.yellow, and the background of any bottom sheet to
Home is set to QuizScreen, which we’ll add right now. QuizScreen is a HookWidget with a gradient background and Scaffold.
To fetch questions using our quiz repository, we can make a FutureProvider that returns a
List<Questions> by calling our
getQuestions method. We use
ref.watch to access our
quizRepositoryProvider instead of
ref.read because we want our quiz repository to give us new questions whenever we refresh this provider.
As I mentioned before, there are currently 24 different categories with IDs ranging from 9 to 32, inclusive, so we can generate a random category using
Random.nextInt(). And we’ll set the difficulty to
We can now get this provider by doing useProvider(quizQuestionsProvider).
We’ll also create a
PageController with Flutter Hooks so we can programmatically animate our PageView to the next page.
The body of our scaffold calls
.when on our
quizQuestionsProvider to build different UI when our future has data, is loading, or has an error.
When our future has data, we call
_buildBody and pass in
pageController, and the returned questions.
For loading, we
return const Center(child: CircularProgressIndicator());
And for error, we’ll return a QuizError that checks if the error is of type
Failure, and returns the error message.
Let’s write our QuizError underneath this widget. It’s a centered column with a text widget and CustomButton we use throughout our app.
When the user taps on this button we refresh our
quizRepositoryProvider to make the API call again.
CustomButton takes a String title and VoidCallback onTap. It’s a styled container with a gesture detector and text widget.
We’ll define a boxShadow above as we’re going to also use it for our AnswerCard and CircularIcon widgets.
Back in our QuizScreen, we need to add a bottomSheet. We use
maybeWhen because we only want to show a widget when we have a
List<Question> from the API. We display the CustomButton only after a question is answered and move to the next page in our pageview when we’re not at the last question in our list.
We still have to write our
_buildBody method. If no questions are found, we return a quiz error. If the current quiz status is complete, then we return QuizResults, otherwise we return our QuizQuestions.
QuizResults displays the number of questions the user got correct and has a CustomButton to start a new quiz. We also have to reset our quizController’s state.
QuizQuestions takes in the
questions list to display each question to our users. It returns a
PageView.builder with the physics set to
NeverScrollablePhysics to disable users from scrolling. Using the index to grab each question, we display the question number in a Text widget with the actual question underneath. Since some strings have encoded HTML character entities, we can decode them with the HTML character entities package.
Let’s add a divider and a column with children mapped to a list of AnswerCards. The
onTap calls our quiz controller’s
submitAnswer method passing in the
AnswerCard has a String
isDisplayingAnswer, and VoidCallback
onTap. The build method is a GestureDetector with a child Container. The decoration has our B
oxShadow we defined earlier and only displays a border if we are currently displaying the correct answer. Because we only want to display a red border around the answer a user selects, we also include an
The container’s child is a row widget with a Flexible Text answer and a CircularIcon that only displays after submitting an answer.
CircularIcon takes in IconData and Color. It returns a circle container with a BoxShadow and white icon.
🎉 Success 🎉
We’re now all done with our Flutter Riverpod Quiz App. Check out my YouTube channel and courses for more Flutter content.
Get notified about new courses and updates