Flutter สร้าง UI แอพพลิเคชั่นร้านค้าออนไลน์ (for My Workshop Record)
บทความนี้เราจะอธิบายขั้นตอนการสร้างส่วนหน้าแอพพลิเคชั่นของร้านค้าออนไลน์ ซึ่งมีการกำหนดหน้าตาออกมาเรียบร้อยแล้ว เหตุผลก็เพราะว่าจะต้องการบันทึกความรู้ที่ใช้ในการสร้างแอพเบื้องต้น ฝึกการออกแบบ widget ต่างๆ เช่น Row, Column, Container, Image, Icon และอีกหลายๆ widget ประกอบกับ การฝึกมองแอพให้เป็น widget ย่อยๆ และเข้าใจถึงการประกอบกันให้เป็นแอพของ widget ก็เป็นทักษะที่นักพัฒนา Flutter ต้องฝึกบ่อยๆ
ลักษณะแอพพลิเคชันที่กำหนด เป็นแบบนี้
มีการกำหนด widget ของ UI แบ่งออกเป็นส่วนต่างๆ มีดังนี้
เมื่อมีแบบแล้ว ก็เริ่มต้นสร้างกันเลย
จาก prototype ด้านบน หน้าแรกของแอพ จะมี widget tree diagram เป็นแบบนี้
โดยจะมี widget ที่ห่อหุ้มหน้าแอพทั้งหมดเอาไว้อีกทีนึง ได้แก่
MaterialApp = widget ที่ห่อหุ้ม widget อื่นๆอยู่ กำหนดภาพรวมของแอพพลิเคชันเอาไว้ เช่น ชื่อแอพ หรือ ส่วน body แอพ
ในตัวอย่าง เราจะสร้างคลาส MyApp ที่เป็น StatelessWidget ให้คืนค่าออกมาเป็น MaterialApp เพื่อเอาไปใช้ห่อ่หุ้ม widget ต่างๆที่จะสร้างต่อจากนี้ให้แสดงผลออกมาเวลารันโค้ด
Scaffold = widget จัดการ layout อัติโนมัติของแอพพลิเคชั่น
เริ่มต้นเขียน widget ที่ห่อหุ้มหน้าแอพ แบบนี้
Contents
Page 1 : MyShoppingPage
--Column 1 / Row 1 : สร้างแถบเมนู
--Column 1 / Row 2 : ชื่อสินค้า (shoesName)
--Column 1 / Row 3 : รูปแสดงสินค้า (shoesImage)
--Column 1 / Row 4 : Rating สินค้า
--Column 1 / Row 5 : ราคาสินค้า (price)
--Column 1 / Row 6 : ปุ่มเพิ่มสินค้าในตะกร้า (addButton)
--รวม widget ของหน้าแรก
--ช่องว่างระหว่าง wiget : gapBetweenWidget
Page : MySummaryPage
--สร้างคลาสสำหรับสร้างปุ่มวงกลม : CircleButton
--Column 2 / Row 1 : สร้างแถบเมนู
--Column 2 / Row 2 : สร้างหัวข้อหน้าสรุป : summaryTitle
--สร้างคลาสรูปภาพรองเท้า
--Column 2 / Row 3 : รายละเอียดสินค้าที่เลือก
--Column 2 / Row 4 : สร้างรายละเอียดแจ้งยอดชำระ
--สร้างคลาสปุ่มสี่เหลี่ยมสำหรับ Add to cart และ Checkout
--Column 2 / Row 5 : ปุ่มเช็คเอาท์
--ประกอบร่าง Page 2
การแก้ไขความกว้างแถบเมนู
การเชื่อมกันระหว่าง MyShoppingPage กับ MySummaryPage
--เพิ่ม Navigator.push()
--Navigator.pop()
สร้างหน้าแรก : MyShoppingPage
มาพูดถึงส่วนของหน้าแรกกันบ้าง
การกำหนด widget ของหน้าแรกนั้น เราจะสร้าง StatelessWidget class ขึ้นมา ชื่อว่า MyShoppingPage ละกัน โดยให้มันพ่น Material ออกมา จากนั้นให้ MaterialApp ที่ถูกคืนค่าออกมาจาก MyApp เรียกใช้งาน MyShoppingPage ให้เป็นส่วน body ของมัน
เริ่มจาก เค้าโครง widget ของหน้าแอพแบบนี้
ตั้งชื่อ widget หน้าแรกว่า MyShoppingPage
Column 1 / Row 1 : สร้างแถบเมนู
ต่อมาเราก็ทำการสร้าง widget อื่นๆ ที่จะใช้รวมเป็นหน้าแอพออกมา
เริ่มจาก ปุ่มเมนู
สร้างปุ่มเมนู (menuButton)
var menuButton = Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.grey,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(Icons.menu_sharp),
color: Colors.black,
onPressed: () {},
),
),
);
ปุ่มตะกร้าสินค้า (cartButton)
ปุ่มตะกร้าสินค้า เป็นปุ่มที่กดเพื่อเข้าไปดูสินค้าที่เราเลือกก่อนจะชำระเงิน
var cartButton = Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.grey,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(IconData(59870, fontFamily: 'MaterialIcons')),
color: Colors.black,
onPressed: () {},
),
),
);
ตรง onPressed เราจะสร้างฟังก์ชั่น โดยปุ่มทั้งสองจะอยู่บนแถวเดียวกัน ดังนั้นก็สร้าง Row() มาห่อหุ่มเอาไว้
var menuBar = Row(
children: [
menuButton,
cartButton,
],
);
ลอง debug ดูรูปร่าง widget ที่เราสร้างขึ้นมาว่าเป็นยังไงบ้าง
โอเค ได้ปุ่มเมนู และปุ่มตะกร้าสินค้ามาแล้ว ต่อมา ทำการจัดตำแหน่งให้มันอยู่มุมซ้าย และมุมขวาของหน้าจอ ขั้นตอนนี้เราจะใช้ Expanded() เข้ามาจัดการ
var menuBar = Row(
children: [
Expanded(child:Row(children: [menuButton], textDirection: TextDirection.ltr)),
Expanded(child:Row(children: [cartButton], textDirection: TextDirection.rtl)),
],
);
ให้ Expanded()
หุ้ม Row()
เอาไว้ แล้วข้างในนั้นจะมี widget ปุ่มที่เราสร้างขึ้นมาอีกทีนึง จากนั้นกำหนดตำแหน่งให้แต่ละปุ่มด้วย textDirection
properties
เมื่อเสร็จขั้นตอนนี้ เราจะได้แถวของปุ่มเรียบร้อยแล้ว
Column 1 / Row 2 : ชื่อสินค้า (shoesName)
ต่อมา เป็นข้อความที่เป็นชื่อสินค้า
var shoesName = Text(
"Space Shoes",
style: TextStyle(fontFamily: 'Sora', fontSize: 36),
);
Column 1 / Row 3 : รูปแสดงสินค้า (shoesImage)
ต่อมาเป็นรูปแสดงสินค้า
var shoesImage = Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 232, 237, 243),
image: const DecorationImage(
image: NetworkImage(
'https://images.pexels.com/photos/2529148/pexels-photo-2529148.jpeg?cs=srgb&dl=pexels-melvin-buezo-2529148.jpg&fm=jpg'),
fit: BoxFit.cover,
),
border: Border.all(width: 1.0, color: Color.fromARGB(255, 232, 237, 243)),
borderRadius: BorderRadius.circular(10)),
width: 297,
height: 288,
);
โครงสร้าง widget อันนี้ เราทำการสร้าง Container
เพื่อห่อหุ้ม image เอาไว้ เพื่อกำหนดขอบเขตและความโค้งของขอบรูปได้ (จาก properties ที่ชื่อ borderRadius
ที่อยู่ใน BoxDecoration
) รวมถึง กำหนดขนาด (width
และ height
) ของรูปได้เช่นเดียวกัน
Column 1 / Row 4 : Rating สินค้า
ต่อมา โฟกัสกันต่อที่ส่วน Rating สินค้า ภายในจะมี widget ย่อยๆ คือ ไอค่อนรูปดาว และ ข้อความแสดงจำนวนการให้คะแนน เราก็สร้าง widget ขึ้นมาทีละอัน
เริ่มจาก ดวงดาวทั้งห้า
var starRow = Row(
children: [
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0)),
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0)),
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0)),
Icon(Icons.star, color: Color.fromARGB(255, 236, 234, 234)),
Icon(Icons.star, color: Color.fromARGB(255, 236, 234, 234)),
],
);
จะมีดาวสีเหลืองสามดวง และสีเทาสองดวง
และก็ ข้อความ
var rating =
Text("136 Rating", style: TextStyle(fontFamily: 'Sora', fontSize: 18));
ต่อมาก็เอา widget ทั้งสองมารวมไว้ด้วยกัน ด้วย Row()
var rowOfRating = Row(
children: [starRow, rating], mainAxisAlignment: MainAxisAlignment.center);
ตั้งค่าให้ widget อยู่ตรงกลางด้วย mainAxisAlignment ตั้งให้เป็น center
Column 1 / Row 5 : ราคาสินค้า (price)
เสร็จจาก Rating แล้ว เราก็สร้างข้อความแสดงราคาสินค้า
var price =
Text("2300 THB", style: TextStyle(fontFamily: 'Sora', fontSize: 18));
Column 1 / Row 6 : ปุ่มเพิ่มสินค้าในตะกร้า (addButton)
ตบท้ายด้วยปุ่มเพิ่มสินค้าในตะกร้า
var addButton = Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 91, 229, 97),
borderRadius: BorderRadius.circular(10)),
child: new ElevatedButton(
onPressed: () {},
child:
Text("Add to cart", style: TextStyle(fontFamily: 'Sora', fontSize: 18)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.green),
),
),
width: 297,
height: 60,
);
ปุ่มจะมีข้อความ "Add to cart" พื้นสีเขียว สังเกตว่าจะใช้ Container ห่อหุ่ม ElevatedButton
แทนที่จะใช้ ElevatedButton
ตรงๆเลย เหตุผลเพราะ ต้องการกำหนดรัศมีของขอบปุ่มให้เป็นไปตามแบบ และกำหนดขนาดด้วยนั่นเอง
รวม widget ของหน้าแรก
เมื่อสร้าง widget ที่จะนำมารวมในหน้าแสดงสินค้าเสร็จแล้ว จากนั้นก็รวมกันลงในหน้า MyShoppingPage ได้เลย
class MyShoppingPage extends StatelessWidget {
MyShoppingPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: Container(
child: Column(
children: [
menuBar,
shoesName,
shoesImage,
rowOfRating,
price,
addButton,
],
),
),
);
}
}
เมื่อลองรันดู จะได้หน้าแอพออกมาเป็นดังนี้
มันก็มี widget ที่ต้องการครบแล้วนะ แต่การเว้นระยะห่างยังต้องปรับปรุง โอเค เรามาสร้าง widget ที่ใช้เว้นระยะกันดีกว่า (ใช่แล้วครับ ทุกสิ่งทุกอย่างใน Flutter สามารถมองเป็น widget ได้หมด แม้กระทั่งช่องไฟระหว่างบรรทัด) ในที่นี่เราจะใช้ widget SizedBox()
เป็นตัวสร้าง
ช่องว่างระหว่าง wiget : gapBetweenWidget
พิจารณาในหน้าแอพ จะมีการกำหนดระยะห่างระหว่าง widget อยู่หลายจุด ดังนั้น สร้างเป็นออบเจคเพื่อให้สื่อความหมาย แล้วดึงเอาไปใช้น่าจะสะดวกกว่า ขอตั้งชื่อว่า gapBetweenWidget ละกัน
var gapBetweenWidget =
(double width, double height) => SizedBox(height: height, width: width);
เสร็จแล้ว เราก้เอาไปใช้กำหนดระยะห่างในหน้าแอพแบบนี้
เสร็จแล้ว ลองรันดูอีกทีนึง
จะเห็นว่ามีการกำหนดระยะห่างออกมาสวยงามตามแบบที่กำหนดเอาไว้แล้ว
สร้างหน้าที่สอง : MySummaryPage
ส่วนของหน้าแรกได้เสร็จเรียบร้อยลงไปแล้ว ต่อมาก็ทำการสร้าง widget แสดงหน้าที่สองกัน
เค้าโครง widget ก็เหมือนหน้าแรกเลย เราตั้งชื่อว่า MySummaryPage
หน้าที่สองจะเป็นส่วนที่ใช้แสดงรายละเอียดของการเลือกสินค้า และราคารวมที่ต้องชำระ เพราะฉะนั้น มันจะประกอบไปด้วย widget ต่างๆ ดังนี้
สร้างคลาสสำหรับสร้างปุ่มวงกลม : CircleButton
ก่อนที่จะสร้างปุ่มขึ้นมา เราตั้งข้อสังเกตว่า รูปร่างของปุ่มมันก็เหมือนๆกันกับหน้าที่แล้วเลยนี่หว่า เพียงแต่ต่างกันที่ไอค่อน และฟังก์ชั่นที่เราจะให้มันทำงานเท่านั้น งั้นเราก็สร้างคลาสขึ้นมาสำหรับสร้างปุ่มที่มีรูปร่างวงกลมขึ้นมาเลยดีกว่า
คลาสชื่อ CircleButton
ที่รับ argument สองตัวคือ iconSharp
ที่เป็นรูปร่างไอค่อนที่เราต้องการ และ onPressedFunc
ที่เป็นฟังก์ชั่นที่เราจะใส่ให้ปุ่มทำงานเมื่อมีการกดปุ่มนั้น
ต่อมาก็สร้างตัวแปรที่เก็บฟังก์ชั่นเปล่าๆเอาไว้ เพื่อใช้เป็นพารามิเตอร์ ในการสร้างปุ่มที่เรายังไม่ต้องการให้มันทำวานอะไร เวลากดปุ่มนั้นก็จะไม่มีอะไรเกิดขึ้น
var blank = () => {};
Reassignment : menuButton, cartButton
เมื่อมีคลาส CircleButton
มาให้ใช้แล้ว เราก็เอามาใช้กับปุ่ม menuButton
และ cartButton
ของหน้าแรก ให้มันเป็นมาตรฐานเหมือนกันเลยดีฝ่า โดยการใช้คลาสมาแทนโค้ดเดิมเลยเพื่อลดบรรทัด
var menuButton = CircleButton(Icons.menu_sharp, blank);
var cartButton = CircleButton(Icons.shopping_cart, blank);
Column 2 / Row 1 : สร้างแถบเมนู
สร้างปุ่มย้อนกลับ (backButton)
var backButton = CircleButton(Icons.navigate_before, blank);
รับ argument มาสองตัวคือ Icons.navigate_before
ที่เป็นเครื่องหมายหัวลูกศรชี้ไปทางซ้าย และอีกอันคือ blank คือฟังก์ชั่นเปล่าๆที่สร้างเตรียมเอาไว้นั่นเอง
สร้างปุ่มผู้ใช้งาน (userButton)
ไปค่อนจะเป็นรูปคน เพื่อให้แสดงถึงตัวของผู้ใช้งาน หรือข้อมูลผู้ใช้งาน
var userButton = CircleButton(Icons.person, blank);
เมื่อสร้างสองปุ่มเสร็จแล้ว ก็เอามารวมกัน เหมือนปุ่มในหน้าแรกอ่าแหละ ตั้งชื่อกลุ่มของปุ่มว่า menuBarInSummary
var menuBarInSummary = Row(
children: [
Expanded(
child: Row(children: [backButton], textDirection: TextDirection.ltr)),
Expanded(
child: Row(children: [userButton], textDirection: TextDirection.rtl)),
],
);
Column 2 / Row 2 : สร้างหัวข้อหน้าสรุป : summaryTitle
สร้าง Text
widget ขึ้นมา
var summaryTitle = Text(
"YOUR CART",
style: TextStyle(fontFamily: 'Sora', fontSize: 24),
);
สร้างคลาสรูปภาพรองเท้า
ใช้เพื่อเป็นคลาสไปสร้าง widget รูปสินค้ารองเท้าสำหรับใช้ในแอพทั้งสองหน้า ตั้งชื่อว่า ShoesImageItem
คลาส ShoesImageItem
จะรับ argument เพียง 2 ตัว ความกว้าง (width
) และความสูง (height
) เพื่อเอาไว้ใช้ปรับให้เข้ากับ layout ในส่วนนั้นๆ ซึ่งรูปภาพจะกำหนดให้เป็นรูปเดียวกันไปเลยไม่ต้องเปลี่ยน การแสดงผลก็จะมีลักษณะเดียวกับ widget shoesImage
ที่สร้างในหน้าแรกเลย
Reassignment : shoesImage
เมื่อสร้างคลาสมาแล้วก็ต้องเปลี่ยนการประกาศ shoesImage
ซะใหม่ เพื่อความประหยัด
var shoesImage = ShoesImageItem(297, 288);
เหลือแค่นี้เอง ลดบรรทัดไปได้เยอะ
Column 2 / Row 3 : รายละเอียดสินค้าที่เลือก
ในส่วนนี้ เราจะแบ่งการสร้าง widget ย่อยๆ ออกมาได้เป็น รูปภาพ ชื่อสินค้า เรทติ้ง และดาว
สร้าง widget รูปภาพ ชื่อตัวแปร shoesImageInSummary
var shoesImageInSummary = ShoesImageItem(125, 121);
สร้าง widget ชื่อสินค้า เก็บค่าในตัวแปร shoesNameSummary
var shoesNameSummary = TextItem("Space Shoes", 14);
สร้าง widget ข้อความแสดงเรทติ้ง เก็บไว้ในตัวแปร ratingInSummary
var ratingInSummary = TextItem("136 Rating", 10);
สร้าง widget กลุ่มของไอค่อนดาว เหมือนในหน้าแรก แต่ขนาดจะเล็กกว่า
var starRowInSummary = Row(
children: [
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0), size: 15),
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0), size: 15),
Icon(Icons.star, color: Color.fromARGB(255, 255, 240, 0), size: 15),
Icon(Icons.star, color: Color.fromARGB(255, 236, 234, 234), size: 15),
Icon(Icons.star, color: Color.fromARGB(255, 236, 234, 234), size: 15),
],
);
ปรับขนาดดาวลง เป็น 15 ให้สอดคล้องกับที่จะใช้ในหน้านี้
แล้วเอา widget ที่สร้างมาประกอบกัน ภายใน widget Container
var productDetailInSummary = Container(
child: Row(
children: [
Expanded(
child: Row(
children: [shoesImageInSummary],
textDirection: TextDirection.ltr)),
Expanded(
child: Row(children: [
Column(
children: [
shoesNameSummary,
gapBetweenWidget(0, 50),
ratingInSummary,
gapBetweenWidget(0, 20),
starRowInSummary
],
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start)
], textDirection: TextDirection.rtl)),
],
),
width: 297);
widget ถูกเก็บในตัวแปรชื่อ productDetailInSummary
มีความกว้างเท่ากับ widget menuBarInSummary
คือ 297 ตามแบบที่กำหนด
Column 2 / Row 4 : สร้างรายละเอียดแจ้งยอดชำระ
ประกาศตัวแปร billing
เพื่อเก็บค่า widget Container
ที่เราจะสร้างเพื่อเป็นส่วนพื้นที่ของรายละเอียดมูลค่า
var billing = Container(
decoration: BoxDecoration(
color: Colors.grey[300], borderRadius: BorderRadius.circular(10)),
width: 297,
height: 234);
สร้างคลาสปุ่มสี่เหลี่ยมสำหรับ Add to cart และ Checkout
สร้างคลาส ชื่อว่า AcceptButtonItem
รับค่าสองค่าเข้ามา ตัวหนึ่งจะเป็นข้อความ ส่วนอีกตัวหนึ่งจะเป็น widget Colors
เป็นสีที่ใช้เป็นพื้นหลังของปุ่มนั้น
Reassignment : ปุ่มกดยืนยัน
เมื่อสร้างคลาส AcceptButtonItem
แล้ว ให้สร้างออบเจคที่รับค่า
var addButton = AcceptButtonItem("Add to cart", Colors.green);
Column 2 / Row 5 : ปุ่มเช็คเอาท์
สร้างออบเจคจากคลาส AcceptButtonItem
แล้วเก็บในตัวแปร checkoutButton แบบนี้
var checkoutButton = AcceptButtonItem("Checkout", Colors.red);
ประกอบร่าง Page 2
นำเอา widget ทั้งหมดที่เราสร้างสำหรับ Column 2 มารวมกัน แบบนี้
ลอง debug หน้าเพจสองหน่อยเป็นไง
การแก้ไขความกว้างแถบเมนู
ปัญหานึงที่เราพบตั้งแต่ตอนแรกแต่เราไม่สนใจมัน คือ แถบเมนูที่เราสร้าง มีขนาดกว้างเท่าๆกับขนาดหน้าจอเลย ซึ่งที่จริงก็ไม่ใช่ปัญหา แค่เราปรับขนาดหน้าจอก็เพียงพอ แต่ไหนๆก็มาถึงนี่ละ ขอปรับขนาดให้มันตรงกับเพื่อนๆมันตั้งแต่ตอนนี้เลยละกัน
ในหน้าเพจ MyShoppingPage
ให้แก้ menuBar
โดยเพิ่ม Container ครอบอีกที แล้วกำหนด width ให้มัน
var menuBar = Container(
child: Row(
children: [
Expanded(
child: Row(children: [menuButton], textDirection: TextDirection.ltr)),
Expanded(
child: Row(children: [cartButton], textDirection: TextDirection.rtl)),
],
),
width: 297,
);
ไหนดูซิ
ได้ละ
เช่นเดียวกันกับหน้า MySummaryPage
ทำแบบเดียวกันกับหน้า MySummaryPage
ตรง menuBarInSummary
ด้วย
var menuBarInSummary = Container(
child: Row(
children: [
Expanded(
child: Row(children: [backButton], textDirection: TextDirection.ltr)),
Expanded(
child: Row(children: [userButton], textDirection: TextDirection.rtl)),
],
),
width: 297,
);
ตรวจสอบ
โอเค ผ่าน ต่อไป ไปดูกันต่อที่ การเชื่อมหน้าเพจทั้งสองเอาไว้ ด้วยการกดปุ่ม
การเชื่อมกันระหว่าง MyShoppingPage กับ MySummaryPage
ในการออกแบบ เราจะให้ มีการกดปุ่มตะกร้าสินค้า เพื่อนำไปยังหน้า MySummaryPage
ดังนั้น เราจะกำหนดฟังก์ชั่นขึ้นมาเพื่อรอให้มีอีเวนท์เกิดขึ้น (Listening) เรียกว่า Callback Function
นั่นเอง
ในการ callback เราจะใช้ widget ชื่อว่า Navigator
มี 2 แบบคือ
Navigator.push()
สำหรับ เชื่อมไปยัง widget หน้าถัดไป
Navigator.pop()
สำหรับ เชื่อมกลับมายัง widget หน้าที่แล้ว
เพิ่ม Navigator.push()
รูปแบบการกำหนดฟังก์ชั่นใน onPressed properties
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
}
จะเห็นว่า ต้องมีการใช้ออบเจค context ที่สร้างมาจาก BuildContext ซึ่งจะเป็นตัวเชื่อมต่อกับ reference กับ widget ทั้งหมดในเพจนั้นๆ ดังนั้น จำเป็นที่จะต้องกำหนดฟังก์ชั่นใน widget ของเพจ แทนที่จะกำหนดภายนอก แล้วเรียกไปใช้เหมือน widget ตัวอื่นๆ
ดังนั้นจึงขอยุบตัวแปร menuBar ออกจากหน้า MyShoppingPage แล้วเอาค่าของมันมาวางแทนแบบนี้
class MyShoppingPage extends StatelessWidget { MyShoppingPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Material( child: Container( child: Column( children: [ Container( child: Row( children: [ Expanded( child: Row( children: [menuButton], textDirection: TextDirection.ltr)), Expanded( child: Row(children: [ CircleButton(Icons.shopping_cart, () { Navigator.push(context, MaterialPageRoute(builder: (context) { return MySummaryPage(); })); return null; }) ], textDirection: TextDirection.rtl)), ], ), width: 297), gapBetweenWidget(0, 30), shoesName, gapBetweenWidget(0, 20), shoesImage, gapBetweenWidget(0, 10), rowOfRating, gapBetweenWidget(0, 10), price, gapBetweenWidget(0, 10), AcceptButtonItem("Add to cart", Colors.green, () { Navigator.push(context, MaterialPageRoute(builder: (context) { return MySummaryPage(); })); return null; }), ], ), margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), ), ); } }
มีการเพิ่ม margin ให้กับ Container ของ แถบเมนูด้วย เพื่อให้ตรงกับหน้าที่สอง
ในส่วนของปุ่มตะกร้าสินค้า มีการสร้างออบเจคจากคลาส CircleButton
ซึ่ง argument ตัวที่สอง กำหนดไว้ให้เป็นฟังก์ชั่น ซึ่งเราก็จะเอาฟังก์ชั่นที่ครอบ Navigator.push()
ลงไป แล้วให้ฟังก์ชั่นนั้นคืนค่า null
ออกมา ซึ่งถ้าเราใช้ Navigator.push()
แทนลงไปตรงๆ จะเกิด error แบบนี้
The argument type 'Future<dynamic>' can't be assigned to the parameter type 'Map<dynamic, dynamic> Function()
เป็นเพราะว่า Navigator.push()
นั้นไม่ใช่ Function แต่เป็นชนิด Map
จากตัวอย่าง เราก็ทำการยุบตัวแปร addButton
ทิ้งไปเช่นเดียวกัน ในกรณีที่ต้องการจะให้ปุ่ม Add to cart
มีการทำงานเหมือนกับปุ่มตะกร้าสินค้า คือกดแล้วจะเปลี่ยนไปยังหน้าที่สองนั่นเอง
เพิ่ม Navigator.pop()
ส่วนหน้า MySummaryPage ก็เช่นเดียวกัน ตรงส่วนตัวแปร menuBarInSummary ก็จะถูกยุบทิ้งเช่นเดียวกัน เนื่องจากเราต้องการให้ปุ่ม backButton ได้ใช้งานฟังก์ชั่น Navigator.pop() เพื่อให้กดย้อนมายังหน้าที่แล้ว (หน้าแรกนั่นแหละ)
เปลี่ยนโค้ดเป็นแบบนี้
class MySummaryPage extends StatelessWidget { MySummaryPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Material( child: Container( child: Column( children: [ Container( child: Row( children: [ Expanded( child: Row(children: [ CircleButton(Icons.navigate_before, () { Navigator.pop(context); return null; }) ], textDirection: TextDirection.ltr)), Expanded( child: Row( children: [userButton], textDirection: TextDirection.rtl)), ], ), width: 297), gapBetweenWidget(0, 30), summaryTitle, gapBetweenWidget(0, 20), productDetailInSummary, gapBetweenWidget(0, 20), billing, gapBetweenWidget(0, 10), checkoutButton, ], ), margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), ), ); } }
ลองกดรันดู จะมีหน้าแอพปรากฏขึ้น จากนั้นทดสอบกดปุ่มตะกร้าสินค้า
หน้าแอพทำงานได้ปกติ ไม่มีผิดพลาด แต่เราพบว่า เมื่อกดปุ่ม หน้าเพจจะเปลี่ยนหน้าเพจแบบสไลด์จากด้านล่าง ซึ่งถ้าไม่ต้องการจะเปลี่ยนแปลง ก็สามารถจบที่ขั้นตอนนี้ได้ แต่เราอยากให้มันเปลี่ยนหน้าจอแบบสไลด์จากด้านข้างแทน
ให้เพิ่มโค้ดนี้เข้ามา ตามตัวอย่างจาก แหล่งอ้างอิง
มีการสร้างฟังก์ชั่นชื่อว่า createRoute
เป็นชนิด Route
ที่ภายในจะคืน widget PageRouteBuilder
โดยใน properties ที่ชื่อว่า transitionsBuilder
นี่แหละที่ใช้เปลี่ยนรูปแบบการสไลด์หน้าต่าง โดยสังเกตที่ค่า begin
มีการใช้ Offset
แล้วตั้งค่าเป็น 1.0, 0.0 ตามแกน x แกน y นั่นเอง
แล้วเปลี่ยนฟังก์ชั่นที่ครอบ Navigator.push()
ของ MyShoppingPage
เป็นดังนี้
ส่วน MySummaryPage
เราจะไม่เปลี่ยนแปลงค่าอะไร ใช้ Navigator.pop()
เหมือนเดิม
ลองกดรันดูอีกครั้ง เราจะพบว่ามันสามารถเปลี่ยนหน้าเพจแบบสไลด์ด้านข้างได้แล้ว เย้
เมื่อสร้างแอพได้ตามที่กำหนดแล้ว สำหรับบทความนี้ก็ขอจบแต่เพียงเท่านี้ครับ
References
Layout widget
Layout
Material
Material App
Material Class
Scaffolf Class
AppBar Class
Container Class
BoxDecoration Class
Color Class
Icon Class
ElevatedButton Class
MaterialStateProperty Class
How to change the color of elevatedbutton in Flutter
SizedBox Class
Navigation Basic
Page Route Animation
HTML Color
Summary about build context widget key in Flutter