Menu
CoddyTech

Cửa hàng: Chi tiết sản phẩm

Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 21 trên 36.

Chạm vào một sản phẩm trong cửa hàng, bạn sẽ mở trang của sản phẩm đó: một hình ảnh lớn, giá và vài dòng giới thiệu. Đó là màn hình thứ hai, ProductScreen, nhận sản phẩm thông qua hàm khởi tạo.

Để có thể chạm vào toàn bộ thẻ, hãy đặt InkWell bên trong Card, bao quanh nội dung. Thành phần này chạy onTap và tạo hiệu ứng gợn sóng bạn thấy trong các ứng dụng Material; Clip.antiAlias của thẻ giữ hiệu ứng gợn sóng bên trong các góc bo tròn.

Card(
  clipBehavior: Clip.antiAlias,
  child: InkWell(
    onTap: () {
      Navigator.push(context, MaterialPageRoute(
        builder: (context) => ProductScreen(product: product),
      ));
    },
    child: Column(...),
  ),
)

Trang sản phẩm dùng ListView, vì vậy bạn có thể cuộn phần mô tả dài. AppBar hiển thị tên sản phẩm và mũi tên quay lại, mà Flutter tự thêm cho bạn khi mở một màn hình mới.

Để hiển thị giá thật lớn, hãy bắt đầu từ headlineSmall trong giao diện rồi chỉ thay đổi những gì cần thiết bằng copyWith: textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold). Có ?. vì kiểu giao diện có thể là null.

challenge icon

Thử thách

Trung bình

Tạo một trang cho mỗi sản phẩm:

  • viết một ProductScreen nhận product có required, với tên sản phẩm trong AppBar của trang
  • phần thân là một ListView có khoảng đệm 24 pixel: một ô cao 240 pixel, có màu product.color.shade50, các góc bo tròn 24 pixel và biểu tượng có kích thước 120, sau đó là khoảng cách 24 pixel, giá (dùng headlineSmall in đậm với màu chính), khoảng cách 12 pixel và phần mô tả trong bodyLarge
  • bọc Column của mỗi thẻ trong InkWell để mở trang của sản phẩm đó

Tự làm thử

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

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

final money = NumberFormat.currency(locale: 'en_US', symbol: '\$');

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
      ),
      home: const ShopScreen(),
    );
  }
}

class Product {
  const Product({
    required this.name,
    required this.price,
    required this.icon,
    required this.color,
    required this.description,
  });

  final String name;
  final double price;
  final IconData icon;
  final MaterialColor color;
  final String description;
}

const List<Product> products = [
  Product(
    name: 'Headphones',
    price: 59,
    icon: Icons.headphones,
    color: Colors.indigo,
    description: 'Wireless headphones with 30 hours of battery.',
  ),
  Product(
    name: 'Watch',
    price: 129,
    icon: Icons.watch,
    color: Colors.teal,
    description: 'A smart watch that counts your steps.',
  ),
  Product(
    name: 'Backpack',
    price: 45.5,
    icon: Icons.backpack,
    color: Colors.orange,
    description: 'Room for a laptop, a lunch and a jacket.',
  ),
  Product(
    name: 'Mug',
    price: 12,
    icon: Icons.coffee,
    color: Colors.brown,
    description: 'Holds a big coffee and keeps it warm.',
  ),
  Product(
    name: 'Camera',
    price: 1299,
    icon: Icons.photo_camera,
    color: Colors.blueGrey,
    description: 'A mirrorless camera for sharp photos.',
  ),
  Product(
    name: 'Sneakers',
    price: 89.99,
    icon: Icons.directions_run,
    color: Colors.red,
    description: 'Light running shoes for every day.',
  ),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Coddy Shop')),
      body: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(16),
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        childAspectRatio: 0.8,
        children: [
          for (Product product in products) ProductCard(product: product),
        ],
      ),
    );
  }
}

class ProductCard extends StatelessWidget {
  const ProductCard({super.key, required this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Container(
              width: double.infinity,
              alignment: Alignment.center,
              color: product.color.shade50,
              child: Icon(product.icon, size: 56, color: product.color),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 4),
                Text(
                  money.format(product.price),
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Dự án

Tự luyện tập: Trình biên dịch Flutter online