Flutter 入门实战:从零打造高性能、可扩展的卡片列表页面
一、环境准备与项目初始化
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 避免重建整个列表
通过 Provider 的 watch 仅监听必要状态,配合 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)
更多推荐


所有评论(0)