Flutter MDC-102: Cấu trúc và bố cục vật liệu

1. Giới thiệu

logo_components_color_2x_web_96dp.png

Thành phần Material (MDC) giúp nhà phát triển triển khai Material Design. Được tạo bởi một nhóm kỹ sư và nhà thiết kế trải nghiệm người dùng tại Google, MDC có hàng chục thành phần giao diện người dùng đẹp mắt và có chức năng, đồng thời có sẵn cho Android, iOS, web và Flutter.material.io/develop

Trong lớp học lập trình MDC-101, bạn đã dùng 2 Thành phần Material để tạo một trang đăng nhập: trường văn bản và nút có hiệu ứng gợn mực. Bây giờ, hãy mở rộng nền tảng này bằng cách thêm thành phần điều hướng, cấu trúc và dữ liệu.

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

Trong lớp học lập trình này, bạn sẽ tạo một màn hình chính cho ứng dụng Shrine. Đây là một ứng dụng thương mại điện tử bán quần áo và đồ gia dụng. Thư mục này sẽ chứa:

  • Thanh ứng dụng trên cùng
  • Danh sách dạng lưới chứa đầy sản phẩm

Android

iOS

ứng dụng thương mại điện tử có thanh ứng dụng trên cùng và một lưới chứa đầy sản phẩm

ứng dụng thương mại điện tử có thanh ứng dụng trên cùng và một lưới chứa đầy sản phẩm

Các thành phần và hệ thống con Material Flutter trong lớp học lập trình này

  • Thanh ứng dụng trên cùng
  • Lưới
  • Thẻ

Bạn đánh giá thế nào về mức độ kinh nghiệm của mình trong việc phát triển bằng Flutter?

Người mới bắt đầu Trung cấp Thành thạo

2. Thiết lập môi trường phát triển Flutter

Bạn cần có 2 phần mềm để hoàn thành bài thực hành này: Flutter SDKmột trình chỉnh sửa.

Bạn có thể chạy lớp học lập trình này bằng bất kỳ thiết bị nào sau đây:

  • Một thiết bị Android hoặc iOS thực được kết nối với máy tính và được đặt ở Chế độ nhà phát triển.
  • Trình mô phỏng iOS (bạn cần cài đặt các công cụ Xcode).
  • Trình mô phỏng Android (cần thiết lập trong Android Studio).
  • Một trình duyệt (bạn cần có Chrome để gỡ lỗi).
  • Dưới dạng ứng dụng máy tính cho Windows, Linux hoặc macOS. Bạn phải phát triển trên nền tảng mà bạn dự định triển khai. Vì vậy, nếu muốn phát triển một ứng dụng máy tính cho Windows, bạn phải phát triển trên Windows để truy cập vào chuỗi bản dựng thích hợp. Có những yêu cầu cụ thể theo hệ điều hành được đề cập chi tiết trên docs.flutter.dev/desktop.

3. Tải ứng dụng khởi đầu của lớp học lập trình xuống

Bạn đang tiếp tục từ MDC-101?

Nếu bạn đã hoàn thành MDC-101, thì mã của bạn sẽ được chuẩn bị cho lớp học lập trình này. Chuyển đến bước: Thêm thanh ứng dụng trên cùng.

Bạn muốn bắt đầu từ đầu?

Tải ứng dụng khởi đầu của lớp học lập trình xuống

Ứng dụng khởi đầu nằm trong thư mục material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series.

...hoặc sao chép từ GitHub

Để sao chép lớp học lập trình này từ GitHub, hãy chạy các lệnh sau:

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 102-starter_and_101-complete

Mở dự án và chạy ứng dụng

  1. Mở dự án trong trình chỉnh sửa mà bạn chọn.
  2. Làm theo hướng dẫn để "Chạy ứng dụng" trong phần Bắt đầu: Lái thử cho trình chỉnh sửa bạn chọn.

Thành công! Bạn sẽ thấy trang đăng nhập Shrine trong lớp học lập trình MDC-101 trên thiết bị của mình.

Android

iOS

trang đăng nhập có các trường tên người dùng và mật khẩu, nút huỷ và tiếp theo

trang đăng nhập có các trường tên người dùng và mật khẩu, nút huỷ và tiếp theo

Bây giờ, màn hình đăng nhập đã trông ổn, hãy điền một số sản phẩm vào ứng dụng.

