1. 키포인트
리렌더링을 통해 변경되는 widget에 함수를 전달해 줄 수 있고, 전달 받은 widget에서의 함수는 다시 역으로 전달해준 widget으로 값이 전달 할 수 있다.
2. 파일 트리
3. 흐름
큰 어려운 개념은 없다. 흐름에 따라서 가면 충분히 이해 할 수 있음.
- main.dart
import 'package:flutter/material.dart' ;
import 'package:quiz_app/quiz.dart' ;
void main () {
runApp ( const Quiz ()) ;
}
1) Quiz 위젯 호출
- quiz.dart
import 'package:flutter/material.dart' ;
import 'package:quiz_app/start_screen.dart' ;
import 'package:quiz_app/questions_screen.dart' ;
import 'package:quiz_app/data/questions.dart' ;
import 'package:quiz_app/results_screen.dart' ;
class Quiz extends StatefulWidget {
const Quiz ({ super . key}) ;
@override
State < Quiz > createState () {
return _QuizState () ;
}
}
class _QuizState extends State < Quiz > {
List < String > selectedAnswers = [] ;
var activeScreen = 'start-screen' ;
void switchScreen () {
setState (() {
activeScreen = 'questions-screen' ;
}) ;
}
void chooseAnswer ( String answer) {
selectedAnswers . add (answer) ;
if (selectedAnswers . length == questions . length) {
setState (() {
activeScreen = 'results-screen' ;
}) ;
}
}
void restartQuiz () {
setState (() {
selectedAnswers = [] ;
activeScreen = 'questions-screen' ;
}) ;
}
@override
Widget build (context) {
Widget screenWidget = StartScreen (switchScreen) ;
if (activeScreen == 'questions-screen' ) {
screenWidget = QuestionsScreen (
onSelectAnswer : chooseAnswer ,
) ;
}
if (activeScreen == 'results-screen' ) {
screenWidget = ResultsScreen (
chosenAnswers : selectedAnswers ,
onRestart : restartQuiz ,
) ;
}
return MaterialApp (
home : Scaffold (
body : Container (
decoration : const BoxDecoration (
gradient : LinearGradient (
colors : [
Color . fromARGB ( 255 , 78 , 13 , 151 ) ,
Color . fromARGB ( 255 , 107 , 15 , 168 ) ,
] ,
begin : Alignment . topLeft ,
end : Alignment . bottomRight ,
) ,
) ,
child : screenWidget ,
) ,
) ,
) ;
}
}
1) 해당 화면은 리렌더링되는게 있기 떄문에 stateful
2) 각 질문 화면에서 답변을 저장할 selectedAnswer 빈 리스트 셋업
3) child: screenWidget : 상태에 따라 화면(위젯)이 바뀔 수 있도록 설정
처음 상태는 StartScreen이고 switchScreen 함수를 보내준다. 해당 함수는 setState로 터치하면 activeScreen 설정 텍스트 값이 초기 start-screen 에서 questions-screen으로 변경 된다.
StartScreen화면
QuestionsScreen화면
그리고 QuestionsScreen위젯에는 chooseAnswer함수를 넘겨 주는데 해당 함수는, 선택된 값을 빈 배열에 넣어주는 역할과, 해당 배열의 총 갯수와 질문의 갯수가 동일하면 모든 답변이 완료된 것으로 보고 activeScreen의 텍스트 값을 results-screen으로 변경하여 화면을 ResultsScreen위젯으로 바꿔준다.
ResultsScreen화면
ResultsScreen에는 저장되었던 답변 리스트의 값과, QuestionsScreen으로 돌아가게 하는 함수를 보내준다. (이 때 답변 리스트는 초기화 해야함)
- quiz_questions.dart(models폴더)
class QuizQuestion {
const QuizQuestion ( this . text , this . answers) ;
final String text ;
final List < String > answers ;
List < String > getShuffledAnswers () {
final shuffledList = List . of (answers) ;
shuffledList . shuffle () ;
return shuffledList ;
}
}
1) model 설정은 질문이 들어가는 Text와 답변 목록인 answers가 있음
2) getShuffledAnswers 메소드를 통해 답변의 순서를 랜덤화 시켜줌.
- questions.dart (data폴더)
import 'package:quiz_app/models/quiz_question.dart' ;
const questions = [
QuizQuestion (
'What are the main building blocks of Flutter UIs?' ,
[
'Widgets' ,
'Components' ,
'Blocks' ,
'Functions' ,
] ,
) ,
QuizQuestion ( 'How are Flutter UIs built?' , [
'By combining widgets in code' ,
'By combining widgets in a visual editor' ,
'By defining widgets in config files' ,
'By using XCode for iOS and Android Studio for Android' ,
]) ,
QuizQuestion (
'What \' s the purpose of a StatefulWidget?' ,
[
'Update UI as data changes' ,
'Update data as UI changes' ,
'Ignore data changes' ,
'Render UI that does not depend on data' ,
] ,
) ,
QuizQuestion (
'Which widget should you try to use more often: StatelessWidget or StatefulWidget?' ,
[
'StatelessWidget' ,
'StatefulWidget' ,
'Both are equally good' ,
'None of the above' ,
] ,
) ,
QuizQuestion (
'What happens if you change data in a StatelessWidget?' ,
[
'The UI is not updated' ,
'The UI is updated' ,
'The closest StatefulWidget is updated' ,
'Any nested StatefulWidgets are updated' ,
] ,
) ,
QuizQuestion (
'How should you update data inside of StatefulWidgets?' ,
[
'By calling setState()' ,
'By calling updateData()' ,
'By calling updateUI()' ,
'By calling updateState()' ,
] ,
) ,
] ;
1) 각 데이터를 이전에 설정한 QuizQuestion 모델에 사용하여. 다른곳에서 해당 값을 사용 할 수 있게 세팅해줌
- start_screen.dart
import 'package:flutter/material.dart' ;
import 'package:google_fonts/google_fonts.dart' ;
class StartScreen extends StatelessWidget {
const StartScreen ( this . startQuiz , { super . key}) ;
final void Function () startQuiz ;
@override
Widget build (context) {
return Center (
child : Column (
mainAxisSize : MainAxisSize . min ,
children : [
Image . asset (
'assets/images/quiz-logo.png' ,
width : 300 ,
color : const Color . fromARGB ( 150 , 255 , 255 , 255 ) ,
) ,
// Opacity(
// opacity: 0.6,
// child: Image.asset(
// 'assets/images/quiz-logo.png',
// width: 300,
// ),
// ),
const SizedBox (height : 80 ) ,
Text (
'Learn Flutter the fun way!' ,
style : GoogleFonts . lato (
color : const Color . fromARGB ( 255 , 237 , 223 , 252 ) ,
fontSize : 24 ,
) ,
) ,
const SizedBox (height : 30 ) ,
OutlinedButton . icon (
onPressed : startQuiz ,
style : OutlinedButton . styleFrom (
foregroundColor : Colors . white ,
) ,
icon : const Icon ( Icons . arrow_right_alt) ,
label : const Text ( 'Start Quiz' ) ,
)
] ,
) ,
) ;
}
}
1) asset을 사용하기 위해서는 assets폴더를 만든 후 진행. 그리고 pubspec.yaml 파일에서 아래와 같이 추가해주어야함
assets폴더/images폴더 추가 및 이미지파일 넣기
pubspec.yaml파일에 assets 추가
2) GoogleFonts는 dart 패키지에서 다운
2) startQuiz함수를 통해 QuestionsScreen으로 넘어감
-questions.screen.dart
import 'package:flutter/material.dart' ;
import 'package:google_fonts/google_fonts.dart' ;
import 'package:quiz_app/answer_button.dart' ;
import 'package:quiz_app/data/questions.dart' ;
class QuestionsScreen extends StatefulWidget {
const QuestionsScreen ({
super . key ,
required this . onSelectAnswer ,
}) ;
final void Function ( String answer) onSelectAnswer ;
@override
State < QuestionsScreen > createState () {
return _QuestionsScreenState () ;
}
}
class _QuestionsScreenState extends State < QuestionsScreen > {
var currentQuestionIndex = 0 ;
void answerQuestion ( String selectedAnswer) {
widget . onSelectAnswer (selectedAnswer) ;
// currentQuestionIndex = currentQuestionIndex + 1;
// currentQuestionIndex += 1;
setState (() {
currentQuestionIndex ++ ; // increments the value by 1
}) ;
}
@override
Widget build (context) {
final currentQuestion = questions[currentQuestionIndex] ;
return SizedBox (
width : double . infinity ,
child : Container (
margin : const EdgeInsets . all ( 40 ) ,
child : Column (
mainAxisAlignment : MainAxisAlignment . center ,
crossAxisAlignment : CrossAxisAlignment . stretch ,
children : [
Text (
currentQuestion . text ,
style : GoogleFonts . lato (
color : const Color . fromARGB ( 255 , 201 , 153 , 251 ) ,
fontSize : 24 ,
fontWeight : FontWeight . bold ,
) ,
textAlign : TextAlign . center ,
) ,
const SizedBox (height : 30 ) ,
... currentQuestion . getShuffledAnswers () . map ((answer) {
return AnswerButton (
answerText : answer ,
onTap : () {
answerQuestion (answer) ;
} ,
) ;
})
] ,
) ,
) ,
) ;
}
}
1) QuestionsScreen에서도 리렌더링을 해야하기 때문에 stateful
2) onSelectAnswer 함수를 통해 선택된 값이 역으로 상위 위젯으로 전달되고, 상위 위젯에서는 해당 값을 저장 및 선택된 값과 총 질문의 개수를 비교하여 ResultsScreen으로 변경할지 결
onSelectAnswer를 아래 state화면에서 사용하기 위해서는 widget를 붙여줘야함.
3) currentQuestionIndex를 통해 답변을 하나 선택하면 1씩 증가시켜주고, 해당 인덱스 값을 questions인덱스로 넘겨주어 questions의 값이 순서대로 하나씩 보여지게됨
4) children안에 배열값을 추가하기 위해 ... 을 사용했고. 해당 ...currentQuestion.getShuffledAnswer().map을 부분을 통해
섞인 answers 배열을 볼 수 있음. 그리고 answerButton 위젯을 하나 추가로 만들었음.
- answer_button.dart
import 'package:flutter/material.dart' ;
class AnswerButton extends StatelessWidget {
const AnswerButton ({
super . key ,
required this . answerText ,
required this . onTap ,
}) ;
final String answerText ;
final void Function () onTap ;
@override
Widget build ( BuildContext context) {
return ElevatedButton (
onPressed : onTap ,
style : ElevatedButton . styleFrom (
padding : const EdgeInsets . symmetric (
vertical : 10 ,
horizontal : 40 ,
) ,
backgroundColor : const Color . fromARGB ( 255 , 33 , 1 , 95 ) ,
foregroundColor : Colors . white ,
shape : RoundedRectangleBorder (
borderRadius : BorderRadius . circular ( 40 ) ,
) ,
) ,
child : Text (
answerText ,
textAlign : TextAlign . center ,
) ,
) ;
}
}
1) 버튼 위젯 부분
- results_screen.dart
import 'package:flutter/material.dart' ;
import 'package:quiz_app/data/questions.dart' ;
import 'package:quiz_app/questions_summary/questions_summary.dart' ;
import 'package:google_fonts/google_fonts.dart' ;
class ResultsScreen extends StatelessWidget {
const ResultsScreen ({
super . key ,
required this . chosenAnswers ,
required this . onRestart ,
}) ;
final void Function () onRestart ;
final List < String > chosenAnswers ;
List < Map < String , Object >> getSummaryData () {
final List < Map < String , Object >> summary = [] ;
for ( var i = 0 ; i < chosenAnswers . length ; i ++ ) {
summary . add (
{
'question_index' : i ,
'question' : questions[i] . text ,
'correct_answer' : questions[i] . answers[ 0 ] ,
'user_answer' : chosenAnswers[i]
} ,
) ;
}
return summary ;
}
@override
Widget build ( BuildContext context) {
final summaryData = getSummaryData () ;
final numTotalQuestions = questions . length ;
final numCorrectQuestions = summaryData . where ((data) {
return data[ 'user_answer' ] == data[ 'correct_answer' ] ;
}) . length ;
return SizedBox (
width : double . infinity ,
child : Container (
margin : const EdgeInsets . all ( 40 ) ,
child : Column (
mainAxisAlignment : MainAxisAlignment . center ,
children : [
Text (
'You answered $ numCorrectQuestions out of $ numTotalQuestions questions correctly!' ,
style : GoogleFonts . lato (
color : const Color . fromARGB ( 255 , 230 , 200 , 253 ) ,
fontSize : 20 ,
fontWeight : FontWeight . bold ,
) ,
textAlign : TextAlign . center ,
) ,
const SizedBox (
height : 30 ,
) ,
QuestionsSummary (summaryData) ,
const SizedBox (
height : 30 ,
) ,
TextButton . icon (
onPressed : onRestart ,
style : TextButton . styleFrom (
foregroundColor : Colors . white ,
) ,
icon : const Icon ( Icons . refresh) ,
label : const Text ( 'Restart Quiz!' ) ,
)
] ,
) ,
) ,
) ;
}
}
1) 기존 질문 및 답변과 유저 답변을 getSummaryData함수를 통해 summary 리스트에 추가해줌. 해당 함수의 리턴 값은 summary 리스트
2) 실제 정답과 유저의 답변이 몇개나 맞았는지 확인하기 위해 where를 사용.
3) onRestart 함수를 통해 답변 초기화 및 QuestionsScreen으로 이동