สำหรับสาย Cross-Platform แบบ Flutter นั้นวันนี้เลยออกแบบบทเรียนแบบ Repid Series เคลมไว เป็นไว เข้าใจ รัวๆ ให้ลองมาทำกัน โดยโจทย์คือการเชื่อม Web Services JSON กับ List
บทเรียนก่อนหน้าก็ไปติดตั้งก่อน:
ทำการชี้ Location ไปที่ Folder ที่เราต้องการจะสร้างโปรเจ็ค แล้วเปิด VS Code ขึ้นมาทำการรันคำสั่งบน Terminal สร้าง Project ขึ้นมาโดย:
$ flutter create <<ชื่อ Project>>
หลังจากนั้นเราจะเห็นว่า VS Code เราจะมี Folder ใหม่ชื่อเดียวกับ Project ของเราขึ้นมา ขั้นตอนต่อไปให้เราไปที่ Folder ชื่อ “lib” เปิดไฟล์ main.dart ขึ้นมา ทำการลบคำสั่งทิ้งทั้งหมดแล้ว พิมพ์คำสั่งต่อไปนี้:
import 'package:flutter/material.dart'; void main() => runApp(MainApp()); class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Hello World', home: Scaffold( appBar: AppBar( title: Text('Hello World'), ), body: Center( child: Text('Hello World'), ), ), ); } }
หลังจากนั้นพิมพ์ Cmd+Shift+P หรือ Ctrl + Shift + P เพื่อเรียก Emulator ที่เราอยากจะใช้ขึ้นมาซึ่งถ้าเป็น iOS ก็ลง XCode ให้เรียบร้อย ถ้า Android ก็ลง AVD ให้เรียบร้อยก่อน
ก็จะเปิดหน้าจอหลังรัน Emulator เสร็จแล้ว หลังจากนั้นไปที่ Terminal ให้พิมพ์:
$ flutter run
หรือถ้าเจาะจงเครื่อง (ตัวอย่างใช้ชื่อ Emulator ว่า “Pixel XL API 29”)
$ flutter run -d "Pixel XL API 29"
ระบบก็จะรันหน้าจอดังนี้:
อธิบายก็คือ ใน main.dart เนี่ยจะมีส่วนที่แสดงผล โดยเริ่มรันที่ Class ของ App เราชื่อ MainApp() ผ่านเมธอดชื่อ void Main() ด้วยคำสั่ง runApp(คลาสที่เราจะใช้เป็นหลักในการทำงาน)
void main() => runApp(MainApp());
ในคลาสที่ถูกเรียกผ่านคำสั่ง runApp() ก็คือ MainApp() เราก็จะไปดูรายละเอียดของมันกันหน่อย ส่วนประกอบคือการแสดงผลตัวอักษร String ที่ AppTitle Bar และ Content ส่วน Body ว่า “Hello World”
class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Hello World', home: Scaffold( appBar: AppBar( title: Text('Hello World'), ), body: Center( child: Text('Hello World'), ), ), ); } }
โดยแสดงผล Widget ผ่าน MaterialApp ซึ่งเป็น Library ส่วนของ Material Design UI ของทา Google ผ่าน Import ส่วน Header คือ:
import 'package:flutter/material.dart';
โดยคำสั่ง Widget ที่เราใช้แสดงผลคือ build() ซึ่ง ภายใน build() จะมีการทำงานบนหน้าจอ Context ปัจจุบัน โดย title คือชื่อแอปพลิเคชัน และใน home() ก็คือหน้าแรกของแอปพลิเคชัน เราจะใช้ appBar และ Body มาแสดงผลโดย AppBar คือชื่อ Title ของแอปพลิเคชันแถบสีฟ้า และ Body นั้นคือ Center Text ที่ถูกเขียน String ลงไปผ่าน child: Text()
@override Widget build(BuildContext context) { return MaterialApp( title: 'Hello World', home: Scaffold( appBar: AppBar( title: 'Hello World', ), body: Center( child: Text('Hello World'), ), ), ); }
Stateless widget ในคลาสของ MainApp ที่ถูก Extend นับว่าเป็น widget ไม่ต้องมีการเปลี่ยนแปลง state เหมาะกับส่วนที่เป็น Static ซึ่ง Stateless widget เหมาะกับออกแบบ widget ที่ไม่ต้องเปลี่ยนแปลงอะไรมากเช่นกำหนดส่วนหน้าจอของ UI เช่น Text, Button หรืออะไรที่ไม่ต้องมีความยืดหยุ่นในการเปลี่ยนแปลงเมื่อมีการรันแอปพลิเคชันไปแล้ว
ทีนี้ก็จะมีอีกส่วนที่ทำงานร่วมกับ Stateless widget คือ StatefulWidget เป็น widget ที่เน้นการทำงานสำหรับการเปลี่ยนแปลงของ state โดยจะมีคำสั่ง setState() เพื่อกำหนดการเปลี่ยนแปลง เช่น Checkbox, Radio, Slider, Form และ TextField หรือแม้แต่ ListView โดยการเรียกใช้คำสั่ง setState() เป็นการบอกให้คลาสหลักรู้ว่ามีบางอย่างเปลี่ยนแปลงเกิดขึ้นกับ state และ App ต้องทำการ rerun หรือ ทำคำสั่ง build() ส่วน Context นั้นใหม่ทันที
ดังนั้นตัว App จึงได้รับผลจากการเปลี่ยนแปลงที่เกิดขึ้น State ใช้งานเปลี่ยนค่าได้ต่อเนื่องในทุกช่วงเวลาที่มีการใช้งาน ของ widget เอาง่ายๆ การทำงานของทั้งสอง State นั้นอาจจะเรียกสั่นๆว่า Stateless คือ Passive ส่วน Stateful นั้นคือ Active นั่นเอง เดี๋ยวในท้ายของบทความจะมีการออกแบบ Stateful Widget มาใช้
ทีนี้เรามาดูการทำงานแบบแยกคลาสกันหน่อยดีกว่าให้สร้างไฟล์ใหม่ใน Folder ชื่อ “lib” ตั้งชื่อว่า strings.dart ครับ
เปิด strings.dart ขึ้นมาหลังจากนั้นให้สร้าง Class ของ Strings ดังนั้น:
class Strings { static String appName = "Another App Name"; }
คลาสของ Strings() ประกอบไปด้วยตัวแปร String ชื่อ appName มีค่า value คือ “Another App Name” ทีนี้ให้เรากลับไปที่ main.dart ไปที่คลาส MainApp() ของเราให้เราเรียกใช้คลาส Strings ที่สร้างขึ้นกับแอปของเราให้ทำการประกาศ Header เรียกใช้ดังนี้:
import 'strings.dart';
นั่นแปลว่าเราจะเรียก Attribute หรือ Element ไปจนถึง Class ต่างๆ จาก Strings ได้แล้ว ทีนี้ให้เราแก้ไขส่วนของ MainApp() ของ main.dart ดังนี้:
lass MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: Strings.appName, home: Scaffold( appBar: AppBar( title: Text(Strings.appName), ), body: Center( child: Text('Hello World'), ), ), ); } }
จะเห็นว่าเราได้ดึง Strings.appName มาปรากฏที่ ส่วนของ title ของแอปพลิเคชัน และ appBar ให้มีคำว่า Another App Name
Concept OOP ง่ายๆ สบายๆ เอาล่ะเรามาทำงานร่วมกับ JSON ตามสไตล์ Cook Book แบบ Rapid Series กันดีกว่า ให้เราเตรียมไฟล์ Web Services ดังนี้:
https://enet5-7f9f6.firebaseio.com/Books.json
[ { "id" : 0, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/06/cover/MagazineCover_185.jpg", "title" : "Life After Covid-19" }, { "id" : 1, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/05/cover/MagazineCover_184.jpg", "title" : "Creativity Must Go On" }, { "id" : 2, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/04/cover/MagazineCover_182.jpg", "title" : "Living with COVID-19" }, { "id" : 3, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/03/cover/MagazineCover_180.jpg", "title" : "Look Isan Now" }, { "id" : 4, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/02/cover/MagazineCover_181.jpg", "title" : "VERTICAL LIVING: WHY DO WE ALWAYS LIVE HIGHER?" }, { "id" : 5, "thumbnail" : "https://www.creativethailand.org/admin/public/uploads/images/2563/01/cover/MagazineCover_177.jpg", "title" : "THE FUTURE OF WORK IS NOW" } ]
เราจะสร้าง List มารับค่า เรียก Web Services โดยเอา Key title มาแสดงกันครับ
ให้เราไปเปิดไฟล์ pubspec.yaml
ขั้นตอนต่อไปนี้คือการเพิ่ม dependencies เหมือน Sync Library อื่นๆ เข้าไปใน Project ของเราให้สามารถทำงานได้เต็มประสิทธิภาพนั่นเองจาก Library ของนักพัฒนาใน Flutter ซึ่งการทำงานจะเหมือน CocoaPod, Gradle นั่นแหละครับ เราจะใช้ http มาดึงข้อมูล JSON ให้ไปที่ dependecies: ครับ เพิ่ม http เข้าไปคือ:
http: ^0.12.0+2
ตำแหน่งไหนเหรออ่ะ ดูตามตัวอย่าง:
กลับไปที่ main.dart ให้ประกาศส่วนของ http เพิ่มดังนี้:
import 'dart:convert'; import 'package:http/http.dart' as http;
สร้าง Class ใหม่กันชื่อว่า MainContextApp โดยให้เป็น StatefulWidget เพราะข้อมูลจะมีการเปลี่ยน Active ทันทีเมื่อมีการโหลดหน้าใหม่ เพราะถูกเติมเต็มด้วย Containers ที่มาจาก Web Services ดึงผ่าน JSON
class MainContextApp extends StatefulWidget { @override createState() => ContextState(); }
โดยมี ContextState() ที่เราจะเรียกใช้งานใน MainContextApp() ให้เราไปเพิ่ม อีก Class ว่า
class ContextState extends State<MainContextApp> { var _books = []; final _headerText = const TextStyle(fontSize: 18.0); @override void initState() { super.initState(); _loadData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(Strings.appName), ), body: ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: _books.length, itemBuilder: (BuildContext context, int position) { return _buildRow(position); }), ); } Widget _buildRow(int i) { return ListTile( title: Text( "${_books[i]["title"]}", style: _headerText, )); } _loadData() async { String dataURL = "https://enet5-7f9f6.firebaseio.com/Books.json"; http.Response response = await http.get(dataURL); setState(() { _books = json.decode(response.body); }); } }
อธิบาย Code ของ Dart
สร้างตัวแปรเป็นชุด Array และ Style ของการแสดงผลตัวอักษรขึ้นมา ขนาด 18pts ตามตัวอย่างนี้
var _books = []; final _headerText = const TextStyle(fontSize: 18.0);
ใน ContextState() จะมีการทำงานส่วนของ Widget ดังนี้:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(Strings.appName), ), body: ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: _books.length, itemBuilder: (BuildContext context, int position) { return _buildRow(position); }), ); }
ส่วนของ Body เป็น ListView โดย ListView จะทำการ builder สร้างรายการแต่ละ item ขึ้นมาโดย return ตามจำนวนของ JSON items ที่ถูกวนเก็บในตัวแปร _books แล้วไปทำการแสดงผลสร้าง Template ของแถว Row ผ่านฟังก์ชัน _buildrow(position) ตามตำแหน่ง Position หรือ Index ของ Array:
เราเลยต้องสร้าง ฟังก์ชัน _buildrow() ดังนี้:
Widget _buildRow(int i) { return ListTile( title: Text( "${_books[i]["title"]}", style: _headerText, )); }
ซึ่ง _books[i][“title”] คือไปเรียก Key ของ JSON ที่ชื่อ “title” นั่นเองเพื่อมาแสดง
ส่วนของการ Load ข้อมูลจาก JSON ทำงานโดยฟังก์ชัน 2 ส่วนนี้คือ:
_loadData() async { String dataURL = "https://enet5-7f9f6.firebaseio.com/Books.json"; http.Response response = await http.get(dataURL); setState(() { _books = json.decode(response.body); }); }
เรียก Response ทำ JSON Parser ผ่าน http.Response ที่ถูกภาษาก็ทำงานเหมือนกันไปหมด จนไม่ต้องอธิบายอะไรแล้วล่ะส่วนนี้ Get ค่า JSON ผ่านตัวแปร dataURL เข้า http.get() ดังนั้นกลับไปที่ ContextState() เพื่อเข้า context นี้เราจะต้องทำงาน StateFullWidget ทันทีผ่าน:
@override void initState() { super.initState(); _loadData(); }
เพื่อให้มันทำงานแบบ Init() เริ่มต้น กลับไปที่ คลาสของ MainApp() ให้แก้ไข Code จากเดิม:
class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: Strings.appName, home: Scaffold( appBar: AppBar( title: Text(Strings.appName), ), body: Center( child: Text('Hello World'), ), ), ); } }
แก้ไขเป็น:
class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( //3 title: Strings.appName, home: MainContextApp(), ); } }
เพื่อให้ส่วนเมธอด home ไปทำงานผ่านคลาส MainContextApp() ไปเลยเมื่อเริ่มทำงาน
คำสั่งทั้งหมดของ main.dart ต้องเป็นดังนี้:
import 'package:flutter/material.dart'; import 'strings.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; void main() => runApp(MainApp()); class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: Strings.appName, home: MainContextApp(), ); } } class ContextState extends State<MainContextApp> { var _books = []; final _headerText = const TextStyle(fontSize: 18.0); @override void initState() { super.initState(); _loadData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(Strings.appName), ), body: ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: _books.length, itemBuilder: (BuildContext context, int position) { return _buildRow(position); }), ); } Widget _buildRow(int i) { return ListTile( title: Text( "${_books[i]["title"]}", style: _headerText, )); } _loadData() async { String dataURL = "https://enet5-7f9f6.firebaseio.com/Books.json"; http.Response response = await http.get(dataURL); setState(() { _books = json.decode(response.body); }); } } class MainContextApp extends StatefulWidget { @override createState() => ContextState(); }
ทำการรันคำสั่ง:
$ flutter run
จะเห็นว่าเราดึงข้อมูลจาก JSON มาปรากฏบน ListView ง่ายผ่าน dart และ Flutter ได้แบบไม่ต้องคิดไรมาก และ คลาสการทำงานก็ไม่ได้ซับซ้อนอะไรครับ
บทเรียนในซีรีย์ Rapid Series ของ Flutter ตอนต่อไปคือแอปพลิเคชันตัวเดิมนะครับ ใช้พัฒนาต่อสัก 3-4 บทเรียนต่อไปครับ เก็บ Project นี้ไว้