4. Thêm thanh ứng dụng trên cùng

Ngay bây giờ, nếu nhấp vào nút "Tiếp theo", bạn sẽ thấy màn hình chính có nội dung "Bạn đã làm được!". Thật tuyệt! Nhưng giờ đây, người dùng không có hành động nào để thực hiện hoặc không biết họ đang ở đâu trong ứng dụng. Để trợ giúp, đã đến lúc thêm thành phần điều hướng.

Material Design cung cấp các mẫu điều hướng giúp đảm bảo khả năng sử dụng ở mức độ cao. Một trong những thành phần dễ thấy nhất là thanh ứng dụng trên cùng.

Để cung cấp chế độ điều hướng và giúp người dùng truy cập nhanh vào các thao tác khác, hãy thêm một thanh ứng dụng trên cùng.

Thêm tiện ích AppBar

Trong home.dart, hãy thêm AppBar vào Scaffold và xoá const được đánh dấu:

return const Scaffold(
  // TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
  ),

Việc thêm AppBar vào trường appBar: của Scaffold sẽ giúp chúng ta có được bố cục hoàn hảo mà không tốn phí, giữ AppBar ở đầu trang và nội dung ở bên dưới.

Thêm tiện ích Văn bản

Trong home.dart, hãy thêm tiêu đề vào AppBar:

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: const Text('SHRINE'),
    // TODO: Add trailing buttons (102)

Lưu dự án.

Android

iOS

thanh ứng dụng có Shrine làm tiêu đề

thanh ứng dụng có Shrine làm tiêu đề

Nhiều thanh ứng dụng có một nút bên cạnh tiêu đề. Hãy thêm một biểu tượng trình đơn vào ứng dụng của chúng ta.

Thêm một IconButton hàng đầu

Trong khi vẫn ở home.dart, hãy đặt một IconButton cho trường leading: của AppBar. (Đặt trước trường title: để mô phỏng thứ tự từ đầu đến cuối):

    // TODO: Add buttons and title (102)
    leading: IconButton(
      icon: const Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

Lưu dự án.

Android

iOS

thanh ứng dụng có tiêu đề là Shrine và biểu tượng trình đơn ba đường kẻ

thanh ứng dụng có tiêu đề là Shrine và biểu tượng trình đơn ba đường kẻ

Biểu tượng trình đơn (còn gọi là "nút hamburger") xuất hiện đúng vị trí mà bạn mong đợi.

Bạn cũng có thể thêm các nút vào phía sau tiêu đề. Trong Flutter, những thao tác này được gọi là "actions" (hành động).

Thêm hành động

Bạn có thể thêm 2 IconButton nữa.

Thêm chúng vào thực thể AppBar sau tiêu đề:

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: const Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: const Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

Lưu dự án. Màn hình chính của bạn sẽ có dạng như sau:

Android

iOS

thanh ứng dụng có tiêu đề là Shrine và biểu tượng trình đơn ba đường kẻ, cùng các biểu tượng tìm kiếm và tuỳ chỉnh ở cuối

thanh ứng dụng có tiêu đề là Shrine và biểu tượng trình đơn ba đường kẻ, cùng các biểu tượng tìm kiếm và tuỳ chỉnh ở cuối

Giờ đây, ứng dụng có một nút chính, một tiêu đề và hai thao tác ở bên phải. Thanh ứng dụng cũng hiển thị độ nâng bằng cách dùng một bóng mờ tinh tế cho thấy thanh ứng dụng nằm trên một lớp khác so với nội dung.

5. Thêm thẻ vào lưới

Giờ đây, khi ứng dụng của chúng ta đã có một số cấu trúc, hãy sắp xếp nội dung bằng cách đặt nội dung đó vào các thẻ.

Thêm GridView

Hãy bắt đầu bằng cách thêm một thẻ bên dưới thanh ứng dụng trên cùng. Bản thân tiện ích Card không có đủ thông tin để bố trí chính nó ở nơi chúng ta có thể nhìn thấy, vì vậy, chúng ta sẽ muốn đóng gói tiện ích này trong một tiện ích GridView.

Thay thế Center trong nội dung của Scaffold bằng GridView:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: Build a grid of cards (102)
  children: <Widget>[Card()],
),

Hãy cùng xem xét mã đó. GridView gọi hàm khởi tạo count() vì số lượng mục mà nó hiển thị là có thể đếm được và không phải là vô hạn. Tuy nhiên, nó cần thêm thông tin để xác định bố cục.

crossAxisCount: chỉ định số lượng mục trên. Chúng ta muốn có 2 cột.

Trường padding: cung cấp khoảng trống ở cả 4 phía của GridView. Tất nhiên, bạn không thể thấy khoảng đệm ở các cạnh dưới cùng hoặc bên phải vì chưa có thành phần con GridView nào bên cạnh các cạnh đó.

Trường childAspectRatio: xác định kích thước của các mặt hàng dựa trên tỷ lệ khung hình (chiều rộng trên chiều cao).

Theo mặc định, GridView tạo các ô có cùng kích thước.

Chúng tôi có một thẻ nhưng thẻ đó trống. Hãy thêm các tiện ích con vào thẻ của chúng ta.

Bố trí nội dung

Thẻ phải có các vùng dành cho hình ảnh, tiêu đề và văn bản phụ.

Cập nhật các thành phần con của GridView:

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    clipBehavior: Clip.antiAlias,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              const SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

Đoạn mã này thêm một tiện ích Cột dùng để bố trí các tiện ích con theo chiều dọc.

crossAxisAlignment: field chỉ định CrossAxisAlignment.start, tức là "căn chỉnh văn bản theo cạnh trên".

Tiện ích AspectRatio quyết định hình dạng của hình ảnh bất kể loại hình ảnh nào được cung cấp.

Phần đệm sẽ đưa văn bản vào từ bên cạnh một chút.

Hai tiện ích Text được xếp chồng theo chiều dọc với 8 điểm trống ở giữa (SizedBox). Chúng ta tạo một Column khác để đặt các thành phần này vào bên trong Padding.

Lưu dự án.

Android

iOS

một mục duy nhất có hình ảnh, tiêu đề và văn bản phụ

một mục duy nhất có hình ảnh, tiêu đề và văn bản phụ

Trong bản xem trước này, bạn có thể thấy thẻ được lồng vào từ cạnh, có các góc bo tròn và đổ bóng (thể hiện độ nâng của thẻ). Toàn bộ hình dạng này được gọi là "vùng chứa" trong Material. (Đừng nhầm lẫn với lớp tiện ích thực tế có tên là Container.)

Thẻ thường xuất hiện trong một bộ sưu tập cùng với các thẻ khác. Hãy bố trí chúng dưới dạng một bộ sưu tập trong lưới.

6. Tạo bộ sưu tập thẻ

Bất cứ khi nào có nhiều thẻ trên một màn hình, các thẻ đó sẽ được nhóm lại thành một hoặc nhiều bộ sưu tập. Các thẻ trong một bộ sưu tập nằm trên cùng một mặt phẳng, tức là các thẻ có cùng độ cao khi ở trạng thái nghỉ (trừ phi các thẻ được nhấc lên hoặc kéo, nhưng chúng ta sẽ không làm điều đó ở đây).

Nhân bản thẻ thành một bộ sưu tập

Hiện tại, Thẻ của chúng ta được tạo trong trường children: của GridView. Đó là rất nhiều mã lồng nhau có thể khó đọc. Hãy trích xuất nó thành một hàm có thể tạo ra bao nhiêu thẻ trống tuỳ ý và trả về một danh sách các Thẻ.

Tạo một hàm riêng tư mới phía trên hàm build() (hãy nhớ rằng các hàm bắt đầu bằng dấu gạch dưới là API riêng tư):

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) {
      return Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            AspectRatio(
              aspectRatio: 18.0 / 11.0,
              child: Image.asset('assets/diamond.png'),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const <Widget>[
                  Text('Title'),
                  SizedBox(height: 8.0),
                  Text('Secondary Text'),
                ],
              ),
            ),
          ],
        ),
      );
    },
  );
  return cards;
}

Chỉ định các thẻ đã tạo cho trường children của GridView. Nhớ thay thế mọi nội dung trong GridView bằng mã mới này:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // Replace
),

Lưu dự án.

Android

iOS

lưới các mục có hình ảnh, tiêu đề và văn bản phụ

lưới các mục có hình ảnh, tiêu đề và văn bản phụ

Các thẻ có xuất hiện nhưng chưa hiển thị nội dung nào. Bây giờ là thời điểm thích hợp để thêm dữ liệu sản phẩm.

Thêm dữ liệu sản phẩm

