MDC-102 Flutter: โครงสร้างวัสดุและเลย์เอาต์

1. บทนำ

logo_components_color_2x_web_96dp.png

Material Components (MDC) ช่วยให้นักพัฒนาแอปใช้งาน Material Design ได้ MDC สร้างขึ้นโดยทีมวิศวกรและนักออกแบบ UX ของ Google โดยมีคอมโพเนนต์ UI ที่สวยงามและใช้งานได้หลายสิบรายการ และพร้อมใช้งานสำหรับ Android, iOS, เว็บ และ Flutter.material.io/develop

ใน Codelab MDC-101 คุณใช้คอมโพเนนต์ Material 2 รายการเพื่อสร้างหน้าลงชื่อเข้าใช้ ได้แก่ ช่องข้อความและปุ่มที่มีระลอกหมึก ตอนนี้มาต่อยอดจากพื้นฐานนี้ด้วยการเพิ่มการนำทาง โครงสร้าง และข้อมูลกัน

สิ่งที่คุณจะสร้าง

ในโค้ดแล็บนี้ คุณจะได้สร้างหน้าจอหลักสำหรับแอปชื่อ Shrine ซึ่งเป็นแอปอีคอมเมิร์ซที่ขายเสื้อผ้าและของใช้ในบ้าน โดยจะประกอบด้วยข้อมูลต่อไปนี้

  • แถบแอปด้านบน
  • รายการตารางกริดที่เต็มไปด้วยผลิตภัณฑ์

Android

iOS

แอปอีคอมเมิร์ซที่มีแถบแอปด้านบนและตารางผลิตภัณฑ์

แอปอีคอมเมิร์ซที่มีแถบแอปด้านบนและตารางผลิตภัณฑ์

คอมโพเนนต์และระบบย่อยของ Material Flutter ในโค้ดแล็บนี้

  • แถบแอปด้านบน
  • ตารางกริด
  • การ์ด

คุณจะให้คะแนนระดับประสบการณ์ในการพัฒนาแอปด้วย Flutter เท่าไร

ผู้ฝึกหัด ขั้นกลาง ผู้ชำนาญ

2. ตั้งค่าสภาพแวดล้อมในการพัฒนา Flutter

คุณต้องมีซอฟต์แวร์ 2 อย่างเพื่อทำแล็บนี้ให้เสร็จสมบูรณ์ ได้แก่ Flutter SDK และโปรแกรมแก้ไข

คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้

  • อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาแอป
  • โปรแกรมจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
  • Android Emulator (ต้องตั้งค่าใน Android Studio)
  • เบราว์เซอร์ (ต้องใช้ Chrome สำหรับการแก้ไขข้อบกพร่อง)
  • ในรูปแบบแอปพลิเคชันเดสก์ท็อป Windows, Linux หรือ macOS คุณต้องพัฒนาบนแพลตฟอร์มที่วางแผนจะใช้งาน ดังนั้น หากต้องการพัฒนาแอปเดสก์ท็อป Windows คุณต้องพัฒนาบน Windows เพื่อเข้าถึงห่วงโซ่การสร้างที่เหมาะสม มีข้อกำหนดเฉพาะของระบบปฏิบัติการที่อธิบายไว้โดยละเอียดใน docs.flutter.dev/desktop

3. ดาวน์โหลดแอปเริ่มต้นของ Codelab

หากเคยเรียน MDC-101 มาก่อน

หากคุณทำ MDC-101 เสร็จแล้ว คุณควรเตรียมโค้ดสำหรับ Codelab นี้ ข้ามไปยังขั้นตอนเพิ่มแถบแอปด้านบน

หากคุณเพิ่งเริ่มต้น

ดาวน์โหลดแอป Codelab เริ่มต้น

แอปเริ่มต้นอยู่ในไดเรกทอรี material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series

...หรือโคลนจาก GitHub

หากต้องการโคลนโค้ดแล็บนี้จาก GitHub ให้เรียกใช้คำสั่งต่อไปนี้

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 102-starter_and_101-complete

