Menu
CoddyTech

Trắc nghiệm: Chơi lại

Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 17 trên 36.

Một bài trắc nghiệm khiến mọi người thích thú sẽ được chơi lại lần thứ hai. Thêm nút Chơi lại vào màn hình kết quả để đưa bạn về câu hỏi 1 với điểm số bằng 0.

Bản thân nút này chỉ đóng màn hình kết quả: Navigator.pop(context). Việc bắt đầu lại thuộc về màn hình trắc nghiệm, vì chỉ số và điểm số nằm trong trạng thái của màn hình đó.

Navigator.push trả về một Future, hoàn tất khi màn hình được đẩy lên bị đóng. Hãy để _next là async và await lệnh push: các dòng sau đó sẽ chạy khi bạn quay lại, và đây là thời điểm để đặt lại.

await Navigator.push(context, MaterialPageRoute(...));
setState(() {
  _index = 0;
  _picked = null;
  _score = 0;
});

Mũi tên quay lại trên màn hình kết quả cũng đóng màn hình đó, vì vậy nó cũng bắt đầu một lượt mới. Màn hình kết quả không có màn hình nào để quay lại, nên đó là điều bạn muốn.

challenge icon

Thử thách

Trung bình

Thêm nút chơi lại:

  • bên dưới điểm số trên màn hình kết quả, sau khoảng trống 32 pixel, thêm FilledButton.icon cùng Icons.replay và nhãn Play again để đóng màn hình
  • thêm async vào _next, dùng await với thao tác push, sau đó đặt lại chỉ số, câu trả lời và điểm số về ban đầu bên trong setState

Tự làm thử

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const QuizScreen(),
    );
  }
}

class Question {
  const Question(this.text, this.answers, this.correct);

  final String text;
  final List<String> answers;
  final int correct;
}

const List<Question> questions = [
  Question('Which widget stacks its children from top to bottom?', ['Row', 'Column', 'Stack', 'Wrap'], 1),
  Question('Which language do you write Flutter apps in?', ['Kotlin', 'Swift', 'Dart', 'JavaScript'], 2),
  Question('What does setState do?', ['Rebuilds the widget', 'Saves the app', 'Closes the screen', 'Plays a sound'], 0),
  Question('Which widget shows a list you can scroll?', ['Container', 'SizedBox', 'Padding', 'ListView'], 3),
  Question('Which widget adds space around its child?', ['Padding', 'Center', 'Icon', 'Text'], 0),
];

class QuizScreen extends StatefulWidget {
  const QuizScreen({super.key});

  @override
  State<QuizScreen> createState() => _QuizScreenState();
}

class _QuizScreenState extends State<QuizScreen> {
  int _index = 0;
  int? _picked;
  int _score = 0;

  Question get _question => questions[_index];
  bool get _answered => _picked != null;
  bool get _isLast => _index == questions.length - 1;

  void _pick(int answer) {
    if (_answered) return;
    setState(() {
      _picked = answer;
      if (answer == _question.correct) {
        _score++;
      }
    });
  }

  void _next() {
    if (_isLast) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => ResultsScreen(score: _score, total: questions.length),
        ),
      );
      return;
    }
    setState(() {
      _index++;
      _picked = null;
    });
  }

  Color? _colorFor(int answer) {
    if (!_answered) return null;
    if (answer == _question.correct) return Colors.green;
    if (answer == _picked) return Colors.red;
    return null;
  }

  @override
  Widget build(BuildContext context) {
    final question = _question;
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Quiz'),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 16),
            child: Center(
              child: Text('Score: $_score', style: textTheme.titleMedium),
            ),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Question ${_index + 1} of ${questions.length}',
              style: textTheme.labelLarge,
            ),
            const SizedBox(height: 8),
            LinearProgressIndicator(
              value: (_index + 1) / questions.length,
              minHeight: 8,
              borderRadius: BorderRadius.circular(4),
            ),
            const SizedBox(height: 24),
            Text(question.text, style: textTheme.headlineSmall),
            const SizedBox(height: 24),
            for (int i = 0; i < question.answers.length; i++)
              Padding(
                padding: const EdgeInsets.only(bottom: 12),
                child: FilledButton.tonal(
                  style: FilledButton.styleFrom(
                    backgroundColor: _colorFor(i),
                    foregroundColor: _colorFor(i) == null ? null : Colors.white,
                    padding: const EdgeInsets.all(16),
                  ),
                  onPressed: () => _pick(i),
                  child: Text(question.answers[i]),
                ),
              ),
            if (_answered) ...[
              const SizedBox(height: 12),
              Text(
                _picked == question.correct
                    ? 'Correct!'
                    : 'The answer is ${question.answers[question.correct]}',
                textAlign: TextAlign.center,
                style: textTheme.titleMedium,
              ),
              const SizedBox(height: 16),
              FilledButton(
                onPressed: _next,
                child: Text(_isLast ? 'See results' : 'Next'),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class ResultsScreen extends StatelessWidget {
  const ResultsScreen({super.key, required this.score, required this.total});

  final int score;
  final int total;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Results')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                'You scored $score out of $total',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Dự án

Tự luyện tập: Trình biên dịch Flutter online