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 các kỹ sư và nhà thiết kế trải nghiệm người dùng tại Google, MDC có nhiều thành phần giao diện người dùng đẹp mắt, dễ sử dụng và 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 đã sử dụng 2 Thành phần Material để xây dựng trang đăng nhập: các trường văn bản và nút có gợn sóng của 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ẽ xây dựng màn hình chính cho ứng dụng Shrine, một ứng dụng thương mại điện tử bán quần áo và đồ gia dụng. Bản tóm tắt sẽ chứa:

  • Thanh ứng dụng trên cùng
  • Một danh sách lưới gồm nhiều 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 gồm nhiều 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 gồm nhiều 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 khi phát triển Flutter?

Người mới tập 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 phòng thí nghiệm này – Flutter SDKtrình chỉnh sửa.

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

  • Một thiết bị Android hoặc iOS thực kết nối với máy tính của bạn và được đặt ở Chế độ nhà phát triển.
  • Trình mô phỏng iOS (yêu cầu cài đặt công cụ Xcode).
  • Trình mô phỏng Android (yêu cầu thiết lập trong Android Studio).
  • Trình duyệt (cần có Chrome để gỡ lỗi).
  • Dưới dạng ứng dụng 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 Windows dành cho máy tính, bạn phải phát triển trên Windows để truy cập vào chuỗi bản dựng phù hợp. Có các 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

Tiếp tục từ MDC-101?

Nếu đã hoàn thành MDC-101, bạn sẽ cần chuẩn bị mã nguồn 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ắt đầu từ đầu?

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

Ứ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ệp trên 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: Dùng thử cho trình chỉnh sửa mà bạn đã chọn.

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

Android

iOS

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

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

Giờ đây, khi màn hình đăng nhập đã ổ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 bạn nhấp vào nút "Tiếp theo" bạn sẽ thấy màn hình chính cho biết "Bạn đã làm được!". Thật tuyệt! Nhưng giờ đây, người dùng không cần làm gì hoặc không biết vị trí của họ trong ứng dụng. Để khắc phục điều này, đã đến lúc thêm tính năng điều hướng.

Material Design cung cấp các mẫu điều hướng đảm bảo mức độ hữu dụng 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 khả năng đ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 một 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)
  ),

Thêm AppBar vào trường appBar: của Scaffold, cung cấp cho chúng ta một bố cục hoàn hảo miễn phí, giữ AppBar ở đầu trang và phần 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ó tiêu đề của Đền

thanh ứng dụng có tiêu đề của Đền

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 iconButton ở đầu

Khi vẫn ở trong home.dart, hãy đặt iconButton cho trường leading: của AppBar. (Đặt trước trường title: để bắt chước 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

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

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

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

Bạn cũng có thể thêm các nút vào phần sau của tiêu đề. Trong Flutter, chúng được gọi là "hành động".

Thêm thao tác

Còn chỗ cho hai Nút Biểu tượng 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

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

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

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

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

Ứng dụng của chúng ta hiện đã có cấu trúc nhất định, hãy sắp xếp nội dung bằng cách đưa 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. Chỉ riêng tiện ích Thẻ là chưa có đủ thông tin để bố trí ở nơi chúng ta có thể nhìn thấy, vì vậy chúng ta sẽ gói gọn tiện ích này vào tiện ích GridView.

Thay thế Center (Trung tâm) trong phần thân 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 giải nén mã đó. GridView gọi hàm khởi tạo count() vì số lượng mục mà nó hiển thị có thể đếm được và không phải là vô hạn. Nhưng cần thêm thông tin để xác định bố cục.

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

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

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

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

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

Bố cục nội dung

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

Cập nhật 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'),
            ],
          ),
        ),
      ],
    ),
  )
],

Mã này sẽ thêm tiện ích Cột được dùng để bố trí các tiện ích con theo chiều dọc.

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

Tiện ích AspectRatio sẽ quyết định hình ảnh hiển thị bất kể loại hình ảnh được cung cấp.

Khoảng đệm đư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 chúng (SizedBox). Chúng ta tạo một Cột khác để đưa các cột đó vào trong Khoảng đệm.

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 ghép từ cạnh, với các góc bo tròn và bóng (thể hiện độ cao của thẻ). Toàn bộ hình dạng đượ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 gọi là Vùng chứa.)

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 với nhau thành một hoặc nhiều bộ sưu tập. Các thẻ trong bộ sưu tập là các thẻ đồng phẳng, có nghĩa là các thẻ có cùng độ cao khi nghỉ ngơi với nhau (trừ phi các thẻ được chọn hoặc kéo, nhưng chúng ta sẽ không làm việc đó ở đây).

Nhân thẻ vào một bộ sưu tập

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

Tạo một hàm riêng tư mới phía trên hàm build() (lưu ý 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. Hãy nhớ thay thế mọi thứ có 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

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

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

Các thẻ đã có nhưng chưa hiện nội dung nào. Giờ là lúc bạn có thể 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 hình ảnh, tên và giá. Hãy thêm phương thức đó vào các tiện ích 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ô 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 các 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 được. Chúng tôi có một thay đổi nữa.

Ngoài ra, hãy thay đổi hàm build() để truyền BuildContext vào _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 bằng cách nóng.

Android

iOS

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

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

Bạn có thể nhận thấy rằng chúng tôi không thêm bất kỳ khoảng trống theo chiều dọc nào giữa các thẻ. Lý do là theo mặc định, khoảng cách giữa trên và dưới cùng là 4 điểm.

Lưu dự án.

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

Thêm 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 mặt hàng có hình ảnh, tiêu đề sản phẩm và giá bị cắt

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

7. Xin chúc mừng!

Ứng dụng của chúng ta 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 sản phẩm. Chỉ trong vài dòng mã, chúng ta đã thêm thanh ứng dụng trên cùng (có tiêu đề và 3 nút) và thẻ (để trình bày nội dung ứng dụng). Màn hình chính của chúng ta hiện đã đơn giản và dễ sử dụng với cấu trúc cơ bản và nội dung dễ thao tác.

Các bước tiếp theo

Với thanh ứng dụng, thẻ, trường văn bản và nút trên cùng, chúng ta hiện đã 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 Danh mục tiện ích thành phần Material.

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

Tôi đã có thể hoàn thành lớp học lập trình này với khá nhiều thời gian và công sức

Hoàn toàn đồng ý Đồng ý Bình thường 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 ý Bình thường Không đồng ý Hoàn toàn không đồng ý