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

1. บทนำ

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

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

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

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

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

e9c6b402cd8003fd.png

และนี่คือ Filip ที่จะพาคุณไปเรียนรู้ Codelab ทั้งหมด

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

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

ผู้แก้ไข

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

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

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

228c71510a8e868.png

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

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

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

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

16695777c07f18e5.png

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

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

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

ติดตั้ง Flutter

ดูคำแนะนำล่าสุดเกี่ยวกับวิธีติดตั้ง Flutter SDK ได้ที่ docs.flutter.dev

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

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

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

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

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

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

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

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

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

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

260a7d97f9678005.png

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

คุณจะเขียนทับเนื้อหาของไฟล์ 3 ไฟล์ด้วยโครงข่ายพื้นฐานของแอป

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

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

e2a5bab0be07f4f7.png

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

pubspec.yaml

name: namer_app
description: A new Flutter project.

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

version: 0.0.1+1

environment:
  sdk: ^3.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

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

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

a781f218093be8e0.png

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

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

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

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

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

e54c671c9bb4d23d.png

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

lib/main.dart

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

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

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

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

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

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

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

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

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

4. เพิ่มปุ่ม

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

เปิดแอป

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

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

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

f96e7dfb0937d7f4.png

การโหลดใหม่ร้อนๆ ครั้งแรก

ที่ด้านล่างของ 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 ในที่ทำงาน การโหลดซ้ำแบบ Hot จะทริกเกอร์เมื่อคุณบันทึกการเปลี่ยนแปลงลงในไฟล์ต้นฉบับ

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

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

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

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'),
          ),

        ],
      ),
    );

// ...

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

หลักสูตรเร่งรัดของ Flutter ใน 5 นาที

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

lib/main.dart

// ...

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

// ...

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

lib/main.dart

// ...

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

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

// ...

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

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

lib/main.dart

// ...

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

// ...

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

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

lib/main.dart

// ...

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

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

// ...

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

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

ต่อไป คุณจะต้องเชื่อมต่อปุ่มกับรัฐ

พฤติกรรมแรกของคุณ

เลื่อนไปที่ MyAppState และเพิ่มเมธอด getNext

lib/main.dart

// ...

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

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

// ...

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

ส่วนที่เหลือคือการเรียกใช้เมธอด getNext จาก Callback ของปุ่ม

lib/main.dart

// ...

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

// ...

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

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

5. ทำให้แอปสวยขึ้น

นี่คือลักษณะหน้าตาของแอปในขณะนั้น

3dd8a9d8653bdc56.png

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

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

2bbee054d81a3127.png

แยกวิดเจ็ต

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

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

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

lib/main.dart

// ...

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

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

// ...

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

ตอนนี้ ให้เรียกเมนูเปลี่ยนโครงสร้างภายใน ใน VS Code คุณสามารถทำได้ด้วยวิธีใดวิธีหนึ่งใน 2 วิธีต่อไปนี้

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

หรือ

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

ในเมนู Refactor ให้เลือก Extract Widget กำหนดชื่อ เช่น 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() ที่อยู่ในชั้นเรียนดังกล่าว ก่อนหน้านี้ ให้เรียกเมนู Refactor ในวิดเจ็ต Text แต่คราวนี้คุณจะไม่ได้แตกวิดเจ็ต

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

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

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

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

ซึ่งจะรวมวิดเจ็ต Padding ด้วย รวมถึง Text ที่มีวิดเจ็ต Card

6031adbc0a11e16b.png

ธีมและสไตล์

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

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

lib/main.dart

// ...

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

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

// ...

ข้อความ 2 บรรทัดใหม่เหล่านี้ทำงานหนักมาก

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

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

a136f7682c204ea1.png

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

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

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

TextTheme

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

lib/main.dart

// ...

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

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

// ...

สิ่งที่อยู่เบื้องหลังการเปลี่ยนแปลงนี้

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

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

2405e9342d28c193.png

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

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

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

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

d1fad7944fb890ea.png

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

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

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

lib/main.dart

// ...

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

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

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

// ...

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

จัด UI ให้อยู่กึ่งกลาง

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

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

lib/main.dart

// ...

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

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

// ...

วิธีนี้จะทำให้เด็กๆ อยู่ใน Column ตามแกนหลัก (แนวตั้ง)

b555d4c7f5000edf.png

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

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

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

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

455688d93c30d154.png

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

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

สำหรับการเปลี่ยนแปลงที่ไม่บังคับ MyHomePage จะมีโค้ดต่อไปนี้

lib/main.dart

// ...

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

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

// ...

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

3d53d2b071e2f372.png

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

6. เพิ่มฟังก์ชัน

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

e6b01a8c90df8ffa.png

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

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

lib/main.dart

// ...

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

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

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

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

// ...

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

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

เพิ่มปุ่ม

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

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

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

6bbda6c1835a1ae.png

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

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

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

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

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

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

e52d9c0937cc0823.jpeg

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

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

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

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

setState

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

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

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

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

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

// ...

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

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

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

ใช้ดัชนีที่เลือก

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

lib/main.dart

// ...

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

// ...

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

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

5685cf886047f6ec.png

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

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

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

lib/main.dart

// ...

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

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

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


// ...

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

การตอบกลับ

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

a8873894c32e0d0b.png

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

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

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

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

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

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

ระบบจะเรียกใช้ Callback builder ของ LayoutBuilder ทุกครั้งที่ข้อจำกัดเปลี่ยนแปลง ซึ่งจะเกิดขึ้นในกรณีต่างๆ เช่น

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

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

lib/main.dart

// ...

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

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

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


// ...

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

ปัญหาอย่างเดียวที่ยังมีอยู่คือการแทนที่ Placeholder ด้วยหน้าจอรายการโปรดจริง เราจะกล่าวถึงหัวข้อถัดไป

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

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

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

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

ลองดูคำแนะนำต่อไปนี้

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

f0444bba08f205aa.png

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

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

252f7c4a212c94d2.png

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

ชั้นเรียน FavoritesPage ใหม่มีดังนี้

lib/main.dart

// ...

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

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

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

นี่คือสิ่งที่วิดเจ็ตทำ

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

ตอนนี้ส่วนที่เหลือคือการแทนที่วิดเจ็ต Placeholder ด้วย FavoritesPage เรียบร้อย!

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

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

ยินดีด้วย

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

d6e3d5f736411f13.png

สรุปประเด็นที่ได้พูดถึง

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

มีอะไรต่อไป

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