Last active
January 13, 2026 13:52
-
-
Save PlugFox/66bca21faaaf8613857ade983c78a47b to your computer and use it in GitHub Desktop.
Questions example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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