แอป Flutter แอปแรกของคุณ

1. บทนำ

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

แอปพลิเคชันจะสร้างชื่อที่ฟังดูเท่ เช่น "newstay", "lightstream", "mainbrake" หรือ "graypine" ผู้ใช้สามารถขอชื่อถัดไป ตั้งชื่อปัจจุบันเป็นรายการโปรด และดูรายการชื่อที่ชื่นชอบในหน้าแยกต่างหาก แอปปรับเปลี่ยนตามขนาดหน้าจอต่างๆ

สิ่งที่คุณจะได้เรียนรู้

  • ข้อมูลเบื้องต้นเกี่ยวกับวิธีการทำงานของ Flutter
  • การสร้างเลย์เอาต์ใน Flutter
  • เชื่อมโยงการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) กับลักษณะการทํางานของแอป
  • จัดระเบียบโค้ด Flutter
  • การทําให้แอปปรับเปลี่ยนตามอุปกรณ์ (สําหรับหน้าจอต่างๆ)
  • การสร้างรูปลักษณ์ของแอปให้สอดคล้องกัน

คุณจะเริ่มด้วยสคาฟเฟิลด์พื้นฐานเพื่อให้ข้ามไปยังส่วนที่สนใจได้ทันที

e9c6b402cd8003fd.png

และ Filip จะพาคุณท่องไปทั่วทั้งโค้ดแล็บ

คลิกถัดไปเพื่อเริ่มใช้งานห้องทดลอง

2. ตั้งค่าสภาพแวดล้อม Flutter

ผู้แก้ไข

เราจะถือว่าคุณจะใช้ Visual Studio Code (VS Code) เป็นสภาพแวดล้อมการพัฒนาเพื่อให้ Codelab นี้เข้าใจง่ายที่สุด ซึ่งให้บริการฟรีและใช้งานได้บนแพลตฟอร์มหลักทั้งหมด

แต่คุณใช้เครื่องมือแก้ไขใดก็ได้ตามต้องการ ไม่ว่าจะเป็น Android Studio, IntelliJ IDE อื่นๆ, Emacs, Vim หรือ Notepad++ เครื่องมือเหล่านี้ใช้ได้กับ Flutter ทั้งหมด

เราขอแนะนำให้ใช้ VS Code สำหรับ Codelab นี้ เนื่องจากวิธีการเริ่มต้นด้วยแป้นพิมพ์ลัดเฉพาะของ VS Code การพูดว่า "คลิกที่นี่" หรือ "กดแป้นนี้" นั้นง่ายกว่าการพูดว่า "ดำเนินการที่เหมาะสมในเครื่องมือแก้ไขเพื่อดำเนินการ X"

228c71510a8e868.png

เลือกเป้าหมายการพัฒนา

Flutter เป็นเครื่องมือแบบหลายแพลตฟอร์ม แอปของคุณจะทำงานได้ในระบบปฏิบัติการต่อไปนี้

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • เว็บ

อย่างไรก็ตาม แนวทางปฏิบัติทั่วไปคือเลือกระบบปฏิบัติการเดียวที่จะพัฒนาเป็นหลัก ข้อมูลนี้คือ "เป้าหมายการพัฒนา" ซึ่งเป็นระบบปฏิบัติการที่แอปของคุณทำงานอยู่ในระหว่างการพัฒนา

16695777c07f18e5.png

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

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

เลือกเลย โปรดทราบว่าคุณสามารถเรียกใช้แอปในระบบปฏิบัติการอื่นๆ ในภายหลังได้เสมอ แต่การมีเป้าหมายการพัฒนาที่ชัดเจนจะช่วยให้ขั้นตอนถัดไปราบรื่นขึ้น

ติดตั้ง Flutter

วิธีการล่าสุดในการติดตั้ง Flutter SDK จะอยู่เสมอที่ docs.flutter.dev

วิธีการในเว็บไซต์ Flutter ครอบคลุมทั้งการติดตั้ง SDK เอง รวมถึงเครื่องมือที่เกี่ยวข้องกับเป้าหมายการพัฒนาและปลั๊กอินเครื่องมือแก้ไข โปรดทราบว่าสำหรับโค้ดแล็บนี้ คุณจะต้องติดตั้งสิ่งต่อไปนี้เท่านั้น

  1. Flutter SDK
  2. Visual Studio Code พร้อมปลั๊กอิน Flutter
  3. ซอฟต์แวร์ที่จําเป็นสําหรับเป้าหมายการพัฒนาที่เลือก (เช่น Visual Studio เพื่อกำหนดเป้าหมายเป็น Windows หรือ Xcode เพื่อกำหนดเป้าหมายเป็น macOS)

ในส่วนถัดไป คุณจะได้สร้างโปรเจ็กต์ Flutter โปรเจ็กต์แรก

หากพบปัญหาจนถึงตอนนี้ คุณอาจพบว่าคําถามและคําตอบเหล่านี้ (จาก StackOverflow) มีประโยชน์ในการแก้ปัญหา

คำถามที่พบบ่อย

3. สร้างโปรเจ็กต์

สร้างโปรเจ็กต์ Flutter โปรเจ็กต์แรก

เปิด Visual Studio Code แล้วเปิดแผงคำสั่ง (ใช้ F1 หรือ Ctrl+Shift+P หรือ Shift+Cmd+P) เริ่มพิมพ์ "flutter new" เลือกคำสั่ง Flutter: โปรเจ็กต์ใหม่

ถัดไป ให้เลือกแอปพลิเคชัน แล้วเลือกโฟลเดอร์ที่จะสร้างโปรเจ็กต์ ซึ่งอาจเป็นไดเรกทอรีบ้านหรือชื่ออย่าง C:\src\

สุดท้าย ให้ตั้งชื่อโปรเจ็กต์ เช่น namer_app หรือ my_awesome_namer

260a7d97f9678005.png

ตอนนี้ Flutter จะสร้างโฟลเดอร์โปรเจ็กต์และ VS Code จะเปิดโฟลเดอร์ดังกล่าว

ตอนนี้คุณเขียนทับเนื้อหาของไฟล์ 3 ไฟล์ด้วยสคาฟเฟิลดพื้นฐานของแอป

คัดลอกและวางแอปเริ่มต้น

ในแผงด้านซ้ายของ VS Code ให้ตรวจสอบว่าได้เลือก Explorer แล้ว และเปิดไฟล์ pubspec.yaml

e2a5bab0be07f4f7.png

แทนที่เนื้อหาของไฟล์นี้ด้วยข้อมูลต่อไปนี้

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.6.0

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

ไฟล์ pubspec.yaml จะระบุข้อมูลพื้นฐานเกี่ยวกับแอป เช่น เวอร์ชันปัจจุบัน ทรัพยากรที่ต้องพึ่งพา และชิ้นงานที่จะใช้กับแอป

ถัดไป ให้เปิดไฟล์การกําหนดค่าอีกไฟล์หนึ่งในโปรเจ็กต์ analysis_options.yaml

