Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active January 13, 2026 13:52
Show Gist options
  • Select an option

  • Save PlugFox/66bca21faaaf8613857ade983c78a47b to your computer and use it in GitHub Desktop.

Select an option

Save PlugFox/66bca21faaaf8613857ade983c78a47b to your computer and use it in GitHub Desktop.
Questions example
/*
* Questions example
* https://gist.github.com/PlugFox/66bca21faaaf8613857ade983c78a47b
* https://dartpad.dev?id=66bca21faaaf8613857ade983c78a47b
* Mike Matiunin <plugfox@gmail.com>, 13 January 2026
*/
// ignore_for_file: curly_braces_in_flow_control_structures, avoid_print
import 'dart:async';
import 'package:flutter/material.dart';
/// A unique identifier for a question.
typedef QuestionID = String;
/// A question with a unique [id], the question [text], and an optional [answer].
@immutable
class Question {
const Question({required this.id, required this.text, required this.answer});
const Question.unanswered({required this.id, required this.text})
: answer = null;
final QuestionID id;
final String text;
final bool? answer;
}
/// Record representing an answered question.
typedef Answer = ({QuestionID id, String text, bool answer});
/// A collection of [Question]s identified by their [QuestionID]s.
extension type Questions._(Map<QuestionID, Question> questions) {
Questions(Iterable<Question> questions)
: this._(<QuestionID, Question>{
for (final question in questions) question.id: question,
});
/// Returns the next unanswered question, or `null` if all questions are answered.
Question? get nextQuestion {
for (final question in questions.values) {
if (question.answer != null) continue;
return question;
}
return null;
}
/// All questions in this collection.
List<Answer> get answers => <Answer>[
for (final question in questions.values)
if (question case Question(:final id, :final text, :final answer?))
(id: id, text: text, answer: answer),
];
/// Answers the question with the given [id].
void answer(QuestionID id, {required bool answer}) {
if (questions[id] case final question?)
questions[id] = Question(
id: question.id,
text: question.text,
answer: answer,
);
}
/// Resets all questions to unanswered.
void reset() => questions.updateAll(
(id, question) => Question.unanswered(id: question.id, text: question.text),
);
}
void main() => runZonedGuarded<void>(
() => runApp(const App()),
(error, stackTrace) => print('$error'),
);
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(
title: 'Questions',
debugShowCheckedModeBanner: false,
home: QuestionsScreen(),
);
}
class QuestionsScreen extends StatefulWidget {
const QuestionsScreen({super.key});
@override
State<QuestionsScreen> createState() => _QuestionsScreenState();
}
class _QuestionsScreenState extends State<QuestionsScreen> {
final Questions _questions = Questions(<Question>[
const Question.unanswered(id: 'q1', text: 'Is the sky blue?'),
const Question.unanswered(id: 'q2', text: 'Is grass green?'),
const Question.unanswered(id: 'q3', text: 'Is fire hot?'),
const Question.unanswered(id: 'q4', text: 'Is water wet?'),
]);
Question? _question;
@override
void initState() {
super.initState();
_question = _questions.nextQuestion;
}
void _answerQuestion(bool answer) {
final question = _question;
if (question == null) return;
setState(() {
_questions.answer(question.id, answer: answer);
_question = _questions.nextQuestion;
});
}
void _resetQuestions() {
setState(() {
_questions.reset();
_question = _questions.nextQuestion;
});
}
Widget _buildQuestion(Question question) => Column(
key: ValueKey(question.id),
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: <Widget>[
Text(
question.text,
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: <Widget>[
ElevatedButton(
onPressed: () => _answerQuestion(true),
child: const Text('Yes'),
),
ElevatedButton(
onPressed: () => _answerQuestion(false),
child: const Text('No'),
),
],
),
],
);
Widget _buildResults() => ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 64),
shrinkWrap: true,
children: <Widget>[
const Text(
'Results',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
for (final (:id, :text, :answer) in _questions.answers)
Center(
child: SizedBox(
width: 640,
child: ListTile(
key: ValueKey(id),
title: Text(text),
trailing: switch (answer) {
true => const Icon(Icons.check, color: Colors.green),
false => const Icon(Icons.close, color: Colors.red),
},
),
),
),
],
);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Questions'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _resetQuestions,
tooltip: 'Reset Questions',
),
const SizedBox(width: 8),
],
),
body: SafeArea(
child: Center(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: switch (_question) {
Question question => _buildQuestion(question),
null => _buildResults(),
},
),
),
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment