List and Map (Quiz App)

Create Map or Dictionary of Questions and Answers

คลาส Map() ใช้สร้างแมพหรือดิกชันนารี ที่เก็บค่า key:value หรือจะสร้างจาก { } ก็ได้
เราจะสร้างรายการคำถาม 4 คำถาม แต่ละคำถามมี 4 คำตอบ ด้วยแมพดังนี้

main.dart
questions คือ List ของ map ที่แต่ละรายการประกอบด้วย String:String คำถามและ String:List คำตอบ 4 รายการ
	var questions = [
	{
	  'question': 'คุณชอบสีอะไร',
	  'answer': ['แดง', 'ดำ', 'น้ำเงิน', 'ขาว']
	},
	{
	  'question': 'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
	  'answer': ['แมว', 'กระต่าย', 'นกแก้ว', 'เสือ']
	},
	{
	  'question': 'ชอบอาหารอะไรที่สุด',
	  'answer': ['ซุบเนื้อ', 'ต้มยำกุ้ง', 'กะเพราไก่', 'ไข่เจียว']
	},
	{
	  'question': 'ภาษาคอมที่ชื่นชอบคืออะไร',
	  'answer': ['dart', 'python', 'c++', 'swift']
	}
  ];	

ในการแสดงคำถาม ให้เรียกใช้วิดเจ็ต Question ให้ส่ง String คำถามเหมือนเดิม โดยเลือกจากคีย์ question ของแมพ

main.dart
Question คือวิดเจ็ต
questions คือรายการคำถามคำตอบทั้งหมด List ของ Map
_questionId คือหมายเลขคำถาม Int
question คือคำถามของ _questionId นั้นๆ String
 
	children: [    // List
	Question(		// widget
	  questions[_questionId]['question'],
	),
	Answer()
  ],

ในการแสดงคำตอบ ให้เลือกรายการคำตอบ answer List จาก _questionId ของรายการคำถาม questions List

main.dart
questions[_questionId]['answer'] คือรายการคำถามคำตอบทั้งหมด List ของ Map
questions[][].map((){}) คือ map() มีฟังก์ชันเป็นพารามิเตอร์ ที่ประมวลผลสมาชิกทุกตัวในลิสต์ questions[][] นั่นคือจะรีเทินค่าผลลัพธ์ตามคีย์หรือพารามิเตอร์ของ questions[][] ในที่นี้คือ map ของคีย์ answer ที่ส่งไปยังพารามิเตอร์ของฟังก์ชันใน map() โดย map() จะตรวจสอบสมาชิกทุกตัวใน questions[][answer]
map((answer){}) return iterable items เป็นชนิด map ในที่นี้คือรายการคำตอบ answer ของคำถามที่แสดงอยู่
แต่ children ต้องการ Widget
question คือคำถามของ _questionId นั้นๆ เป็นชนิด String
 
	children: [    // List
	Question(		// widget
	  questions[_questionId]['question'],
	),
	questions[_questionId]['answer'].map((answer) {  // map
	  return Answer(answer);
	}) 
  ],
toList() converts map into List
เปลี่ยนรายการในแมพให้เป็นรายการของ List
แต่ children ต้องการ Widget
 
	children: [    // List
	Question(
	  questions[_questionId]['question'],
	),
	(questions[_questionId]['answer'] as List).map((answer) {  // List of widget
	  return Answer(answer);  
	}).toList(), // List in a List
  ],
spread operator ...
แยกแต่ละรายการของ List ของ widget มาเป็น item widget แต่ละรายการของ answer ซึ่งก็คือ Widget ตามที่ children ต้องการ
 
	children: [    // List
	Question(
	  questions[_questionId]['question'],
	),
	...(questions[_questionId]['answer'] as List).map((answer) { // widget
	  return Answer(answer);
	}).toList(), // item of List
  ],

แก้ไข constructor ของวิดเจ็ต Answer ให้รับพารามิเตอร์สองตัวคือฟังก์ชัน Function และคำตอบ String
และส่งคำตอบ answer ไปเป็นพารามิเตอร์ของ Text widget

 
	class Answer extends StatelessWidget {
		final Function handler;
		final String answer;
		Answer(this.handler, this.answer);
		@override
		Widget build(BuildContext context) {
		  return Container(
			width: double.infinity,
			color: Colors.orange,
			child: ElevatedButton(
			  onPressed: handler,
			  child: Text(answer),
			  style: ElevatedButton.styleFrom(
				primary: Colors.blue, // background
				onPrimary: Colors.black, // foreground
			  ),
			),
		  );
		}
	  }

main.dart
ส่งพารามิเตอร์ฟังก์ชัน Function และคำตอบ String ไปยังวิดเจ็ต Answer

 
	children: [
	Question(
	  questions[_questionId]['question'],
	),
	...(questions[_questionId]['answer'] as List).map((answer) {
	  return Answer(_answer, answer);  // Widget(Function, String)
	}).toList(),
  ],

Question/Answer Widget 1


Question/Answer Widget 2


Question/Answer Widget 3


Question/Answer Widget 4


End of Quiz / Restart Quiz

ตรวจสอบเงื่อนไข เมื่อตอบครบทุกคำถาม แสดงข้อความกึ่งกลางหน้าจอด้วย Center() พร้อมปุ่มกดให้เริ่มใหม่

main.dart
ย้ายรายการคำถามไปไว้ในคลาส _MyAppState เพื่อจะได้ตรวจสอบจำนวนคำถามทั้งหมดได้
	class _MyAppState extends State {
		var questions = [
		{
		  'question': 'คุณชอบสีอะไร',
		  'answer': ['แดง', 'ดำ', 'น้ำเงิน', 'ขาว']
		},
		{
		  'question': 'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
		  'answer': ['แมว', 'กระต่าย', 'นกแก้ว', 'เสือ']
		},
		{
		  'question': 'ชอบอาหารอะไรที่สุด',
		  'answer': ['ซุบเนื้อ', 'ต้มยำกุ้ง', 'กะเพราไก่', 'ไข่เจียว']
		},
		{
		  'question': 'ภาษาคอมที่ชื่นชอบคืออะไร',
		  'answer': ['dart', 'python', 'c++', 'swift']
		}
	  ];
		...
	}	
main.dart
ตรวจสอบเงื่อนไข และเปลี่ยนคำถาม
? : คือ ternary operator
exp1 ? exp2 : exp3
ถ้า exp1 เป็น true ทำ exp2 คือคำถามถัดไป
ถ้า exp1 เป็น false ทำ exp3 คือให้เริ่มใหม่
เมื่อหมดคำถามให้แสดงปุ่มกดเพื่อเริ่มใหม่ด้วยวิดเจ็ต Center() และ ElevatedButton()
	return MaterialApp(
		home: Scaffold(
		  appBar: AppBar(
			title: Text('My App Bar'),
		  ),
		  body: _questionId < questions.length
			  ? Column(
				  children: [
					Question(
					  questions[_questionId]['question'],
					),
					...(questions[_questionId]['answer'] as List)
						.map((answer) {
					  return Answer(_answer, answer);
					}).toList(),
				  ],
				)
			  : Center(
                child: ElevatedButton(
                    child:
                        Text('Done!, $_questionId questions. Click to restart'),
                    onPressed: _answer),
              ),
		),
	  );	
main.dart
แก้ไขฟังก์ชัน _answer ให้รีเซ็ต _questionId เพื่อเริ่มใหม่
	void _answer() {
		if (_questionId == questions.length) _questionId = -1;
		setState(() {
		  _questionId += 1;
		});
		print(_questionId);
	  }

Restart Quiz


Split Widget

เมื่อวิดเจ็ตเริ่มซับซ้อน เช่นวิดเจ็ต Column() ซึ่งมีหลายๆคำสั่งหรือฟังก์ชัน อยู่ในวิดเจ็ต เราสามารถแยก Column() ออกไปเป็นวิดเจ็ตใหม่ เพื่อให้โปรแกรมดูกระชับขึ้น

Split Widget