a781f218093be8e0.png

แทนที่เนื้อหาด้วยข้อมูลต่อไปนี้

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_final_fields: false
    unnecessary_breaks: true
    use_key_in_widget_constructors: false

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

สุดท้าย ให้เปิดไฟล์ main.dart ในไดเรกทอรี lib/

e54c671c9bb4d23d.png

แทนที่เนื้อหาของไฟล์นี้ด้วยข้อมูลต่อไปนี้

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

โค้ด 50 บรรทัดนี้เป็นแอปทั้งหมดจนถึงตอนนี้

ในส่วนถัดไป ให้เรียกใช้แอปพลิเคชันในโหมดแก้ไขข้อบกพร่องและเริ่มพัฒนา

4. เพิ่มปุ่ม

ขั้นตอนนี้จะเพิ่มปุ่มถัดไปเพื่อสร้างการจับคู่คําใหม่

เปิดแอป

ก่อนอื่น ให้เปิด lib/main.dart แล้วตรวจสอบว่าคุณได้เลือกอุปกรณ์เป้าหมายแล้ว คุณจะเห็นปุ่มที่แสดงอุปกรณ์เป้าหมายปัจจุบันที่มุมขวาล่างของ VS Code คลิกเพื่อเปลี่ยน

ขณะที่ lib/main.dart เปิดอยู่ ให้หาปุ่ม "เล่น" b0a5d0200af5985d.png ที่มุมขวาบนของหน้าต่าง VS Code แล้วคลิกปุ่มดังกล่าว

หลังจากผ่านไปประมาณ 1 นาที แอปจะเปิดในโหมดแก้ไขข้อบกพร่อง ดูเหมือนว่ายังไม่มีข้อมูลมากนัก

f96e7dfb0937d7f4.png

การโหลดซ้ำแบบ Hot ครั้งแรก

ที่ด้านล่างของ lib/main.dart ให้เพิ่มข้อความลงในสตริงของออบเจ็กต์ Text แรก แล้วบันทึกไฟล์ (ด้วย Ctrl+S หรือ Cmd+S) ตัวอย่างเช่น

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

สังเกตว่าแอปเปลี่ยนแปลงทันที แต่คำแบบสุ่มยังคงเหมือนเดิม นี่คือการทำงานของ Hot Reload แบบมีสถานะอันโด่งดังของ Flutter การโหลดซ้ำขณะทำงานจะเริ่มต้นเมื่อคุณบันทึกการเปลี่ยนแปลงในไฟล์ต้นฉบับ

คำถามที่พบบ่อย

การเพิ่มปุ่ม

ถัดไป ให้เพิ่มปุ่มที่ด้านล่างของ Column ใต้อินสแตนซ์ Text รายการที่ 2

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

เมื่อบันทึกการเปลี่ยนแปลงแล้ว แอปจะอัปเดตอีกครั้ง ปุ่มจะปรากฏขึ้น และเมื่อคุณคลิกปุ่ม Debug Console ใน VS Code จะแสดงข้อความกดปุ่มแล้ว

หลักสูตรเร่งรัดเกี่ยวกับ Flutter ใน 5 นาที

แม้ว่าการดูคอนโซลการแก้ไขข้อบกพร่องจะสนุกมาก แต่คุณก็ต้องการให้ปุ่มทําอะไรที่มีประโยชน์มากกว่า แต่ก่อนที่จะดำเนินการดังกล่าว ให้ดูโค้ดใน lib/main.dart อย่างละเอียดเพื่อทำความเข้าใจวิธีการทำงาน

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

คุณจะเห็นฟังก์ชัน main() ที่ด้านบนสุดของไฟล์ ในรูปแบบปัจจุบัน คำสั่งนี้จะบอกให้ Flutter เรียกใช้แอปที่กําหนดไว้ใน MyApp เท่านั้น

lib/main.dart

// ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

คลาส MyApp ขยายมาจาก StatelessWidget วิดเจ็ตคือองค์ประกอบที่คุณใช้สร้างแอป Flutter ทุกแอป ดังที่คุณเห็น แม้แต่แอปเองก็เป็นวิดเจ็ต

โค้ดใน MyApp จะตั้งค่าทั้งแอป โดยจะสร้างสถานะทั่วทั้งแอป (ดูข้อมูลเพิ่มเติมในภายหลัง) ตั้งชื่อแอป กําหนดธีมภาพ และตั้งค่าวิดเจ็ต "หน้าแรก" ซึ่งเป็นจุดเริ่มต้นของแอป

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

ถัดไปคือคลาส MyAppState ซึ่งกำหนดสถานะของแอป นี่เป็นครั้งแรกที่คุณลองใช้ Flutter ดังนั้น Codelab นี้จะเน้นไปที่เนื้อหาที่เข้าใจง่าย การจัดการสถานะแอปใน Flutter ทำได้หลายวิธี ตัวอย่างที่อธิบายได้ง่ายที่สุดคือ ChangeNotifier ซึ่งเป็นแนวทางที่แอปนี้ใช้

  • MyAppState กำหนดข้อมูลที่จำเป็นต่อการทำงานของแอป ขณะนี้มีเพียงตัวแปรเดียวที่มีคู่คําแบบสุ่มปัจจุบัน คุณจะเพิ่มข้อมูลในภายหลัง
  • คลาสสถานะจะขยาย ChangeNotifier ซึ่งหมายความว่าสามารถแจ้งเตือนผู้อื่นเกี่ยวกับการเปลี่ยนแปลงของตนเองได้ เช่น หากคู่คําปัจจุบันมีการเปลี่ยนแปลง วิดเจ็ตบางรายการในแอปจะต้องทราบ
  • ระบบจะสร้างสถานะและส่งไปยังทั้งแอปโดยใช้ ChangeNotifierProvider (ดูโค้ดด้านบนใน MyApp) ซึ่งช่วยให้วิดเจ็ตทั้งหมดในแอปเข้าถึงสถานะได้ d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           //  1
    var appState = context.watch<MyAppState>();  //  2

    return Scaffold(                             //  3
      body: Column(                              //  4
        children: [
          Text('A random AWESOME idea:'),        //  5
          Text(appState.current.asLowerCase),    //  6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       //  7
      ),
    );
  }
}

