Flutter

5-4. Riverpod (QuizApp)

모리선생 2023. 4. 18. 07:38
728x90

목표

Riverpod을 사용하여서 앱의 상태관리를 하는 법에 대해 예제를 사용하여 확인한다. Quiz App을 만듦으로써 Riverpod 이외에도 다양한 위젯들을 적용하여 확장할 수 있는 방법들을 확인하여 보자 (예시: showSnakBar).


Quiz App을 만들어보자. 다음의 순서대로 만들면 된다.

참고로 @ 표시는 file의 이름 앞에 붙여주는 접두사라 생각을 하자. 예를 들어 @ quiz.dart이라고 한다면, quiz.dart 파일을 만든 후 해당 코드들을 입력해주길 바란다.

 

  • dependencies (@ pubspec.yaml)
  flutter_riverpod: ^2.3.4

 

  • @ quiz.dart

어떤 형태의 자료를 불러올 것인가를 선언하는 곳이다. class Question을 만들어 주고 String, int등을 사용해서 question, options, answer 등의 데이터들을 불러온다. 이때 options의 경우 여러가지를 불러 올것이기에 List로 감싸서 불러준다.

class Question {
  final String question;
  final List<String> options;
  final int answer;

  Question(
      {required this.question, required this.options, required this.answer});
}

 

  • quiz_state.dart

ChangeNotifier를 사용하여 앱의 변경 상태를 확인하는 클래스를 만들도록 하자. ChangeNotifier를 사용하였을때, QuizState 클래스에서는 '퀴즈의 정답 갯수 그리고 현재 페이지의 위치'등을 확인할 수 있도록 설정할 것이다. currentIndex는 현재 페이지 그리고 _score는 초기 점수와 그리고 누적 점수들을 의미한다. 

notifyLinsteners()로 대기를 하고 있던 ChangeNotifierProvider는 notifyListeners()가 호출이 될때 자신의 자식을 재빌드하여서 UI를 업데이트 한다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quiz_app_practice/question_bank.dart';

final quizStateProvider =
    ChangeNotifierProvider<QuizState>((ref) => QuizState());

class QuizState extends ChangeNotifier {
  int _currentIndex = 0;
  int get currentIndex => _currentIndex;
  set currentIndex(int index) {
    _currentIndex = index;
    notifyListeners();
  }

  int _score = 0;
  int get score => _score;

  void reset() {
    _score = 0;
    _currentIndex = 0;
    notifyListeners();
  }

  void answer(int index, int answer) {
    if (answer == questionBank[index].answer) {
      _score++;
    }
    if (index < questionBank.length - 1) {
      _currentIndex = index + 1;
    }
    notifyListeners();
  }
}

여기서 questionBank라고 있는 것은 질문, 답, 옵션들을 적어놓은 클래스이다. 조금 있다가 보게될 것이니, 일단은 적어놓자. answer == questionBank[index].answer가 if구문을 통해 검증이 되면 _score++를 통해 점수를 부여한다. 계속해서 다음 페이지로 넘어가야 하니 index < questionBank.length - 1 검증을 통해서 _currentIndex = index + 1 다음 페이지로 넘어갈 수 있도록 조건문을 걸어준다. 자, 그럼 우리가 필요한건 score, currentIndex 등의 값이 screen.dart에 필요할 것이다.

 

  • quiz_screen.dart

UI를 담당하는 부분이다. Riverpod 혹은 Flutter를 공부하면서 느끼는 것이지만 UI에서 어떤 정보를 출력해야하는지에 대해 생각을 함에 따라 UI를 어떤식으로 구성을 해야할지도 중요한 부분인 듯 하다. 여기서는 간단하게 질문 - 옵션 - Next Page 등으로 간단하게 앱을 구성하였으나, 원한다면 추가적으로 더 꾸며보아도 된다.

 

Riverpod에서는 StatelessWidget과 비슷한 개념의 ConsumerWidget이 있다. 단순하다. 그냥 비슷하다고 생각하자. quizStateProvider (@ quiz_state.dart에서 선언)을 quizState로 재선언을 해준다 (상태를 계속해서 확인해야하니까. ref.watch로 감싸주었다).

 

questionBank내 quizstate.currentIndex에 해당하는 페이지의 질문을 불러오고자 .question을 불러준다. 즉 questionBank[quizstate.currentIndex].question 을 card내에 불러준다. 같은 방식으로 option들도 불러주되, 이번에는 .option.length를 붙여서 option의 수만큼 객체를 불러준다.

 

quizstate.currentIndex == questionbank.length - 1 일때 showsnakbar를 사용하여 현재 점수가 얼마인지 확인할 수 있도록 한다. 팝업은 좀 굴욕적인듯 해서. SnackBar로 처리했다. 개인에 따라 다르게 만들 수도 있다.

import 'package:flutter/material.dart';
import 'package:quiz_app_practice/quiz_state.dart';
import 'package:quiz_app_practice/question_bank.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final quizStateProvider =
    ChangeNotifierProvider<QuizState>((ref) => QuizState());

class QuizScreen extends ConsumerWidget {
  const QuizScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final quizState = ref.watch(quizStateProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Quiz App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Expanded(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          questionBank[quizState.currentIndex].question,
                          textAlign: TextAlign.center,
                          style: const TextStyle(
                              fontSize: 20, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 16),
                        ...List.generate(
                          questionBank[quizState.currentIndex].options.length,
                          (index) => ElevatedButton(
                            onPressed: () {
                              quizState.answer(quizState.currentIndex, index);
                            },
                            child: Text(
                              questionBank[quizState.currentIndex]
                                  .options[index],
                              style: const TextStyle(fontSize: 16),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(16),
              child: ElevatedButton(
                onPressed: () {
                  if (quizState.currentIndex == questionBank.length - 1) {
                    final snackBar = SnackBar(
                      content: Text(
                          'Your score is ${quizState.score} out of ${questionBank.length}'),
                    );
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                    quizState.reset();
                  } else {
                    quizState.currentIndex++;
                  }
                },
                child: Text(quizState.currentIndex == questionBank.length - 1
                    ? 'Finish'
                    : 'Next'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  • question_bank.dart

이 부분은 quiz의 종류들을 적어 놓은 곳이다. 여기는 3개 밖에 안했지만, 20개를 하면 총 20개의 문제가 나올 것이다.

import 'quiz.dart';

List<Question> questionBank = [
  Question(
    question: "What is the capital of France?",
    options: ['Paris', 'Berlin', 'London', 'Madrid'],
    answer: 0,
  ),
  Question(
    question: 'What is the largest planet in our solar system?',
    options: ['Jupitor', 'Saturn', 'Neptune', 'Earth'],
    answer: 0,
  ),
  Question(
    question: 'What is the currency of Korea',
    options: ['Won', 'Yen', 'Dollar', 'Pound'],
    answer: 0,
  ),
];
  • main.dart

자 이제 메인 화면을 만들어서 App bar를 선언해주고 우리가 만든 QuizScreen() 또한 불러준다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quiz_app_practice/quiz_screen.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Quiz App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const QuizScreen(),
    );
  }
}

Quiz App 만들기 완성.

 

실제 구동화면

728x90

'Flutter' 카테고리의 다른 글

[코드분석] Minesweeper - 지뢰찾기 만들기  (0) 2023.04.20
5-5. Riverpod (E-commerce App)  (0) 2023.04.18
5-3. Riverpod (WeatherApp)  (0) 2023.04.17
5-2. Riverpod으로 Todo 앱 만들기  (0) 2023.04.14
[Practice] Todolist 만들기2  (0) 2023.04.13