Tìm hiểu các mẫu hình và bản ghi của Dart

1. Giới thiệu

Dart 3 đưa ra các mẫu cho ngôn ngữ, một danh mục ngữ pháp mới quan trọng. Ngoài cách mới này để viết mã Dart, còn có một số cải tiến về ngôn ngữ khác, bao gồm

  • bản ghi để nhóm dữ liệu thuộc nhiều loại,
  • đối tượng sửa đổi lớp để kiểm soát quyền truy cập, và
  • biểu thức chuyển đổicâu lệnh if-case mới.

Những tính năng này mở rộng lựa chọn của bạn khi viết mã Dart. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng chúng để làm cho mã của mình nhỏ gọn, đơn giản và linh hoạt hơn.

Lớp học lập trình này giả định rằng bạn đã quen thuộc với Flutter và Dart. Nếu bạn cảm thấy tài nguyên đôi chút, hãy cân nhắc việc ôn lại những phần cơ bản bằng các tài nguyên sau:

Sản phẩm bạn sẽ tạo ra

Lớp học lập trình này sẽ tạo một ứng dụng hiển thị tài liệu JSON trong Flutter. Ứng dụng mô phỏng JSON đến từ một nguồn bên ngoài. Tệp JSON chứa dữ liệu tài liệu như ngày sửa đổi, tiêu đề, tiêu đề và đoạn văn bản. Bạn viết mã để đóng gói dữ liệu một cách gọn gàng vào các bản ghi sao cho dữ liệu có thể được chuyển và giải nén ở bất cứ nơi nào tiện ích Flutter của bạn cần đến.

Sau đó, bạn sẽ sử dụng các mẫu để tạo tiện ích phù hợp khi giá trị khớp với mẫu đó. Bạn cũng sẽ thấy cách sử dụng các mẫu để huỷ cấu trúc dữ liệu thành các biến cục bộ.

Ứng dụng cuối cùng mà bạn xây dựng trong lớp học lập trình này, một tài liệu có tiêu đề, ngày sửa đổi gần đây nhất, tiêu đề và đoạn văn.

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

  • Cách tạo bản ghi lưu trữ nhiều giá trị thuộc các loại khác nhau.
  • Cách trả về nhiều giá trị từ một hàm bằng bản ghi.
  • Cách sử dụng các mẫu để so khớp, xác thực và huỷ cấu trúc dữ liệu trong các bản ghi và đối tượng khác.
  • Cách liên kết các giá trị đã so khớp mẫu với biến mới hoặc biến hiện có.
  • Cách sử dụng các tính năng mới của câu lệnh chuyển đổi, biểu thức chuyển đổi và câu lệnh if-case.
  • Cách tận dụng tính năng kiểm tra tính toàn diện để đảm bảo mọi trường hợp đều được xử lý trong câu lệnh chuyển đổi hoặc biểu thức chuyển đổi.

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

  1. Cài đặt Flutter SDK.
  2. Thiết lập một trình chỉnh sửa, chẳng hạn như Visual Studio Code (VS Code).
  3. Thực hiện các bước Thiết lập nền tảng cho ít nhất một nền tảng mục tiêu (iOS, Android, Máy tính hoặc trình duyệt web).

3. Tạo dự án

Trước khi đi sâu vào các mẫu, bản ghi và các tính năng mới khác, hãy dành chút thời gian để tạo một dự án Flutter đơn giản để bạn viết toàn bộ mã của mình.

Tạo một dự án Flutter

  1. Dùng lệnh flutter create để tạo một dự án mới có tên là patterns_codelab. Cờ --empty ngăn việc tạo ứng dụng bộ đếm chuẩn trong tệp lib/main.dart. Bạn vẫn phải xoá tệp này.
flutter create --empty patterns_codelab
  1. Sau đó, mở thư mục patterns_codelab bằng VS Code.
code patterns_codelab

Ảnh chụp màn hình của Mã VS cho thấy dự án được tạo bằng "flutter create" .

Đặt phiên bản SDK tối thiểu

  • Đặt quy tắc ràng buộc về phiên bản SDK để dự án của bạn phụ thuộc vào Dart 3 trở lên.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Thiết lập dự án

