Flutter for OpenHarmony:从零到一:构建购物APP的骨架与精美UI

Flutter for OpenHarmony:从零到一:构建购物APP的骨架与精美UI

引言

在移动互联网时代,一个直观、流畅且美观的用户界面(UI)是任何成功应用的敲门砖。对于开发者而言,如何高效地构建出符合设计规范的UI,是日常工作的核心挑战之一。Flutter,凭借其声明式UI框架和丰富的Material

Design组件库,为我们提供了一个近乎完美的解决方案。

本文将带你从零开始,手把手构建一个名为"淘淘购物"的APP。我们的目标不仅仅是让它能跑起来,更要让它看起来专业、用起来顺手。我们将聚焦于应用的基础骨架------底部导航栏,以及两个关键页面:"首页"和"我的"。通过这个过程,你将深入理解ScaffoldAppBarListViewGridView等核心Widget的组合使用技巧,并掌握如何构建复杂的、响应式的布局。

完整效果展示

完整代码展示

dart 复制代码
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '淘淘购物',
      theme: ThemeData(
        primaryColor: Colors.orange,
        scaffoldBackgroundColor: Colors.grey[100],
        useMaterial3: true,
      ),
      home: const MainScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;
  final List<Widget> _screens = [
    const HomeScreen(),
    const CategoryScreen(),
    const CartScreen(),
    const ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Colors.orange,
        unselectedItemColor: Colors.grey,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.category),
            label: '分类',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.shopping_cart),
            label: '购物车',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

