Ứng dụng Flutter đầu tiên của bạn

1. Giới thiệu

Flutter là bộ công cụ giao diện người dùng của Google để tạo các ứng dụng cho thiết bị di động, web và máy tính chỉ từ một cơ sở mã duy nhất. Trong lớp học lập trình này, bạn sẽ tạo ứng dụng Flutter sau đây:

Ứng dụng này tạo ra những cái tên thú vị, chẳng hạn như "newstay", "lightstream", "mainbrake" hoặc "graypine". Người dùng có thể yêu cầu tên tiếp theo, thêm tên hiện tại vào danh sách yêu thích và xem lại danh sách các tên yêu thích trên một trang riêng. Ứng dụng thích ứng với nhiều kích thước màn hình.

Kiến thức bạn sẽ học được

  • Kiến thức cơ bản về cách hoạt động của Flutter
  • Tạo bố cục trong Flutter
  • Kết nối các hoạt động tương tác của người dùng (như nhấn nút) với hành vi của ứng dụng
  • Sắp xếp gọn gàng mã Flutter
  • Cải thiện khả năng thích ứng của ứng dụng (dành cho nhiều loại màn hình)
  • Có được giao diện nhất quán & cảm nhận của ứng dụng

Bạn sẽ bắt đầu với một scaffold (giàn giáo) cơ bản để có thể chuyển thẳng đến các phần thú vị.

e9c6b402cd8003fd.png

Đây là Filip sẽ hướng dẫn bạn toàn bộ lớp học lập trình!

Nhấp vào tiếp theo để bắt đầu phòng thí nghiệm.

2. Thiết lập môi trường Flutter

Người chỉnh sửa

Để lớp học lập trình này trở nên đơn giản nhất có thể, chúng tôi giả định rằng bạn sẽ sử dụng Visual Studio Code (VS Code) làm môi trường phát triển. Dịch vụ này hoàn toàn miễn phí và hoạt động trên tất cả các nền tảng chính.

Tất nhiên, bạn có thể sử dụng bất cứ trình chỉnh sửa nào mình thích: Android Studio, các IDE IntelliJ khác, Emacs, Vim hoặc Notepad++. Tất cả đều dùng được Flutter.

Bạn nên sử dụng Mã VS cho lớp học lập trình này vì hướng dẫn sẽ sử dụng các phím tắt dành riêng cho Mã VS theo mặc định. Nói những câu như "nhấp vào đây" sẽ dễ dàng hơn hoặc "nhấn phím này" thay vì những câu đại loại như "thực hiện hành động thích hợp trong trình chỉnh sửa để làm X".

228c71510a8e868.pngS

Chọn mục tiêu phát triển

Flutter là một bộ công cụ đa nền tảng. Ứng dụng của bạn có thể chạy trên bất kỳ hệ điều hành nào sau đây:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • web

Tuy nhiên, bạn nên chọn một hệ điều hành mà bạn chủ yếu sẽ phát triển. Đây là "mục tiêu phát triển" của bạn – hệ điều hành mà ứng dụng chạy trong quá trình phát triển.

16695777c07f18e5.pngS

Ví dụ: giả sử bạn đang sử dụng máy tính xách tay Windows để phát triển một ứng dụng Flutter. Nếu chọn Android làm mục tiêu phát triển của mình, bạn thường kết nối thiết bị Android với máy tính xách tay Windows bằng cáp USB và quá trình phát triển ứng dụng của bạn sẽ chạy trên thiết bị Android đi kèm đó. Tuy nhiên, bạn cũng có thể chọn Windows làm mục tiêu phát triển, nghĩa là quá trình phát triển ứng dụng sẽ chạy dưới dạng một ứng dụng Windows cùng với trình chỉnh sửa.

Bạn có thể sẽ muốn chọn web làm mục tiêu phát triển của mình. Nhược điểm của lựa chọn này là bạn sẽ mất một trong những tính năng phát triển hữu ích nhất của Flutter: Stateful Hot Tải lại. Flutter không thể tải lại nhanh các ứng dụng web.

Chọn ngay bây giờ. Lưu ý: Sau này, bạn luôn có thể chạy ứng dụng trên các hệ điều hành khác. Mục đích của việc này chỉ là có mục tiêu phát triển rõ ràng giúp bước tiếp theo suôn sẻ hơn.

Cài đặt Flutter

Các hướng dẫn mới nhất về cách cài đặt Flutter SDK luôn có tại docs.flutter.dev.

Các hướng dẫn trên trang web Flutter không chỉ đề cập đến cách cài đặt SDK mà còn cả các công cụ liên quan đến mục tiêu phát triển cũng như các trình bổ trợ trình chỉnh sửa. Hãy nhớ rằng đối với lớp học lập trình này, bạn chỉ cần cài đặt như sau:

  1. SDK Flutter
  2. Mã Visual Studio với trình bổ trợ Flutter
  3. Phần mềm mà mục tiêu phát triển mà bạn đã chọn yêu cầu (ví dụ: Visual Studio để nhắm đến Windows hoặc Xcode để nhắm đến macOS)

Trong phần tiếp theo, bạn sẽ tạo dự án Flutter đầu tiên của mình.

Nếu đã gặp sự cố cho đến thời điểm này, bạn có thể thấy một số câu hỏi và câu trả lời sau đây (từ StackOverflow) hữu ích cho việc khắc phục sự cố.

Câu hỏi thường gặp

3. Tạo một dự án

Tạo dự án Flutter đầu tiên của bạn

Chạy Visual Studio Code rồi mở bảng lệnh (bằng F1 hoặc Ctrl+Shift+P hoặc Shift+Cmd+P). Bắt đầu nhập "flutter new". Chọn lệnh Flutter: New Project (Flutter: Dự án mới).

Tiếp theo, hãy chọn Application (Ứng dụng) rồi chọn một thư mục để tạo dự án. Đây có thể là thư mục gốc của bạn hoặc chẳng hạn như C:\src\.

Cuối cùng, hãy đặt tên cho dự án của bạn. Ví dụ: namer_app hoặc my_awesome_namer.

260a7d97f9678005.pngS

Bây giờ, Flutter sẽ tạo thư mục dự án của bạn và VS Code sẽ mở thư mục đó.

Bây giờ, bạn sẽ ghi đè nội dung của 3 tệp bằng một scaffold cơ bản của ứng dụng.

Sao chép và Dán ứng dụng ban đầu