Ở bước này, bạn tạo hoặc cập nhật hai tệp Dart:

  • Tệp main.dart chứa các tiện ích cho ứng dụng và
  • Tệp data.dart cung cấp dữ liệu của ứng dụng.

Bạn sẽ tiếp tục sửa đổi cả hai tệp này trong các bước tiếp theo.

Xác định dữ liệu cho ứng dụng

  • Tạo một tệp mới, lib/data.dart và thêm mã sau vào tệp đó:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

Hãy tưởng tượng một chương trình nhận dữ liệu từ nguồn bên ngoài, chẳng hạn như luồng I/O hoặc yêu cầu HTTP. Trong lớp học lập trình này, bạn sẽ đơn giản hoá trường hợp sử dụng thực tế hơn bằng cách mô phỏng dữ liệu JSON nhận được thông qua một chuỗi nhiều dòng trong biến documentJson.

Dữ liệu JSON được xác định trong lớp Document. Trong phần sau của lớp học lập trình này, bạn sẽ thêm các hàm trả về dữ liệu từ JSON đã phân tích cú pháp. Lớp này định nghĩa và khởi tạo trường _json trong hàm khởi tạo.

Chạy ứng dụng

Lệnh flutter create tạo tệp lib/main.dart trong cấu trúc tệp Flutter mặc định.

  1. Để tạo điểm bắt đầu cho ứng dụng, hãy thay thế nội dung của main.dart bằng đoạn mã sau:

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
  runApp(const DocumentApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: DocumentScreen(
        document: Document(),
      ),
    );
  }
}

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Title goes here'),
      ),
      body: const Column(
        children: [
          Center(
            child: Text('Body goes here'),
          ),
        ],
      ),
    );
  }
}

Bạn đã thêm hai tiện ích sau vào ứng dụng:

  • DocumentApp thiết lập phiên bản mới nhất của Material Design để tuỳ chỉnh giao diện người dùng.
  • DocumentScreen cung cấp bố cục hình ảnh của trang bằng cách sử dụng tiện ích Scaffold.
  1. Để đảm bảo mọi thứ hoạt động trơn tru, hãy chạy ứng dụng trên máy chủ của bạn bằng cách nhấp vào Run and Debug (Chạy và gỡ lỗi):

Hình ảnh thông báo &quot;Chạy và gỡ lỗi&quot; , có sẵn trong &quot;Chạy và gỡ lỗi&quot; của thanh hoạt động ở phía bên trái.

  1. Theo mặc định, Flutter chọn bất kỳ nền tảng mục tiêu nào có sẵn. Để thay đổi nền tảng đích, hãy chọn nền tảng hiện tại trên Thanh trạng thái:

Ảnh chụp màn hình bộ chọn nền tảng mục tiêu trong VS Code.

Bạn sẽ thấy một khung trống có phần tử titlebody được xác định trong tiện ích DocumentScreen:

Ảnh chụp màn hình của ứng dụng được tạo trong bước này.

5. Tạo và trả về bản ghi

Ở bước này, bạn sử dụng các bản ghi để trả về nhiều giá trị từ một lệnh gọi hàm. Sau đó, bạn gọi hàm đó trong tiện ích DocumentScreen để truy cập và phản ánh các giá trị đó trong giao diện người dùng.

Tạo và trả về một bản ghi

  • Trong data.dart, hãy thêm một phương thức getter mới vào lớp Tài liệu có tên là metadata. Phương thức này sẽ trả về một bản ghi:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {           // Add from here...
    const title = 'My Document';
    final now = DateTime.now();

    return (title, modified: now);
  }                                                      // to here.
}

Loại dữ liệu trả về cho hàm này là một bản ghi có hai trường, một bản ghi thuộc loại String và một bản ghi thuộc loại DateTime.

Câu lệnh trả về tạo một bản ghi mới bằng cách đặt hai giá trị trong dấu ngoặc đơn, (title, modified: now).

Trường đầu tiên là theo vị trí và chưa được đặt tên, còn trường thứ hai là modified.

