Ứ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 để xây dựng các ứng dụng cho thiết bị di động, web và máy tính chỉ từ một bộ mã cơ sở. 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 tên nghe hay, 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 danh sách tên yêu thích trên một trang riêng. Ứng dụng có khả nă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 lượt tương tác của người dùng (chẳng hạn như lượt nhấn nút) với hành vi của ứng dụng
  • Sắp xếp mã Flutter gọn gàng
  • Giúp ứng dụng của bạn thích ứng (với nhiều màn hình)
  • Đạt được giao diện nhất quán cho ứng dụng của bạn

Bạn sẽ bắt đầu bằng một cấu trúc cơ bản để có thể chuyển thẳng đến các phần thú vị.

e9c6b402cd8003fd.png

Và đây là Filip hướng dẫn bạn thực hiện toàn bộ lớp học lập trình!

Nhấp vào nút 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 diễn ra suôn sẻ 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. Ứng dụng này 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 kỳ trình chỉnh sửa nào mà bạn thích: Android Studio, các IDE IntelliJ khác, Emacs, Vim hoặc Notepad++. Tất cả đều hoạt động với Flutter.

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

228c71510a8e868.png

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, thông thường, bạn nên chọn một hệ điều hành duy nhất mà bạn sẽ chủ yếu 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 của bạn chạy trong quá trình phát triển.

16695777c07f18e5.png

Ví dụ: giả sử bạn đang 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, bạn thường kết nối một thiết bị Android với máy tính xách tay Windows bằng cáp USB và ứng dụng đang phát triển của bạn sẽ chạy trên thiết bị Android được kết nối đó. Tuy nhiên, bạn cũng có thể chọn Windows làm mục tiêu phát triển. Điều này có nghĩa là ứng dụng đang phát triển của bạn 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ể muốn chọn web làm mục tiêu phát triển. 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: Tải lại nóng có trạng thái. Flutter không thể tải lại nhanh các ứng dụng web.

Hãy chọn ngay. Lưu ý: Bạn luôn có thể chạy ứng dụng của mình trên các hệ điều hành khác sau này. Chỉ là việc xác định rõ mục tiêu phát triển sẽ giúp bước tiếp theo diễn ra suôn sẻ hơn.

Cài đặt Flutter

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

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

  1. Flutter SDK
  2. Visual Studio Code có trình bổ trợ Flutter
  3. Phần mềm mà mục tiêu phát triển 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.

Nếu gặp vấn đề 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 (trên StackOverflow) sau đây 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

Khởi chạy Visual Studio Code và 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 chính của bạn hoặc một thư mục nào đó như C:\src\.

Cuối cùng, hãy đặt tên cho dự án của bạn. Chẳng hạn như namer_app hoặc my_awesome_namer.

260a7d97f9678005.png

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

Giờ đây, bạn sẽ ghi đè nội dung của 3 tệp bằng một cấu trúc 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 VS Code, hãy nhớ chọn Explorer (Trình khám phá) rồi mở tệp pubspec.yaml.

e2a5bab0be07f4f7.png

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

pubspec.yaml

name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.9.0

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

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

flutter:
  uses-material-design: true

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

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 mà Flutter cần tuân thủ khi phân tích mã của bạn. Vì đây là lần đầu tiên bạn tìm hiểu về Flutter, nên bạn đang yêu cầu trình phân tích hoạt động ở mức độ cơ bản. Bạn luôn có thể điều chỉnh chế độ này sau. Trên thực tế, khi tiến gần đến việc xuất bản một ứng dụng sản xuất thực tế, bạn gần như chắc chắn sẽ muốn trình phân tích nghiêm ngặt hơn thế này.

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 nội dung sau:

lib/main.dart

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

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

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

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

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

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

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

50 dòng mã này là toàn bộ mã nguồn của ứng dụng cho đến thời điểm hiện tại.

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 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ị đích. Ở góc dưới cùng bên phải của VS Code, bạn sẽ thấy một nút cho biết thiết bị mục tiêu hiện tại. Nhấp để thay đổi.

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

Sau khoảng một phút, ứng dụng của bạn sẽ khởi chạy ở chế độ gỡ lỗi. Hiện tại, bạn chưa thấy gì nhiều:

