Implementing a simple survey app with conditional questions in Flutter

Michel Thomas
5 min readMay 16, 2021
Photo by Jens Lelie on Unsplash

Introduction

In life, we are met with a myriad of choices that we have to take. Sometimes it all boils down to a simple polar question that decides what follows next. Regarding whether it was the right decision or not, is an entirely different matter altogether but what we do know, with absolute certainty, is that the choices that we make, tell us a lot about who we are and what we’d do when faced with a certain obstacle or maybe perhaps even an opportunity. Surveys are the best way to profile a person with their consent.

Here, we’re going to create a simple survey app using a package that I wrote called ‘flutter_survey’. It handles the creation and state of a dynamic questionnaire/survey/data collection form with conditional/nested questions like the ones you see on Google forms that show or hide the questions that follow, based on the user’s input. All you need to do is frame the questions in a specific way and the package handles the widget creation and the state management for you.

flutter_survey package in action

As you can see from the gif above, you can nest questions. They’re dynamically rendered based on the user’s input. You can even mark them as mandatory which will force the user to enter a value.

Head over to pub.dev to install the package.

Usage

Now let’s start by exploring the format and structure of the Question object.

Question(
question : String,
isSingleChoice : bool,
answerChoices : Map<String, List<Question>?>,
isMandatory : bool,
errorText : String,
answers : List<String>
)

Let’s explore the fields of the object that dictates the entire flow of the survey.

  • question : Used to store the actual string of text that is to be rendered.
  • isSingleChoice : If false then it is a Multiple Choice Question.
  • answerChoices : Is basically a set of unique answers or answer that can be selected by the user. Leaving this empty or setting it to null will cause the answer to be a TextFormField that accepts a string of text from the user. The keys of this Map are the answers whereas the value happens to be a nullable List of Question objects. Three cases that can be observed here are :

A. Sentence Based Answer

answerChoices={} 

answersChoices is not set : Implies that the answer will need to typed in by the user.

B. Choice Based Answer

answerChoices={
"Possible Answer 1" : null,
"Possible Answer 2" : null,
.
.
.
"Possible Answer n" : null
}

answerChoices has n number of keys with null as their values. What this basically means is that answers listed above have no corresponding branch or List of questions that follow it upon being selected.

C. Conditional/Nested Questions

answerChoices={
"Possible Answer 1" : null,
"Possible Answer 2" : [
Question(
question: "Possible Question 1.2.1.1",
answerChoices : {
Question(
question : "Possible Question 1.2.1.2",
answerChoices:{
....Question(
question:"Possible Question 1.2.1.n"
answerChoices : {...}
)

}
)}
)
.
.
.
Question(
question : "Possible Question 1.2.2.n"
)
], //List of 'Possible Answer 2' ends here
.
.
.
"Possible Answer n" : null
}

Nesting of questions( theoretically infinite) occurs in this case. The structure is fairly intuitive. In action, selecting an answer would render the set of questions that follow it and selecting an answer from one of those questions could possible confront you with more questions that follow that particular answer. They are dynamically shown depending on the selected choice. There is no limit on how far can we go on extending this structure. Possibly, till the point we can comprehend it.

  • isMandatory : As the name implies, setting it to true will force the user to enter an input as validation fails if the field is empty.
  • answers : A mutable property, basically stores the list of answers selected by the user.
  • errorText : An optional parameter for the message to be shown upon failure in validation. This is specific to the question.

Now let’s combine a bunch of questions and form a structure:

This structure will determine the order in which the questions appear, whether they’re nested or not.

Pass the list of questions to the Survey Widget

This is the main widget that handles the survey. The list of questions is passed to the initialData field.

The Survey widget has a bunch of other useful fields:

  • defaultErrorText : An optional parameter for the message to be shown upon failure in validation. This applies to all questions that don’t have the errorText property set.
  • builder : A function that can be used to construct custom form field widgets to render the question.
final Widget Function(BuildContext context, Question question,      void Function(List<String>) update)? builder;

The question parameter contains all the information required to construct a field of the form. The update parameter is a callback that is used to update the internal state of the Survey widget.

  • onNext : Is a callback that can be called with the result of the survey which happens to be a List of QuestionResult objects.

Let us now explore the structure of a QuestionResult object:

QuestionResult(
question : String,

children: List<QuestionResult>,

answers : List<String>
)
  • question : The field contains the actual question as a string of text.
  • children : The List of questions that are a part of this question or nested under this question in the List of Question objects passed to the initialData field of the Survey widget.
  • answers : The answer(s) selected or typed in by the user.

Validation

Performing validation is as simple as wrapping the Survey widget with a Form widget and using a Global Key to access the validate() method.

Here’s the full code for the project:

And that’s it. I hope this article was worth your time.

Links:

--

--