Truy cập các trường bản ghi

  1. Trong tiện ích DocumentScreen, hãy gọi phương thức getter metadata trong phương thức build để bạn có thể nhận bản ghi và truy cập vào các giá trị của bản ghi:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final metadataRecord = document.metadata;              // Add this line.

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),                    // Modify this line,
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Phương thức getter metadata trả về một bản ghi được gán cho biến cục bộ metadataRecord. Bản ghi là một cách đơn giản và dễ dàng để trả về nhiều giá trị từ một lệnh gọi hàm và gán các giá trị đó cho một biến.

Để truy cập vào từng trường riêng lẻ có trong bản ghi đó, bạn có thể sử dụng phần tử bản ghi tích hợp sẵn cú pháp getter.

  • Để nhận một trường vị trí (trường không có tên, chẳng hạn như title), hãy sử dụng phương thức getter $<num> trên bản ghi. Thao tác này chỉ trả về các trường chưa được đặt tên.
  • Các trường được đặt tên như modified không có phương thức getter vị trí, vì vậy, bạn có thể sử dụng trực tiếp tên của phương thức đó, chẳng hạn như metadataRecord.modified.

Để xác định tên của một phương thức getter cho một trường vị trí, hãy bắt đầu từ $1 và bỏ qua các trường đã đặt tên. Ví dụ:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Tải lại nhanh để xem các giá trị JSON hiển thị trong ứng dụng. Trình bổ trợ VS Code Dart sẽ tải lại nhanh mỗi khi bạn lưu tệp.

Ảnh chụp màn hình ứng dụng, cho thấy tên và ngày sửa đổi.

Bạn có thể thấy rằng trên thực tế, mỗi trường đều duy trì kiểu của nó.

  • Phương thức Text() lấy một Chuỗi làm đối số đầu tiên.
  • Trường modified là một DateTime và được chuyển đổi thành String bằng cách sử dụng nội suy chuỗi.

Một cách an toàn về kiểu khác để trả về các loại dữ liệu khác nhau là xác định một lớp chi tiết hơn.

6. Khớp và huỷ cấu trúc bằng các mẫu

Bản ghi có thể thu thập nhiều loại dữ liệu một cách hiệu quả và dễ dàng truyền đi. Bây giờ, hãy cải thiện mã bằng cách sử dụng mẫu.

Mẫu biểu thị cấu trúc mà một hoặc nhiều giá trị có thể nhận, chẳng hạn như sơ đồ thiết kế. Mẫu so sánh với giá trị thực tế để xác định xem chúng có khớp hay không.

Khi chúng khớp với nhau, một số mẫu sẽ huỷ cấu trúc giá trị khớp bằng cách lấy dữ liệu từ đó. Việc giải cấu trúc cho phép bạn giải nén các giá trị từ một đối tượng để gán các giá trị đó cho các biến cục bộ hoặc thực hiện so khớp thêm trên các giá trị đó.

Huỷ cấu trúc một bản ghi thành các biến cục bộ

  1. Tái cấu trúc phương thức build của DocumentScreen để gọi metadata và sử dụng phương thức này để khởi chạy khai báo biến mẫu:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final (title, modified: modified) = document.metadata;   // Modify

    return Scaffold(
      appBar: AppBar(
        title: Text(title),                                  // Modify
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

Mẫu bản ghi (title, modified: modified) chứa 2 mẫu biến khớp với các trường của bản ghi do metadata trả về.

  • Biểu thức khớp với mẫu phụ vì kết quả là một bản ghi có hai trường, một trong số đó có tên là modified.
  • Vì chúng khớp nhau, mẫu khai báo biến giải cấu trúc biểu thức, truy cập vào các giá trị của biểu thức đó và liên kết chúng với các biến cục bộ mới cùng kiểu và tên, String titleDateTime modified.

Có cách viết tắt khi tên của một trường và biến điền vào trường đó giống nhau. Tái cấu trúc phương thức build của DocumentScreen như sau.

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;            // Modify

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',
            ),
          ),
        ],
      ),
    );
  }
}

Cú pháp của mẫu biến :modified là viết tắt của modified: modified. Nếu muốn một biến cục bộ mới có tên khác, bạn có thể viết modified: localModified.

  1. Tải lại nhanh để xem kết quả tương tự như trong bước trước. Hành vi này hoàn toàn giống nhau; bạn vừa làm cho mã của mình ngắn gọn hơn.

7. Sử dụng các mẫu để trích xuất dữ liệu

Trong một số ngữ cảnh nhất định, các mẫu không chỉ khớp và huỷ cấu trúc mà còn có thể đưa ra quyết định về tác động của mã, dựa trên việc mẫu có khớp hay không. Đây được gọi là mẫu có thể từ chối.

Mẫu khai báo biến mà bạn sử dụng trong bước cuối cùng là mẫu không thể từ chối: giá trị phải khớp với mẫu, nếu không, đó là lỗi và việc huỷ cấu trúc sẽ không xảy ra. Hãy nghĩ đến bất kỳ thao tác khai báo hoặc chỉ định biến nào; bạn không thể chỉ định giá trị cho một biến nếu chúng không cùng loại.

Mặt khác, mẫu có thể điều chỉnh được dùng trong ngữ cảnh luồng điều khiển:

  • Họ kỳ vọng rằng một số giá trị mà họ so sánh sẽ không khớp.
  • Các giá trị này được thiết kế để ảnh hưởng đến luồng kiểm soát, dựa vào việc giá trị có khớp hay không.
  • Chúng không làm gián đoạn quá trình thực thi kèm theo lỗi nếu chúng không khớp nhau mà chỉ chuyển sang câu lệnh tiếp theo.
  • Chúng có thể huỷ cấu trúc và liên kết các biến chỉ sử dụng được khi chúng khớp

Đọc các giá trị JSON mà không có mẫu

Trong phần này, bạn sẽ đọc dữ liệu mà không cần so khớp mẫu để xem cách các mẫu có thể giúp bạn xử lý dữ liệu JSON như thế nào.

  • Thay thế phiên bản trước đó của metadata bằng phiên bản đọc các giá trị của bản đồ _json. Sao chép và dán phiên bản này của metadata vào lớp Document:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json.containsKey('metadata')) {                     // Modify from here...
      final metadataJson = _json['metadata'];
      if (metadataJson is Map) {
        final title = metadataJson['title'] as String;
        final localModified =
            DateTime.parse(metadataJson['modified'] as String);
        return (title, modified: localModified);
      }
    }
    throw const FormatException('Unexpected JSON');          // to here.
  }
}

Mã này xác thực rằng dữ liệu có cấu trúc chính xác mà không cần sử dụng mẫu. Ở bước sau, bạn sử dụng tính năng so khớp mẫu để thực hiện cùng một quy trình xác thực mà sử dụng ít mã hơn. Bot thực hiện 3 bước kiểm tra trước khi làm bất cứ việc gì khác:

  • JSON chứa cấu trúc dữ liệu mà bạn mong đợi: if (_json.containsKey('metadata'))
  • Dữ liệu có loại bạn mong đợi: if (metadataJson is Map)
  • Dữ liệu không rỗng và điều này được ngầm xác nhận trong lần kiểm tra trước đó.

Đọc các giá trị JSON bằng mẫu bản đồ

Với mẫu có thể từ chối, bạn có thể xác minh rằng JSON có cấu trúc dự kiến bằng cách sử dụng mẫu bản đồ.

  • Thay thế phiên bản trước đó của metadata bằng mã này:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json                                                // Modify from here...
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

Ở đây, bạn sẽ thấy một loại câu lệnh if mới (được giới thiệu trong Dart 3), là if-case. Nội dung trường hợp chỉ thực thi nếu mẫu trường hợp khớp với dữ liệu trong _json. So khớp này thực hiện các bước kiểm tra tương tự như bạn đã viết trong phiên bản đầu tiên của metadata để xác thực JSON sắp tới. Mã này xác thực những mục sau:

  • _json là một loại Bản đồ.
  • _json chứa khoá metadata.
  • _json không rỗng.
  • _json['metadata'] cũng là một loại Bản đồ.
  • _json['metadata'] chứa các khoá titlemodified.
  • titlelocalModified là các chuỗi và không rỗng.

Nếu giá trị không khớp, mẫu sẽ rút lại (từ chối tiếp tục thực thi) và chuyển sang mệnh đề else. Nếu việc so khớp thành công, mẫu này sẽ huỷ cấu trúc các giá trị của titlemodified khỏi bản đồ và liên kết chúng với các biến cục bộ mới.

Để có danh sách đầy đủ các mẫu, hãy xem bảng trong phần Mẫu của thông số kỹ thuật của tính năng.

8. Chuẩn bị ứng dụng để biết thêm mẫu

Cho đến nay, bạn đã xử lý phần metadata của dữ liệu JSON. Ở bước này, bạn sẽ tinh chỉnh logic nghiệp vụ thêm một chút để xử lý dữ liệu trong danh sách blocks và kết xuất dữ liệu đó vào ứng dụng.

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

Tạo một lớp lưu trữ dữ liệu

  • Thêm một lớp mới là Block vào data.dart. Lớp này dùng để đọc và lưu trữ dữ liệu cho một trong các khối trong dữ liệu JSON.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

Hàm khởi tạo nhà máy fromJson() sử dụng cùng một trường hợp if-case với một mẫu ánh xạ mà bạn đã thấy trước đây.

Lưu ý rằng json khớp với mẫu bản đồ, mặc dù một trong các khoá checked không được tính trong mẫu. Mẫu bản đồ bỏ qua mọi mục nhập trong đối tượng ánh xạ không được tính rõ ràng trong mẫu.

Trả về danh sách đối tượng Chặn

  • Tiếp theo, hãy thêm một hàm mới là getBlocks() vào lớp Document. getBlocks() phân tích cú pháp JSON thành các thực thể của lớp Block và trả về danh sách các khối để kết xuất trong giao diện người dùng của bạn:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

  List<Block> getBlocks() {                                  // Add from here...
    if (_json case {'blocks': List blocksJson}) {
      return [for (final blockJson in blocksJson) Block.fromJson(blockJson)];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }                                                          // to here.
}

Hàm getBlocks() trả về danh sách đối tượng Block mà sau này bạn sử dụng để tạo giao diện người dùng. Câu lệnh if-case quen thuộc sẽ thực hiện việc xác thực và truyền giá trị của siêu dữ liệu blocks vào một List mới có tên là blocksJson (không có mẫu, bạn sẽ cần dùng phương thức toList() để truyền).

Hằng danh sách chứa một bộ sưu tập cho để điền các đối tượng Block vào danh sách mới.

Phần này không giới thiệu tính năng liên quan đến mẫu mà bạn chưa thử trong lớp học lập trình này. Trong bước tiếp theo, bạn sẽ chuẩn bị hiển thị các mục trong danh sách trong giao diện người dùng.

9. Sử dụng các mẫu để hiển thị tài liệu

Bây giờ, bạn đã huỷ cấu trúc và kết hợp lại dữ liệu JSON thành công bằng cách sử dụng câu lệnh if-case và các mẫu có thể từ chối. Nhưng nếu-case-case chỉ là một trong các cải tiến để kiểm soát cấu trúc luồng đi kèm với mẫu. Bây giờ, bạn sẽ áp dụng kiến thức về các mẫu có thể từ chối để chuyển đổi câu lệnh.

Kiểm soát nội dung được hiển thị bằng cách sử dụng các mẫu có câu lệnh chuyển đổi

  • Trong main.dart, hãy tạo một tiện ích mới là BlockWidget. Tiện ích này sẽ xác định kiểu của từng khối dựa trên trường type của khối đó.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

Câu lệnh chuyển đổi trong phương thức build chuyển đổi trên trường type của đối tượng block.

  1. Câu lệnh trường hợp đầu tiên sử dụng mẫu chuỗi không đổi. Mẫu này khớp nếu block.type bằng giá trị không đổi h1.
  2. Câu lệnh trường hợp thứ hai sử dụng mẫu-hoặc logic với hai mẫu chuỗi không đổi làm mẫu phụ. Mẫu này khớp nếu block.type khớp với một trong hai mẫu phụ p hoặc checkbox.
  1. Trường hợp cuối cùng là mẫu ký tự đại diện, _. Ký tự đại diện trong trường hợp chuyển đổi khớp với mọi nội dung khác. Các mệnh đề này hoạt động giống như mệnh đề default nhưng vẫn được phép sử dụng trong các câu lệnh chuyển đổi (chỉ chi tiết hơn một chút).

Bạn có thể dùng mẫu ký tự đại diện ở bất cứ nơi nào cho phép mẫu – ví dụ: trong một mẫu khai báo biến: var (title, _) = document.metadata;

Trong trường hợp này, ký tự đại diện không ràng buộc bất kỳ biến nào. Thao tác này sẽ loại bỏ trường thứ hai.

Trong phần tiếp theo, bạn sẽ tìm hiểu về các tính năng chuyển đổi khác sau khi hiển thị các đối tượng Block.

Hiển thị nội dung tài liệu

Tạo một biến cục bộ chứa danh sách đối tượng Block bằng cách gọi getBlocks() trong phương thức build của tiện ích DocumentScreen.

  1. Thay thế phương thức build hiện tại trong DocumentationScreen bằng phiên bản này:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final blocks = document.getBlocks();                           // Add this line

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $modified'),                        // Modify from here
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),                                                       // to here.
        ],
      ),
    );
  }
}

Dòng BlockWidget(block: blocks[index]) tạo một tiện ích BlockWidget cho mỗi mục trong danh sách các khối được trả về từ phương thức getBlocks().

  1. Chạy ứng dụng và bạn sẽ thấy các khối xuất hiện trên màn hình:

Ảnh chụp màn hình ứng dụng hiển thị nội dung trong &quot;các khối&quot; phần dữ liệu JSON.

10. Sử dụng biểu thức chuyển đổi

Mẫu bổ sung nhiều chức năng cho switchcase. Để có thể sử dụng chúng ở nhiều nơi hơn, Dart đã chuyển đổi biểu thức. Một loạt các trường hợp có thể trực tiếp cung cấp giá trị cho câu lệnh trả về hoặc lệnh gán biến.

Chuyển đổi câu lệnh chuyển đổi thành biểu thức chuyển đổi

Trình phân tích Dart cung cấp các tính năng hỗ trợ để giúp bạn thay đổi mã.

  1. Di chuyển con trỏ đến câu lệnh chuyển đổi từ phần trước.
  2. Nhấp vào bóng đèn để xem các trợ lý hiện có.
  3. Chọn hỗ trợ Chuyển đổi sang biểu thức chuyển đổi.

Ảnh chụp màn hình cột &quot;chuyển đổi sang biểu thức chuyển đổi&quot; có sẵn trong Mã VS.

Phiên bản mới của mã này sẽ có dạng như sau:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;                                          // Modify from here
    textStyle = switch (block.type) {
      'h1' => Theme.of(context).textTheme.displayMedium,
      'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
      _ => Theme.of(context).textTheme.bodySmall
    };                                                             // to here.

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

Biểu thức chuyển đổi trông tương tự như câu lệnh chuyển đổi, nhưng sẽ loại bỏ từ khoá case và sử dụng => để tách mẫu khỏi phần thân. Không giống như câu lệnh chuyển đổi, biểu thức chuyển đổi trả về một giá trị và có thể được sử dụng ở bất kỳ nơi nào có thể sử dụng biểu thức.

11. Sử dụng các mẫu đối tượng

Dart là ngôn ngữ hướng đối tượng, do đó, mẫu áp dụng cho mọi đối tượng. Ở bước này, bạn bật mẫu đối tượng và huỷ cấu trúc các thuộc tính đối tượng để cải thiện logic hiển thị ngày trên giao diện người dùng.

Trích xuất thuộc tính từ các mẫu đối tượng

Trong phần này, bạn cải thiện cách hiển thị ngày sửa đổi gần nhất bằng cách sử dụng các mẫu.

  • Thêm phương thức formatDate vào main.dart:

lib/main.dart

String formatDate(DateTime dateTime) {
  final today = DateTime.now();
  final difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: final days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: final days) => '$days days from now',
  };
}

Phương thức này trả về một biểu thức chuyển đổi bật giá trị difference, đối tượng Duration. Thuộc tính này thể hiện khoảng thời gian từ today đến modified trong dữ liệu JSON.

Mỗi trường hợp của biểu thức chuyển đổi sẽ sử dụng một mẫu đối tượng phù hợp bằng cách gọi phương thức getter trên các thuộc tính inDaysisNegative của đối tượng. Cú pháp có vẻ như đang tạo đối tượng Duration nhưng thực tế lại truy cập vào các trường trên đối tượng difference.

Ba trường hợp đầu tiên sử dụng các mẫu con không đổi 0, 1-1 để khớp với thuộc tính đối tượng inDays và trả về chuỗi tương ứng.

Hai trường hợp gần đây nhất xử lý khoảng thời gian vượt quá hôm nay, hôm qua và ngày mai:

  • Nếu thuộc tính isNegative khớp với mẫu hằng số booleantrue, nghĩa là ngày sửa đổi nằm trong quá khứ, thì thuộc tính đó sẽ hiển thị ngày trước.
  • Nếu trường hợp đó không xác định được sự khác biệt, thì thời hạn phải là số ngày dương (không cần xác minh rõ ràng bằng isNegative: false). Vì vậy, ngày sửa đổi là ngày trong tương lai và hiển thị số ngày kể từ bây giờ.

Thêm logic định dạng cho tuần

  • Thêm hai trường hợp mới vào hàm định dạng để xác định khoảng thời gian dài hơn 7 ngày sao cho giao diện người dùng có thể hiển thị các trường hợp đó dưới dạng tuần:

lib/main.dart

String formatDate(DateTime dateTime) {
  final today = DateTime.now();
  final difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: final days) when days > 7 => '${days ~/ 7} weeks from now', // Add from here
    Duration(inDays: final days) when days < -7 =>
      '${days.abs() ~/ 7} weeks ago',                                            // to here.
    Duration(inDays: final days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: final days) => '$days days from now',
  };
}

Mã này giới thiệu các mệnh đề bảo vệ:

  • Mệnh đề bảo vệ sử dụng từ khoá when sau mẫu trường hợp.
  • Có thể dùng các hàm này trong trường hợp if, câu lệnh chuyển đổi và biểu thức chuyển đổi.
  • Phương thức này chỉ thêm điều kiện vào mẫu sau khi mẫu được so khớp.
  • Nếu mệnh đề bảo vệ có giá trị false, thì toàn bộ mẫu sẽ bị rút lại và quá trình thực thi sẽ chuyển sang trường hợp tiếp theo.

Thêm ngày mới được định dạng vào giao diện người dùng

  1. Cuối cùng, hãy cập nhật phương thức build trong DocumentScreen để sử dụng hàm formatDate:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final formattedModifiedDate = formatDate(modified);            // Add this line
    final blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'),           // Modify this line
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. Tải lại nhanh để xem các thay đổi trong ứng dụng của bạn:

Ảnh chụp màn hình ứng dụng hiển thị chuỗi &quot;Lần sửa đổi gần đây nhất: 2 tuần trước&quot; bằng cách sử dụng hàm formatDate().

12. Đóng dấu một lớp để chuyển đổi toàn diện

Lưu ý rằng bạn không sử dụng ký tự đại diện hoặc trường hợp mặc định ở cuối nút chuyển gần đây nhất. Mặc dù nên luôn đưa vào trường hợp các giá trị có thể vượt qua, nhưng bạn cũng có thể sử dụng ví dụ đơn giản như thế này vì bạn biết các trường hợp bạn đã xác định có tính đến tất cả giá trị có thể có inDays có thể nhận.

Khi mọi trường hợp trong một nút chuyển được xử lý, quá trình này được gọi là nút chuyển toàn diện. Ví dụ: việc bật một loại bool đã hoàn tất khi gặp các trường hợp cho truefalse. Việc bật loại enum là chưa đầy đủ khi có trường hợp cho từng giá trị của enum, vì enum đại diện cho số cố định của các giá trị không đổi.

Dart 3 mở rộng phạm vi kiểm tra tính toàn diện cho các đối tượng và hệ phân cấp lớp bằng đối tượng sửa đổi lớp mới sealed. Tái cấu trúc lớp Block dưới dạng một lớp cấp cao kín.