f96e7dfb0937d7f4.png

First Hot Reload

Ở cuối lib/main.dart, hãy thêm nội dung vào chuỗi trong đối tượng Text đầu tiên và 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),
        ],
      ),
    );

// ...

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. Tính năng tải lại nhanh sẽ được kích hoạt khi bạn lưu các thay đổi vào một 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 phiên bản Text thứ hai.

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ẽ hiện thông báo đã nhấn nút!.

Khoá học cấp tốc về Flutter trong 5 phút

Mặc dù Bảng điều khiển gỡ lỗi rất thú vị, nhưng bạn muốn nút này làm được điều gì đó có ý nghĩa hơn. Tuy nhiên, trước khi làm việc đó, hãy xem xét kỹ hơn mã trong lib/main.dart để hiểu cách hoạt động của mã.

lib/main.dart

// ...

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

// ...

Ở đầu tệp, bạn sẽ thấy hàm main(). Ở dạng hiện tại, tệp 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(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

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

Mã trong MyApp thiết lập toàn bộ ứng dụng. Mã này tạo trạng thái trên toàn ứng dụng (sẽ nói thêm về vấn đề này sau), đặt tên cho ứng dụng, xác định giao diện trực quan và đặt tiện ích "home" (trang chủ) – đ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 xác định trạng thái của ứng dụng. Đây là lần đầu tiên bạn khám phá Flutter, vì vậy lớp học lập trình này sẽ giữ cho mọi thứ đơn giản và tập trung. 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 áp dụng.

  • MyAppState xác định dữ liệu mà ứng dụng cần để hoạt động. Hiện tại, nó chỉ chứa một biến duy nhất với cặp từ ngẫu nhiên hiện tại. Bạn sẽ bổ sung thêm vào danh sách này sau.
  • Lớp trạng thái mở rộng ChangeNotifier, nghĩa là lớp này có thể thông báo cho những lớp khác về các thay đổi của chính nó. Ví dụ: nếu cặp từ hiện tại thay đổi, 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 cách sử dụ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 bắt được 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 đã sửa đổi. Mỗi dòng được đánh số bên dưới tương ứng với một chú thích số dòng trong mã ở trên:

  1. Mỗi tiện ích đều xác định một phương thức build() được tự động 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 đều phải trả về một tiện ích hoặc (thường xuyên hơn) 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 thực tế.
  4. Column là một trong những tiện ích bố cục cơ bản nhất trong Flutter. Thành phần này lấy một số lượng bất kỳ các thành phần con và đặt chúng vào một cột từ trên xuống dưới. Theo mặc định, cột sẽ đặt các phần tử con ở trên cùng. Bạn sẽ sớm thay đổi điều này để cột được căn giữa.
  5. Bạn đã thay đổi tiện ích Text này ở bước đầu tiên.
  6. Tiện ích Text thứ hai này lấy appState và truy cập vào thành viên duy nhất của lớp đó, current (là một WordPair). WordPair cung cấp một số phương thức truy xuất hữu ích, chẳng hạn như asPascalCase hoặc asSnakeCase. Ở đây, chúng ta dùng asLowerCase nhưng bạn có thể thay đổi giá trị này ngay bây giờ nếu muốn dùng một trong các lựa chọn thay thế.
  7. Lưu ý cách mã Flutter sử dụng nhiều dấu phẩy ở cuối. Bạn không cần dùng dấu phẩy này vì children là thành phần cuối cùng (và cũng là thành phần duy nhất) của danh sách tham số Column này. Tuy nhiên, bạn nên sử dụng dấu phẩy ở cuối: dấu phẩy này giúp việc thêm nhiều thành viên trở nên dễ dàng và cũng đóng vai trò là gợi ý để trình định dạng tự động của Dart đặt một dòng mới ở đó. Để 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 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 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 ngẫu nhiên mới. 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 mọi người xem MyAppState đều nhận được thông báo.

Tất cả những gì còn lại là gọi phương thức getNext từ 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. Ứng dụng sẽ tạo một cặp từ ngẫu nhiên mới mỗi khi bạn nhấn nút 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 mắt hơn.

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

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

3dd8a9d8653bdc56.png

Không tốt. 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, đây 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ạ và toàn bộ ứng dụng chỉ có màu đen và trắng.

Phần này giải quyết những vấn đề này bằng cách cải thiện thiết kế của ứng dụng. Mục tiêu cuối cùng của phần này là một nội dung như sau:

2bbee054d81a3127.png

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 hiện có dạng như sau: Text(appState.current.asLowerCase). Để thay đổi thành một thứ gì đó 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 biệt. Việc có các tiện ích riêng biệt cho các phần logic riêng biệt của giao diện người dùng là một cách quan trọng để quản lý độ 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, nhưng trước khi 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ì cần thiết. 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ì.

Vì lý 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 đề cập đế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 hai cách:

  1. Nhấp chuột phải vào đoạn mã 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ái cấu trúc...) trong trình đơn thả xuống,

