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

1. Giới thiệu

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

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

Những tính năng này mở rộng các lựa chọn bạn có 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 các thành phần này để giúp mã của bạn trở nên gọn gàng, tinh 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 mình chưa nắm vững kiến thức cơ bản, hãy tham khảo 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 tạo một ứng dụng hiển thị tài liệu JSON trong Flutter. Ứng dụng này 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 đề phụ và đoạn vă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 để có thể chuyển và giải nén dữ liệu đó ở bất cứ nơi nào các tiện ích Flutter của bạn cần.

Sau đó, bạn 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 để phân tách dữ liệu thành các biến cục bộ.

Ứng dụng cuối cùng mà bạn sẽ tạo trong lớp học lập trình này là 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 một bản ghi lưu trữ nhiều giá trị thuộc nhiều loại.
  • Cách trả về nhiều giá trị từ một hàm bằng cách sử dụng bản ghi.
  • Cách sử dụng mẫu để so khớp, xác thực và phân tách dữ liệu từ các bản ghi và đối tượng khác.
  • Cách liên kết các giá trị khớp mẫu với các biến mới hoặc hiện có.
  • Cách sử dụng các chức năng mới của câu lệnh switch, biểu thức switch và câu lệnh if-case.
  • Cách tận dụng kiểm tra tính đầy đủ để đảm bảo rằng mọi trường hợp đều được xử lý trong câu lệnh switch hoặc biểu thức switch.

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 nhắm 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 tìm hiểu về 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 mà bạn sẽ viết tất cả 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 tiêu chuẩn trong tệp lib/main.dart mà bạn vẫn phải xoá.
flutter create --empty patterns_codelab
  1. Sau đó, mở thư mục patterns_codelab bằng VS Code.
code patterns_codelab

VS Code hiển thị dự án đã tạo

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

  • Đặt điều kiện ràng buộc về phiên bản SDK cho 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 sẽ tạo hoặc cập nhật 2 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, rồi thêm đoạn 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ừ mộ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 đến bằng 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 xác định và khởi tạo trường _json trong hàm khởi tạo của lớp.

Chạy ứng dụng

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

  1. Để tạo điểm xuất phát 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(),
      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 2 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 để tạo giao diện cho giao diện người dùng.
  • DocumentScreen cung cấp bố cục trực quan của trang bằng cách sử dụng tiện ích Scaffold.
  1. Để đảm bảo mọi thứ đều chạy trơn tru, hãy chạy ứng dụng trên máy chủ lưu trữ bằng cách nhấp vào Chạy và gỡ lỗi:

Nút &quot;Chạy và gỡ lỗi&quot;

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

Bộ chọn nền tảng nhắm mục tiêu trong VS Code

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

Ứng dụng được tạo trong bước này.

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

Trong bước này, bạn sẽ 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ào các giá trị và phản ánh chúng 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 Document có tên là metadata để 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.
}

Kiểu dữ liệu trả về cho hàm này là một bản ghi có 2 trường, một trường có kiểu String và trường còn lại có kiểu 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à trường vị trí và không có tên, còn trường thứ hai có tên là modified.

Truy cập vào các trường của 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: [                                        // And the following line.
          Center(child: Text('Last modified ${metadataRecord.modified}')),
        ],
      ),
    );
  }
}

Phương thức getter metadata trả về một bản ghi được chỉ định 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 duy nhất và chỉ định các giá trị đó cho một biến.

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

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

Để xác định tên của một phương thức getter cho trường vị trí, hãy bắt đầu từ $1 và bỏ qua các trường có 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 xuất hiện trong ứng dụng. Trình bổ trợ Dart của VS Code sẽ tải lại nhanh mỗi khi bạn lưu một tệp.

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

Bạn có thể thấy rằng mỗi trường thực sự vẫn giữ nguyên kiểu dữ liệu.

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

Cách khác để trả về các loại dữ liệu khác nhau một cách an toàn về kiểu là xác định một lớp. Cách này sẽ dài dòng hơn.

6. So khớp và phân tách bằng 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 dữ liệu đó. Giờ đây, hãy cải thiện mã của bạn bằng cách sử dụng các mẫu.

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

Khi khớp, một số mẫu sẽ phân tách cấu trúc giá trị đã khớp bằng cách kéo dữ liệu ra khỏi giá trị đó. Phân rã cho phép bạn giải nén các giá trị từ một đối tượng để gán chúng cho các biến cục bộ hoặc thực hiện thêm thao tác so khớp trên các giá trị đó.

Phân tách 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à dùng phương thức này để khởi tạo một 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 from here...
      body: Column(children: [Center(child: Text('Last modified $modified'))]),
    );                                                       // To here.
  }
}

Mẫu bản ghi (title, modified: modified) chứa hai 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 này khớp với mẫu con vì kết quả là một bản ghi có hai trường, trong đó có một trường có tên là modified.
  • Vì chúng khớp nhau, mẫu khai báo biến sẽ phân tách biểu thức, truy cập vào các giá trị của biểu thức và liên kết các giá trị đó với các biến cục bộ mới có cùng kiểu và tên, String titleDateTime modified.

Có một 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 có một biến cục bộ mới với tên khác, bạn có thể viết modified: localModified.

  1. Tải lại nóng để 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 chỉ cần làm cho mã của mình ngắn gọn hơn.

7. Sử dụng 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à phân tách cấu trúc mà còn có thể đưa ra quyết định về những việc mà mã sẽ làm, dựa trên việc mẫu có khớp hay không. Đây được gọi là mẫu có thể bác bỏ.

Mẫu khai báo biến mà bạn đã dùng ở bước cuối cùng là một mẫu không thể bác bỏ: giá trị phải khớp với mẫu hoặc đó là một lỗi và quá trình phân tách sẽ không xảy ra. Hãy nghĩ đến mọi khai báo hoặc chỉ định biến; bạn không thể chỉ định giá trị cho một biến nếu chúng không cùng kiểu.

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

  • Họ dự kiến rằng một số giá trị mà họ so sánh sẽ không khớp.
  • Mục đích của chúng là ảnh hưởng đến luồng điều khiển, dựa trên 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 bằng một lỗi nếu không khớp, chúng 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ỉ dùng được khi chúng khớp

Đọc các giá trị JSON 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 làm việc với dữ liệu JSON.

  • Thay thế phiên bản trước của metadata bằng phiên bản đọc các giá trị từ bản đồ _json. Sao chép và dán phiên bản metadata này 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 sử dụng mẫu. Trong một bước sau, bạn sẽ sử dụng tính năng so khớp mẫu để thực hiện quy trình xác thực tương tự bằng ít mã hơn. Thư viện này thực hiện 3 bước kiểm tra trước khi làm bất cứ việc gì khác:

  • Tệp 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 mà bạn mong đợi: if (metadataJson is Map)
  • Dữ liệu không phải là giá trị rỗng, được xác nhận ngầm trong bước kiểm tra trước đó.

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

Với một mẫu có thể bác bỏ, 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 đoạn mã sau:

lib/data.dart

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

  (String, {DateTime modified}) get metadata {
    if (_json case {                                         // Modify from here...
      '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 (ra mắt trong Dart 3), đó là if-case. Phần nội dung của 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. Hoạt động 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 metadata đầu tiên để xác thực JSON đến. Mã này xác thực những điều sau:

  • _json là một loại Bản đồ.
  • _json chứa một 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 có giá trị rỗng.

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

Để xem danh sách đầy đủ các mẫu, hãy xem bảng trong phần Mẫu của quy cách tính năng.

8. Chuẩn bị ứng dụng cho nhiều mẫu hơn

Cho đến nay, bạn đã giải quyết phần metadata của dữ liệu JSON. Trong 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à hiển thị dữ liệu đó trong ứ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, Block, vào data.dart. Lớp này được 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 với mẫu bản đồ mà bạn đã thấy trước đây.

Bạn sẽ thấy dữ liệu JSON có dạng như mẫu dự kiến, mặc dù có thêm một phần thông tin gọi là checked không có trong mẫu. Điều này là do khi bạn sử dụng những loại mẫu này (gọi là "mẫu bản đồ"), chúng chỉ quan tâm đến những thứ cụ thể mà bạn đã xác định trong mẫu và bỏ qua mọi thứ khác trong dữ liệu.

Trả về danh sách các đối tượng Block

  • Tiếp theo, hãy thêm một hàm mới (getBlocks()) vào lớp Document. getBlocks() phân tích cú pháp JSON thành các phiên bản 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:

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ề một danh sách các đối tượng Block. Bạn sẽ sử dụng danh sách này sau để tạo giao diện người dùng. Một câu lệnh if-case quen thuộc sẽ thực hiện quy trình 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 (nếu không có mẫu, bạn sẽ cần phương thức toList() để truyền).

Chuỗi ký tự danh sách chứa một collection for để điền vào danh sách mới bằng các đối tượng Block.

Phần này không giới thiệu bất kỳ tính năng nào 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 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 mẫu để hiển thị tài liệu

Giờ đây, bạn đã phân tách 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ể bác bỏ. Nhưng if-case chỉ là một trong những điểm cải tiến đối với cấu trúc luồng điều khiển đi kèm với các mẫu. Bây giờ, bạn sẽ áp dụng kiến thức về các mẫu có thể bác bỏ cho câu lệnh switch.

Kiểm soát nội dung được kết xuất 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, BlockWidget, để xác định kiểu của từng khối dựa trên trường type.

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 switch 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ột mẫu chuỗi hằng. Mẫu sẽ khớp nếu block.type bằng với giá trị hằng số h1.
  2. Câu lệnh case thứ hai sử dụng mẫu logical-or với 2 mẫu chuỗi hằng số làm mẫu con. Mẫu này sẽ khớp nếu block.type khớp với một trong hai mẫu con 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 các trường hợp chuyển đổi sẽ khớp với mọi trường hợp khác. Chúng hoạt động giống như các mệnh đề default, vẫn được phép dùng trong câu lệnh switch (chỉ là chúng 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 được phép dùng mẫu, chẳng hạn như trong mẫu khai báo biến: var (title, _) = document.metadata;

Trong ngữ cảnh này, ký tự đại diện không liên kết với 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 thêm về các tính năng của thành phần chuyển đổi 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 các đố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 có 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 từng 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, sau đó bạn sẽ thấy các khối xuất hiện trên màn hình:

Ứng dụng hiển thị nội dung trong phần &quot;blocks&quot; của dữ liệu JSON.

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

Các 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 có biểu thức switch. Một loạt các trường hợp có thể cung cấp giá trị trực tiếp cho câu lệnh gán biến hoặc câu lệnh trả về.

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

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

  1. Di chuyển con trỏ đến câu lệnh switch trong phần trước.
  2. Nhấp vào biểu tượng bóng đèn để xem các lượt hỗ trợ có sẵn.
  3. Chọn tính năng hỗ trợ Convert to switch expression (Chuyển đổi thành biểu thức chuyển đổi).

Tính năng hỗ trợ &quot;chuyển đổi thành biểu thức switch&quot; có trong VS Code.

Phiên bản mới của mã này 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 switch trông tương tự như câu lệnh switch, nhưng biểu thức này loại bỏ từ khoá case và sử dụng => để tách mẫu khỏi phần nội dung trường hợp. Không giống như câu lệnh switch, biểu thức switch trả về một giá trị và có thể được dùng ở bất kỳ vị trí nào có thể dùng biểu thức.

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

Dart là một ngôn ngữ hướng đối tượng, vì vậy các mẫu áp dụng cho tất cả các đối tượng. Trong bước này, bạn sẽ bật một mẫu đối tượng và phân tách các thuộc tính đối tượng để cải thiện logic kết xuất ngày của giao diện người dùng.

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

Trong phần này, bạn sẽ 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 dựa trên giá trị difference, một đối tượng Duration. Đại diện cho khoảng thời gian giữa today và giá trị modified trong dữ liệu JSON.

Mỗi trường hợp của biểu thức switch đều sử dụng một mẫu đối tượng khớp bằng cách gọi các phương thức truy xuất trên các thuộc tính inDaysisNegative của đối tượng. Cú pháp này có vẻ như đang tạo một đối tượng Duration, nhưng thực ra lại đang 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 hằng số 0, 1-1 để so khớp thuộc tính đối tượng inDays và trả về chuỗi tương ứng.

Hai trường hợp cuối cùng xử lý khoảng thời gian ngoài 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ố boolean true, tức là ngày sửa đổi đã qua, thì thuộc tính này sẽ hiển thị ngày trước.
  • Nếu trường hợp đó không nắm bắt được sự khác biệt, thì thời lượng phải là một số ngày dương (không cần xác minh rõ ràng bằng isNegative: false), do đó, ngày sửa đổi sẽ là ngày trong tương lai và hiển thị ngày kể từ bây giờ.

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

  • Thêm 2 trường hợp mới vào hàm định dạng để xác định những khoảng thời gian dài hơn 7 ngày, nhờ đó giao diện người dùng có thể hiển thị các khoảng thời gian đó 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',
  };
}

Đoạn mã này giới thiệu mệnh đề bảo vệ:

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

Thêm ngày mới đị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 để 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:

Ứng dụng hiển thị chuỗi &quot;Sửa đổi lần gần nhất: 2 tuần trước&quot; bằng hàm formatDate().

12. Niêm phong 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 công tắc cuối cùng. Mặc dù bạn nên luôn thêm một trường hợp cho các giá trị có thể rơi vào, nhưng trong một ví dụ đơn giản như thế này, bạn có thể không cần làm vậy vì bạn biết các trường hợp mà bạn xác định sẽ tính đến tất cả các giá trị có thể inDays có thể nhận.

Khi mọi trường hợp trong một câu lệnh switch được xử lý, đó được gọi là câu lệnh switch đầy đủ. Ví dụ: việc bật một loại bool là toàn diện khi loại đó có các trường hợp cho truefalse. Việc bật loại enum cũng là một quá trình toàn diện khi có các trường hợp cho từng giá trị của enum, vì enum biểu thị một số lượng cố định các giá trị hằng số.

Dart 3 mở rộng quy trình kiểm tra tính đầy đủ cho các đối tượng và hệ thống 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 của bạn dưới dạng một siêu lớp kín.

Tạo các lớp con

  • Trong data.dart, hãy tạo 3 lớp mới là 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 trong số này tương ứng với các giá trị type khác nhau trong JSON ban đầu: 'h1', 'p''checkbox'.

Niêm phong siêu lớ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 một biểu thức switch 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 trình sửa đổi lớp, 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ì trình phân tích biết các kiểu phụ của lớp này, nên trình phân tích sẽ báo cáo lỗi nếu một câu lệnh switch không bao gồm một trong các kiểu phụ và không đầy đủ.

Sử dụng biểu thức chuyển đổi để hiển thị các 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 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ề TextStyle. Bây giờ, bạn chuyển đổi một thực thể của chính đối tượng Block và so khớp với mẫu đối tượng đại diện cho các lớp con của đối tượng đó, trích xuất các thuộc tính của đối tượng trong quá trình này.

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

Ngoài ra, hãy lưu ý rằng việc sử dụng biểu thức switch ở đây cho phép bạn truyền trực tiếp kết quả đế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 nhanh để xem dữ liệu JSON của hộp đánh dấu được kết xuất lần đầu tiên:

Ứng dụng hiển thị hộp đánh dấu &quot;Tìm hiểu 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, câu lệnh switch và case nâng cao, 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 khám phá được một phần nhỏ trong số các tính năng này. Để biết thêm thông tin về các mẫu, hãy xem quy cách tính năng.

Các loại mẫu khác nhau, các bối cảnh khác nhau mà chúng có thể xuất hiện và khả năng lồng ghép các mẫu con khiến cho các khả năng về hành vi dường như là vô tận. Nhưng chúng rất dễ thấy.

Bạn có thể hình dung mọi cách để hiển thị nội dung trong Flutter bằng cách sử dụng các mẫu. Bằng cách 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 để tạo 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ề các mẫu, bản ghi, câu lệnh switch và case 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 Dart.

Tài liệu tham khảo

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

Để biết thông số kỹ thuật chi tiết cho từng tính năng mới, hãy xem tài liệu thiết kế ban đầu: