Flutter สร้าง UI แอพพลิเคชั่นร้านค้าออนไลน์ (for My Workshop Record)

บทความนี้เราจะอธิบายขั้นตอนการสร้างส่วนหน้าแอพพลิเคชั่นของร้านค้าออนไลน์ ซึ่งมีการกำหนดหน้าตาออกมาเรียบร้อยแล้ว เหตุผลก็เพราะว่าจะต้องการบันทึกความรู้ที่ใช้ในการสร้างแอพเบื้องต้น ฝึกการออกแบบ widget ต่างๆ เช่น Row, Column, Container, Image, Icon และอีกหลายๆ widget ประกอบกับ การฝึกมองแอพให้เป็น widget ย่อยๆ และเข้าใจถึงการประกอบกันให้เป็นแอพของ widget ก็เป็นทักษะที่นักพัฒนา Flutter ต้องฝึกบ่อยๆ

ลักษณะแอพพลิเคชันที่กำหนด เป็นแบบนี้

Shopping Online App Wireframe

มีการกำหนด widget ของ UI แบ่งออกเป็นส่วนต่างๆ มีดังนี้

เมื่อมีแบบแล้ว ก็เริ่มต้นสร้างกันเลย

จาก prototype ด้านบน หน้าแรกของแอพ จะมี widget tree diagram เป็นแบบนี้

Widget tree ของ เพจหน้าแรก
Widget tree ของ เพจหน้าที่สอง

โดยจะมี 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

Pub Dev

Shoes Image

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