一、环境准备与项目初始化

1.1 安装 Flutter SDK

前往 Flutter 官网 下载最新稳定版 SDK,并配置环境变量。验证安装是否成功:


bash

编辑

flutter --version
flutter doctor

确保 flutter doctor 输出中无红色错误(尤其是 Android toolchain 和 IDE 插件)。

1.2 创建新项目

在终端执行:


bash

编辑

flutter create --org com.example flutter_card_demo
cd flutter_card_demo

该命令会生成标准 Flutter 项目结构,包括 lib/, test/, android/, ios/ 等目录。

1.3 添加依赖

我们将使用以下第三方库:

  • http:用于网络请求
  • provider:轻量级状态管理
  • cached_network_image:高效加载并缓存网络图片

编辑 pubspec.yaml 文件,在 dependencies 下添加:


yaml

编辑

dependencies:
  flutter:
    sdk: flutter
  http: ^0.16.0
  provider: ^6.1.2
  cached_network_image: ^3.4.1

然后执行:


bash

编辑

flutter pub get

💡 提示:所有依赖版本请以 pub.dev 最新为准。


二、基础 UI 构建:静态卡片列表

我们先不考虑网络数据,用模拟数据快速搭建 UI。

2.1 主应用入口

修改 lib/main.dart


dart

编辑

import 'package:flutter/material.dart';
import 'package:flutter_card_demo/card_list_page.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Card Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true, // 启用 Material 3 设计
      ),
      home: const CardListPage(),
    );
  }
}

2.2 卡片列表页面(静态版)

创建 lib/card_list_page.dart


dart

编辑

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('商品推荐'),
        centerTitle: true,
      ),
      body: ListView.builder(
        padding: const EdgeInsets.only(top: 12),
        itemCount: 6,
        itemBuilder: (context, index) => _buildCard(context, index),
      ),
    );
  }

  Widget _buildCard(BuildContext context, int index) {
    final titles = [
      '无线蓝牙耳机',
      '智能手表',
      '便携咖啡机',
      '折叠电动滑板车',
      '高清运动相机',
      '多功能料理锅'
    ];
    final prices = ['¥299', '¥899', '¥459', '¥1999', '¥1299', '¥699'];
    final images = List.generate(6, (i) => 'https://picsum.photos/300/200?random=$i');

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Card(
        elevation: 3,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        clipBehavior: Clip.hardEdge, // 防止子 widget 超出圆角
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 图片区域
            SizedBox(
              height: 180,
              child: CachedNetworkImage(
                imageUrl: images[index],
                fit: BoxFit.cover,
                placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                errorWidget: (context, url, error) => const Icon(Icons.error),
              ),
            ),
            // 文字信息
            Padding(
              padding: const EdgeInsets.all(14),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    titles[index],
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          fontWeight: FontWeight.bold,
                        ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    prices[index],
                    style: const TextStyle(
                      color: Colors.red,
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

✅ 关键点说明:

  • 使用 CachedNetworkImage 替代 Image.network,自动缓存图片,提升滚动流畅度。
  • clipBehavior: Clip.hardEdge 确保图片不会超出 Card 的圆角边界。
  • maxLines + TextOverflow.ellipsis 防止长文本溢出。

运行效果如下:


三、接入真实数据:网络请求与状态管理

静态数据只是演示,真实 App 需要从服务器获取数据。

3.1 定义数据模型

创建 lib/models/product.dart


dart

编辑

class Product {
  final String id;
  final String title;
  final String price;
  final String imageUrl;

  Product({
    required this.id,
    required this.title,
    required this.price,
    required this.imageUrl,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'],
      title: json['title'],
      price: '¥${json['price']}',
      imageUrl: json['image'],
    );
  }
}

3.2 模拟 API 接口

我们将使用 JSONPlaceholder 的 /photos 接口作为示例(虽然字段不完全匹配,但可用于演示)。

实际项目中,请替换为你自己的 API。

3.3 创建数据服务类

新建 lib/services/product_service.dart


dart

编辑

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/product.dart';

class ProductService {
  static const String _baseUrl = 'https://jsonplaceholder.typicode.com';

  Future<List<Product>> fetchProducts() async {
    final response = await http.get(Uri.parse('$_baseUrl/photos?_limit=20'));

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      // 模拟映射到 Product(因 photos 缺少 price,我们随机生成)
      return data.map((item) {
        return Product(
          id: item['id'].toString(),
          title: item['title'],
          price: '¥${(100 + (item['id'] % 10) * 50)}',
          imageUrl: item['url'],
        );
      }).toList();
    } else {
      throw Exception('Failed to load products');
    }
  }
}

3.4 使用 Provider 进行状态管理

创建 lib/providers/product_provider.dart


dart

编辑

import 'package:flutter/foundation.dart';
import '../models/product.dart';
import '../services/product_service.dart';

class ProductProvider with ChangeNotifier {
  List<Product> _products = [];
  bool _isLoading = false;
  String? _error;

  List<Product> get products => _products;
  bool get isLoading => _isLoading;
  String? get error => _error;

  Future<void> loadProducts() async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _products = await ProductService().fetchProducts();
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

3.5 在主应用中注入 Provider

修改 main.dart


dart

编辑

import 'package:provider/provider.dart';
import 'providers/product_provider.dart';

// 在 MyApp 中包裹 MultiProvider
@override
Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => ProductProvider()),
    ],
    child: MaterialApp(
      title: 'Flutter Card Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const CardListPage(),
    ),
  );
}