// ...

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

  1. วิดเจ็ตทุกรายการจะกำหนดเมธอด build() ที่เรียกใช้โดยอัตโนมัติทุกครั้งที่สถานการณ์ของวิดเจ็ตเปลี่ยนแปลง เพื่อให้วิดเจ็ตเป็นข้อมูลล่าสุดอยู่เสมอ
  2. MyHomePage ติดตามการเปลี่ยนแปลงสถานะปัจจุบันของแอปโดยใช้เมธอด watch
  3. เมธอด build ทุกรายการต้องแสดงผลวิดเจ็ต หรือ (โดยทั่วไปแล้ว) ต้นไม้ของวิดเจ็ตที่ฝังอยู่ ในกรณีนี้ วิดเจ็ตระดับบนสุดคือ Scaffold คุณจะไม่ได้ใช้ Scaffold ในโค้ดแล็บนี้ แต่วิดเจ็ตนี้มีประโยชน์และพบในแอป Flutter ส่วนใหญ่ในชีวิตจริง
  4. Column เป็นหนึ่งในวิดเจ็ตเลย์เอาต์พื้นฐานที่สุดใน Flutter โดยจะรับจำนวนรายการย่อยเท่าใดก็ได้และวางไว้ในคอลัมน์จากบนลงล่าง โดยค่าเริ่มต้น คอลัมน์จะวางองค์ประกอบย่อยไว้ที่ด้านบน คุณจะสามารถเปลี่ยนค่านี้ในเร็วๆ นี้เพื่อให้คอลัมน์อยู่ตรงกลาง
  5. คุณเปลี่ยนวิดเจ็ต Text นี้ในขั้นตอนแรก
  6. วิดเจ็ต Text ตัวที่ 2 นี้ใช้ appState และเข้าถึงสมาชิกเพียงคนเดียวของคลาสนั้น ซึ่งก็คือ current (ซึ่งเป็น WordPair) WordPair มีตัวรับข้อมูลที่มีประโยชน์หลายรายการ เช่น asPascalCase หรือ asSnakeCase ในส่วนนี้ เราใช้ asLowerCase แต่คุณเปลี่ยนเป็นสัญลักษณ์อื่นได้หากต้องการ
  7. สังเกตว่าโค้ด Flutter ใช้คอมมาต่อท้ายอย่างมาก ไม่จำเป็นต้องใส่คอมมานี้ เนื่องจาก children เป็นสมาชิกรายการสุดท้าย (และเพียงรายการเดียว) ของรายการพารามิเตอร์ Column รายการนี้ แต่โดยทั่วไปแล้ว เราขอแนะนำให้ใช้คอมมาต่อท้าย เนื่องจากทำให้การเพิ่มสมาชิกเพิ่มเติมเป็นเรื่องง่าย และยังเป็นคำแนะนำสำหรับโปรแกรมจัดรูปแบบอัตโนมัติของ Dart ในการใส่บรรทัดใหม่ ดูข้อมูลเพิ่มเติมได้ที่การจัดรูปแบบโค้ด

ถัดไป คุณจะต้องเชื่อมต่อปุ่มกับสถานะ

พฤติกรรมแรก

เลื่อนไปที่ MyAppState แล้วเพิ่มวิธีการ getNext

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  //  Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

เมธอด getNext() ใหม่จะกำหนด current ใหม่ด้วย WordPair แบบสุ่มใหม่ และยังเรียก notifyListeners()(เมธอดของ ChangeNotifier) ที่ช่วยให้มั่นใจว่าทุกคนที่ดู MyAppState จะได้รับแจ้ง

เหลือเพียงการเรียกเมธอด getNext จาก Callback ของปุ่ม

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

บันทึกและลองใช้แอปเลย ซึ่งจะสร้างคู่คำแบบสุ่มใหม่ทุกครั้งที่คุณกดปุ่มถัดไป

ในส่วนถัดไป คุณจะทำให้อินเทอร์เฟซผู้ใช้ดูสวยงามขึ้น

5. ทําให้แอปดูสวยขึ้น

ลักษณะของแอปในขณะนี้

3dd8a9d8653bdc56.png

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

ส่วนนี้จะแก้ไขปัญหาเหล่านี้ด้วยการออกแบบแอป เป้าหมายสุดท้ายของส่วนนี้มีลักษณะดังต่อไปนี้

2bbee054d81a3127.png

แตกไฟล์วิดเจ็ต

บรรทัดที่มีหน้าที่แสดงคู่คำปัจจุบันจะมีลักษณะดังนี้ Text(appState.current.asLowerCase) หากต้องการเปลี่ยนเป็นรูปแบบที่ซับซ้อนมากขึ้น คุณควรแยกบรรทัดนี้ออกเป็นวิดเจ็ตแยกต่างหาก การมีวิดเจ็ตแยกต่างหากสําหรับส่วนต่างๆ ของ UI ที่เป็นตรรกะเป็นวิธีจัดการความซับซ้อนใน Flutter ที่สำคัญ

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

ด้วยเหตุนี้ ให้เขียนวิดเจ็ต MyHomePage ใหม่ดังนี้

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();  
    var pair = appState.current;                 //  Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                //  Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

เยี่ยมมาก วิดเจ็ต Text ไม่ได้อ้างอิงถึง appState ทั้งหมดอีกต่อไป

จากนั้นเปิดเมนูปรับโครงสร้าง ใน VS Code คุณดำเนินการนี้ได้ด้วย 2 วิธีดังนี้

  1. คลิกขวาที่โค้ดที่ต้องการปรับโครงสร้าง (ในกรณีนี้คือ Text) แล้วเลือกปรับโครงสร้าง... จากเมนูแบบเลื่อนลง

หรือ

  1. เลื่อนเคอร์เซอร์ไปยังโค้ดส่วนที่ต้องการปรับโครงสร้าง (ในกรณีนี้คือ Text) แล้วกด Ctrl+. (Win/Linux) หรือ Cmd+. (Mac)

ในเมนูปรับโครงสร้าง ให้เลือกแยกวิดเจ็ต กําหนดชื่อ เช่น BigCard แล้วคลิก Enter

ซึ่งจะสร้างคลาสใหม่ BigCard ที่ท้ายไฟล์ปัจจุบันโดยอัตโนมัติ คลาสจะมีลักษณะดังต่อไปนี้

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

สังเกตว่าแอปยังคงทํางานได้แม้จะมีการแยกส่วนโค้ด

เพิ่มการ์ด

ตอนนี้ได้เวลาเปลี่ยนวิดเจ็ตใหม่นี้ให้เป็น UI ที่โดดเด่นตามที่เราได้จินตนาการไว้ตั้งแต่ต้นส่วนนี้แล้ว

ค้นหาคลาส BigCard และเมธอด build() ภายในคลาส เรียกเมนูปรับโครงสร้างในวิดเจ็ต Text เช่นเดียวกับก่อนหน้านี้ แต่ครั้งนี้คุณจะไม่ได้ดึงข้อมูลวิดเจ็ต

แต่ให้เลือกตัดขึ้นบรรทัดใหม่โดยเพิ่มระยะห่างแทน ซึ่งจะสร้างวิดเจ็ตหลักใหม่รอบๆ วิดเจ็ต Text ชื่อ Padding หลังจากบันทึกแล้ว คุณจะเห็นคําแบบสุ่มมีระยะห่างมากขึ้น

เพิ่มระยะห่างจากขอบจากค่าเริ่มต้น 8.0 เช่น ใช้ 20 เพื่อให้มีระยะห่างมากขึ้น

ถัดไป ให้เลื่อนขึ้นอีก 1 ระดับ วางเคอร์เซอร์บนวิดเจ็ต Padding ดึงเมนูปรับโครงสร้างขึ้นมา แล้วเลือกตัดขึ้นบรรทัดใหม่ด้วยวิดเจ็ต...