Trong ngăn bên trái của Mã VS, hãy nhớ chọn Explorer rồi mở tệp pubspec.yaml.

e2a5bab0be07f4f7.png

Thay thế nội dung của tệp này bằng:

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

Tệp pubspec.yaml chỉ định thông tin cơ bản về ứng dụng, chẳng hạn như phiên bản hiện tại của ứng dụng, các phần phụ thuộc của ứng dụng và tài sản mà ứng dụng sẽ mang theo.

Tiếp theo, hãy mở một tệp cấu hình khác trong dự án, analysis_options.yaml.

a781f218093be8e0.png

Thay thế nội dung của tệp bằng nội dung sau:

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

Tệp này xác định mức độ nghiêm ngặt của Flutter khi phân tích mã của bạn. Vì đây là lần đầu tiên bạn sử dụng Flutter, nên bạn sẽ yêu cầu người phân tích làm việc này thật dễ dàng. Bạn luôn có thể điều chỉnh nội dung này vào lúc khác. Trên thực tế, khi tiến gần hơn đến việc xuất bản một ứng dụng chính thức thực tế, gần như chắc chắn bạn sẽ muốn làm cho trình phân tích trở nên nghiêm ngặt hơn.

Cuối cùng, hãy mở tệp main.dart trong thư mục lib/.

e54c671c9bb4d23d.png

Thay thế nội dung của tệp này bằng:

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),
        ],
      ),
    );
  }
}

Cho đến nay, 50 dòng mã này là toàn bộ ứng dụng.

Trong phần tiếp theo, hãy chạy ứng dụng ở chế độ gỡ lỗi và bắt đầu phát triển.

4. Thêm nút

Bước này sẽ thêm nút Next (Tiếp theo) để tạo một cặp từ mới.

Chạy ứng dụng

Trước tiên, hãy mở lib/main.dart và đảm bảo bạn đã chọn thiết bị mục tiêu. Ở góc dưới cùng bên phải của VS Code, bạn sẽ thấy một nút cho thấy thiết bị mục tiêu hiện tại. Hãy nhấp để thay đổi.

Khi lib/main.dart đang mở, hãy tìm nút "phát" Nút b0a5d0200af5985d.png ở góc trên bên phải cửa sổ của VS Code rồi nhấp vào nút đó.

Sau khoảng 1 phút, ứng dụng của bạn sẽ khởi chạy ở chế độ gỡ lỗi. Có vẻ như giao diện vẫn chưa được nhiều:

f96e7dfb0937d7f4.png

Tải lại nóng lần đầu

Ở cuối lib/main.dart, hãy thêm nội dung vào chuỗi trong đối tượng Text đầu tiên rồi lưu tệp (bằng Ctrl+S hoặc Cmd+S). Ví dụ:

lib/main.dart

// ...

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

// ...

Hãy lưu ý cách ứng dụng thay đổi ngay lập tức nhưng từ ngẫu nhiên vẫn giữ nguyên. Đây là tính năng Tải lại nhanh có trạng thái nổi tiếng của Flutter. Quá trình tải lại nóng được kích hoạt khi bạn lưu các thay đổi đối với tệp nguồn.

Câu hỏi thường gặp

Thêm một nút

Tiếp theo, hãy thêm một nút ở cuối Column, ngay bên dưới thực thể thứ hai của 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'),
          ),

        ],
      ),
    );

// ...

Khi bạn lưu thay đổi, ứng dụng sẽ cập nhật lại: Một nút sẽ xuất hiện và khi bạn nhấp vào nút đó, Bảng điều khiển gỡ lỗi trong VS Code sẽ cho thấy thông báo đã nhấn nút!.

Một khoá học cấp tốc về Flutter trong 5 phút

Thật thú vị khi xem Bảng điều khiển gỡ lỗi, nhưng bạn lại muốn nút này thực hiện một thao tác có ý nghĩa hơn. Tuy nhiên, trước khi đi vào phần đó, hãy xem xét kỹ hơn mã trong lib/main.dart để hiểu cách hoạt động của mã này.

lib/main.dart

// ...

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

// ...

Ở đầu tệp, bạn sẽ thấy hàm main(). Ở dạng hiện tại, lệnh này chỉ yêu cầu Flutter chạy ứng dụng được xác định trong 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(),
      ),
    );
  }
}

// ...

Lớp MyApp mở rộng StatelessWidget. Tiện ích là các phần tử dùng để bạn tạo ra mọi ứng dụng Flutter. Như bạn có thể thấy, ngay cả chính ứng dụng cũng là một tiện ích.

Mã trong MyApp thiết lập toàn bộ ứng dụng. Lớp này tạo trạng thái trên toàn ứng dụng (sẽ nói thêm về nội dung này ở phần sau), đặt tên cho ứng dụng, xác định chủ đề hình ảnh và đặt "trang chủ" tiện ích con—điểm bắt đầu của ứng dụng.

lib/main.dart

// ...

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

// ...

Tiếp theo, lớp MyAppState sẽ xác định trạng thái...well...của ứng dụng. Đây là lần đầu tiên bạn tìm hiểu về Flutter, vì vậy, lớp học lập trình này sẽ chỉ đơn giản và tập trung vào đó. Có nhiều cách hiệu quả để quản lý trạng thái ứng dụng trong Flutter. Một trong những cách dễ giải thích nhất là ChangeNotifier, phương pháp mà ứng dụng này sử dụng.

  • MyAppState xác định dữ liệu mà ứng dụng cần để hoạt động. Hiện tại, lớp này chỉ chứa một biến duy nhất có cặp từ ngẫu nhiên hiện tại. Bạn sẽ thêm nội dung này sau.
  • Lớp trạng thái mở rộng ChangeNotifier, có nghĩa là lớp này có thể thông báo cho người khác về các thay đổi của riêng mình. Ví dụ: nếu cặp từ hiện tại thay đổi thì một số tiện ích trong ứng dụng cần biết.
  • Trạng thái được tạo và cung cấp cho toàn bộ ứng dụng bằng ChangeNotifierProvider (xem mã ở trên trong MyApp). Điều này cho phép mọi tiện ích trong ứng dụng nắm giữ trạng thái. 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
      ),
    );
  }
}

// ...