เปิดโปรเจ็กต์และเรียกใช้แอป

  1. เปิดโปรเจ็กต์ในโปรแกรมตัดต่อที่ต้องการ
  2. ทำตามวิธีการเพื่อ "เรียกใช้แอป" ในส่วนเริ่มต้นใช้งาน: ทดลองใช้สำหรับโปรแกรมแก้ไขที่คุณเลือก

สำเร็จ! คุณควรเห็นหน้าเข้าสู่ระบบ Shrine จาก Codelab MDC-101 ในอุปกรณ์

Android

iOS

หน้าเข้าสู่ระบบที่มีช่องชื่อผู้ใช้และรหัสผ่าน ปุ่มยกเลิกและปุ่มถัดไป

หน้าเข้าสู่ระบบที่มีช่องชื่อผู้ใช้และรหัสผ่าน ปุ่มยกเลิกและปุ่มถัดไป

ตอนนี้หน้าจอเข้าสู่ระบบดูดีแล้ว เรามาป้อนข้อมูลผลิตภัณฑ์บางอย่างลงในแอปกัน

4. เพิ่มแถบแอปด้านบน

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

Material Design มีรูปแบบการนำทางที่ช่วยให้มั่นใจได้ถึงความสามารถในการใช้งานในระดับสูง คอมโพเนนต์ที่มองเห็นได้ชัดเจนที่สุดอย่างหนึ่งคือแถบแอปด้านบน

มาเพิ่มแถบแอปด้านบนเพื่อช่วยในการนำทางและให้ผู้ใช้เข้าถึงการดำเนินการอื่นๆ ได้อย่างรวดเร็วกัน

เพิ่มวิดเจ็ต AppBar

ใน home.dart ให้เพิ่ม AppBar ลงใน Scaffold และนำ const ที่ไฮไลต์ออก

return const Scaffold(
  // TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
  ),

การเพิ่ม AppBar ลงในฟิลด์ appBar: ของ Scaffold จะทำให้เราได้เลย์เอาต์ที่สมบูรณ์แบบโดยไม่ต้องเสียค่าใช้จ่าย โดยจะเก็บ AppBar ไว้ที่ด้านบนของหน้าและเนื้อหาไว้ด้านล่าง

เพิ่มวิดเจ็ตข้อความ

ใน home.dart ให้เพิ่มชื่อใน AppBar ดังนี้

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: const Text('SHRINE'),
    // TODO: Add trailing buttons (102)

บันทึกโปรเจ็กต์

Android

iOS

แถบแอปที่มี Shrine เป็นชื่อ

แถบแอปที่มี Shrine เป็นชื่อ

แถบแอปหลายแถบมีปุ่มข้างชื่อ มาเพิ่มไอคอนเมนูในแอปกัน

เพิ่ม IconButton ที่นำหน้า

