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ó hàng chục thành phần giao diện người dùng đẹp mắt, dễ sử dụng và được cung cấp 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à 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 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?

Tân binh 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ó hai phần mềm để hoàn thành lớp học này – SDK Fluttermộ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à đặt thành 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 ứng dụng dành cho máy tính Windows, bạn phải phát triển trên Windows để truy cập vào chuỗi xây dựng thích 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ã cho lớp học lập trình này. Bỏ qua 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 cho 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: Chạy 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ó 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

Giờ đây, 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 có thể thấy màn hình chính cho biết: "Bạn đã làm được!". Thật tuyệt! Tuy nhiên, người dùng hiện không cần làm gì hoặc không biết vị trí của họ trong ứng dụng. Để hỗ trợ bạn, đã đế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.

Để đ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 làm nổi bật:

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ẽ cung cấp cho chúng ta một bố cục hoàn hảo mà không mất phí, giữ AppBar ở đầu trang và phần thân 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 đề là Đền

thanh ứng dụng có tiêu đề là Đền

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

Thêm IconButton dẫn đầu

Khi vẫn ở home.dart, hãy đặ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 đề Đền thờ và biểu tượng trình đơn ba đường kẻ

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à "hamburger") xuất hiện ngay tại nơi bạn mong đợi.

Bạn cũng có thể thêm các nút vào phía 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 có tiêu đề là Shrine (Đền thờ) và biểu tượng trình đơn dạng bánh hamburger, cùng với biểu tượng tìm kiếm và 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

Giờ đây, ứ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. Chỉ riêng tiện ích Card (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ẽ muốn gói gọn tiện ích này vào trong 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 hiển thị là có thể đếm được và không phải 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 trên cả 4 cạnh 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 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 ta có một thẻ nhưng thẻ đó đang trống. Hãy thêm các 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 (Văn bản) được xếp chồng theo chiều dọc với 8 điểm khoảng trống giữa các tiện ích (SizedBox). Chúng ta tạo một Cột khác để chứa các cột đó bên 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, 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. Đó là 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 có 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 ta cần thực hiện 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.

Android

iOS

một lưới gồm các 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 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 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 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 giá trị đó thành .fitWidth để thu phóng 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ỉ với 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). 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 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 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 thời gian và công sức hợp lý

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