ให้ทำตามขั้นตอนดังนี้

  1. สร้างไฟล์ใหม่ชื่อ quiz.dart
  2. สร้าง StatelessWidget ชื่อ Quiz() ในไฟล์ quiz.dart
  3. ในฟังก์ชัน build ของ Quiz() ให้ return Column() ที่คัดลอกมาจากโค้ดใน main.dart
  4. สร้าง constructor เพื่อรับค่าต่างๆที่จำเป็นมาจาก main.dart ในที่นี้คือ questions, questionId และ Function handler
  5. quiz.dart
    		import 'package:flutter/material.dart';
    		import './question.dart';
    		import './answer.dart';
    		
    		class Quiz extends StatelessWidget {
    		  final List> questions;
    		  final int questionId;
    		  final Function handler;
    		
    		  Quiz({this.questions, this.questionId, this.handler});
    		  @override
    		  Widget build(BuildContext context) {
    			return Column(
    			  children: [
    				Question(
    				  questions[questionId]['question'],
    				),
    				...(questions[questionId]['answer'] as List).map((answer) {
    				  return Answer(handler, answer);
    				}).toList(),
    			  ],
    			);
    		  }
    		}		
    	
  6. สร้างไฟล์ใหม่ชื่อ restart.dart
  7. สร้าง StatelessWidget ชื่อ Restart() ในไฟล์ restart.dart
  8. ในฟังก์ชัน build ของ Quiz() ให้ return Center() ที่คัดลอกมาจากโค้ดใน main.dart
  9. สร้าง constructor เพื่อรับค่าต่างๆที่จำเป็นมาจาก main.dart ในที่นี้คือ questionId และ Function handler
  10. restart.dart
    		import 'package:flutter/material.dart';
    
    		class Restart extends StatelessWidget {
    		  final Function handler;
    		  final int questionId;
    		  Restart(this.questionId,this.handler);
    		  
    		  @override
    		  Widget build(BuildContext context) {
    			return Center(
    			  child: ElevatedButton(
    				  child: Text('Done!, $questionId questions. Click to restart'),
    				  onPressed: handler),
    			);
    		  }
    		}
    	
  11. เรียกใช้วิดเจ็ต Quiz() และ Restart() จาก main.dart
    เปลี่ยนตัวแปร questions[] ให้เป็น private ด้วยการใส่ _นำหน้าชื่อ _questions เพื่อให้เหมือนกับตัวแปรอื่นๆ
  12. main.dart
    		import 'package:flutter/material.dart';
    		import './quiz.dart';
    		import './restart.dart';
    		...
    		@override
    		Widget build(BuildContext context) {
    		  return MaterialApp(
    			home: Scaffold(
    				appBar: AppBar(
    				  title: Text('My App Bar'),
    				),
    				body: _questionId < _questions.length
    					? Quiz(
    						handler: _answer,
    						questions: _questions,
    						questionId: _questionId,
    					  )
    					: Restart(
    						_questionId,
    						_answer,
    					  )),
    		  );
    		}
    		...
    	

run app ควรจะได้ผลลัพธ์เหมือนเดิม

Quiz


Restart Quiz


Give Score to Quiz

ในกรณีนีที่เป็นการถามความพึงพอใจ หรือการสอบ เราสามารถให้คะแนนจากแต่ละคำตอบได้
ถ้าถามคความพึงพอใจ แต่ละคำตอบก็จะมีคะแนนต่างๆกันไป
ถ้าเป็นการสอบ ก็จะมีคะแนนเฉพาะคำตอบที่ถูกต้อง
เมื่อจบการถามตอบก็สรุปคะแนน

Quiz Score


ให้ทำตามขั้นตอนดังนี้

  1. เปลี่ยนรายการคำตอบจาก List of String [String] เป็น List of Map [{String:Object}]
    โดยเพิ่มคีย์ text สำหรับชื่อคำตอบ และคีย์ score สำหรับคะแนนแต่ละคำตอบ
    main.dart
  2. 			...
    			var _questions = [
    			{
    			  'question': 'คุณชอบสีอะไร',
    			  'answer': [
    				{'text': 'แดง', 'score': 10},
    				{'text': 'ดำ', 'score': 7},
    				{'text': 'น้ำเงิน', 'score': 5},
    				{'text': 'ขาว', 'score': 1},
    			  ]
    			},
    			{
    			  'question': 'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
    			  'answer': [
    				{'text': 'แมว', 'score': 10},
    				{'text': 'กระต่าย', 'score': 7},
    				{'text': 'นกแก้ว', 'score': 5},
    				{'text': 'เสือ', 'score': 1}
    			  ]
    			},
    			{
    			  'question': 'ชอบอาหารอะไรที่สุด',
    			  'answer': [
    				{'text': 'ซุบเนื้อ', 'score': 10},
    				{'text': 'ต้มยำกุ้ง', 'score': 7},
    				{'text': 'กะเพราไก่', 'score': 5},
    				{'text': 'ไข่เจียว', 'score': 1}
    			  ]
    			},
    			{
    			  'question': 'ภาษาคอมที่ชื่นชอบคืออะไร',
    			  'answer': [
    				{'text': 'dart', 'score': 10},
    				{'text': 'python', 'score': 7},
    				{'text': 'c++', 'score': 5},
    				{'text': 'swift', 'score': 1}
    			  ]
    			}
    		  ];	
    		  ...		
    		
  3. ประกาศตัวแปรเก็บคะแนนรวม _totalScore
    แก้ไขฟังก์ชัน _answer ให้รับค่าคะแนนของแต่ละคำตอบมารวบรวม
  4. main.dart
    		...
    		var _questionId = 0;
    		var _totalScore = 0;
    		void _answer(int score) {
    		  _totalScore += score;
    		  if (_questionId == _questions.length) {
    			_questionId = -1;
    			_totalScore = 0;
    		  }
    		  setState(() {
    			_questionId += 1;
    		  });
    		  print(_questionId);
    		  print(_totalScore);
    		}		
    		...
    	
  5. แก้ไข constructor ของวิดเจ็ต Restart() ให้รับค่าคะแนนรวม เพื่อสรุป
    ส่งคะแนนรวมไปยัง Restart()
  6. main.dart
    		...
    		@override
    		Widget build(BuildContext context) {
    		  //if (_questionId == questions.length) _questionId = 0;
    		  return MaterialApp(
    			home: Scaffold(
    				appBar: AppBar(
    				  title: Text('My App Bar'),
    				),
    				body: _questionId < _questions.length
    					? Quiz(
    						handler: _answer,
    						questions: _questions,
    						questionId: _questionId,
    					  )
    					: Restart(
    						_questionId,
    						_answer,
    						_totalScore,
    					  )),
    		  );
    		}
    		...	
    	
    restart.dart
    		import 'package:flutter/material.dart';
    
    		class Restart extends StatelessWidget {
    		  final Function handler;
    		  final int questionId;
    		  final int totalScore;
    		  Restart(this.questionId, this.handler, this.totalScore);
    		  String get resultText {
    			String text;
    			if (totalScore >= 35) {
    			  text = 'Excellent';
    			} else if (totalScore >= 28) {
    			  text = 'Good';
    			} else if (totalScore >= 20) {
    			  text = 'OK';
    			} else {
    			  text = 'Poor';
    			}
    			return text;
    		  }
    		
    		  @override
    		  Widget build(BuildContext context) {
    			return Center(
    			  child: ElevatedButton(
    				  child: Text(
    					  'Done!,\n$questionId questions, Score: $totalScore,
    					  \nYou are $resultText\nClick to restart',
    					  style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
    					  textAlign: TextAlign.center),
    				  onPressed: () => handler(0)),
    			);
    		  }
    		}		
    	
    เนื่องจากฟังก์ชัน _answer หรือ handler ต้องมีพารามิเตอร์ตอนเรียกใช้งาน แต่พารามิเตอร์ของ onPressed เป็นฟังก์ชันที่ไม่มีพารามิเตอร์
    ให้เราสร้างฟังก์ชันไม่มีชื่อ (){} หรือแบบสั้น ()=>handler(score) ที่ไม่มีพารามิเตอร์เพื่อส่งที่อยู่ของฟังก์ชันไปยัง onPressed และเมื่อฟังก์ชันถูกเรียกใช้งาน ก็ให้ส่งพารามิเตอร์ score ไปยังฟังก์ชัน
    () หมายถึงที่อยู่ของฟังก์ชัน pointer to function or address fo function
    {} or =>handler(score) หมายถึงการทำงานของฟังก์ชันซึ่งต้องส่งพารามิเตอร์ 1 ตัวไปยังฟังก์ชัน
    		onPressed: ()=>handler(0),
    	
  7. ทำนองเดียวกัน ใน Quiz() ให้เราปรับพารามิเตอร์ที่ส่งไปยัง Answer() ให้สอดคล้องกับ List of Map ของคำตอบ
  8. quiz.dart
    		import 'package:flutter/material.dart';
    		import './question.dart';
    		import './answer.dart';
    		
    		class Quiz extends StatelessWidget {
    		final List> questions;
    		  final int questionId;
    		  final Function handler;
    		
    		  Quiz({@required this.questions, @required this.questionId, @required this.handler});
    		  @override
    		  Widget build(BuildContext context) {
    			return Column(
    			  children: [
    				Question(
    				  questions[questionId]['question'],
    				),
    				...(questions[questionId]['answer'] as List>).map((answer) {
    				  return Answer(()=>handler(answer['score']), answer['text']);
    				}).toList(),
    			  ],
    			);
    		  }
    		}		
    	

    เราแก้รายการคำตอบจาก list [String] เป็น list [Map]
    และปรับพารามิเตอร์ที่ส่งไปยัง Answer() ให้ชนิดตรงกัน

    run app ควรจะได้ผลลัพธ์เหมือนเดิม

    Quiz


    Quiz Score


    const and final

    final: runtime constant value (lock after re-assign at runtime)
    const: compile time constant value (always lock)

     
    const x = const ['list'];
    const y = ['list'];
    x = []; // error
    y = []; // error
    var z = const ['list'];
    z.add('list2'); // error, value const
    var z2 = const ['list'];
    z2 = ['list2']; // ok, z is var
    print(z2);
    var a = ['list'];
    a.add('list2'); // ok
    print(a);
    a = []; // ok
    print(a);
    final String str = 'string';
    str = 'string2';   // error
    

    สรุปที่เรียนรู้

    Summary

    2020. mnet50.com