Cuối cùng là MyHomePage, tiện ích mà bạn vừa sửa đổi. Mỗi dòng được đánh số bên dưới liên kết với một nhận xét số dòng trong mã ở trên:

  1. Mỗi tiện ích xác định một phương thức build() và tự động được gọi mỗi khi hoàn cảnh của tiện ích thay đổi để tiện ích luôn được cập nhật.
  2. MyHomePage theo dõi các thay đổi đối với trạng thái hiện tại của ứng dụng bằng phương thức watch.
  3. Mọi phương thức build phải trả về một tiện ích hoặc (thường là) một cây tiện ích lồng nhau. Trong trường hợp này, tiện ích cấp cao nhất là Scaffold. Bạn sẽ không làm việc với Scaffold trong lớp học lập trình này, nhưng đây là một tiện ích hữu ích và có trong phần lớn các ứng dụng Flutter trong thế giới thực.
  4. Column là một trong những tiện ích có bố cục cơ bản nhất trong Flutter. Phương thức này lấy số lượng phần tử con bất kỳ và đưa chúng vào một cột từ trên xuống dưới. Theo mặc định, cột này sẽ đặt các phần tử con ở trên cùng theo cách trực quan. Bạn sẽ sớm thay đổi chế độ cài đặt này để cột được căn giữa.
  5. Bạn đã thay đổi tiện ích Text này trong bước đầu tiên.
  6. Tiện ích Text thứ hai này sử dụng appState và truy cập vào thành phần duy nhất của lớp đó, current (là WordPair). WordPair cung cấp một số phương thức getter hữu ích, chẳng hạn như asPascalCase hoặc asSnakeCase. Ở đây, chúng tôi sử dụng asLowerCase nhưng bạn có thể thay đổi điều này ngay bây giờ nếu muốn một trong các phương án thay thế.
  7. Hãy lưu ý cách mã Flutter sử dụng nhiều dấu phẩy theo sau. Dấu phẩy cụ thể này không cần ở đây, vì children là thành viên cuối cùng (và cũng duy nhất) của danh sách tham số Column cụ thể này. Tuy nhiên, nhìn chung, sử dụng dấu phẩy theo sau là một ý tưởng hay: chúng khiến việc thêm nhiều thành viên trở nên đơn giản, đồng thời cũng đóng vai trò là gợi ý để công cụ định dạng tự động của Dart đặt dòng mới vào đó. Để biết thêm thông tin, hãy xem phần Định dạng mã.

Tiếp theo, bạn sẽ kết nối nút này với trạng thái.

Hành vi đầu tiên của bạn

Di chuyển đến MyAppState rồi thêm một phương thức getNext.

lib/main.dart

// ...

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

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

// ...

Phương thức getNext() mới sẽ chỉ định lại current bằng một WordPair mới ngẫu nhiên. Phương thức này cũng gọi notifyListeners()(một phương thức của ChangeNotifier) để đảm bảo rằng bất kỳ ai đang xem MyAppState đều nhận được thông báo.

Việc còn lại là gọi phương thức getNext qua lệnh gọi lại của nút.

lib/main.dart

// ...

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

// ...

Lưu và dùng thử ứng dụng ngay. Thao tác này sẽ tạo một cặp từ ngẫu nhiên mới mỗi khi bạn nhấn nút Next (Tiếp theo).

Trong phần tiếp theo, bạn sẽ làm cho giao diện người dùng trở nên đẹp hơn.

5. Làm cho ứng dụng đẹp hơn

Đây là giao diện hiện tại của ứng dụng.

3dd8a9d8653bdc56.png.

Không tốt lắm. Thành phần trung tâm của ứng dụng – cặp từ được tạo ngẫu nhiên – sẽ dễ thấy hơn. Suy cho cùng thì đó là lý do chính khiến người dùng sử dụng ứng dụng này! Ngoài ra, nội dung ứng dụng bị lệch tâm một cách kỳ lạ, toàn bộ ứng dụng toàn màu đen một cách nhàm chán và màu trắng.

Phần này đề cập đến những vấn đề này thông qua việc xây dựng thiết kế của ứng dụng. Mục tiêu cuối cùng của phần này là như sau:

2bbee054d81a3127.pngS

Trích xuất tiện ích

Dòng chịu trách nhiệm hiển thị cặp từ hiện tại có dạng như sau: Text(appState.current.asLowerCase). Để thay đổi thuộc tính này thành nội dung phức tạp hơn, bạn nên trích xuất dòng này thành một tiện ích riêng. Việc sử dụng các tiện ích riêng biệt cho các phần logic riêng biệt trên giao diện người dùng là một cách quan trọng để quản lý sự phức tạp trong Flutter.

Flutter cung cấp một trình trợ giúp tái cấu trúc để trích xuất các tiện ích. Tuy nhiên, trước khi bạn sử dụng, hãy đảm bảo rằng dòng được trích xuất chỉ truy cập vào những gì nó cần. Hiện tại, dòng này truy cập vào appState, nhưng thực sự chỉ cần biết cặp từ hiện tại là gì.

Do đó, hãy viết lại tiện ích MyHomePage như sau:

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'),
          ),
        ],
      ),
    );
  }
}

// ...

Tuyệt. Tiện ích Text không còn tham chiếu đến toàn bộ appState nữa.

Bây giờ, hãy gọi trình đơn Refactor (Tái cấu trúc). Trong VS Code, bạn có thể thực hiện việc này theo một trong 2 cách sau:

  1. Nhấp chuột phải vào đoạn mã bạn muốn tái cấu trúc (Text trong trường hợp này) rồi chọn Refactor... từ trình đơn thả xuống,

OR

  1. Di chuyển con trỏ đến đoạn mã bạn muốn tái cấu trúc (trong trường hợp này là Text) và nhấn Ctrl+. (Win/Linux) hoặc Cmd+. (Mac).

Trong trình đơn Refactor (Tái cấu trúc), hãy chọn Extract Widget (Trích xuất tiện ích). Gán một tên, chẳng hạn như BigCard rồi nhấp vào Enter.

Thao tác này sẽ tự động tạo một lớp mới là BigCard ở cuối tệp hiện tại. Lớp này có dạng như sau:

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);
  }
}

// ...

Hãy lưu ý cách ứng dụng tiếp tục hoạt động ngay cả trong quá trình tái cấu trúc này.

Thêm thẻ

Đã đến lúc đưa tiện ích mới này vào phần nổi bật của giao diện người dùng mà chúng ta đã hình dung ở đầu phần này.

Tìm lớp BigCard và phương thức build() trong lớp đó. Như trước đó, hãy gọi trình đơn Refactor (Tái cấu trúc) trên tiện ích Text. Tuy nhiên, lần này bạn sẽ không giải nén tiện ích.

Thay vào đó, hãy chọn Xuống dòng tự động. Thao tác này sẽ tạo một tiện ích mẹ mới xung quanh tiện ích Text có tên là Padding. Sau khi lưu, bạn sẽ thấy từ ngẫu nhiên đã có nhiều khoảng trống hơn.

Tăng khoảng đệm từ giá trị mặc định là 8.0. Ví dụ: sử dụng giá trị như 20 cho khoảng đệm rộng hơn.

Tiếp theo, hãy di chuyển lên cấp cao hơn một cấp. Đặt con trỏ lên tiện ích Padding, kéo trình đơn Tái cấu trúc lên rồi chọn Gói bằng tiện ích....

Điều này cho phép bạn chỉ định tiện ích mẹ. Nhập "Thẻ" rồi nhấn phím Enter.

Thao tác này sẽ gói tiện ích Padding và theo đó cũng bao bọc Text, với một tiện ích Card.

6031adbc0a11e16b.png.

Giao diện và kiểu

Để thẻ nổi bật hơn, hãy sơn thẻ bằng màu sắc phong phú hơn. Ngoài ra, bạn nên duy trì một bảng phối màu nhất quán, hãy sử dụng Theme của ứng dụng để chọn màu.

Thực hiện các thay đổi sau đối với phương thức build() của 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),
      ),
    );
  }

// ...

Hai dòng mới này có rất nhiều chức năng:

  • Trước tiên, mã này yêu cầu giao diện hiện tại của ứng dụng bằng Theme.of(context).
  • Sau đó, mã này sẽ định nghĩa màu của thẻ giống với thuộc tính colorScheme của giao diện. Bảng phối màu chứa nhiều màu, trong đó primary là màu nổi bật nhất, xác định màu của ứng dụng.

Thẻ hiện được tô bằng màu chính của ứng dụng:

a136f7682c204ea1.png

Bạn có thể thay đổi màu này và bảng phối màu của toàn bộ ứng dụng bằng cách cuộn lên đến MyApp và thay đổi màu gốc cho ColorScheme tại đó.

Chú ý đến cách màu sắc tạo hiệu ứng động mượt mà. Đây được gọi là ảnh động ngầm ẩn. Nhiều tiện ích Flutter sẽ nội suy trơn tru giữa các giá trị để giao diện người dùng không chỉ "nhảy" giữa các trạng thái.

Nút nâng cao bên dưới thẻ cũng thay đổi màu. Đó là sức mạnh của việc sử dụng Theme trên toàn ứng dụng thay vì các giá trị được mã hoá cứng.

TextTheme

Thẻ vẫn có một vấn đề: văn bản quá nhỏ và khó đọc màu. Để khắc phục vấn đề này, hãy thực hiện các thay đổi sau đối với phương thức build() của 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),
      ),
    );
  }

// ...

Điều gì gây ra thay đổi này:

  • Khi dùng theme.textTheme,, bạn có thể truy cập vào giao diện phông chữ của ứng dụng. Lớp học này bao gồm các thành viên như bodyMedium (đối với văn bản chuẩn có kích thước trung bình), caption (đối với chú thích của hình ảnh) hoặc headlineLarge (đối với dòng tiêu đề lớn).
  • Thuộc tính displayMedium là một kiểu lớn dành cho văn bản hiển thị. Từ hiển thị được sử dụng trong ý nghĩa kiểu chữ ở đây, chẳng hạn như trong kiểu chữ hiển thị. Tài liệu về displayMedium cho biết rằng "kiểu hiển thị được dành riêng cho văn bản ngắn và quan trọng", chính xác là trường hợp sử dụng của chúng ta.
  • Theo lý thuyết, thuộc tính displayMedium của giao diện có thể là null. Dart (ngôn ngữ lập trình mà bạn viết ứng dụng này) an toàn với giá trị rỗng. Vì vậy, Dart sẽ không cho phép bạn gọi phương thức của đối tượng có thể là null. Tuy nhiên, trong trường hợp này, bạn có thể sử dụng toán tử ! ("toán tử bang") để đảm bảo Dart bạn biết mình đang làm gì. (displayMedium chắc chắn không rỗng trong trường hợp này. Tuy nhiên, lý do chúng ta biết điều này lại nằm ngoài phạm vi của lớp học lập trình này.)
  • Việc gọi copyWith() trên displayMedium sẽ trả về một bản sao của kiểu văn bản cùng với các thay đổi do bạn xác định. Trong trường hợp này, bạn chỉ thay đổi màu của văn bản.
  • Để dùng màu mới, bạn cần truy cập lại vào giao diện của ứng dụng. Thuộc tính onPrimary của bảng phối màu xác định một màu phù hợp để sử dụng trên màu chính của ứng dụng.

Bây giờ, ứng dụng sẽ có dạng như sau:

2405e9342d28c193.pngs

Nếu bạn thấy thích, hãy thay đổi thẻ thêm. Dưới đây là một số ý tưởng:

  • copyWith() cho phép bạn thay đổi nhiều kiểu văn bản hơn là chỉ màu sắc. Để xem danh sách đầy đủ các thuộc tính mà bạn có thể thay đổi, hãy đặt con trỏ vào vị trí bất kỳ bên trong dấu ngoặc đơn của copyWith() rồi nhấn Ctrl+Shift+Space (Win/Linux) hoặc Cmd+Shift+Space (Mac).
  • Tương tự, bạn có thể thay đổi thêm về tiện ích Card. Ví dụ: bạn có thể phóng to độ bóng của thẻ bằng cách tăng giá trị của tham số elevation.
  • Hãy thử nghiệm các màu sắc. Ngoài theme.colorScheme.primary, còn có .secondary, .surface và vô số các mục khác. Tất cả các màu này đều có màu tương đương với onPrimary.

Cải thiện khả năng tiếp cận

Flutter giúp người dùng dễ dàng truy cập vào ứng dụng theo mặc định. Ví dụ: mọi ứng dụng Flutter đều sẽ hiển thị chính xác tất cả các thành phần văn bản và thành phần tương tác trong ứng dụng đó cho các trình đọc màn hình như TalkBack và VoiceOver.

d1fad7944fb890ea.png

Tuy nhiên, đôi khi chúng tôi vẫn cần một số thao tác. Trong trường hợp của ứng dụng này, trình đọc màn hình có thể gặp vấn đề khi phát âm một số cặp từ được tạo. Mặc dù con người không gặp khó khăn khi xác định hai từ trong cheaphead, trình đọc màn hình có thể phát âm ph ở giữa từ đó là f.

Một giải pháp đơn giản là thay thế pair.asLowerCase bằng "${pair.first} ${pair.second}". Phần sau sử dụng nội suy chuỗi để tạo một chuỗi (chẳng hạn như "cheap head") từ hai từ có trong pair. Việc sử dụng hai từ riêng biệt thay vì từ ghép sẽ đảm bảo trình đọc màn hình xác định đúng các từ này và mang lại trải nghiệm tốt hơn cho người dùng khiếm thị.

Tuy nhiên, bạn nên giữ tính đơn giản về hình ảnh của pair.asLowerCase. Sử dụng thuộc tính semanticsLabel của Text để ghi đè nội dung hình ảnh của tiện ích văn bản bằng nội dung ngữ nghĩa phù hợp hơn với trình đọc màn hình:

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}",
        ),
      ),
    );
  }

// ...

Giờ đây, trình đọc màn hình phát âm chính xác từng cặp từ được tạo, nhưng giao diện người dùng vẫn giữ nguyên. Hãy thử thực hiện thao tác này bằng cách sử dụng trình đọc màn hình trên thiết bị của bạn.

Căn giữa giao diện người dùng

Giờ đây, cặp từ ngẫu nhiên đã được thể hiện với hình ảnh đủ tinh tế, đã đến lúc đặt cặp từ đó vào giữa cửa sổ/màn hình của ứng dụng.

Trước tiên, hãy nhớ rằng BigCard là một phần của Column. Theo mặc định, các cột sẽ gộp các cột con lên trên cùng, nhưng chúng ta có thể dễ dàng ghi đè cột này. Chuyển đến phương thức build() của MyHomePage và thực hiện thay đổi sau:

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'),
          ),
        ],
      ),
    );
  }
}

// ...

Thao tác này sẽ căn giữa các phần tử con bên trong Column dọc theo trục chính (dọc).

b555d4c7f5000edf.png

Các thành phần con đã được căn giữa dọc theo trục chữ thập của cột (nói cách khác, chúng đã được căn giữa theo chiều ngang). Tuy nhiên, bản thân Column không nằm ở chính giữa bên trong Scaffold. Chúng ta có thể xác minh điều này bằng cách sử dụng Trình kiểm tra tiện ích.

Bản thân Trình kiểm tra tiện ích nằm ngoài phạm vi của lớp học lập trình này, nhưng bạn có thể thấy rằng khi Column được làm nổi bật, phần tử này không chiếm toàn bộ chiều rộng của ứng dụng. Mô hình này chỉ chiếm đủ không gian theo chiều ngang tuỳ theo nhu cầu của các phần tử con.

Bạn có thể chỉ căn giữa cột đó. Đặt con trỏ vào Column, gọi trình đơn Refactor (Tái cấu trúc) (bằng Ctrl+. hoặc Cmd+.) rồi chọn Wrap with Center (Ngắt dòng bằng phần giữa).

Bây giờ, ứng dụng sẽ có dạng như sau:

455688d93c30d154.pngS

Nếu muốn, bạn có thể chỉnh sửa thêm.

  • Bạn có thể xoá tiện ích Text ở phía trên BigCard. Có thể lập luận rằng văn bản mô tả không còn cần thiết nữa vì giao diện người dùng vẫn có nghĩa ngay cả khi không có văn bản đó. Và theo cách đó, mọi thứ sẽ sạch hơn.
  • Bạn cũng có thể thêm tiện ích SizedBox(height: 10) từ BigCard đến ElevatedButton. Bằng cách này, giữa 2 tiện ích sẽ tách biệt hơn một chút. Tiện ích SizedBox chỉ chiếm dung lượng và không hiển thị bất kỳ nội dung nào. Mã này thường được dùng để tạo các "khoảng cách" trực quan.

Với các thay đổi không bắt buộc, MyHomePage sẽ chứa mã sau:

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'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Và ứng dụng sẽ có dạng như sau:

3d53d2b071e2f372.pngS

Trong phần tiếp theo, bạn sẽ thêm tính năng yêu thích (hay "thích") từ được tạo.

6. Thêm chức năng

Ứng dụng hoạt động và thậm chí đôi khi còn cung cấp những cặp từ thú vị. Tuy nhiên, mỗi khi người dùng nhấp vào Tiếp theo, mỗi cặp từ sẽ biến mất vĩnh viễn. Tốt hơn là bạn nên có một cách để "ghi nhớ" gợi ý hay nhất: chẳng hạn như "Thích" .

e6b01a8c90df8ffa.png

Thêm logic kinh doanh

Di chuyển đến MyAppState rồi thêm mã sau:

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();
  }
}

// ...

Kiểm tra các thay đổi:

  • Bạn đã thêm một tài sản mới có tên là favorites vào MyAppState. Thuộc tính này được khởi tạo bằng một danh sách trống: [].
  • Bạn cũng đã chỉ định rằng danh sách chỉ có thể chứa các cặp từ: <WordPair>[] bằng cách sử dụng generic. Điều này giúp ứng dụng của bạn mạnh mẽ hơn—Dart thậm chí từ chối chạy ứng dụng của bạn nếu bạn cố thêm bất kỳ thứ gì khác ngoài WordPair vào ứng dụng đó. Đổi lại, bạn có thể sử dụng danh sách favorites khi biết rằng sẽ không bao giờ có đối tượng không mong muốn (như null) ẩn trong đó.
  • Bạn cũng thêm một phương thức mới là toggleFavorite(). Phương thức này sẽ xoá cặp từ hiện tại khỏi danh sách từ khoá yêu thích (nếu đã có) hoặc thêm phương thức này (nếu chưa có). Trong cả hai trường hợp, mã sẽ gọi notifyListeners(); sau đó.

Thêm nút

Với "logic kinh doanh" đã đến lúc làm việc lại với giao diện người dùng. Đặt nút 'Thích' ở bên trái nút "Tiếp theo" nút yêu cầu có Row. Tiện ích Row tương đương với Column theo chiều ngang mà bạn đã thấy trước đó.

Trước tiên, hãy gói nút hiện có trong một Row. Chuyển đến phương thức build() của MyHomePage, đặt con trỏ lên ElevatedButton, gọi trình đơn Refactor (Tái cấu trúc) bằng Ctrl+. hoặc Cmd+. rồi chọn Wrap with Row (Ngắt dòng bằng hàng).

Khi lưu, bạn sẽ thấy Row hoạt động tương tự như Column. Theo mặc định, lớp này sẽ gộp các phần tử con ở bên trái. (Column đã xếp các phần tử con lên trên cùng.) Để khắc phục vấn đề này, bạn có thể sử dụng phương pháp tương tự như trước đây, nhưng với mainAxisAlignment. Tuy nhiên, để phục vụ mục đích giáo dục (học tập), hãy sử dụng mainAxisSize. Thao tác này sẽ yêu cầu Row không chiếm hết không gian ngang có sẵn.

Thực hiện thay đổi như sau:

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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Giao diện người dùng trở về trạng thái trước đó.

3d53d2b071e2f372.pngS

Tiếp theo, hãy thêm nút Thích và kết nối nút này với toggleFavorite(). Đối với thử thách, trước tiên, hãy thử tự làm việc này mà không xem khối mã bên dưới.

e6b01a8c90df8ffa.png

Nếu bạn không thực hiện giống hệt như cách thực hiện dưới đây thì cũng không sao. Trên thực tế, đừng lo lắng về biểu tượng trái tim, trừ phi bạn thực sự muốn thực hiện một thử thách lớn.

Không thành công cũng không sao cả. Dù sao đây cũng là giờ đầu tiên bạn sử dụng Flutter.

252f7c4a212c94d2.pngS

Dưới đây là một cách để thêm nút thứ hai vào MyHomePage. Lần này, hãy sử dụng hàm khởi tạo ElevatedButton.icon() để tạo nút có biểu tượng. Ở đầu phương thức build, hãy chọn biểu tượng phù hợp tuỳ vào việc cặp từ hiện tại đã có trong mục yêu thích hay chưa. Ngoài ra, hãy lưu ý sử dụng SizedBox một lần nữa để giữ hai nút cách nhau một chút.

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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Ứng dụng sẽ có dạng như sau:

Rất tiếc, người dùng không thể xem các mục yêu thích. Đã đến lúc thêm hoàn toàn một màn hình riêng biệt vào ứng dụng. Hẹn gặp lại bạn trong phần tiếp theo!

7. Thêm dải điều hướng

Hầu hết ứng dụng không thể hiển thị vừa hết mọi thứ trên một màn hình. Có lẽ ứng dụng cụ thể này có thể, nhưng để phục vụ mục đích giáo dục, bạn sẽ tạo một màn hình riêng cho mục yêu thích của người dùng. Để chuyển đổi giữa hai màn hình, bạn sẽ triển khai StatefulWidget đầu tiên.

f62c54f5401a187.png

Để làm được phần chính của bước này càng sớm càng tốt, hãy chia MyHomePage thành 2 tiện ích riêng biệt.

Chọn toàn bộ MyHomePage, xoá và thay thế bằng mã sau:

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'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

Khi lưu, bạn sẽ thấy mặt hình ảnh của giao diện người dùng đã sẵn sàng, nhưng vẫn chưa hoạt động. Việc nhấp vào TAG︎ (trái tim) trên dải điều hướng sẽ không có tác dụng gì.

388bc25fe198c54a.pngS

Kiểm tra các thay đổi.

  • Trước tiên, hãy lưu ý rằng toàn bộ nội dung của MyHomePage đều được trích xuất vào một tiện ích mới là GeneratorPage. Phần duy nhất của tiện ích MyHomePage cũ không được trích xuất là Scaffold.
  • MyHomePage mới chứa Row có 2 phần tử con. Tiện ích đầu tiên là SafeArea và tiện ích thứ hai là Expanded.
  • SafeArea đảm bảo phần tử con của nó không bị che khuất bởi một rãnh phần cứng hoặc thanh trạng thái. Trong ứng dụng này, tiện ích bao bọc NavigationRail để ngăn thanh trạng thái trên thiết bị di động che khuất các nút điều hướng.
  • Bạn có thể thay đổi dòng extended: false trong NavigationRail thành true. Thao tác này hiển thị các nhãn bên cạnh biểu tượng. Trong bước sau, bạn sẽ tìm hiểu cách tự động thực hiện việc này khi ứng dụng có đủ không gian ngang.
  • Dải điều hướng có hai điểm đến (Nhà riêngMục yêu thích), cùng với các biểu tượng và nhãn tương ứng. Mã này cũng xác định selectedIndex hiện tại. Chỉ mục được chọn bằng 0 sẽ chọn đích đến đầu tiên, chỉ mục được chọn là 0 sẽ chọn đích đến thứ hai, v.v. Hiện tại, mã này bị mã hoá cứng thành 0.
  • Dải điều hướng cũng xác định điều sẽ xảy ra khi người dùng chọn một trong các đích đến bằng onDestinationSelected. Hiện tại, ứng dụng chỉ xuất ra giá trị chỉ mục được yêu cầu bằng print().
  • Thành phần con thứ hai của Row là tiện ích Expanded. Các tiện ích mở rộng cực kỳ hữu ích trong hàng và cột — chúng cho phép bạn thể hiện bố cục, trong đó một số tiện ích con chỉ chiếm đủ không gian cần thiết (trong trường hợp này là SafeArea) và các tiện ích khác sẽ chiếm nhiều không gian còn lại nhất có thể (Expanded, trong trường hợp này). Một cách để hiểu về các tiện ích Expanded là chúng có tính "tham lam". Nếu bạn muốn hiểu rõ hơn về vai trò của tiện ích này, hãy thử gói tiện ích SafeArea bằng một Expanded khác. Bố cục thu được sẽ có dạng như sau:

6bbda6c1835a1ae.png.

  • Hai tiện ích Expanded đã tự chia tách mọi không gian ngang có sẵn, mặc dù dải điều hướng chỉ thực sự cần một lát cắt ở bên trái.
  • Bên trong tiện ích Expanded có một Container màu và bên trong vùng chứa có GeneratorPage.

Tiện ích không có trạng thái so với tiện ích có trạng thái

Cho đến thời điểm hiện tại, MyAppState đã đáp ứng tất cả các nhu cầu của tiểu bang của bạn. Đó là lý do tại sao tất cả các tiện ích bạn đã viết cho đến nay đều không có trạng thái. Chúng không chứa bất kỳ trạng thái có thể thay đổi nào của chính chúng. Không có tiện ích nào có thể tự thay đổi—các tiện ích này phải trải qua MyAppState.