Tạo lớp con

  • Trong data.dart, hãy tạo 3 lớp mới – HeaderBlock, ParagraphBlockCheckboxBlock – mở rộng Block:

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

Mỗi lớp này tương ứng với các giá trị type khác nhau từ JSON gốc: 'h1', 'p''checkbox'.

Ấn tượng đẳng cấp cao cấp

  • Đánh dấu lớp Blocksealed. Sau đó, hãy tái cấu trúc trường hợp if dưới dạng biểu thức chuyển đổi để trả về lớp con tương ứng với type được chỉ định trong JSON:

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

Từ khoá sealed là một đối tượng sửa đổi lớp, có nghĩa là bạn chỉ có thể mở rộng hoặc triển khai lớp này trong cùng một thư viện. Vì biết các loại phụ của lớp này, nên trình phân tích sẽ báo cáo lỗi nếu nút chuyển không bao gồm được một trong các loại phụ đó và không đầy đủ.

Sử dụng một biểu thức chuyển đổi để hiển thị tiện ích

  1. Cập nhật lớp BlockWidget trong main.dart bằng một biểu thức chuyển đổi sử dụng các mẫu đối tượng cho từng trường hợp:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:final text) => Text(
            text,
            style: Theme.of(context).textTheme.displayMedium,
          ),
        ParagraphBlock(:final text) => Text(text),
        CheckboxBlock(:final text, :final isChecked) => Row(
            children: [
              Checkbox(value: isChecked, onChanged: (_) {}),
              Text(text),
            ],
          ),
      },
    );
  }
}

Trong phiên bản đầu tiên của BlockWidget, bạn đã bật một trường của đối tượng Block để trả về một TextStyle. Bây giờ, bạn sẽ chuyển đổi một thực thể của chính đối tượng Block và so khớp với các mẫu đối tượng đại diện cho các lớp con của nó, trích xuất các thuộc tính của đối tượng trong quy trình này.

Trình phân tích Dart có thể kiểm tra để đảm bảo mỗi lớp con được xử lý trong biểu thức chuyển đổi vì bạn đã đặt Block làm lớp kín.

Ngoài ra, xin lưu ý rằng việc sử dụng biểu thức chuyển đổi ở đây cho phép bạn truyền kết quả trực tiếp đến phần tử child, thay vì câu lệnh trả về riêng biệt cần thiết trước đó.

  1. Tải lại nóng để xem dữ liệu JSON của hộp đánh dấu được kết xuất lần đầu tiên:

Ảnh chụp màn hình ứng dụng cho thấy hộp đánh dấu &quot; Learn Dart 3&quot;

13. Xin chúc mừng

Bạn đã thử nghiệm thành công với các mẫu, bản ghi, nút chuyển nâng cao và hộp đựng cũng như các lớp kín. Bạn đã đề cập đến rất nhiều thông tin, nhưng chỉ mới sử dụng được phần nổi bật của những tính năng này. Để biết thêm thông tin về mẫu, hãy xem quy cách tính năng.

Các loại mẫu khác nhau, các ngữ cảnh khác nhau mà chúng có thể xuất hiện và sự lồng ghép có thể của các mẫu con khiến khả năng hành vi dường như vô tận. Tuy nhiên, những hình ảnh đó rất dễ thấy.

Bạn có thể tưởng tượng vô số cách thức hiển thị nội dung trong Flutter bằng các mẫu. Khi sử dụng các mẫu, bạn có thể trích xuất dữ liệu một cách an toàn để xây dựng giao diện người dùng chỉ bằng vài dòng mã.

Tiếp theo là gì?

  • Hãy xem tài liệu về mẫu, bản ghi, trường hợp và nút chuyển nâng cao cũng như đối tượng sửa đổi lớp trong phần Ngôn ngữ của tài liệu về Dart.

Tài liệu tham khảo

Xem mã mẫu đầy đủ theo từng bước trong kho lưu trữ flutter/codelabs.

Để biết thông số kỹ thuật chuyên sâu cho từng tính năng mới, hãy xem các tài liệu thiết kế gốc: