แอป 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