ซึ่งช่วยให้คุณระบุวิดเจ็ตหลักได้ พิมพ์ "การ์ด" แล้วกด Enter

ซึ่งจะรวมวิดเจ็ต Padding และ Text ไว้ในวิดเจ็ต Card

6031adbc0a11e16b.png

ธีมและสไตล์

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

ทําการเปลี่ยนแปลงต่อไปนี้กับเมธอด build() ของ BigCard

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       //  Add this.

    return Card(
      color: theme.colorScheme.primary,    //  And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

บรรทัดใหม่ 2 บรรทัดนี้ทํางานได้หลายอย่าง

  • ก่อนอื่น โค้ดจะขอธีมปัจจุบันของแอปด้วย Theme.of(context)
  • จากนั้นโค้ดจะกำหนดสีของการ์ดให้เหมือนกับพร็อพเพอร์ตี้ colorScheme ของธีม รูปแบบสีมีสีหลายสี และ primary เป็นสีที่โดดเด่นที่สุดซึ่งกำหนดลักษณะของแอป

ตอนนี้การ์ดจะทาสีด้วยสีหลักของแอป

a136f7682c204ea1.png

คุณเปลี่ยนสีนี้และรูปแบบสีของแอปทั้งหมดได้โดยเลื่อนขึ้นไปยัง MyApp แล้วเปลี่ยนสีเริ่มต้นของ ColorScheme

สังเกตว่าสีเคลื่อนไหวอย่างราบรื่นเพียงใด ซึ่งเรียกว่าภาพเคลื่อนไหวโดยนัย วิดเจ็ต Flutter หลายรายการจะหาค่าเฉลี่ยระหว่างค่าต่างๆ อย่างราบรื่นเพื่อให้ UI ไม่ "กระโดด" ระหว่างสถานะต่างๆ

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

TextTheme

บัตรยังคงมีปัญหาอยู่ เนื่องจากข้อความมีขนาดเล็กเกินไปและสีของข้อความอ่านยาก วิธีแก้ไขคือทําการเปลี่ยนแปลงต่อไปนี้ในเมธอด build() ของ BigCard

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    //  Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        //  Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

เหตุผลที่ทำให้เกิดการเปลี่ยนแปลงนี้

  • เมื่อใช้ theme.textTheme, คุณจะเข้าถึงธีมแบบอักษรของแอป ซึ่งประกอบด้วยสมาชิก เช่น bodyMedium (สำหรับข้อความมาตรฐานขนาดกลาง), caption (สำหรับคำบรรยายรูปภาพ) หรือ headlineLarge (สำหรับบรรทัดแรกขนาดใหญ่)
  • พร็อพเพอร์ตี้ displayMedium เป็นสไตล์ขนาดใหญ่สําหรับข้อความแสดงผล คําว่า display ใช้ในความหมายด้านการจัดรูปแบบตัวอักษร เช่น ในแบบอักษร Display เอกสารประกอบของ displayMedium ระบุว่า "สไตล์การแสดงผลสงวนไว้สำหรับข้อความสั้นๆ ที่สำคัญ" ซึ่งตรงกับกรณีการใช้งานของเรา
  • ในทางทฤษฎีแล้ว พร็อพเพอร์ตี้ displayMedium ของธีมอาจเป็น null ได้ Dart ซึ่งเป็นภาษาโปรแกรมที่คุณใช้เขียนแอปนี้ไม่มีค่า Null จึงจะไม่อนุญาตให้คุณเรียกใช้เมธอดของออบเจ็กต์ที่อาจเป็นnull ในกรณีนี้ คุณสามารถใช้โอเปอเรเตอร์ ! ("โอเปอเรเตอร์เครื่องหมายตกใจ") เพื่อบอก Dart ว่าคุณรู้สิ่งที่ทําอยู่ (displayMedium ไม่ใช่ค่าว่างอย่างแน่นอนในกรณีนี้ เหตุผลที่เราทราบเรื่องนี้อยู่นอกขอบเขตของ Codelab นี้)
  • การเรียกใช้ copyWith() ใน displayMedium จะแสดงสำเนารูปแบบข้อความที่มีการเปลี่ยนแปลงที่คุณกำหนด ในกรณีนี้ คุณจะเปลี่ยนเฉพาะสีของข้อความ
  • หากต้องการใช้สีใหม่ ให้ไปที่ธีมของแอปอีกครั้ง พร็อพเพอร์ตี้ onPrimary ของรูปแบบสีจะกำหนดสีที่เหมาะสําหรับใช้กับสีหลักของแอป

ตอนนี้แอปควรมีลักษณะดังต่อไปนี้

2405e9342d28c193.png

หากต้องการ ให้เปลี่ยนการ์ดเพิ่มเติม ลองดูแนวคิดบางส่วนกัน

  • copyWith() ช่วยให้คุณเปลี่ยนรูปแบบข้อความได้มากกว่าแค่สี หากต้องการดูรายการพร็อพเพอร์ตี้ทั้งหมดที่คุณเปลี่ยนแปลงได้ ให้วางเคอร์เซอร์ไว้ที่ใดก็ได้ภายในวงเล็บของ copyWith() แล้วกด Ctrl+Shift+Space (Win/Linux) หรือ Cmd+Shift+Space (Mac)
  • ในทํานองเดียวกัน คุณยังเปลี่ยนข้อมูลเพิ่มเติมเกี่ยวกับวิดเจ็ต Card ได้ เช่น คุณสามารถขยายเงาของการ์ดได้โดยการเพิ่มค่าของพารามิเตอร์ elevation
  • ลองใช้สีต่างๆ นอกจาก theme.colorScheme.primary แล้วยังมี .secondary, .surface และอีกมากมาย สีเหล่านี้ทั้งหมดมีสีเทียบเท่าใน onPrimary

ปรับปรุงการช่วยเหลือพิเศษ

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

d1fad7944fb890ea.png

แต่บางครั้งก็อาจต้องดำเนินการบางอย่าง ในกรณีของแอปนี้ โปรแกรมอ่านหน้าจออาจมีปัญหาในการออกเสียงคู่คำที่สร้างขึ้นบางคู่ แม้ว่ามนุษย์จะระบุคำ 2 คำใน cheaphead ได้ แต่โปรแกรมอ่านหน้าจออาจออกเสียง ph ตรงกลางคำเป็น f

วิธีที่ง่ายที่สุดคือแทนที่ pair.asLowerCase ด้วย "${pair.first} ${pair.second}" รูปแบบหลังใช้การแทรกสตริงเพื่อสร้างสตริง (เช่น "cheap head") จาก 2 คำที่อยู่ใน pair การใช้ 2 คำแยกกันแทนคำประสมช่วยให้โปรแกรมอ่านหน้าจอระบุคำเหล่านั้นได้อย่างเหมาะสม และมอบประสบการณ์การใช้งานที่ดีขึ้นให้แก่ผู้ใช้ที่มีปัญหาด้านการมองเห็น

อย่างไรก็ตาม คุณอาจต้องคงภาพลักษณ์ของ pair.asLowerCase ไว้ให้เรียบง่าย ใช้พร็อพเพอร์ตี้ semanticsLabel ของ Text เพื่อลบล้างเนื้อหาภาพวิดเจ็ตข้อความด้วยเนื้อหาเชิงความหมายที่เหมาะกับโปรแกรมอ่านหน้าจอมากกว่า โดยทำดังนี้

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        //  Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

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

วาง UI ไว้ตรงกลาง

เมื่อแสดงคู่คำแบบสุ่มอย่างมีสไตล์แล้ว ก็ถึงเวลาวางคู่คำดังกล่าวไว้ตรงกลางหน้าต่าง/หน้าจอของแอป

ก่อนอื่น โปรดทราบว่า BigCard เป็นส่วนหนึ่งของ Column โดยค่าเริ่มต้น คอลัมน์จะรวมรายการย่อยไว้ที่ด้านบน แต่เราลบล้างการดำเนินการนี้ได้โดยง่าย ไปที่เมธอด build() ของ MyHomePage แล้วทําการเปลี่ยนแปลงต่อไปนี้

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  //  Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

ซึ่งจะจัดวางองค์ประกอบย่อยใน Column ไว้ตรงกลางตามแกนหลัก (แนวตั้ง)

b555d4c7f5000edf.png

รายการย่อยจะอยู่ในแนวกลางตามแกนขวางของคอลัมน์อยู่แล้ว (กล่าวคือ รายการย่อยจะอยู่ในแนวกลางตามแนวนอนอยู่แล้ว) แต่ Column เองไม่ได้อยู่กึ่งกลางภายใน Scaffold เรายืนยันข้อมูลนี้ได้โดยใช้เครื่องมือตรวจสอบวิดเจ็ต

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

คุณจัดกึ่งกลางคอลัมน์เองได้ วางเคอร์เซอร์บน Column แล้วเรียกเมนูปรับโครงสร้าง (ด้วย Ctrl+. หรือ Cmd+.) แล้วเลือกตัดขึ้นบรรทัดใหม่โดยจัดกึ่งกลาง

ตอนนี้แอปควรมีลักษณะดังต่อไปนี้

455688d93c30d154.png

คุณปรับแต่งเพิ่มเติมได้หากต้องการ

  • คุณสามารถนำวิดเจ็ต Text เหนือ BigCard ออกได้ อาจมีเหตุผลว่าไม่จำเป็นต้องมีข้อความอธิบาย ("A random AWESOME idea:") อีกต่อไปเนื่องจาก UI นั้นเข้าใจง่ายอยู่แล้ว และวิธีนี้จะทำให้ดูสะอาดตายิ่งขึ้น
  • นอกจากนี้ คุณยังเพิ่มวิดเจ็ต SizedBox(height: 10) ระหว่าง BigCard กับ ElevatedButton ได้ด้วย วิธีนี้จะทำให้วิดเจ็ต 2 รายการแยกกันมากขึ้น วิดเจ็ต SizedBox จะใช้พื้นที่และไม่แสดงผลอะไรด้วยตนเอง ซึ่งมักใช้เพื่อสร้าง "ช่องว่าง" ทางสายตา

เมื่อใช้การเปลี่ยนแปลงที่ไม่บังคับ MyHomePage จะมีโค้ดนี้

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

และแอปจะมีลักษณะดังต่อไปนี้

3d53d2b071e2f372.png

ในส่วนถัดไป คุณจะเพิ่มความสามารถในการตั้งค่าคำที่สร้างขึ้นเป็นรายการโปรด (หรือ "ชอบ")

6. เพิ่มฟังก์ชันการทำงาน

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

e6b01a8c90df8ffa.png

เพิ่มตรรกะทางธุรกิจ

เลื่อนไปที่ MyAppState แล้วเพิ่มโค้ดต่อไปนี้

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  //  Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

ตรวจสอบการเปลี่ยนแปลง

  • คุณได้เพิ่มพร็อพเพอร์ตี้ใหม่ชื่อ favorites ลงใน MyAppState พร็อพเพอร์ตี้นี้เริ่มต้นด้วยลิสต์ว่าง []
  • นอกจากนี้ คุณยังระบุด้วยว่ารายการต้องมีคู่คํา <WordPair>[] เท่านั้น โดยใช้ generics ซึ่งจะช่วยให้แอปมีความเสถียรมากขึ้น เนื่องจาก Dart จะไม่เรียกใช้แอปหากคุณพยายามเพิ่มสิ่งอื่นที่ไม่ใช่ WordPair ลงไป ในทางกลับกัน คุณสามารถใช้รายการ favorites โดยที่มั่นใจได้ว่าจะไม่มีออบเจ็กต์ที่ไม่ต้องการ (เช่น null) ซ่อนอยู่
  • นอกจากนี้ คุณยังเพิ่มเมธอดใหม่ toggleFavorite() ซึ่งจะนําคู่คําปัจจุบันออกจากรายการรายการโปรด (หากมีอยู่แล้ว) หรือเพิ่มคู่คํานั้น (หากยังไม่มี) ไม่ว่าในกรณีใด โค้ดจะเรียก notifyListeners(); ในภายหลัง

เพิ่มปุ่ม

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

ก่อนอื่น ให้ใส่ปุ่มที่มีอยู่ไว้ใน Row ไปที่เมธอด build() ของ MyHomePage วางเคอร์เซอร์บน ElevatedButton เปิดเมนูปรับโครงสร้างด้วย Ctrl+. หรือ Cmd+. แล้วเลือกตัดขึ้นบรรทัดใหม่

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

ทําการเปลี่ยนแปลงต่อไปนี้

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   //  Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

UI จะกลับไปเป็นเหมือนเดิม

3d53d2b071e2f372.png

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

e6b01a8c90df8ffa.png

คุณไม่จำเป็นต้องทำตามขั้นตอนด้านล่างทุกประการ จริงๆ แล้ว คุณไม่ต้องกังวลเกี่ยวกับไอคอนหัวใจ เว้นแต่ว่าคุณต้องการความท้าทายครั้งใหญ่

และหากไม่สำเร็จก็ไม่เป็นไร เพราะนี่เป็นครั้งแรกที่คุณใช้ Flutter

252f7c4a212c94d2.png

ต่อไปนี้เป็นวิธีหนึ่งในการเพิ่มปุ่มที่ 2 ลงใน MyHomePage ครั้งนี้ ให้ใช้คอนสตรคเตอร์ ElevatedButton.icon() เพื่อสร้างปุ่มที่มีไอคอน และที่ด้านบนของวิธีการ build ให้เลือกไอคอนที่เหมาะสม โดยขึ้นอยู่กับว่าคู่คําปัจจุบันอยู่ในรายการโปรดอยู่แล้วหรือไม่ และโปรดสังเกตการใช้ SizedBox อีกครั้งเพื่อให้ปุ่ม 2 ปุ่มนี้อยู่ห่างกันเล็กน้อย

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    //  Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                //  And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

แอปควรมีลักษณะดังนี้

แต่ผู้ใช้จะดูรายการโปรดไม่ได้ ถึงเวลาเพิ่มหน้าจอแยกต่างหากลงในแอปแล้ว แล้วพบกันในส่วนถัดไป

7. เพิ่มแถบข้างสำหรับไปยังส่วนต่างๆ

แอปส่วนใหญ่ไม่สามารถแสดงข้อมูลทั้งหมดในหน้าจอเดียว แอปนี้อาจทำได้ แต่คุณจะต้องสร้างหน้าจอแยกต่างหากสำหรับรายการโปรดของผู้ใช้เพื่อวัตถุประสงค์ในการสอน หากต้องการสลับระหว่าง 2 หน้าจอ คุณจะใช้ StatefulWidget รายการแรก

f62c54f5401a187.png

หากต้องการไปยังส่วนสำคัญของขั้นตอนนี้โดยเร็วที่สุด ให้แยก MyHomePage ออกเป็นวิดเจ็ต 2 รายการแยกกัน

เลือก MyHomePage ทั้งหมด แล้วลบออก แล้วแทนที่ด้วยโค้ดต่อไปนี้

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}


class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

เมื่อบันทึกแล้ว คุณจะเห็น UI เวอร์ชันภาพพร้อมใช้งาน แต่จะไม่ทำงาน การคลิก ♥︎ (หัวใจ) ในแถบนําทางจะไม่ทําอะไร

388bc25fe198c54a.png

ตรวจสอบการเปลี่ยนแปลง

  • ก่อนอื่น โปรดสังเกตว่าเนื้อหาทั้งหมดของ MyHomePage ได้รับการดึงข้อมูลไปยังวิดเจ็ตใหม่ GeneratorPage เฉพาะส่วน Scaffold ของวิดเจ็ต MyHomePage เก่าเท่านั้นที่ไม่มีการดึงข้อมูล
  • MyHomePage ใหม่มี Row ที่มีบุตร 2 คน วิดเจ็ตแรกคือ SafeArea และวิดเจ็ตที่ 2 คือวิดเจ็ต Expanded
  • SafeArea ช่วยให้มั่นใจว่าองค์ประกอบย่อยจะไม่ถูกบดบังโดยรอยบากของฮาร์ดแวร์หรือแถบสถานะ ในแอปนี้ วิดเจ็ตจะตัดขึ้นด้านบนและด้านล่างของ NavigationRail เพื่อป้องกันไม่ให้แถบสถานะของอุปกรณ์เคลื่อนที่บดบังปุ่มการนําทาง
  • คุณเปลี่ยนบรรทัด extended: false ใน NavigationRail เป็น true ได้ ซึ่งจะแสดงป้ายกำกับข้างไอคอน ในขั้นตอนถัดไป คุณจะได้เรียนรู้วิธีดำเนินการนี้โดยอัตโนมัติเมื่อแอปมีพื้นที่แนวนอนเพียงพอ
  • แถบนําทางมีปลายทาง 2 แห่ง (หน้าแรกและรายการโปรด) พร้อมไอคอนและป้ายกำกับที่เกี่ยวข้อง และยังกำหนด selectedIndex ปัจจุบันด้วย ดัชนีที่เลือกเป็น 0 จะเลือกปลายทางแรก ดัชนีที่เลือกเป็น 1 จะเลือกปลายทางที่ 2 และอื่นๆ ขณะนี้ค่านี้ได้รับการฮาร์ดโค้ดเป็น 0
  • แถบนําทางยังกําหนดสิ่งที่จะเกิดขึ้นเมื่อผู้ใช้เลือกปลายทางใดปลายทางหนึ่งด้วย onDestinationSelected ขณะนี้แอปจะแสดงผลเฉพาะค่าดัชนีที่ขอด้วย print()
  • รายการย่อยที่ 2 ของ Row คือวิดเจ็ต Expanded วิดเจ็ตแบบขยายมีประโยชน์อย่างยิ่งในแถวและคอลัมน์ เนื่องจากช่วยให้คุณแสดงเลย์เอาต์ที่วิดเจ็ตย่อยบางรายการใช้พื้นที่เท่าที่จำเป็น (ในกรณีนี้คือ SafeArea) และวิดเจ็ตอื่นๆ ควรใช้พื้นที่ที่เหลือมากที่สุด (ในกรณีนี้คือ Expanded) วิธีหนึ่งในการมองหาวิดเจ็ต Expanded คือ "การแย่งพื้นที่" หากต้องการทําความเข้าใจบทบาทของวิดเจ็ตนี้ได้ดียิ่งขึ้น ให้ลองรวมวิดเจ็ต SafeArea เข้ากับ Expanded อื่น เลย์เอาต์ที่ได้จะมีลักษณะดังนี้

6bbda6c1835a1ae.png

  • วิดเจ็ต Expanded 2 รายการแบ่งพื้นที่แนวนอนทั้งหมดที่มีอยู่ระหว่างกัน แม้ว่าแถบนําทางจะต้องการพื้นที่เพียงเล็กน้อยทางด้านซ้ายเท่านั้น
  • ภายในวิดเจ็ต Expanded จะมี Container สี และภายในคอนเทนเนอร์จะมี GeneratorPage

วิดเจ็ตแบบไม่เก็บสถานะกับแบบเก็บสถานะ

ที่ผ่านมา MyAppState ครอบคลุมความต้องการทั้งหมดของคุณในรัฐ วิดเจ็ตทั้งหมดที่คุณเขียนจนถึงตอนนี้จึงไม่มีสถานะ โดยไม่มีสถานะที่เปลี่ยนแปลงได้ของตนเอง วิดเจ็ตทุกรายการจะเปลี่ยนตัวเองไม่ได้ จะต้องผ่าน MyAppState

แต่เรากำลังจะเปลี่ยนแปลงเรื่องนี้

คุณต้องมีวิธีเก็บค่า selectedIndex ของแถบนําทาง นอกจากนี้ คุณยังต้องสามารถเปลี่ยนค่านี้ได้จากภายในการเรียกกลับ onDestinationSelected

คุณอาจเพิ่ม selectedIndex เป็นพร็อพเพอร์ตี้อีกรายการหนึ่งของ MyAppState และวิธีนี้ได้ผล แต่คุณคงนึกภาพออกว่าสถานะแอปจะเพิ่มขึ้นอย่างรวดเร็วเกินเหตุหากวิดเจ็ตทุกรายการจัดเก็บค่าไว้ในสถานะ

e52d9c0937cc0823.jpeg

สถานะบางอย่างเกี่ยวข้องกับวิดเจ็ตเดียวเท่านั้น ดังนั้นจึงควรอยู่ในวิดเจ็ตนั้น

ป้อน StatefulWidget ซึ่งเป็นวิดเจ็ตประเภทที่มี State ก่อนอื่น ให้แปลง MyHomePage เป็นวิดเจ็ตที่มีสถานะ

วางเคอร์เซอร์บรรทัดแรกของ MyHomePage (บรรทัดที่ขึ้นต้นด้วย class MyHomePage...) แล้วเรียกเมนูปรับโครงสร้างโดยใช้ Ctrl+. หรือ Cmd+. จากนั้นเลือกแปลงเป็น StatefulWidget

IDE จะสร้างคลาสใหม่ให้คุณ _MyHomePageState คลาสนี้ขยาย State จึงจัดการค่าของตัวเองได้ (สามารถเปลี่ยนตัวเองได้) และโปรดทราบว่าเมธอด build จากวิดเจ็ตแบบไม่มีสถานะแบบเก่าได้ย้ายไปอยู่ใน _MyHomePageState แล้ว (แทนที่จะอยู่ในวิดเจ็ต) มีการย้ายวิธีการดังกล่าวโดยถอดความทั้งหมด ไม่มีการแก้ไขใดๆ ในเมธอด build ตอนนี้มีเพียงเนื้อหาที่แสดงอยู่ที่อื่น

setState

วิดเจ็ตที่มีสถานะใหม่ต้องติดตามตัวแปรเพียง 1 รายการเท่านั้น ซึ่งก็คือ selectedIndex ทำการเปลี่ยนแปลง 3 รายการต่อไปนี้ใน _MyHomePageState

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     //  Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    //  Change to this.
              onDestinationSelected: (value) {

                //  Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

ตรวจสอบการเปลี่ยนแปลง

  1. คุณนําตัวแปรใหม่ selectedIndex มาใช้และเริ่มต้นค่าเป็น 0
  2. คุณใช้ตัวแปรใหม่นี้ในคําจํากัดความ NavigationRail แทน 0 ที่ติดหนดไว้ล่วงหน้าซึ่งมีอยู่จนถึงตอนนี้
  3. เมื่อเรียกใช้การเรียกกลับ onDestinationSelected คุณจะกําหนดค่าให้กับ selectedIndex ภายในการเรียก setState() แทนที่จะพิมพ์ค่าใหม่ไปยังคอนโซล การเรียกใช้นี้คล้ายกับเมธอด notifyListeners() ที่ใช้ก่อนหน้านี้ ซึ่งจะตรวจสอบว่า UI อัปเดตแล้ว

ตอนนี้แถบนําทางจะตอบสนองต่อการโต้ตอบของผู้ใช้ แต่พื้นที่ที่ขยายทางด้านขวาจะยังคงเหมือนเดิม เนื่องจากโค้ดไม่ได้ใช้ selectedIndex เพื่อกำหนดว่าหน้าจอใดจะแสดง

ใช้ selectedIndex

วางโค้ดต่อไปนี้ที่ด้านบนของเมธอด build ของ _MyHomePageState ก่อน return Scaffold

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

ตรวจสอบโค้ดนี้

  1. โค้ดจะประกาศตัวแปรใหม่ page ประเภท Widget
  2. จากนั้นคำสั่ง Switch จะกำหนดหน้าจอให้กับ page ตามค่าปัจจุบันใน selectedIndex
  3. เนื่องจากยังไม่มี FavoritesPage ให้ใช้ Placeholder ซึ่งเป็นวิดเจ็ตที่มีประโยชน์ซึ่งจะวาดสี่เหลี่ยมผืนผ้าที่มีกากบาทไว้ทุกที่ที่คุณวางไว้ ซึ่งจะระบุว่า UI ส่วนนั้นยังไม่เสร็จ

5685cf886047f6ec.png

  1. การใช้หลักการ "ทำงานให้เสร็จเร็วที่สุด" จะทำให้คำสั่ง Switch แสดงข้อผิดพลาดหาก selectedIndex ไม่ใช่ 0 หรือ 1 วิธีนี้จะช่วยป้องกันข้อบกพร่องที่อาจเกิดขึ้นในอนาคต หากคุณเพิ่มปลายทางใหม่ในแถบนําทางและลืมอัปเดตโค้ดนี้ โปรแกรมจะขัดข้องในขั้นตอนการพัฒนา (แทนที่จะให้คุณเดาว่าทําไมสิ่งต่างๆ จึงทํางานไม่ได้ หรือให้คุณเผยแพร่โค้ดที่มีข้อบกพร่องไปยังเวอร์ชันที่ใช้งานจริง)

เมื่อ page มีวิดเจ็ตที่คุณต้องการแสดงทางด้านขวาแล้ว คุณคงเดาออกแล้วว่าต้องเปลี่ยนแปลงอะไรอีก

_MyHomePageState หลังจากการเปลี่ยนแปลงครั้งเดียวที่เหลือมีดังนี้

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  //  Here.
            ),
          ),
        ],
      ),
    );
  }
}


// ...

ตอนนี้แอปจะสลับระหว่าง GeneratorPage กับตัวยึดตำแหน่งที่จะกลายเป็นหน้ารายการโปรดในเร็วๆ นี้

การตอบกลับ

ถัดไป ให้ทำให้แถบนําทางปรับเปลี่ยนตามบริบท กล่าวคือ ให้แสดงป้ายกำกับโดยอัตโนมัติ (โดยใช้ extended: true) เมื่อมีพื้นที่เพียงพอ

a8873894c32e0d0b.png

Flutter มีวิดเจ็ตหลายรายการที่ช่วยให้คุณทำให้แอปตอบสนองได้โดยอัตโนมัติ ตัวอย่างเช่น Wrap เป็นวิดเจ็ตที่คล้ายกับ Row หรือ Column ซึ่งจะตัดองค์ประกอบย่อยไปยัง "บรรทัด" (เรียกว่า "รัน") ถัดไปโดยอัตโนมัติเมื่อมีพื้นที่แนวตั้งหรือแนวนอนไม่เพียงพอ มี FittedBox ซึ่งเป็นวิดเจ็ตที่ปรับขนาดองค์ประกอบย่อยให้พอดีกับพื้นที่ว่างโดยอัตโนมัติตามข้อกำหนดของคุณ

แต่ NavigationRail จะไม่แสดงป้ายกำกับโดยอัตโนมัติเมื่อมีพื้นที่เพียงพอ เนื่องจากไม่สามารถทราบว่าพื้นที่เพียงพอในบริบทใด ขึ้นอยู่กับคุณในฐานะนักพัฒนาแอปที่จะตัดสินใจ

สมมติว่าคุณเลือกที่จะแสดงป้ายกำกับเฉพาะในกรณีที่ MyHomePage มีความกว้างอย่างน้อย 600 พิกเซล

วิดเจ็ตที่จะใช้ในกรณีนี้คือ LayoutBuilder ซึ่งจะช่วยให้คุณเปลี่ยนโครงสร้างวิดเจ็ตได้ตามพื้นที่ว่างที่มี

อีกครั้ง ให้ใช้เมนู Refactor ของ Flutter ใน VS Code เพื่อทำการเปลี่ยนแปลงที่จำเป็น แต่ครั้งนี้จะซับซ้อนขึ้นเล็กน้อย โดยทำดังนี้

  1. วางเคอร์เซอร์ที่ Scaffold ในเมธอด build ของ _MyHomePageState
  2. เปิดเมนูปรับโครงสร้างด้วย Ctrl+. (Windows/Linux) หรือ Cmd+. (Mac)
  3. เลือกตัดด้วย Builder แล้วกด Enter
  4. แก้ไขชื่อ Builder ที่เพิ่มใหม่เป็น LayoutBuilder
  5. แก้ไขรายการพารามิเตอร์การเรียกกลับจาก (context) เป็น (context, constraints)

ระบบจะเรียก builder ของ LayoutBuilder ทุกครั้งที่ข้อจำกัดมีการเปลี่ยนแปลง กรณีนี้อาจเกิดขึ้นได้เมื่อเกิดเหตุการณ์ต่อไปนี้

  • ผู้ใช้ปรับขนาดหน้าต่างของแอป
  • ผู้ใช้หมุนโทรศัพท์จากโหมดแนวตั้งเป็นโหมดแนวนอนหรือกลับกัน
  • วิดเจ็ตบางรายการข้าง MyHomePage มีขนาดเพิ่มขึ้น ทำให้ข้อจำกัดของ MyHomePage เล็กลง
  • และอื่นๆ

ตอนนี้โค้ดจะตัดสินใจได้ว่าจะแสดงป้ายกำกับหรือไม่โดยค้นหา constraints ปัจจุบัน ทำการเปลี่ยนแปลงบรรทัดเดียวต่อไปนี้ในเมธอด build ของ _MyHomePageState

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  //  Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

ตอนนี้แอปจะตอบสนองต่อสภาพแวดล้อม เช่น ขนาดหน้าจอ การวางแนว และแพลตฟอร์ม กล่าวคือ อุปกรณ์ตอบสนอง

สิ่งที่ต้องทำที่เหลืออยู่คือแทนที่ Placeholder นั้นด้วยหน้าจอรายการโปรดจริง ซึ่งเราจะพูดถึงในส่วนถัดไป

8. เพิ่มหน้าใหม่

จำวิดเจ็ต Placeholder ที่เราใช้แทนหน้ารายการโปรดได้ไหม

ได้เวลาแก้ไขปัญหานี้

หากอยากลองทำอะไรใหม่ๆ ให้ลองทำขั้นตอนนี้ด้วยตนเอง เป้าหมายของคุณคือแสดงรายการ favorites ในวิดเจ็ตแบบไม่มีสถานะใหม่ FavoritesPage แล้วแสดงวิดเจ็ตนั้นแทน Placeholder

คำแนะนำบางส่วนมีดังนี้

  • หากต้องการให้ Column เลื่อน ให้ใช้วิดเจ็ต ListView
  • อย่าลืมเข้าถึงอินสแตนซ์ MyAppState จากวิดเจ็ตใดก็ได้โดยใช้ context.watch<MyAppState>()
  • หากต้องการลองใช้วิดเจ็ตใหม่ด้วย ListTile ก็มีพร็อพเพอร์ตี้ต่างๆ เช่น title (โดยทั่วไปสําหรับข้อความ) leading (สําหรับไอคอนหรือรูปโปรไฟล์) และ onTap (สําหรับการโต้ตอบ) อย่างไรก็ตาม คุณสามารถสร้างผลลัพธ์ที่คล้ายกันโดยใช้วิดเจ็ตที่คุณรู้จักอยู่แล้ว
  • Dart อนุญาตให้ใช้วงเล็บปีกกา for ภายในนิพจน์คอลเล็กชัน ตัวอย่างเช่น หาก messages มีรายการสตริง คุณอาจมีโค้ดดังต่อไปนี้

f0444bba08f205aa.png

ในทางกลับกัน หากคุณคุ้นเคยกับโปรแกรมเชิงฟังก์ชันมากกว่า Dart ให้คุณเขียนโค้ดได้เช่นกัน เช่น messages.map((m) => Text(m)).toList() และแน่นอน คุณสามารถสร้างรายการวิดเจ็ตและเพิ่มลงในรายการนั้นภายในเมธอด build ได้ทุกเมื่อ

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

252f7c4a212c94d2.png

ต่อไปนี้เป็นวิธีหนึ่งในการใช้หน้ารายการโปรด วิธีการติดตั้งใช้งาน (หวังว่า) จะช่วยสร้างแรงบันดาลใจให้คุณลองใช้โค้ด ปรับปรุง UI และปรับแต่งให้เหมาะกับตัวเอง

คลาส FavoritesPage ใหม่มีดังนี้

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

การทำงานของวิดเจ็ตมีดังนี้

  • โดยจะรับสถานะปัจจุบันของแอป
  • หากรายการโปรดว่างเปล่า ระบบจะแสดงข้อความที่กึ่งกลางว่ายังไม่มีรายการโปรด
  • มิฉะนั้น ระบบจะแสดงรายการ (เลื่อนได้)
  • รายการจะเริ่มต้นด้วยข้อมูลสรุป (เช่น คุณมีรายการโปรด 5 รายการ*)
  • จากนั้นโค้ดจะวนดูรายการโปรดทั้งหมดและสร้างวิดเจ็ต ListTile ให้กับรายการโปรดแต่ละรายการ

ขั้นตอนสุดท้ายคือแทนที่วิดเจ็ต Placeholder ด้วย FavoritesPage เท่านี้แหละ

คุณดูโค้ดสุดท้ายของแอปนี้ได้ในที่เก็บ Codelab ใน GitHub

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

ยินดีด้วย

ดูคุณสิ คุณนำสคาฟเฟลด์ที่ไม่ทำงานซึ่งมีวิดเจ็ต Column และ Text 2 รายการมาสร้างเป็นแอปเล็กๆ ที่ตอบสนองได้ดีและน่าพอใจ

d6e3d5f736411f13.png

สิ่งที่เราได้พูดถึง

  • ข้อมูลเบื้องต้นเกี่ยวกับวิธีการทำงานของ Flutter
  • การสร้างเลย์เอาต์ใน Flutter
  • เชื่อมโยงการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) กับลักษณะการทํางานของแอป
  • จัดระเบียบโค้ด Flutter
  • การทำให้แอปปรับเปลี่ยนตามอุปกรณ์ได้
  • การสร้างรูปลักษณ์ของแอปให้สอดคล้องกัน

มีอะไรต่อไป

  • ลองใช้แอปที่คุณเขียนระหว่างการทดลองนี้เพิ่มเติม
  • ดูโค้ดของเวอร์ชันขั้นสูงนี้ของแอปเดียวกันเพื่อดูวิธีเพิ่มรายการที่เคลื่อนไหว ไล่ระดับสี การเฟดเข้าออก และอื่นๆ
  • ศึกษาเส้นทางการเรียนรู้โดยไปที่ flutter.dev/learn