Ứng dụng có một số sản phẩm kèm theo hình ảnh, tên và giá. Hãy thêm tiện ích đó vào các tiện ích mà chúng ta đã có trong thẻ

Sau đó, trong home.dart, hãy nhập một gói mới và một số tệp mà chúng tôi cung cấp cho một mô hình dữ liệu:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/product.dart';
import 'model/products_repository.dart';

Cuối cùng, hãy thay đổi _buildGridCards() để tìm nạp thông tin sản phẩm và sử dụng dữ liệu đó trong thẻ:

// TODO: Make a collection of cards (102)

// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      clipBehavior: Clip.antiAlias,
      // TODO: Adjust card heights (103)
      child: Column(
        // TODO: Center items on the card (103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: Adjust the box size (102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: Align labels to the bottom and center (103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: Change innermost Column (103)
                children: <Widget>[
                 // TODO: Handle overflowing labels (103)
                 Text(
                    product.name,
                    style: theme.textTheme.titleLarge,
                    maxLines: 1,
                  ),
                  const SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.titleSmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

LƯU Ý: Chưa biên dịch và chạy. Chúng tôi có một thay đổi nữa.

Ngoài ra, hãy thay đổi hàm build() để truyền BuildContext đến _buildGridCards() trước khi bạn cố gắng biên dịch:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

Khởi động lại ứng dụng.

Android

iOS

một lưới các mặt hàng có hình ảnh, tiêu đề sản phẩm và giá

một lưới các mặt hàng có hình ảnh, tiêu đề sản phẩm và giá

Bạn có thể nhận thấy chúng tôi không thêm khoảng trống theo chiều dọc giữa các thẻ. Nguyên nhân là do theo mặc định, các thành phần này có 4 điểm lề ở trên cùng và dưới cùng.

Lưu dự án.

Dữ liệu sản phẩm xuất hiện nhưng hình ảnh có thêm khoảng trống xung quanh. Theo mặc định, các hình ảnh được vẽ bằng BoxFit có kích thước .scaleDown (trong trường hợp này). Hãy thay đổi thành .fitWidth để các nút này phóng to một chút và loại bỏ khoảng trắng thừa.

Thêm một trường fit: vào hình ảnh có giá trị BoxFit.fitWidth:

  // TODO: Adjust the box size (102)
  fit: BoxFit.fitWidth,

Android

iOS

một lưới các mặt hàng có hình ảnh bị cắt, tiêu đề sản phẩm và giá

Các sản phẩm của chúng tôi hiện đang xuất hiện hoàn hảo trong ứng dụng!

7. Xin chúc mừng!

Ứng dụng của chúng tôi có một quy trình cơ bản đưa người dùng từ màn hình đăng nhập đến màn hình chính, nơi họ có thể xem các sản phẩm. Chỉ với một vài dòng mã, chúng ta đã thêm một thanh ứng dụng trên cùng (có tiêu đề và 3 nút) và các thẻ (để trình bày nội dung của ứng dụng). Giờ đây, màn hình chính của chúng tôi đã trở nên đơn giản và hữu ích, với cấu trúc cơ bản và nội dung có thể thực hiện.

Các bước tiếp theo

Với thanh ứng dụng trên cùng, thẻ, trường văn bản và nút, chúng ta đã sử dụng 4 thành phần cốt lõi trong thư viện Material Flutter! Bạn có thể khám phá thêm bằng cách truy cập vào Danh mục tiện ích thành phần Material.

Mặc dù hoạt động đầy đủ, nhưng ứng dụng của chúng tôi chưa thể hiện bất kỳ thương hiệu hoặc quan điểm cụ thể nào. Trong MDC-103: Tuỳ chỉnh giao diện Material Design bằng màu sắc, hình dạng, độ cao và kiểu chữ, chúng ta sẽ tuỳ chỉnh kiểu của các thành phần này để thể hiện một thương hiệu hiện đại, sống động.

Tôi đã hoàn thành lớp học lập trình này trong một khoảng thời gian hợp lý và không tốn nhiều công sức

Hoàn toàn đồng ý Đồng ý Trung lập Không đồng ý Hoàn toàn không đồng ý

Tôi muốn tiếp tục sử dụng Thành phần Material trong tương lai

Hoàn toàn đồng ý Đồng ý Không đồng ý cũng không phản đối Không đồng ý Hoàn toàn không đồng ý