HOẶC

  1. Di chuyển con trỏ đến đoạn mã mà bạn muốn tái cấu trúc (Text trong trường hợp này) rồi 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). Chỉ định 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, 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);
  }
}

// ...

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ẻ

Giờ là lúc biến tiện ích mới này thành phần giao diện người dùng nổi bật 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 đây, 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 trích xuất tiện ích.

Thay vào đó, hãy chọn Bọc bằng khoảng đệm. 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 một giá trị như 20 để có khoảng đệm rộng rãi hơn.

Tiếp theo, hãy tăng thêm một cấp. Đặt con trỏ lên tiện ích Padding, kéo trình đơn Refactor (Tái cấu trúc) lên rồi chọn Wrap with widget... (Bọc 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 Enter.

Thao tác này sẽ bao bọc tiện ích Padding và do đó cũng bao bọc Text bằng tiện ích Card.

lib/main.dart

// ...

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

  final WordPair pair;

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

// ...

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

6031adbc0a11e16b.png

Giao diện và kiểu

Để thẻ nổi bật hơn, hãy tô màu đậm hơn cho thẻ. Vì luôn nên duy trì bảng phối màu nhất quán, hãy 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 thực hiện rất nhiều việc:

  • Trước tiên, mã sẽ 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 xác định 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 và primary là màu nổi bật nhất, xác định màu của ứng dụng.

Thẻ hiện được vẽ 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 di chuyển lên MyApp rồi thay đổi màu gốc cho ColorScheme tại đó.

Hãy lưu ý cách màu sắc chuyển động mượt mà. Đây được gọi là ảnh động ngầm. Nhiều tiện ích Flutter sẽ nội suy mượt mà 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 có độ nâng 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ố định giá trị trong mã.

TextTheme

Thẻ vẫn có vấn đề: văn bản quá nhỏ và màu sắc khó đọc. Để 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),
      ),
    );
  }

// ...

Nguyên nhân của thay đổi này:

  • Bằng cách sử dụng theme.textTheme,, bạn sẽ truy cập vào giao diện phông chữ của ứng dụng. Lớp này bao gồm các thành phần như bodyMedium (đối với văn bản tiêu 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 tiêu đề lớn).
  • Thuộc tính displayMedium là một kiểu lớn dùng để hiển thị văn bản. Từ display (hiển thị) được dùng theo nghĩa in ấn ở đây, chẳng hạn như trong kiểu chữ hiển thị. Tài liệu cho 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" – đúng với 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 đang dùng để viết ứng dụng này) là ngôn ngữ an toàn rỗng, nên ngôn ngữ này sẽ không cho phép bạn gọi các phương thức của những đối tượng có khả năng 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ử dấu chấm than") để đảm bảo Dart biết bạn đang làm gì. (displayMedium chắc chắn không có giá trị rỗng trong trường hợp này. Lý do chúng ta biết điều này 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 với những thay đổi mà 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.
  • Để lấy màu mới, bạn sẽ 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.png

Nếu muốn, bạn có thể thay đổi thẻ thêm nữa. Dưới đây là một số ý tưởng:

  • copyWith() cho phép bạn thay đổi nhiều yếu tố khác về kiểu chữ chứ không 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 bất kỳ vị trí nào 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 với các màu sắc. Ngoài theme.colorScheme.primary, còn có .secondary, .surface và vô số các loại khác. Tất cả các màu này đều có màu tương đương onPrimary.

Cải thiện khả năng hỗ trợ tiếp cận

Theo mặc định, Flutter giúp ứng dụng dễ tiếp cận. Ví dụ: mọi ứng dụng Flutter đều hiển thị chính xác tất cả văn bản và các phần tử tương tác trong ứng dụng cho trình đọc màn hình như TalkBack và VoiceOver.

d1fad7944fb890ea.png

Tuy nhiên, đôi khi bạn cần phải làm một số việc. Trong trường hợp ứ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 vấn đề khi xác định hai từ trong cheaphead, nhưng trình đọc màn hình có thể phát âm ph ở giữa từ là f.

Giải pháp là thay thế pair.asLowerCase bằng "${pair.first} ${pair.second}". Phương thức thứ hai sử dụng tính nă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ì một từ ghép sẽ giúp trình đọc màn hình xác định chúng một cách thích hợp 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ữ cho pair.asLowerCase có hình ảnh đơn giản. Sử dụng thuộc tính Text của semanticsLabel để ghi đè nội dung trực quan của tiện ích văn bản bằng nội dung ngữ nghĩa phù hợp hơn cho 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ử tính năng này bằng cách sử dụng trình đọc màn hình trên thiết bị.

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

Giờ đây, khi cặp từ ngẫu nhiên đã được trình bày với đủ nét đặc sắc về hình ảnh, đã đế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ẽ dồn các thành phần con lên trên cùng, nhưng chúng ta có thể ghi đè hành vi này. Chuyển đến phương thức build() của MyHomePage rồi 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 (trục dọc).

b555d4c7f5000edf.png

Các phần tử con đã được căn giữa dọc theo trục chéo của cột (nói cách khác, các phần tử con đã được căn giữa theo chiều ngang). Nhưng Column chính nó không được căn 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 Widget Inspector (Công cụ kiểm tra tiện ích).

Bản thân Widget Inspector 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, nó không chiếm toàn bộ chiều rộng của ứng dụng. Nó chỉ chiếm không gian theo chiều ngang khi các thành phần con cần.

Bạn chỉ cần căn giữa cột. Đặt con trỏ lên 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 (Bọc bằng Center).

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

455688d93c30d154.png

Nếu muốn, bạn có thể điều chỉnh thêm.

  • Bạn có thể xoá tiện ích Text ở phía trên BigCard. Có thể cho rằng văn bản mô tả ("Một ý tưởng TUYỆT VỜI ngẫu nhiên:") 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 này. Và cách này sẽ gọn gàng hơn.
  • Bạn cũng có thể thêm tiện ích SizedBox(height: 10) trong khoảng thời gian từ BigCard đến ElevatedButton. Bằng cách này, sẽ có một khoảng cách lớn hơn một chút giữa hai tiện ích. Tiện ích SizedBox chỉ chiếm dung lượng và không tự hiển thị nội dung nào. Thẻ này thường được dùng để tạo "khoảng trống" 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'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Ứng dụng sẽ có giao diện như sau:

3d53d2b071e2f372.png

Trong phần tiếp theo, bạn sẽ thêm khả năng đánh dấu từ đã tạo là mục yêu thích (hoặc "thích").

6. Thêm chức năng

Ứng dụng hoạt động và đôi khi còn cung cấp các cặp từ thú vị. Nhưng 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. Sẽ tốt hơn nếu có cách để "ghi nhớ" những đề xuất hay nhất, chẳng hạn như nút "Thích".

e6b01a8c90df8ffa.png

Thêm logic nghiệp vụ

Di chuyển đến MyAppState rồi thêm đoạn 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 thuộc tính mới vào MyAppState có tên là favorites. 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 các kiểu chung. Điều này giúp ứng dụng của bạn trở nên mạnh mẽ hơn – Dart thậm chí sẽ từ chối chạy ứng dụng nếu bạn cố gắng thêm bất kỳ thứ gì khác ngoài WordPair vào ứng dụng. Đổi lại, bạn có thể dùng danh sách favorites mà không cần lo lắng vì không bao giờ có bất kỳ đối tượng không mong muốn nào (chẳng hạn như null) ẩn trong đó.
  • Bạn cũng đã thêm một phương thức mới, toggleFavorite(), phương thức này sẽ xoá cặp từ hiện tại khỏi danh sách yêu thích (nếu cặp từ đó đã có trong danh sách) hoặc thêm cặp từ đó (nếu cặp từ đó chưa có trong danh sách). Trong cả hai trường hợp, mã sẽ gọi notifyListeners(); sau đó.

Thêm nút

Sau khi hoàn tất "logic nghiệp vụ", bạn có thể bắt đầu xử lý giao diện người dùng. Để đặt nút "Thích" ở bên trái nút "Tiếp theo", bạn cần có Row. Tiện ích Row là phiên bản ngang của Column 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 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 (Bọc bằng hàng).build()

Khi lưu, bạn sẽ nhận thấy rằng Row hoạt động tương tự như Column – theo mặc định, nó sẽ gộp các thành phần con ở bên trái. (Column gộp các phần tử con của nó 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, đối với mục đích giáo huấn (học tập), hãy sử dụng mainAxisSize. Điều này cho biết Row không được chiếm hết không gian ngang có sẵn.

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: 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 sẽ trở lại trạng thái trước đó.

3d53d2b071e2f372.png

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

e6b01a8c90df8ffa.png

Bạn không cần phải làm theo chính xác cách làm dưới đây. Thực tế là bạn không cần lo lắng về biểu tượng trái tim trừ phi bạn thực sự muốn một thử thách lớn.

Bạn cũng hoàn toàn có thể thất bại – dù sao thì đây cũng là giờ đầu tiên bạn làm việc với Flutter.

252f7c4a212c94d2.png

Sau đây là một cách để thêm nút thứ hai vào MyHomePage. Lần này, hãy dùng hàm khởi tạo ElevatedButton.icon() để tạo một nút có biểu tượng. Và ở đầu phương thức build, hãy chọn biểu tượng thích hợp tuỳ thuộc 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 ý việc sử dụng lại SizedBox để giữ cho 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 một màn hình hoàn toàn riêng biệt vào ứng dụng của chúng ta. Hẹn gặp lại bạn ở phần tiếp theo!

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

Hầu hết các ứng dụng đều không thể đưa mọi thứ vào một màn hình duy nhất. Ứng dụng cụ thể này có thể làm được, nhưng vì mục đích giảng dạy, 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

Để đi vào trọng tâm 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 tất cả MyHomePage, xoá rồi 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 trực quan của giao diện người dùng đã sẵn sàng nhưng không hoạt động. Khi bạn nhấp vào biểu tượng ♥︎ (trái tim) trong thanh điều hướng, sẽ không có gì xảy ra.

388bc25fe198c54a.png

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 được trích xuất vào một tiện ích mới, 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 một Row có hai phần tử con. Tiện ích đầu tiên là SafeArea, còn tiện ích thứ hai là Expanded.
  • SafeArea đảm bảo rằng thành phần con của nó không bị che khuất bởi một vết cắt trên phần cứng hoặc thanh trạng thái. Trong ứng dụng này, tiện ích sẽ bao quanh NavigationRail để ngăn các nút điều hướng bị thanh trạng thái trên thiết bị di động che khuất, chẳng hạn như vậy.
  • Bạn có thể thay đổi dòng extended: false trong NavigationRail thành true. Thao tác này sẽ hiển thị nhãn bên cạnh biểu tượng. Trong một bước tiếp theo, bạn sẽ tìm hiểu cách thực hiện việc này một cách tự động khi ứng dụng có đủ không gian theo chiều ngang.
  • Thanh điều hướng có 2 đích đến (Trang chủMục yêu thích), cùng các biểu tượng và nhãn tương ứng. Phần này cũng xác định selectedIndex hiện tại. Chỉ mục đã chọn là 0 sẽ chọn đích đến đầu tiên, chỉ mục đã chọn là 1 sẽ chọn đích đến thứ hai, v.v. Hiện tại, giá trị này được mã hoá cứng thành 0.
  • Thanh điều hướng cũng xác định những gì 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 giá trị chỉ mục được yêu cầu bằng print().
  • Phần tử con thứ hai của Row là tiện ích Expanded. Các tiện ích mở rộng rất hữu ích trong các hàng và cột. Chúng cho phép bạn thể hiện bố cục trong đó một số thành phần con chỉ chiếm không gian cần thiết (SafeArea trong trường hợp này) 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 để liên tưởng đến các tiện ích Expanded là chúng "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ử bao bọc tiện ích SafeArea bằng một Expanded khác. Bố cục kết quả sẽ có dạng như sau:

6bbda6c1835a1ae.png

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

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

Cho đến nay, MyAppState đã đáp ứng mọi nhu cầu về trạng thái của bạn. Đó là lý do tất cả các tiện ích bạn đã viết cho đến nay đều là tiện ích không 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 riêng chúng. Không có tiện ích nào có thể tự thay đổi chính nó. Các tiện ích phải thông qua MyAppState.

Điều này sắp thay đổi.

Bạn cần một cách nào đó để giữ giá trị của selectedIndex trong thanh điều hướng. Bạn cũng muốn có thể thay đổi giá trị này 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 này sẽ hiệu quả. Nhưng bạn có thể hình dung rằng trạng thái ứng dụng sẽ nhanh chóng tăng lên một cách vô lý nếu mọi tiện ích đều lưu trữ các giá trị của nó trong trạng thái ứng dụng.

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 đi kèm với 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ỏ lên dòng đầu tiên của MyHomePage (dòng bắt đầu bằng class MyHomePage...) rồi gọi trình đơn Refactor (Tái cấu trúc) bằng cách sử dụng Ctrl+. hoặc Cmd+.. Sau đó, hãy chọn Convert to StatefulWidget (Chuyển đổi thành StatefulWidget).

IDE sẽ tạo một lớp mới cho bạn, _MyHomePageState. Lớp này mở rộng State và do đó có thể quản lý các giá trị của riêng lớp. (Nó có thể tự thay đổi.) Ngoài ra, xin lưu ý rằng phương thức build từ tiện ích cũ, không trạng thái đã chuyển sang _MyHomePageState (thay vì vẫn nằm trong tiện ích). Nội dung này được di chuyển nguyên văn – không có gì thay đổi trong phương thức build. Giờ đây, nó chỉ đơn giản là nằm ở một nơi khác.

setState

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, selectedIndex, và khởi tạo biến đó thành 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 cho đến nay.
  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 sẽ 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() được dùng trước đây – phương thức này đảm bảo rằng giao diện người dùng sẽ cập nhật.

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

Sử dụng selectedIndex

Đặt đoạn 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 xem xét đoạn mã này:

  1. Mã này khai báo một biến mới, page, thuộc loại Widget.
  2. Sau đó, một câu lệnh switch sẽ chỉ định 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 dùng Placeholder; đây là một tiện ích hữu ích vẽ một hình chữ nhật có đường chéo ở bất cứ nơi nào bạn đặt, đánh dấu phần đó của giao diện người dùng là chưa hoàn thành.

5685cf886047f6ec.png

  1. Khi áp dụng nguyên tắc thất bại nhanh, câu lệnh switch cũng đảm bảo sẽ đưa ra lỗi nếu selectedIndex không phải là 0 hoặc 1. Điều này giúp ngăn chặn các lỗi trong tương lai. Nếu bạn thêm một đích đến mới vào thanh điều hướng và quên cập nhật mã này, chương trình sẽ gặp sự cố trong quá trình phát triển (thay vì để 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ã có lỗi vào bản phát hành công khai).

Giờ đây, page chứa tiện ích mà bạn muốn hiển thị ở bên phải, có lẽ bạn có thể đoán được những thay đổi khác cần thiết.

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

// ...

Giờ đây, ứng dụng sẽ chuyển đổi giữa GeneratorPage và phần giữ chỗ sắp trở thành trang Mục yêu thích.

Phản ứng nhanh

Tiếp theo, hãy điều chỉnh dải điều hướng cho phù hợp. Tức là tự động hiện các nhãn (bằng cách sử dụng extended: true) khi có đủ không gian cho các nhãn đó.

a8873894c32e0d0b.png

Flutter cung cấp một số tiện ích giúp bạn tạo ứng dụng tự động thích ứng. Ví dụ: Wrap là một tiện ích tương tự như Row hoặc Column, tự động bao bọc các phần tử 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 cho vừa không gian hiện có theo quy cách của bạn.

Tuy nhiên, NavigationRail không tự động hiện nhãn khi có đủ không gian vì không thể biết đủ không gian trong mọi ngữ cảnh. Bạn (nhà phát triển) sẽ quyết định việc đó.

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

Tiện ích cần sử dụng trong trường hợp này là LayoutBuilder. Bạn có thể thay đổi cây tiện ích tuỳ theo dung lượng trống.

Một lần nữa, hãy 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, vấn đề 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 (Bọc bằng Trình tạo) rồi nhấn phím Enter.
  4. Sửa đổi tên của Builder mới thêm thành LayoutBuilder.
  5. Sửa đổi danh sách tham 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. Điều này xảy ra khi, chẳng hạn như:

  • 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 ngược lại
  • Một số tiện ích bên cạnh MyHomePage tăng kích thước, khiến các quy tắc ràng buộc của MyHomePage nhỏ hơn

Giờ đây, 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 sau đây trên một dòng cho 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, nó có khả năng thích ứng!

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 này sẽ được đề cập trong phần tiếp theo.

8. Thêm trang mới

Bạn có nhớ tiện ích Placeholder mà chúng ta đã dùng thay cho trang Mục yêu thích không?

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

Nếu bạn muốn thử thách bản thân, hãy tự mình 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 trạng thái mới, FavoritesPage, rồi hiển thị tiện ích đó thay vì Placeholder.

Sau đây là một vài gợi ý:

  • Khi bạn muốn một Column có thể cuộn, 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 từ mọi tiện ích bằng cách sử dụng context.watch<MyAppState>().
  • Nếu bạn cũng muốn thử một tiện ích mới, ListTile có các thuộc tính như title (thường dùng cho văn bản), leading (dùng cho biểu tượng hoặc hình đại diện) và onTap (dùng cho các lượt 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 mà bạn đã biết.
  • Dart cho phép sử dụng các vòng lặp for bên trong các giá trị cố định của bộ sưu tập. Ví dụ: nếu messages chứa một danh sách các chuỗi, bạn có thể có mã như sau:

f0444bba08f205aa.png

Mặt khác, nếu bạn quen thuộc hơn với lập trình hàm, thì 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 một danh sách các tiện ích và thêm tiện ích vào danh sách đó một cách bắt buộc trong phương thức build.

Lợi ích của việc tự thêm trang Yêu thích là bạn sẽ học được nhiều hơn bằng cách tự đưa ra quyết định. Nhược điểm là bạn có thể gặp phải vấn đề mà bạn chưa thể tự giải quyết. Hãy nhớ rằng: thất bại 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. Không ai mong đợi bạn có thể phát triển ứng dụng bằng Flutter ngay trong giờ đầu tiên, và bạn cũng không nên mong đợi điều đó.

252f7c4a212c94d2.png

Sau đây chỉ là một cách triển khai trang yêu thích. Cách triển khai này (hy vọng) sẽ truyền cảm hứng để bạn thử nghiệm với mã, cải thiện giao diện người dùng và tạo ra giao diện người dùng của riêng mình.

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

Tiện ích này có những chức năng sau:

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

Giờ đây, bạn chỉ cần thay thế tiện ích Placeholder bằng FavoritesPage. Và thế là xong!

Bạn có thể lấy đoạn mã cuối cùng 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 khung không hoạt động với một Column và hai tiện ích Text, rồi biến nó thành một ứng dụng nhỏ, thú vị và có khả năng thích ứng.

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 lượt tương tác của người dùng (chẳng hạn như lượt nhấn nút) với hành vi của ứng dụng
  • Sắp xếp mã Flutter gọn gàng
  • Đảm bảo ứng dụng của bạn có khả năng phản hồi
  • Đạt được giao diện nhất quán cho ứng dụng của bạn

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

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