ขณะที่ยังอยู่ใน home.dart ให้ตั้งค่า IconButton สำหรับช่อง leading: ของ AppBar (วางไว้ก่อนฟิลด์ title: เพื่อเลียนแบบลำดับจากหน้าไปหลัง)

    // TODO: Add buttons and title (102)
    leading: IconButton(
      icon: const Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

บันทึกโปรเจ็กต์

Android

iOS

แถบแอปที่มี Shrine เป็นชื่อและไอคอนเมนู 3 ขีด

แถบแอปที่มี Shrine เป็นชื่อและไอคอนเมนู 3 ขีด

ไอคอนเมนู (หรือที่เรียกว่า "แฮมเบอร์เกอร์") จะปรากฏในตำแหน่งที่คุณคาดไว้

นอกจากนี้ คุณยังเพิ่มปุ่มที่ด้านท้ายของชื่อได้ด้วย ใน Flutter สิ่งเหล่านี้เรียกว่า "การดำเนินการ"

เพิ่มการดำเนินการ

ยังมีพื้นที่สำหรับ IconButton อีก 2 รายการ

เพิ่มลงในอินสแตนซ์ AppBar หลังจากชื่อ

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: const Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: const Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

บันทึกโปรเจ็กต์ หน้าจอหลักควรมีลักษณะดังนี้

Android

iOS

แถบแอปที่มี Shrine เป็นชื่อและไอคอนเมนู 3 ขีด รวมถึงไอคอนค้นหาและปรับแต่งที่อยู่ท้าย

แถบแอปที่มี Shrine เป็นชื่อและไอคอนเมนู 3 ขีด รวมถึงไอคอนค้นหาและปรับแต่งที่อยู่ท้าย

ตอนนี้แอปมีปุ่มนำ ชื่อ และการดำเนินการ 2 อย่างทางด้านขวา แถบแอปยังแสดงการยกระดับโดยใช้เงาที่ละเอียดอ่อนซึ่งแสดงว่าแถบแอปอยู่บนเลเยอร์ที่ต่างจากเนื้อหา

5. เพิ่มการ์ดในตารางกริด

ตอนนี้แอปของเรามีโครงสร้างแล้ว มาจัดระเบียบเนื้อหาโดยใส่ลงในการ์ดกัน

เพิ่ม GridView

มาเริ่มกันด้วยการเพิ่มการ์ด 1 ใบใต้แถบแอปด้านบน วิดเจ็ต Card เพียงอย่างเดียวมีข้อมูลไม่เพียงพอที่จะจัดวางตัวเองในตำแหน่งที่เราจะเห็นได้ ดังนั้นเราจึงต้องห่อหุ้มวิดเจ็ตนี้ไว้ในวิดเจ็ต GridView

แทนที่ Center ในเนื้อหาของ Scaffold ด้วย GridView ดังนี้

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: Build a grid of cards (102)
  children: <Widget>[Card()],
),

มาดูโค้ดนั้นกัน GridView จะเรียกใช้ตัวสร้าง count() เนื่องจากจำนวนรายการที่แสดงนั้นนับได้และไม่สิ้นสุด แต่ต้องมีข้อมูลเพิ่มเติมเพื่อกำหนดเลย์เอาต์

crossAxisCount: ระบุจำนวนรายการในแนวนอน เราต้องการ 2 คอลัมน์

padding: ฟิลด์จะให้พื้นที่ทั้ง 4 ด้านของ GridView แน่นอนว่าคุณจะมองไม่เห็นระยะขอบที่ด้านท้ายหรือด้านล่างเนื่องจากยังไม่มีองค์ประกอบย่อย GridView อยู่ข้างๆ

ฟิลด์ childAspectRatio: จะระบุขนาดของรายการตามสัดส่วนการแสดงผล (ความกว้างต่อความสูง)

โดยค่าเริ่มต้น GridView จะสร้างไทล์ที่มีขนาดเท่ากันทั้งหมด

เรามีบัตรอยู่ 1 ใบ แต่ไม่มีเงินในบัตร มาเพิ่มวิดเจ็ตย่อยลงในการ์ดกัน

จัดวางเนื้อหา

การ์ดควรมีพื้นที่สำหรับรูปภาพ ชื่อ และข้อความรอง

อัปเดตองค์ประกอบย่อยของ GridView โดยทำดังนี้

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    clipBehavior: Clip.antiAlias,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              const SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

โค้ดนี้จะเพิ่มวิดเจ็ต Column ที่ใช้ในการจัดวางวิดเจ็ตย่อยในแนวตั้ง

crossAxisAlignment: field ระบุ CrossAxisAlignment.start ซึ่งหมายถึง "จัดข้อความไปที่ขอบนำ"

วิดเจ็ต AspectRatio จะกำหนดรูปร่างของรูปภาพไม่ว่าจะเป็นรูปภาพประเภทใดก็ตาม

ระยะห่างภายในจะเลื่อนข้อความจากด้านข้างเข้ามาเล็กน้อย

วิดเจ็ตข้อความทั้ง 2 รายการซ้อนกันในแนวตั้งโดยมีพื้นที่ว่าง 8 จุดคั่นระหว่างกลาง (SizedBox) เราสร้าง Column อีกอันเพื่อวางไว้ใน Padding