3.6 更新页面逻辑:动态加载数据

修改 card_list_page.dart


dart

编辑

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/product_provider.dart';

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

  @override
  State<CardListPage> createState() => _CardListPageState();
}

class _CardListPageState extends State<CardListPage> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<ProductProvider>().loadProducts();
    });
  }

  @override
  Widget build(BuildContext context) {
    final provider = context.watch<ProductProvider>();

    if (provider.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (provider.error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error, size: 60, color: Colors.red),
            const SizedBox(height: 16),
            Text(provider.error!),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => provider.loadProducts(),
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(title: const Text('商品推荐'), centerTitle: true),
      body: RefreshIndicator(
        onRefresh: () => provider.loadProducts(),
        child: ListView.builder(
          padding: const EdgeInsets.only(top: 12),
          itemCount: provider.products.length,
          itemBuilder: (context, index) => _buildCard(provider.products[index]),
        ),
      ),
    );
  }

  Widget _buildCard(Product product) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Card(
        elevation: 3,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        clipBehavior: Clip.hardEdge,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
              height: 180,
              child: CachedNetworkImage(
                imageUrl: product.imageUrl,
                fit: BoxFit.cover,
                placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                errorWidget: (context, url, error) => const Icon(Icons.image_not_supported),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(14),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.title,
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    product.price,
                    style: const TextStyle(color: Colors.red, fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

✅ 新增功能:

  • 下拉刷新(RefreshIndicator
  • 加载中状态
  • 错误提示 + 重试按钮
  • 自动首次加载

四、性能优化与最佳实践

4.1 图片缓存

已使用 cached_network_image,它基于 flutter_cache_manager,自动管理磁盘和内存缓存。

4.2 列表项复用

ListView.builder 本身支持懒加载和 widget 复用,无需额外操作。

4.3 避免重建整个列表

通过 Providerwatch 仅监听必要状态,配合 const 构造函数(若适用)可进一步优化。

4.4 内存泄漏预防

  • 避免在 StatefulWidget 中持有对 BuildContext 的长期引用。
  • 网络请求应在 dispose 中取消(本例使用 Future,无持续监听,风险较低)。

五、国际化(i18n)支持

为 App 添加多语言支持。

5.1 启用 flutter_localizations

pubspec.yaml 中添加:


yaml

编辑

dependencies:
  flutter_localizations:
    sdk: flutter

5.2 配置 MaterialApp


dart

编辑

MaterialApp(
  localizationsDelegates: const [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: const [
    Locale('zh', 'CN'),
    Locale('en', 'US'),
  ],
  // ...
)

5.3 自定义文案(可选)

可使用 easy_localization 等库管理自定义字符串。


六、测试:单元测试与集成测试

6.1 单元测试(Product 模型)

test/models/product_test.dart


dart

编辑

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_card_demo/models/product.dart';

void main() {
  test('Product.fromJson should parse correctly', () {
    final json = {
      'id': '1',
      'title': 'Test Product',
      'price': 100,
      'image': 'http://example.com/image.jpg'
    };

    final product = Product.fromJson(json);

    expect(product.id, '1');
    expect(product.title, 'Test Product');
    expect(product.price, '¥100');
    expect(product.imageUrl, 'http://example.com/image.jpg');
  });
}

运行:


bash

编辑

flutter test

6.2 集成测试(UI 流程)

略(可后续扩展),通常测试“加载成功”、“显示错误”等场景。


七、打包与发布建议

  • Android:配置 android/app/build.gradle 中的 versionCode 和 versionName,生成签名 APK。
  • iOS:需 Xcode 配置 Bundle ID、证书等。
  • 性能分析:使用 flutter run --profile 查看帧率、内存。
  • 发布前检查:移除调试日志、关闭 assert、压缩图片资源。

八、总结

本文带你完成了一个生产级卡片列表页面的完整开发流程,涵盖:

模块技术点
UI 构建Card、ListView、CachedNetworkImage
数据层http、JSON 解析
状态管理Provider
用户体验加载、错误、下拉刷新
工程化模型分离、服务封装、测试
扩展性国际化、性能优化

你不仅学会了“怎么做”,更理解了“为什么这么做”。


九、后续方向

  • 添加点击跳转详情页(Navigator + 参数传递)
  • 实现搜索与筛选功能
  • 集成 Firebase 或自建后端
  • 添加动画(Hero、FadeIn)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