Thông tin này sắp thay đổi.

Bạn cần có cách nào đó để giữ giá trị của selectedIndex của dải điều hướng. Bạn cũng muốn có thể thay đổi giá trị này từ trong lệnh gọi lại onDestinationSelected.

Bạn có thể thêm selectedIndex làm một thuộc tính khác của MyAppState. Và cách làm này sẽ mang lại hiệu quả. Tuy nhiên, bạn có thể tưởng tượng rằng trạng thái ứng dụng sẽ nhanh chóng vượt ra khỏi lý do nếu mọi tiện ích đều lưu trữ giá trị của nó trong đó.

e52d9c0937cc0823.jpeg

Một số trạng thái chỉ liên quan đến một tiện ích duy nhất, vì vậy, trạng thái đó phải luôn nằm trong tiện ích đó.

Nhập StatefulWidget, một loại tiện ích có State. Trước tiên, hãy chuyển đổi MyHomePage thành một tiện ích có trạng thái.

Đặt con trỏ trên dòng đầu tiên của MyHomePage (dòng bắt đầu bằng class MyHomePage...) và gọi trình đơn Refactor (Tái cấu trúc) bằng Ctrl+. hoặc Cmd+.. Sau đó, chọn Convert to StatefulWidget (Chuyển đổi sang StatefulWidget).

IDE tạo một lớp mới cho bạn, _MyHomePageState. Lớp này mở rộng State nên có thể quản lý các giá trị riêng. (Công cụ này có thể tự thay đổi.) Ngoài ra, bạn có thể nhận thấy rằng phương thức build từ tiện ích cũ, không có trạng thái đã chuyển sang _MyHomePageState (thay vì ở trong tiện ích). Phương thức này đã được di chuyển nguyên văn – không có gì trong phương thức build thay đổi. Nó hiện chỉ sống ở một nơi khác.

Trạng thái đặt

Tiện ích có trạng thái mới chỉ cần theo dõi một biến: selectedIndex. Thực hiện 3 thay đổi sau đối với _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(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Kiểm tra các thay đổi:

  1. Bạn giới thiệu một biến mới là selectedIndex và khởi tạo biến đó đến 0.
  2. Bạn sử dụng biến mới này trong định nghĩa NavigationRail thay vì 0 được mã hoá cứng như trước đây.
  3. Khi lệnh gọi lại onDestinationSelected được gọi, thay vì chỉ in giá trị mới vào bảng điều khiển, bạn chỉ định giá trị đó cho selectedIndex bên trong lệnh gọi setState(). Lệnh gọi này tương tự như phương thức notifyListeners() đã sử dụng trước đó. Lệnh gọi này đảm bảo giao diện người dùng cập nhật.

Dải điều hướng hiện phản hồi hoạt động tương tác của người dùng. Tuy nhiên, phần mở rộng ở bên phải vẫn giữ nguyên. Đó là vì mã này không sử dụng selectedIndex để xác định màn hình nào sẽ hiển thị.

Sử dụng selectedIndex

Đặt mã sau ở đầu phương thức build của _MyHomePageState, ngay trước 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');
}

// ...

Hãy kiểm tra đoạn mã này:

  1. Mã này khai báo một biến mới là page thuộc kiểu Widget.
  2. Sau đó, câu lệnh chuyển đổi sẽ gán một màn hình cho page, theo giá trị hiện tại trong selectedIndex.
  3. Vì chưa có FavoritesPage, hãy sử dụng Placeholder; một tiện ích tiện dụng vẽ hình chữ nhật chéo ở bất cứ nơi nào bạn đặt nó, đánh dấu phần giao diện người dùng là chưa hoàn tất.

5685cf886047f6ec.png.

  1. Áp dụng nguyên tắc thất bại nhanh, câu lệnh chuyển đổi cũng đảm bảo gửi ra lỗi nếu selectedIndex không là 0 hoặc 1. Điều này giúp ngăn chặn lỗi trong tương lai. Nếu bạn thêm một đích đến mới vào dải điều hướng và quên cập nhật mã này, thì chương trình sẽ gặp sự cố trong quá trình phát triển (thay vì cho phép bạn đoán lý do khiến mọi thứ không hoạt động hoặc cho phép bạn xuất bản mã bị lỗi vào phiên bản chính thức).

Giờ đây, page chứa tiện ích bạn muốn hiển thị ở bên phải, nên bạn có thể đoán được cần thực hiện thay đổi nào khác.

Sau đây là _MyHomePageState sau thay đổi duy nhất còn lại đó:

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.
            ),
          ),
        ],
      ),
    );
  }
}


// ...

Ứng dụng hiện chuyển đổi giữa GeneratorPage của chúng ta và trình giữ chỗ sẽ sớm trở thành trang Mục yêu thích.

Phản ứng nhanh

Tiếp theo, hãy tạo dải điều hướng thích ứng. Điều này có nghĩa là bạn cần đặt chế độ tự động hiển thị nhãn (sử dụng extended: true) khi có đủ chỗ cho nhãn.

a8873894c32e0d0b.png

Flutter cung cấp một số tiện ích giúp bạn tự động phản hồi ứng dụng. Ví dụ: Wrap là một tiện ích tương tự như Row hoặc Column. Tiện ích này tự động gói thành phần con vào "dòng" tiếp theo (gọi là "chạy") khi không có đủ không gian theo chiều dọc hoặc chiều ngang. Có FittedBox, một tiện ích tự động điều chỉnh phần tử con vào không gian có sẵn theo thông số kỹ thuật của bạn.

Tuy nhiên, NavigationRail không tự động hiển thị nhãn khi có đủ dung lượng vì không thể xác định xem đủ không gian trong mọi ngữ cảnh hay không. Là nhà phát triển, bạn có khả năng thực hiện cuộc gọi đó.

Giả sử bạn quyết định chỉ hiển thị nhãn nếu MyHomePage có chiều rộng tối thiểu là 600 pixel.

Trong trường hợp này, tiện ích cần sử dụng là LayoutBuilder. Cách này cho phép bạn thay đổi cây tiện ích tuỳ vào lượng không gian còn trống.

Một lần nữa, hãy sử dụng trình đơn Refactor (Tái cấu trúc) của Flutter trong VS Code để thực hiện các thay đổi cần thiết. Tuy nhiên, lần này thì phức tạp hơn một chút:

  1. Bên trong phương thức build của _MyHomePageState, hãy đặt con trỏ lên Scaffold.
  2. Gọi trình đơn Refactor (Tái cấu trúc) bằng Ctrl+. (Windows/Linux) hoặc Cmd+. (Mac).
  3. Chọn Wrap with Builder (Ngắt dòng bằng Builder) rồi nhấn phím Enter.
  4. Sửa đổi tên của Builder mới thêm vào LayoutBuilder.
  5. Sửa đổi danh sách thông số gọi lại từ (context) thành (context, constraints).

Lệnh gọi lại builder của LayoutBuilder được gọi mỗi khi các điều kiện ràng buộc thay đổi. Ví dụ: điều này xảy ra khi:

  • Người dùng đổi kích thước cửa sổ của ứng dụng
  • Người dùng xoay điện thoại từ chế độ dọc sang chế độ ngang hoặc quay lại
  • Một số tiện ích bên cạnh MyHomePage sẽ tăng kích thước, làm giảm kích thước các quy tắc ràng buộc của MyHomePage
  • Và v.v.

Bây giờ, mã của bạn có thể quyết định xem có hiển thị nhãn hay không bằng cách truy vấn constraints hiện tại. Thực hiện thay đổi một dòng sau đối với phương thức build của _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,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

Giờ đây, ứng dụng của bạn sẽ phản hồi theo môi trường, chẳng hạn như kích thước màn hình, hướng và nền tảng! Nói cách khác, chiến dịch này phản hồi nhanh!

Việc duy nhất còn lại là thay thế Placeholder đó bằng màn hình Mục yêu thích thực tế. Nội dung đó được đề cập trong phần tiếp theo.

8. Thêm trang mới

Bạn có nhớ tiện ích Placeholder chúng tôi đã dùng thay vì trang Mục yêu thích không?

Đã đến lúc khắc phục vấn đề này.

Nếu bạn thích phiêu lưu, hãy thử tự thực hiện bước này. Mục tiêu của bạn là hiển thị danh sách favorites trong một tiện ích không có trạng thái mới là FavoritesPage, sau đó hiển thị tiện ích đó thay vì Placeholder.

Dưới đây là một vài gợi ý:

  • Khi bạn muốn Column cuộn được, hãy sử dụng tiện ích ListView.
  • Hãy nhớ rằng bạn có thể truy cập vào thực thể MyAppState qua bất kỳ tiện ích nào bằng context.watch<MyAppState>().
  • Nếu bạn cũng muốn dùng thử một tiện ích mới, ListTile có các thuộc tính như title (thường dành cho văn bản), leading (dành cho biểu tượng hoặc hình đại diện) và onTap (dành cho hoạt động tương tác). Tuy nhiên, bạn có thể đạt được hiệu ứng tương tự bằng các tiện ích bạn đã biết.
  • Dart cho phép sử dụng vòng lặp for bên trong giá trị cố định của bộ sưu tập. Ví dụ: nếu messages chứa danh sách các chuỗi, bạn có thể có đoạn mã như sau:

f0444bba08f205aa.png

Mặt khác, nếu đã quen với lập trình chức năng, Dart cũng cho phép bạn viết mã như messages.map((m) => Text(m)).toList(). Và tất nhiên, bạn luôn có thể tạo danh sách tiện ích và bắt buộc phải thêm tiện ích đó bên trong phương thức build.

Lợi ích của việc tự thêm trang Yêu thích là bạn có thể tìm hiểu thêm bằng cách đưa ra quyết định của riêng mình. Nhược điểm là bạn có thể gặp rắc rối mà bản thân bạn chưa thể tự giải quyết. Hãy nhớ: thất bại là không sao cả và là một trong những yếu tố quan trọng nhất của việc học tập. Không ai mong đợi bạn thành công trong việc phát triển Flutter ngay trong giờ đầu tiên, và bạn cũng không nên làm như vậy.

252f7c4a212c94d2.pngS

Những nội dung tiếp theo chỉ là một cách để triển khai trang yêu thích. Cách triển khai mã này (hy vọng) sẽ truyền cảm hứng cho bạn thử nghiệm mã – cải thiện giao diện người dùng và biến nó thành của riêng bạn.

Sau đây là lớp FavoritesPage mới:

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),
          ),
      ],
    );
  }
}

Dưới đây là những việc tiện ích này có thể thực hiện:

  • Nó nhận trạng thái hiện tại của ứng dụng.
  • Nếu danh sách mục yêu thích trống, ứng dụng sẽ hiển thị thông báo ở chính giữa: Chưa có mục yêu thích nào*.*
  • Nếu không, nó sẽ hiển thị một danh sách (có thể cuộn).
  • Danh sách bắt đầu bằng một bản tóm tắt (ví dụ: Bạn có 5 mục yêu thích*.*).
  • Sau đó, mã này sẽ lặp lại qua tất cả các mục yêu thích và tạo tiện ích ListTile cho mỗi mục yêu thích.

Việc còn lại là thay thế tiện ích Placeholder bằng FavoritesPage. Và voilá!

Bạn có thể lấy mã nguồn hoàn chỉnh của ứng dụng này trong kho lưu trữ lớp học lập trình trên GitHub.

9. Các bước tiếp theo

Xin chúc mừng!

Bạn làm tốt lắm! Bạn đã lấy một scaffold không hoạt động được với một tiện ích Column và hai tiện ích Text rồi biến nó thành một ứng dụng nhỏ thú vị và phản hồi nhanh.

d6e3d5f736411f13.png

Nội dung đã đề cập

  • Kiến thức cơ bản về cách hoạt động của Flutter
  • Tạo bố cục trong Flutter
  • Kết nối các hoạt động tương tác của người dùng (như nhấn nút) với hành vi của ứng dụng
  • Sắp xếp gọn gàng mã Flutter
  • Tăng khả năng thích ứng của ứng dụng
  • Có được giao diện nhất quán & cảm nhận của ứng dụng

Ðiều gì kế tiếp?

  • Thử nghiệm nhiều hơn với ứng dụng mà bạn đã viết trong phòng thí nghiệm này.
  • Xem mã phiên bản nâng cao này của cùng một ứng dụng để biết cách thêm danh sách ảnh động, độ dốc, hiệu ứng làm mờ và nhiều thành phần khác.
  • Hãy theo dõi hành trình học tập của bạn bằng cách truy cập vào flutter.dev/learn.