บันทึกโปรเจ็กต์

Android

iOS

รายการเดียวที่มีรูปภาพ ชื่อ และข้อความรอง

รายการเดียวที่มีรูปภาพ ชื่อ และข้อความรอง

ในตัวอย่างนี้ คุณจะเห็นว่าการ์ดอยู่ห่างจากขอบ มีมุมโค้ง และมีเงา (ที่แสดงระดับความสูงของการ์ด) รูปร่างทั้งหมดเรียกว่า "คอนเทนเนอร์" ใน Material (อย่าสับสนกับคลาสวิดเจ็ตจริงที่ชื่อ Container)

โดยปกติแล้ว การ์ดจะแสดงในคอลเล็กชันพร้อมกับการ์ดอื่นๆ เรามาจัดวางเป็นคอลเล็กชันในตารางกัน

6. สร้างคอลเล็กชันการ์ด

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

คูณการ์ดเป็นคอลเล็กชัน

ตอนนี้เราสร้างการ์ดในบรรทัดของฟิลด์ children: ของ GridView ซึ่งเป็นโค้ดที่ซ้อนกันหลายชั้นและอ่านได้ยาก มาแยกส่วนนี้เป็นฟังก์ชันที่สร้างการ์ดเปล่าได้มากเท่าที่เราต้องการ และแสดงผลรายการการ์ดกัน

สร้างฟังก์ชันส่วนตัวใหม่เหนือฟังก์ชัน build() (โปรดทราบว่าฟังก์ชันที่ขึ้นต้นด้วยขีดล่างเป็น API ส่วนตัว)

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) {
      return Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            AspectRatio(
              aspectRatio: 18.0 / 11.0,
              child: Image.asset('assets/diamond.png'),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const <Widget>[
                  Text('Title'),
                  SizedBox(height: 8.0),
                  Text('Secondary Text'),
                ],
              ),
            ),
          ],
        ),
      );
    },
  );
  return cards;
}

กำหนดการ์ดที่สร้างขึ้นให้กับช่อง children ของ GridView อย่าลืมแทนที่ทุกอย่างใน GridView ด้วยโค้ดใหม่นี้

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // Replace
),

บันทึกโปรเจ็กต์

Android

iOS

ตารางกริดของรายการที่มีรูปภาพ ชื่อ และข้อความรอง

ตารางกริดของรายการที่มีรูปภาพ ชื่อ และข้อความรอง

การ์ดปรากฏขึ้น แต่ยังไม่แสดงอะไร ตอนนี้เป็นเวลาที่เหมาะในการเพิ่มข้อมูลผลิตภัณฑ์

เพิ่มข้อมูลผลิตภัณฑ์

แอปมีผลิตภัณฑ์บางอย่างที่มีรูปภาพ ชื่อ และราคา มาเพิ่มวิดเจ็ตนั้นลงในวิดเจ็ตที่เรามีในการ์ดอยู่แล้วกัน

จากนั้นใน home.dart ให้นำเข้าแพ็กเกจใหม่และไฟล์บางไฟล์ที่เราจัดเตรียมไว้สำหรับโมเดลข้อมูล

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/product.dart';
import 'model/products_repository.dart';

สุดท้าย ให้เปลี่ยน _buildGridCards() เพื่อดึงข้อมูลผลิตภัณฑ์ และใช้ข้อมูลนั้นในการ์ด

// TODO: Make a collection of cards (102)

// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      clipBehavior: Clip.antiAlias,
      // TODO: Adjust card heights (103)
      child: Column(
        // TODO: Center items on the card (103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: Adjust the box size (102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: Align labels to the bottom and center (103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: Change innermost Column (103)
                children: <Widget>[
                 // TODO: Handle overflowing labels (103)
                 Text(
                    product.name,
                    style: theme.textTheme.titleLarge,
                    maxLines: 1,
                  ),
                  const SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.titleSmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

หมายเหตุ: ยังไม่คอมไพล์และเรียกใช้ เรามีการเปลี่ยนแปลงอีกอย่างหนึ่ง

นอกจากนี้ ให้เปลี่ยนbuild()ฟังก์ชันเพื่อส่ง BuildContext ไปยัง _buildGridCards() ก่อนที่จะพยายามคอมไพล์

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

รีสตาร์ตร้อนแอป

Android

iOS

ตารางรายการที่มีรูปภาพ ชื่อผลิตภัณฑ์ และราคา

ตารางรายการที่มีรูปภาพ ชื่อผลิตภัณฑ์ และราคา

คุณอาจสังเกตเห็นว่าเราไม่ได้เพิ่มช่องว่างแนวตั้งระหว่างการ์ด เนื่องจากโดยค่าเริ่มต้นแล้วจะมีระยะขอบ 4 จุดที่ด้านบนและด้านล่าง

บันทึกโปรเจ็กต์

ข้อมูลผลิตภัณฑ์แสดงขึ้น แต่รูปภาพมีพื้นที่ว่างรอบๆ รูปภาพจะวาดด้วย BoxFit ขนาด .scaleDown โดยค่าเริ่มต้น (ในกรณีนี้) เรามาเปลี่ยนเป็น .fitWidth เพื่อให้ซูมเข้าเล็กน้อยและนำการเว้นวรรคที่ไม่จำเป็นออก

เพิ่มฟิลด์ fit: ลงในรูปภาพโดยมีค่าเป็น BoxFit.fitWidth ดังนี้

  // TODO: Adjust the box size (102)
  fit: BoxFit.fitWidth,

Android

iOS

ตารางรายการที่มีรูปภาพที่ครอบตัด ชื่อผลิตภัณฑ์ และราคา

ตอนนี้ผลิตภัณฑ์ของเราแสดงในแอปได้อย่างสมบูรณ์แล้ว

7. ยินดีด้วย

แอปของเรามีโฟลว์พื้นฐานที่จะนำผู้ใช้จากหน้าจอเข้าสู่ระบบไปยังหน้าจอหลัก ซึ่งผู้ใช้จะดูผลิตภัณฑ์ได้ เราได้เพิ่มแถบแอปด้านบน (พร้อมชื่อและปุ่ม 3 ปุ่ม) และการ์ด (เพื่อนำเสนอเนื้อหาของแอป) ด้วยโค้ดเพียงไม่กี่บรรทัด ตอนนี้หน้าจอหลักของเราเรียบง่ายและใช้งานได้จริง โดยมีโครงสร้างพื้นฐานและเนื้อหาที่ดำเนินการได้

ขั้นตอนถัดไป

ตอนนี้เราได้ใช้คอมโพเนนต์หลัก 4 รายการจากไลบรารี Material Flutter แล้ว ได้แก่ แถบแอปด้านบน การ์ด ช่องข้อความ และปุ่ม คุณดูข้อมูลเพิ่มเติมได้โดยไปที่แคตตาล็อกวิดเจ็ตคอมโพเนนต์ Material

แม้ว่าแอปจะทำงานได้อย่างเต็มที่ แต่ก็ยังไม่ได้แสดงถึงแบรนด์หรือมุมมองใดๆ โดยเฉพาะ ใน MDC-103: Material Design Theming with Color, Shape, Elevation and Type เราจะปรับแต่งสไตล์ของคอมโพเนนต์เหล่านี้เพื่อแสดงถึงแบรนด์ที่ทันสมัยและมีชีวิตชีวา

ฉันทำ Codelab นี้เสร็จได้โดยใช้เวลาและความพยายามที่สมเหตุสมผล

เห็นด้วยอย่างยิ่ง เห็นด้วย เป็นกลาง ไม่เห็นด้วย ไม่เห็นด้วยอย่างยิ่ง

ฉันต้องการใช้คอมโพเนนต์ Material ต่อไปในอนาคต

เห็นด้วยอย่างยิ่ง เห็นด้วย เฉยๆ ไม่เห็นด้วย ไม่เห็นด้วยอย่างยิ่ง