// 首页
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('淘淘购物'),
        backgroundColor: Colors.orange,
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () {},
          ),
          IconButton(
            icon: const Icon(Icons.message),
            onPressed: () {},
          ),
        ],
      ),
      body: ListView(
        children: [
          // 搜索框
          Container(
            padding: const EdgeInsets.all(10),
            color: Colors.orange,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 15),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(20),
              ),
              child: const TextField(
                decoration: InputDecoration(
                  hintText: '搜索宝贝',
                  border: InputBorder.none,
                  prefixIcon: Icon(Icons.search, color: Colors.grey),
                ),
              ),
            ),
          ),

          // 轮播图区域
          Container(
            height: 150,
            margin: const EdgeInsets.all(10),
            decoration: BoxDecoration(
              color: Colors.orange[300],
              borderRadius: BorderRadius.circular(10),
            ),
            child: const Center(
              child: Text(
                '轮播图区域',
                style: TextStyle(color: Colors.white, fontSize: 20),
              ),
            ),
          ),

          // 快捷入口
          Container(
            padding: const EdgeInsets.all(15),
            color: Colors.white,
            child: GridView.count(
              crossAxisCount: 5,
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              children: const [
                QuickEntry(Icons.shopping_bag, '天猫'),
                QuickEntry(Icons.card_giftcard, '聚划算'),
                QuickEntry(Icons.local_offer, '优惠券'),
                QuickEntry(Icons.phone_android, '数码'),
                QuickEntry(Icons.style, '服饰'),
                QuickEntry(Icons.home, '家居'),
                QuickEntry(Icons.restaurant, '美食'),
                QuickEntry(Icons.flight, '旅行'),
                QuickEntry(Icons.sports_basketball, '运动'),
                QuickEntry(Icons.book, '图书'),
              ],
            ),
          ),

          const SizedBox(height: 10),

          // 推荐商品
          Container(
            color: Colors.white,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Padding(
                  padding: EdgeInsets.all(15),
                  child: Text(
                    '为你推荐',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                GridView.builder(
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  padding: const EdgeInsets.all(10),
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    childAspectRatio: 0.75,
                    crossAxisSpacing: 10,
                    mainAxisSpacing: 10,
                  ),
                  itemCount: _recommendedProducts.length,
                  itemBuilder: (context, index) {
                    return ProductCard(product: _recommendedProducts[index]);
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 快捷入口组件
class QuickEntry extends StatelessWidget {
  final IconData icon;
  final String label;

  const QuickEntry(this.icon, this.label, {super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Icon(icon, color: Colors.orange, size: 30),
        const SizedBox(height: 5),
        Text(label, style: const TextStyle(fontSize: 12)),
      ],
    );
  }
}

// 分类页
class CategoryScreen extends StatelessWidget {
  const CategoryScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('商品分类'),
        backgroundColor: Colors.orange,
      ),
      body: Row(
        children: [
          // 左侧分类列表
          Container(
            width: 100,
            color: Colors.grey[200],
            child: ListView.builder(
              itemCount: _categories.length,
              itemBuilder: (context, index) {
                return Container(
                  padding: const EdgeInsets.symmetric(vertical: 20),
                  decoration: BoxDecoration(
                    color: index == 0 ? Colors.white : null,
                    border: index == 0
                        ? const Border(
                            left: BorderSide(color: Colors.orange, width: 3))
                        : null,
                  ),
                  child: Center(
                    child: Text(
                      _categories[index],
                      style: TextStyle(
                        color: index == 0 ? Colors.orange : Colors.black87,
                        fontWeight:
                            index == 0 ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
          // 右侧商品列表
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(10),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                childAspectRatio: 0.8,
                crossAxisSpacing: 10,
                mainAxisSpacing: 10,
              ),
              itemCount: _subCategories.length,
              itemBuilder: (context, index) {
                return SubCategoryCard(category: _subCategories[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}

// 子分类卡片
class SubCategoryCard extends StatelessWidget {
  final String category;

  const SubCategoryCard({super.key, required this.category});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          width: 60,
          height: 60,
          decoration: BoxDecoration(
            color: Colors.orange[100],
            borderRadius: BorderRadius.circular(10),
          ),
          child: const Icon(Icons.image, color: Colors.orange, size: 30),
        ),
        const SizedBox(height: 8),
        Text(
          category,
          style: const TextStyle(fontSize: 12),
          textAlign: TextAlign.center,
        ),
      ],
    );
  }
}

// 购物车页
class CartScreen extends StatefulWidget {
  const CartScreen({super.key});

  @override
  State<CartScreen> createState() => _CartScreenState();
}

class _CartScreenState extends State<CartScreen> {
  double _totalPrice = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('购物车'),
        backgroundColor: Colors.orange,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _cartItems.length,
              itemBuilder: (context, index) {
                return CartItemCard(
                  item: _cartItems[index],
                  onQuantityChanged: (quantity) {
                    setState(() {
                      _totalPrice = _calculateTotal();
                    });
                  },
                );
              },
            ),
          ),
          // 底部结算栏
          Container(
            padding: const EdgeInsets.all(15),
            decoration: BoxDecoration(
              color: Colors.white,
              boxShadow: [
                BoxShadow(
                  color: Colors.grey.withOpacity(0.3),
                  blurRadius: 5,
                  offset: const Offset(0, -2),
                ),
              ],
            ),
            child: Row(
              children: [
                const Text(
                  '合计: ',
                  style: TextStyle(fontSize: 16),
                ),
                Text(
                  '¥$_totalPrice',
                  style: const TextStyle(
                    fontSize: 24,
                    color: Colors.orange,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Spacer(),
                ElevatedButton(
                  onPressed: () {},
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.orange,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 40,
                      vertical: 12,
                    ),
                  ),
                  child: const Text('结算', style: TextStyle(fontSize: 16)),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  double _calculateTotal() {
    return _cartItems.fold(
        0.0, (sum, item) => sum + item.price * item.quantity);
  }
}

// 我的页面
class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的淘淘'),
        backgroundColor: Colors.orange,
      ),
      body: ListView(
        children: [
          // 用户信息
          Container(
            padding: const EdgeInsets.all(20),
            color: Colors.orange,
            child: Column(
              children: [
                Container(
                  width: 80,
                  height: 80,
                  decoration: const BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                  ),
                  child:
                      const Icon(Icons.person, size: 50, color: Colors.orange),
                ),
                const SizedBox(height: 10),
                const Text(
                  '用户昵称',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 5),
                Container(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.white),
                    borderRadius: BorderRadius.circular(15),
                  ),
                  child: const Text(
                    '登录/注册',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ],
            ),
          ),

          const SizedBox(height: 10),

          // 订单状态
          Container(
            padding: const EdgeInsets.all(15),
            color: Colors.white,
            child: Column(
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text(
                      '我的订单',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Row(
                      children: const [
                        Text('全部订单', style: TextStyle(color: Colors.grey)),
                        Icon(Icons.chevron_right, color: Colors.grey),
                      ],
                    ),
                  ],
                ),
                const SizedBox(height: 15),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: const [
                    OrderStatusIcon(Icons.payment, '待付款'),
                    OrderStatusIcon(Icons.inventory, '待发货'),
                    OrderStatusIcon(Icons.local_shipping, '待收货'),
                    OrderStatusIcon(Icons.star, '待评价'),
                    OrderStatusIcon(Icons.replay, '退款/售后'),
                  ],
                ),
              ],
            ),
          ),

          const SizedBox(height: 10),

          // 功能列表
          Container(
            color: Colors.white,
            child: Column(
              children: const [
                ListTile(
                  leading: Icon(Icons.favorite_border, color: Colors.orange),
                  title: Text('我的收藏'),
                  trailing: Icon(Icons.chevron_right),
                ),
                Divider(height: 1),
                ListTile(
                  leading: Icon(Icons.location_on, color: Colors.orange),
                  title: Text('收货地址'),
                  trailing: Icon(Icons.chevron_right),
                ),
                Divider(height: 1),
                ListTile(
                  leading: Icon(Icons.help_outline, color: Colors.orange),
                  title: Text('联系客服'),
                  trailing: Icon(Icons.chevron_right),
                ),
                Divider(height: 1),
                ListTile(
                  leading: Icon(Icons.settings, color: Colors.orange),
                  title: Text('设置'),
                  trailing: Icon(Icons.chevron_right),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 订单状态图标
class OrderStatusIcon extends StatelessWidget {
  final IconData icon;
  final String label;

  const OrderStatusIcon(this.icon, this.label, {super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Icon(icon, color: Colors.orange, size: 28),
        const SizedBox(height: 5),
        Text(label, style: const TextStyle(fontSize: 12)),
      ],
    );
  }
}

// 商品卡片
class ProductCard extends StatelessWidget {
  final Product product;

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

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(10),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.2),
            blurRadius: 5,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Container(
              decoration: BoxDecoration(
                color: Colors.orange[100],
                borderRadius:
                    const BorderRadius.vertical(top: Radius.circular(10)),
              ),
              child: const Center(
                child: Icon(Icons.image, color: Colors.orange, size: 50),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: const TextStyle(fontSize: 14),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 5),
                Row(
                  children: [
                    Text(
                      '¥${product.price.toStringAsFixed(2)}',
                      style: const TextStyle(
                        color: Colors.orange,
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(width: 5),
                    Text(
                      '${product.sales}人付款',
                      style: const TextStyle(fontSize: 12, color: Colors.grey),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 购物车商品卡片
class CartItemCard extends StatefulWidget {
  final CartItem item;
  final Function(int) onQuantityChanged;

  const CartItemCard({
    super.key,
    required this.item,
    required this.onQuantityChanged,
  });

  @override
  State<CartItemCard> createState() => _CartItemCardState();
}

class _CartItemCardState extends State<CartItemCard> {
  bool _isChecked = true;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(10),
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(10),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.2),
            blurRadius: 5,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          Checkbox(
            value: _isChecked,
            onChanged: (value) {
              setState(() {
                _isChecked = value ?? false;
              });
            },
          ),
          Container(
            width: 80,
            height: 80,
            decoration: BoxDecoration(
              color: Colors.orange[100],
              borderRadius: BorderRadius.circular(5),
            ),
            child: const Icon(Icons.image, color: Colors.orange),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  widget.item.name,
                  style: const TextStyle(fontSize: 14),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      '¥${widget.item.price.toStringAsFixed(2)}',
                      style: const TextStyle(
                        color: Colors.orange,
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Row(
                      children: [
                        IconButton(
                          icon: const Icon(Icons.remove, size: 18),
                          onPressed: () {
                            if (widget.item.quantity > 1) {
                              widget.item.quantity--;
                              widget.onQuantityChanged(widget.item.quantity);
                            }
                          },
                        ),
                        Text('${widget.item.quantity}'),
                        IconButton(
                          icon: const Icon(Icons.add, size: 18),
                          onPressed: () {
                            widget.item.quantity++;
                            widget.onQuantityChanged(widget.item.quantity);
                          },
                        ),
                      ],
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 数据模型
class Product {
  final String name;
  final double price;
  final int sales;

  Product({required this.name, required this.price, required this.sales});
}

class CartItem {
  final String name;
  final double price;
  int quantity;

  CartItem({
    required this.name,
    required this.price,
    this.quantity = 1,
  });
}

// 数据
final List<Product> _recommendedProducts = [
  Product(name: '新款智能手机', price: 2999.00, sales: 1000),
  Product(name: '无线蓝牙耳机', price: 199.00, sales: 5000),
  Product(name: '智能手表', price: 899.00, sales: 2000),
  Product(name: '运动鞋', price: 399.00, sales: 3000),
  Product(name: '男士休闲裤', price: 159.00, sales: 1500),
  Product(name: '女士连衣裙', price: 299.00, sales: 2500),
];

final List<String> _categories = [
  '热门推荐',
  '手机数码',
  '家用电器',
  '电脑办公',
  '家居家装',
  '服装服饰',
  '鞋靴箱包',
  '运动户外',
  '美妆个护',
  '食品生鲜',
];

final List<String> _subCategories = [
  '手机',
  '平板',
  '耳机',
  '充电宝',
  '数据线',
  '保护壳',
  '笔记本',
  '键盘',
  '鼠标',
  '显示器',
  '音响',
  '耳机',
  '冰箱',
  '洗衣机',
  '空调',
  '电视',
  '微波炉',
  '电饭煲',
];

final List<CartItem> _cartItems = [
  CartItem(name: '新款智能手机', price: 2999.00, quantity: 1),
  CartItem(name: '无线蓝牙耳机', price: 199.00, quantity: 2),
  CartItem(name: '智能手表', price: 899.00, quantity: 1),
];

第一步:项目初始化与全局主题配置

进入项目后,打开lib/main.dart,这是我们应用的唯一入口。为了让我们的APP拥有统一的品牌调性,我们需要配置全局主题。观察主流购物APP,橙色系(如淘宝、京东)因其活力、热情和促销感而被广泛采用。我们也将沿用这一经典配色。

dart 复制代码
// lib/main.dart
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '淘淘购物', // 应用标题
      theme: ThemeData(
        primaryColor: Colors.orange, // 主色调设为橙色
        scaffoldBackgroundColor: Colors.grey[100], // 页面背景为浅灰色
        useMaterial3: true, // 使用最新的Material 3设计规范
      ),
      home: const MainScreen(), // 应用启动后显示的首页
      debugShowCheckedModeBanner: false, // 隐藏右上角的"DEBUG"水印
    );
  }
}

这段代码定义了应用的整体外观。primaryColor会影响AppBarFloatingActionButton等组件的默认颜色;scaffoldBackgroundColor则设置了所有页面的背景色,营造出一种干净、舒适的浏览环境。

第二步:构建应用骨架------底部导航栏

几乎所有的电商APP都采用底部导航栏(Bottom Navigation Bar)作为主要的页面切换方式,因为它符合拇指操作热区,用户体验极佳。在Flutter中,Scaffold widget为我们提供了现成的bottomNavigationBar属性。

我们创建一个MainScreen作为应用的根页面,它负责管理四个主要的功能模块:首页、分类、购物车和个人中心。

dart 复制代码
// lib/main_screen.dart (可以单独放在一个文件中)
class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0; // 当前选中的索引
  final List<Widget> _screens = [
    const HomeScreen(),
    const CategoryScreen(),
    const CartScreen(),
    const ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_currentIndex], // 根据索引显示对应页面
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index; // 点击时更新状态,触发UI重建
          });
        },
        type: BottomNavigationBarType.fixed, // 固定模式,即使超过3个item也显示标签
        selectedItemColor: Colors.orange, // 选中项的颜色
        unselectedItemColor: Colors.grey, // 未选中项的颜色
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.category), label: '分类'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '购物车'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

这里的关键在于StatefulWidget的使用。因为底部导航栏的选中状态会改变,所以我们需要一个State来持有_currentIndex变量。当用户点击某个图标时,onTap回调会触发setState,通知Flutter框架UI需要根据新的_currentIndex值进行重建。这是一种非常典型的Flutter状态管理模式。

第三步:精心雕琢"首页"UI

首页是用户的流量入口,信息密度高,布局复杂。我们将其分解为几个核心区域:顶部AppBar、搜索框、轮播图、快捷入口和商品推荐列表。

1. 顶部AppBar与内嵌搜索

为了提升用户体验,我们将搜索框直接放在AppBar下方,并使其与AppBar融为一体。

dart 复制代码
// 在HomeScreen的build方法中
appBar: AppBar(
  title: const Text('淘淘购物'),
  backgroundColor: Colors.orange,
  elevation: 0, // 去掉阴影,使下方搜索框无缝衔接
  actions: [
    IconButton(icon: const Icon(Icons.search), onPressed: () {}),
    IconButton(icon: const Icon(Icons.message), onPressed: () {}),
  ],
),

紧接着,我们用一个Container包裹TextField,并通过设置内外两层Container的背景色(外层橙色,内层白色)和圆角,轻松实现了一个美观的搜索框。

2. 快捷入口 - GridView的妙用

快捷入口通常是一个多行多列的图标网格。GridView.count是实现这种布局的利器。我们将其crossAxisCount设为5,并禁用其滚动(NeverScrollableScrollPhysics),使其成为一个静态的网格。

dart 复制代码
GridView.count(
  crossAxisCount: 5,
  shrinkWrap: true, // 让GridView只占用自身所需高度
  physics: const NeverScrollableScrollPhysics(), // 禁用滚动
  children: const [
    QuickEntry(Icons.shopping_bag, '天猫'),
    // ... 其他入口
  ],
),

QuickEntry是一个我们自定义的StatelessWidget,它接收图标和标签,组合成一个可复用的单元格。这种组件化的思想是构建大型UI的关键。

3. 商品推荐列表 - 嵌套滚动的处理

商品推荐区域本身也是一个GridView(2列),但它需要嵌套在外部的ListView中。直接嵌套会导致滚动冲突。解决方案是:

  • 对内部的GridView同样设置shrinkWrap: true,使其高度由子元素决定。
  • 禁用内部GridView的滚动:physics: const NeverScrollableScrollPhysics()

这样,整个页面的滚动就由最外层的ListView统一接管,保证了流畅的用户体验。

第四步:打造个人中心------"我的"页面

"我的"页面相对结构化,主要由用户信息区、订单状态区和功能列表区组成。

1. 用户信息区

这个区域通常有鲜明的背景色(我们沿用橙色)以突出重要性。头像使用一个圆形的Container配合Icon模拟。登录/注册按钮通过BoxDecoration添加了边框,使其在橙色背景下依然清晰可辨。

2. 订单状态区

订单状态是典型的水平排列图标。我们再次使用Row和自定义的OrderStatusIcon组件来实现。每个状态项都是一个独立的Column,包含图标和文字。

3. 功能列表区

这部分是最标准的列表,ListTile是不二之选。它内置了leading(前置图标)、title(标题)和trailing(后置图标)属性,一行代码就能构建出专业感十足的列表项。Divider用于在列表项之间添加细线分隔,提升视觉层次感。

总结与思考

通过本文,我们成功地搭建起了一个购物APP的完整骨架,并完成了两个核心页面的静态UI。我们学习了:

  1. 全局主题配置 :如何通过ThemeData统一应用风格。
  2. 应用导航 :利用ScaffoldBottomNavigationBar构建标准的多页面应用。
  3. 复杂布局 :组合使用ListViewGridViewRowColumn等基础Widget来实现设计师的稿子。
  4. 组件化开发 :将QuickEntryOrderStatusIcon等重复UI抽象成独立的Widget,提高代码复用性和可维护性。

🌐 加入社区

欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:

👉 开源鸿蒙跨平台开发者社区


技术因分享而进步,生态因共建而繁荣

------ 晚霞的不甘 · 与您共赴鸿蒙跨平台开发之旅

相关推荐
kirk_wang2 小时前
Flutter艺术探索-Freezed代码生成:不可变数据模型实战
flutter·移动开发·flutter教程·移动开发教程
小二·2 小时前
Python Web 开发进阶实战:AI 原生硬件接口 —— 在 Flask + MicroPython 中构建边缘智能设备控制平台
前端·人工智能·python
ElasticPDF-新国产PDF编辑器2 小时前
基于 PDF.js 的 PDF 文字编辑解决方案,纯前端 SDK,跨平台、框架无关、Web 原生
前端·javascript·pdf
带带弟弟学爬虫__2 小时前
速通新Baidu Frida检测
前端·javascript·vue.js·python·网络爬虫
b2077212 小时前
Flutter for OpenHarmony 身体健康状况记录App实战 - 运动分析实现
python·flutter·harmonyos
好学且牛逼的马2 小时前
ES6 核心语法精讲
前端·ecmascript·es6
GISer_Jing2 小时前
一次编码,七端运行:Taro多端统一架构深度解析与电商实战
前端·aigc·taro
子春一2 小时前
Flutter for OpenHarmony:用 Flutter 构建一个数字猜谜游戏:从零开始的交互式应用开发
javascript·flutter·游戏
michael_ouyang3 小时前
IM 消息收发流程方案选型
前端·websocket·网络协议·typescript·electron