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

1. บทนำ

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

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

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

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

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

e9c6b402cd8003fd.png

และนี่คือ Filip ที่จะแนะนำคุณตลอดทั้งโค้ดแล็บ

คลิกถัดไปเพื่อเริ่มแล็บ

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

ผู้แก้ไข

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

แน่นอนว่าคุณสามารถใช้โปรแกรมแก้ไขใดก็ได้ตามต้องการ ไม่ว่าจะเป็น 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 ไป นั่นคือ Hot Reload แบบมีสถานะ Flutter ไม่สามารถโหลดแอปพลิเคชันเว็บแบบด่วนได้

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

ติดตั้ง Flutter

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

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

  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: New Project

จากนั้นเลือกแอปพลิเคชัน แล้วเลือกโฟลเดอร์ที่จะสร้างโปรเจ็กต์ ซึ่งอาจเป็นไดเรกทอรีหน้าแรกของคุณ หรือไดเรกทอรีที่คล้ายกับ 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"
version: 0.1.0

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter
  english_words: ^4.0.0
  provider: ^6.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.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(
          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 Reload ครั้งแรก

ที่ด้านล่างของ 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),
        ],
      ),
    );

// ...

สังเกตว่าแอปเปลี่ยนทันที แต่คำแบบสุ่มยังคงเหมือนเดิม นี่คือการโหลดซ้ำแบบร้อนที่มีสถานะอันโด่งดังของ 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(
          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 จากการเรียกกลับของปุ่ม

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

lib/main.dart

// ...

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

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }
}

// ...

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

6031adbc0a11e16b.png

ธีมและสไตล์

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

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

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 เป็นรูปแบบขนาดใหญ่ที่ออกแบบมาสำหรับข้อความที่แสดง คำว่าแสดงในที่นี้ใช้ในแง่ของการพิมพ์ เช่น ในแบบอักษรที่ใช้แสดง เอกสารประกอบสำหรับ displayMedium ระบุว่า "สไตล์การแสดงผลสงวนไว้สำหรับข้อความสั้นๆ ที่สำคัญ" ซึ่งตรงกับกรณีการใช้งานของเรา
  • ในทางทฤษฎีแล้ว พร็อพเพอร์ตี้ displayMedium ของธีมอาจเป็น null Dart ซึ่งเป็นภาษาโปรแกรมที่คุณใช้เขียนแอปนี้เป็นภาษาที่ปลอดภัยจากค่า Null จึงไม่ยอมให้คุณเรียกใช้เมธอดของออบเจ็กต์ที่อาจเป็น null แต่ในกรณีนี้ คุณสามารถใช้โอเปอเรเตอร์ ! ("โอเปอเรเตอร์แบง") เพื่อให้มั่นใจว่าคุณรู้ว่ากำลังทำอะไรอยู่ (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 โดยค่าเริ่มต้น คอลัมน์จะรวมองค์ประกอบย่อยไว้ที่ด้านบน แต่เราสามารถลบล้างลักษณะนี้ได้ ไปที่เมธอด MyHomePagebuild() แล้วทำการเปลี่ยนแปลงต่อไปนี้

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 ออกได้ อาจกล่าวได้ว่าข้อความอธิบาย ("ไอเดียสุดเจ๋งแบบสุ่ม:") ไม่จำเป็นอีกต่อไปเนื่องจาก 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();
  }
}

// ...

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

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

เพิ่มปุ่ม

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

ก่อนอื่น ให้ห่อปุ่มที่มีอยู่ด้วย Row ไปที่เมธอดของ MyHomePagebuild() วางเคอร์เซอร์บน ElevatedButton เรียกเมนูจัดระเบียบโค้ดด้วย Ctrl+. หรือ Cmd+. แล้วเลือกWrap with Row

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

6bbda6c1835a1ae.png

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

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

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

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

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

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

e52d9c0937cc0823.jpeg

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

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

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

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

setState

วิดเจ็ต Stateful ใหม่นี้จำเป็นต้องติดตามตัวแปรเพียงตัวเดียวเท่านั้น นั่นคือ 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 Callback แทนที่จะเพียงพิมพ์ค่าใหม่ไปยังคอนโซล คุณจะกำหนดค่าให้กับ selectedIndex ภายในเรียกใช้ setState() การเรียกนี้คล้ายกับวิธี notifyListeners() ที่ใช้ก่อนหน้านี้ ซึ่งช่วยให้มั่นใจได้ว่า UI จะอัปเดต

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

ใช้ selectedIndex

วางโค้ดต่อไปนี้ไว้ที่ด้านบนของเมธอด _MyHomePageStatebuild โดยวางไว้หน้า 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 ซึ่งช่วยให้คุณเปลี่ยนวิดเจ็ตทรีได้ตามพื้นที่ว่างที่มี

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

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

ระบบจะเรียกใช้แฮนเดิล LayoutBuilderbuilder ทุกครั้งที่ข้อจํากัดเปลี่ยนแปลง ซึ่งจะเกิดขึ้นเมื่อมีกรณีต่อไปนี้

  • ผู้ใช้ปรับขนาดหน้าต่างของแอป
  • ผู้ใช้หมุนโทรศัพท์จากโหมดแนวตั้งเป็นโหมดแนวนอน หรือหมุนกลับ
  • วิดเจ็ตบางรายการข้าง 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 และ voila!

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

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

ยินดีด้วย

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

d6e3d5f736411f13.png

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

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

ขั้นต่อไปคืออะไร

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