1. 项目概述
在开发手语学习App的过程中,课程列表页面作为用户接触最频繁的界面之一,其设计质量直接影响用户体验和学习效率。本文将详细介绍如何使用Flutter框架实现一个功能完善、视觉美观的课程列表页面,包含从基础布局到高级功能的完整实现方案。
课程列表页面需要解决的核心问题包括:
- 如何高效展示大量课程信息
- 如何清晰呈现课程状态和学习进度
- 如何实现流畅的交互体验
- 如何保证代码的可维护性和扩展性
2. 页面架构设计
2.1 数据模型定义
在实现UI之前,我们需要先设计合理的数据结构。虽然示例中使用的是Map类型,但在实际项目中,我们推荐使用强类型的Dart类来定义课程模型:
dart复制class Lesson {
final String id;
final String title;
final String description;
final int duration; // 分钟数
final Difficulty difficulty;
final bool completed;
final String category;
// 构造函数
Lesson({
required this.id,
required this.title,
required this.description,
required this.duration,
required this.difficulty,
required this.completed,
required this.category,
});
// 从JSON转换
factory Lesson.fromJson(Map<String, dynamic> json) {
return Lesson(
id: json['id'],
title: json['title'],
description: json['description'],
duration: json['duration'],
difficulty: Difficulty.values.firstWhere(
(e) => e.toString().split('.').last == json['difficulty'],
orElse: () => Difficulty.beginner,
),
completed: json['completed'] ?? false,
category: json['category'],
);
}
}
enum Difficulty {
beginner('初级'),
intermediate('中级'),
advanced('高级');
final String label;
const Difficulty(this.label);
}
这种面向对象的设计有以下优势:
- 类型安全:编译器可以检查类型错误
- 可扩展性:方便添加新属性和方法
- 可维护性:业务逻辑集中在模型类中
- 序列化支持:易于与JSON互转
2.2 状态管理方案
对于课程列表这种需要频繁更新的UI,我们推荐使用Riverpod作为状态管理方案:
dart复制final lessonsProvider = FutureProvider.autoDispose.family<List<Lesson>, String>((ref, category) async {
// 从API或本地数据库获取数据
final response = await http.get(Uri.parse('$apiUrl/lessons?category=$category'));
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Lesson.fromJson(json)).toList();
});
这种设计实现了:
- 自动缓存和更新
- 按需加载和自动释放
- 类型安全的参数传递
- 错误处理和加载状态管理
3. UI实现细节
3.1 响应式布局设计
为了适配不同尺寸的设备,我们使用flutter_screenutil库实现响应式布局:
dart复制// 初始化
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812), // iPhone 13尺寸
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return MaterialApp(
title: '手语学习',
theme: ThemeData(
primarySwatch: Colors.teal,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: child,
);
},
child: const HomeScreen(),
);
}
}
在组件中使用响应式单位:
dart复制Container(
width: 60.w, // 宽度适配
height: 60.h, // 高度适配
margin: EdgeInsets.symmetric(vertical: 8.h), // 垂直间距适配
padding: EdgeInsets.all(12.r), // 圆角半径适配
child: Text(
'${index + 1}',
style: TextStyle(fontSize: 24.sp), // 字体大小适配
),
)
3.2 课程卡片实现
课程卡片是列表的核心元素,我们使用Card和InkWell组合实现:
dart复制Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
margin: EdgeInsets.only(bottom: 12.h),
child: InkWell(
borderRadius: BorderRadius.circular(12.r),
onTap: () => _navigateToDetail(context, lesson),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 序号区域
_buildIndexBadge(index),
SizedBox(width: 16.w),
// 课程信息区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
lesson.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
SizedBox(height: 4.h),
// 描述
Text(
lesson.description,
style: TextStyle(
fontSize: 13.sp,
color: Colors.grey.shade600,
height: 1.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
// 元信息
_buildLessonMeta(lesson),
],
),
),
// 状态图标
_buildStatusIcon(lesson),
],
),
),
),
)
3.3 序号徽章组件
序号徽章使用自定义Container实现:
dart复制Widget _buildIndexBadge(int index) {
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: const Color(0xFF00897B).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: const Color(0xFF00897B).withOpacity(0.3),
width: 1.w,
),
),
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: const Color(0xFF00897B),
),
),
),
);
}
3.4 课程元信息组件
课程时长和难度标签的实现:
dart复制Widget _buildLessonMeta(Lesson lesson) {
return Row(
children: [
// 时长
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.access_time,
size: 14.sp,
color: Colors.grey.shade500,
),
SizedBox(width: 4.w),
Text(
'${lesson.duration}分钟',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
),
],
),
SizedBox(width: 16.w),
// 难度
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: _getDifficultyColor(lesson.difficulty).withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: _getDifficultyColor(lesson.difficulty).withOpacity(0.3),
width: 0.5.w,
),
),
child: Text(
lesson.difficulty.label,
style: TextStyle(
fontSize: 10.sp,
color: _getDifficultyColor(lesson.difficulty),
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Color _getDifficultyColor(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.beginner:
return Colors.green;
case Difficulty.intermediate:
return Colors.orange;
case Difficulty.advanced:
return Colors.red;
}
}
3.5 状态图标组件
根据课程完成状态显示不同图标:
dart复制Widget _buildStatusIcon(Lesson lesson) {
return Icon(
lesson.completed ? Icons.check_circle : Icons.play_circle_outline,
color: lesson.completed ? Colors.green : const Color(0xFF00897B),
size: 32.sp,
);
}
4. 高级功能实现
4.1 课程筛选与排序
添加筛选和排序功能提升用户体验:
dart复制class _LessonListScreenState extends State<LessonListScreen> {
SortType _sortType = SortType.sequence;
FilterType _filterType = FilterType.all;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.category),
actions: [
// 排序按钮
PopupMenuButton<SortType>(
icon: const Icon(Icons.sort),
onSelected: (type) => setState(() => _sortType = type),
itemBuilder: (context) => [
const PopupMenuItem(
value: SortType.sequence,
child: Text('按顺序排序'),
),
const PopupMenuItem(
value: SortType.duration,
child: Text('按时长排序'),
),
const PopupMenuItem(
value: SortType.difficulty,
child: Text('按难度排序'),
),
],
),
// 筛选按钮
PopupMenuButton<FilterType>(
icon: const Icon(Icons.filter_list),
onSelected: (type) => setState(() => _filterType = type),
itemBuilder: (context) => [
const PopupMenuItem(
value: FilterType.all,
child: Text('全部课程'),
),
const PopupMenuItem(
value: FilterType.completed,
child: Text('已完成'),
),
const PopupMenuItem(
value: FilterType.incomplete,
child: Text('未完成'),
),
],
),
],
),
body: Consumer(
builder: (context, ref, child) {
final lessonsAsync = ref.watch(lessonsProvider(widget.category));
return lessonsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('加载失败: $error')),
data: (lessons) {
// 应用筛选和排序
final filteredLessons = _applyFilter(lessons, _filterType);
final sortedLessons = _applySort(filteredLessons, _sortType);
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: sortedLessons.length,
itemBuilder: (context, index) {
final lesson = sortedLessons[index];
return _buildLessonCard(context, lesson, index);
},
);
},
);
},
),
);
}
List<Lesson> _applyFilter(List<Lesson> lessons, FilterType type) {
switch (type) {
case FilterType.completed:
return lessons.where((l) => l.completed).toList();
case FilterType.incomplete:
return lessons.where((l) => !l.completed).toList();
case FilterType.all:
default:
return lessons;
}
}
List<Lesson> _applySort(List<Lesson> lessons, SortType type) {
switch (type) {
case SortType.duration:
return lessons..sort((a, b) => a.duration.compareTo(b.duration));
case SortType.difficulty:
return lessons..sort((a, b) => a.difficulty.index.compareTo(b.difficulty.index));
case SortType.sequence:
default:
return lessons;
}
}
}
enum SortType { sequence, duration, difficulty }
enum FilterType { all, completed, incomplete }
4.2 课程解锁机制
实现课程按顺序解锁的功能:
dart复制bool _isLessonLocked(List<Lesson> lessons, int index) {
if (index == 0) return false;
return !lessons[index - 1].completed;
}
Widget _buildLessonCard(BuildContext context, Lesson lesson, int index) {
final isLocked = _isLessonLocked(lessons, index);
return Stack(
children: [
Opacity(
opacity: isLocked ? 0.6 : 1.0,
child: Card(
// ...卡片内容
child: InkWell(
onTap: isLocked
? () => _showLockedDialog(context, lesson)
: () => _navigateToDetail(context, lesson),
// ...
),
),
),
if (isLocked)
Positioned(
right: 12.w,
top: 12.h,
child: Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: Colors.grey.shade700,
shape: BoxShape.circle,
),
child: Icon(
Icons.lock,
size: 16.sp,
color: Colors.white,
),
),
),
],
);
}
void _showLockedDialog(BuildContext context, Lesson lesson) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('课程已锁定'),
content: const Text('请先完成前面的课程才能解锁此课程'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('知道了'),
),
],
),
);
}
4.3 离线下载功能
实现课程离线下载和管理:
dart复制final downloadProvider = StateNotifierProvider<DownloadNotifier, Map<String, DownloadStatus>>((ref) {
return DownloadNotifier();
});
class DownloadNotifier extends StateNotifier<Map<String, DownloadStatus>> {
DownloadNotifier() : super({});
Future<void> downloadLesson(Lesson lesson) async {
// 更新状态为下载中
state = {...state, lesson.id: DownloadStatus.downloading};
try {
// 模拟下载过程
await Future.delayed(const Duration(seconds: 2));
// 下载完成
state = {...state, lesson.id: DownloadStatus.completed};
// 保存到本地数据库
await _saveToLocal(lesson);
} catch (e) {
// 下载失败
state = {...state, lesson.id: DownloadStatus.failed};
}
}
Future<void> _saveToLocal(Lesson lesson) async {
// 实现本地存储逻辑
}
}
enum DownloadStatus {
none,
downloading,
completed,
failed,
}
// 在课程卡片中添加下载按钮
Widget _buildDownloadButton(Lesson lesson) {
return Consumer(
builder: (context, ref, child) {
final downloadStatus = ref.watch(downloadProvider)[lesson.id] ?? DownloadStatus.none;
return IconButton(
icon: _getDownloadIcon(downloadStatus),
iconSize: 24.sp,
onPressed: () {
if (downloadStatus == DownloadStatus.none ||
downloadStatus == DownloadStatus.failed) {
ref.read(downloadProvider.notifier).downloadLesson(lesson);
}
},
);
},
);
}
Widget _getDownloadIcon(DownloadStatus status) {
switch (status) {
case DownloadStatus.downloading:
return const CircularProgressIndicator(strokeWidth: 2);
case DownloadStatus.completed:
return const Icon(Icons.download_done, color: Colors.green);
case DownloadStatus.failed:
return const Icon(Icons.error_outline, color: Colors.red);
case DownloadStatus.none:
default:
return const Icon(Icons.download);
}
}
5. 性能优化技巧
5.1 列表性能优化
对于可能包含大量课程的列表,我们需要特别注意性能优化:
dart复制ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: lessons.length,
itemBuilder: (context, index) {
final lesson = lessons[index];
return _buildLessonCard(context, lesson, index);
},
// 添加以下优化参数
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
cacheExtent: 500, // 预渲染区域高度
)
优化说明:
addAutomaticKeepAlives: false- 对于频繁更新的列表,禁用KeepAlive可以节省内存addRepaintBoundaries: false- 简单的卡片布局不需要额外的重绘边界cacheExtent- 设置合适的预渲染区域减少滚动时的卡顿
5.2 图片加载优化
如果课程包含封面图,使用cached_network_image优化图片加载:
dart复制CachedNetworkImage(
imageUrl: lesson.imageUrl,
width: 80.w,
height: 80.w,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.image)),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.broken_image)),
),
memCacheWidth: (80.w * MediaQuery.of(context).devicePixelRatio).toInt(),
memCacheHeight: (80.w * MediaQuery.of(context).devicePixelRatio).toInt(),
)
5.3 动画优化
为卡片点击添加高性能的动画效果:
dart复制InkWell(
onTap: () => _navigateToDetail(context, lesson),
borderRadius: BorderRadius.circular(12.r),
// 使用Material风格的涟漪效果
splashFactory: InkRipple.splashFactory,
// 自定义动画持续时间
splashColor: const Color(0xFF00897B).withOpacity(0.2),
highlightColor: const Color(0xFF00897B).withOpacity(0.1),
)
6. 测试与调试
6.1 单元测试
为课程列表编写单元测试:
dart复制void main() {
group('课程列表测试', () {
test('课程筛选测试', () {
final lessons = [
Lesson(id: '1', title: '课程1', completed: true, /* 其他参数 */),
Lesson(id: '2', title: '课程2', completed: false, /* 其他参数 */),
];
// 测试全部筛选
expect(filterLessons(lessons, FilterType.all).length, 2);
// 测试已完成筛选
expect(filterLessons(lessons, FilterType.completed).length, 1);
// 测试未完成筛选
expect(filterLessons(lessons, FilterType.incomplete).length, 1);
});
test('课程排序测试', () {
final lessons = [
Lesson(id: '1', duration: 10, difficulty: Difficulty.intermediate),
Lesson(id: '2', duration: 5, difficulty: Difficulty.beginner),
];
// 测试按时长排序
final byDuration = sortLessons(lessons, SortType.duration);
expect(byDuration[0].id, '2');
expect(byDuration[1].id, '1');
// 测试按难度排序
final byDifficulty = sortLessons(lessons, SortType.difficulty);
expect(byDifficulty[0].id, '2');
expect(byDifficulty[1].id, '1');
});
});
}
6.2 Widget测试
测试课程卡片Widget:
dart复制testWidgets('课程卡片Widget测试', (tester) async {
final lesson = Lesson(
id: 'test1',
title: '测试课程',
description: '这是一个测试课程',
duration: 10,
difficulty: Difficulty.beginner,
completed: false,
category: 'test',
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: LessonCard(lesson: lesson, index: 0),
),
),
);
// 验证标题显示
expect(find.text('测试课程'), findsOneWidget);
// 验证描述显示
expect(find.text('这是一个测试课程'), findsOneWidget);
// 验证时长显示
expect(find.text('10分钟'), findsOneWidget);
// 验证难度显示
expect(find.text('初级'), findsOneWidget);
// 验证点击事件
await tester.tap(find.byType(InkWell));
await tester.pump();
});
6.3 集成测试
完整的页面集成测试:
dart复制void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('课程列表集成测试', (tester) async {
// 启动App
await tester.pumpWidget(const MyApp());
// 等待数据加载
await tester.pumpAndSettle();
// 验证初始状态
expect(find.text('基础问候'), findsOneWidget);
expect(find.byType(Card), findsWidgets);
// 测试筛选功能
await tester.tap(find.byIcon(Icons.filter_list));
await tester.pumpAndSettle();
await tester.tap(find.text('已完成'));
await tester.pumpAndSettle();
// 验证筛选结果
expect(find.byType(Card), findsNWidgets(2)); // 假设有2个已完成课程
// 测试排序功能
await tester.tap(find.byIcon(Icons.sort));
await tester.pumpAndSettle();
await tester.tap(find.text('按时长排序'));
await tester.pumpAndSettle();
// 验证排序结果
final firstCard = tester.widgetList<Card>(find.byType(Card)).first;
// 这里需要根据实际实现验证排序结果
});
}
7. 常见问题与解决方案
7.1 列表滚动卡顿
问题现象:当课程数量较多时,列表滚动不流畅
解决方案:
- 确保使用ListView.builder而不是ListView
- 为卡片添加const构造函数
- 简化卡片布局层级
- 使用RepaintBoundary包裹复杂卡片
- 预加载图片资源
dart复制class LessonCard extends StatelessWidget {
const LessonCard({super.key, required this.lesson});
final Lesson lesson;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: // 卡片内容
);
}
}
7.2 状态更新不及时
问题现象:完成课程后返回列表,状态没有立即更新
解决方案:
- 使用状态管理库自动刷新
- 在页面返回时主动刷新数据
- 使用Stream实时更新状态
dart复制// 使用Riverpod的autoDispose确保每次进入页面都刷新数据
final lessonsProvider = FutureProvider.autoDispose.family<List<Lesson>, String>((ref, category) {
return ref.watch(lessonRepositoryProvider).getLessonsByCategory(category);
});
// 或者在页面返回时刷新
@override
void didPopNext() {
super.didPopNext();
ref.invalidate(lessonsProvider);
}
7.3 内存泄漏
问题现象:长时间使用后App内存占用持续增加
解决方案:
- 使用WeakReference持有上下文
- 及时取消网络请求和订阅
- 使用autoDispose自动释放资源
- 避免在全局对象中持有BuildContext
dart复制@override
void dispose() {
_scrollController.dispose();
_subscription?.cancel();
super.dispose();
}
7.4 跨平台适配问题
问题现象:在iOS和Android上显示效果不一致
解决方案:
- 使用Platform.isXX判断平台
- 为不同平台提供不同的样式
- 使用Cupertino和Material组件库
dart复制Card(
elevation: Platform.isIOS ? 0 : 2,
shape: Platform.isIOS
? null
: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
child: // ...
)
8. 项目扩展与优化方向
8.1 服务端集成
将硬编码的课程数据迁移到服务端:
- 设计RESTful API接口
- 实现分页加载
- 添加搜索功能
- 支持个性化推荐
dart复制class LessonRepository {
final Dio _dio;
Future<List<Lesson>> getLessonsByCategory(String category, {int page = 1}) async {
final response = await _dio.get('/lessons', queryParameters: {
'category': category,
'page': page,
'limit': 20,
});
return (response.data['data'] as List)
.map((json) => Lesson.fromJson(json))
.toList();
}
}
8.2 本地持久化
使用Hive或SQLite实现本地缓存:
dart复制class LessonLocalDataSource {
final Box<Lesson> _lessonBox;
Future<List<Lesson>> getLessonsByCategory(String category) async {
return _lessonBox.values
.where((lesson) => lesson.category == category)
.toList();
}
Future<void> cacheLessons(List<Lesson> lessons) async {
await _lessonBox.clear();
await _lessonBox.addAll(lessons);
}
}
8.3 动画增强
为列表添加更丰富的动画效果:
dart复制ListView.builder(
itemBuilder: (context, index) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: LessonCard(
key: ValueKey(lessons[index].id),
lesson: lessons[index],
index: index,
),
);
},
)
8.4 无障碍支持
增强无障碍访问能力:
dart复制Semantics(
label: '课程卡片: ${lesson.title}',
value: '时长${lesson.duration}分钟, 难度${lesson.difficulty.label}',
child: ExcludeSemantics(
child: // 实际卡片内容
),
)
8.5 主题与国际化
支持多主题和国际化:
dart复制MaterialApp(
theme: ThemeData.light().copyWith(
primaryColor: Colors.teal,
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
darkTheme: ThemeData.dark().copyWith(
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
)
9. 项目部署与发布
9.1 构建发布版本
生成发布版APK和IPA:
bash复制# 构建Android发布版
flutter build apk --release
# 或构建App Bundle
flutter build appbundle
# 构建iOS发布版
flutter build ios --release
9.2 应用商店优化
优化应用商店列表:
- 准备高质量的应用截图
- 编写吸引人的应用描述
- 选择合适的关键词
- 设计精美的应用图标
9.3 性能监控
集成Firebase性能监控:
dart复制void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
// 在关键操作中添加跟踪
final trace = FirebasePerformance.instance.newTrace('lesson_list_load');
await trace.start();
// 加载数据...
await trace.stop();
9.4 错误报告
集成Firebase Crashlytics:
dart复制void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
runApp(const MyApp());
}
// 手动记录错误
try {
// 可能出错的代码
} catch (e, s) {
FirebaseCrashlytics.instance.recordError(e, s);
}
10. 项目维护与更新
10.1 版本控制策略
采用语义化版本控制:
- MAJOR版本:不兼容的API修改
- MINOR版本:向下兼容的功能新增
- PATCH版本:向下兼容的问题修正
10.2 持续集成
配置GitHub Actions自动化流程:
yaml复制name: Flutter CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
- run: flutter pub get
- run: flutter test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
- run: flutter pub get
- run: flutter build apk --release
10.3 依赖更新
定期更新依赖项:
bash复制flutter pub outdated
flutter pub upgrade
10.4 用户反馈收集
集成用户反馈系统:
dart复制FloatingActionButton(
onPressed: () => showFeedbackDialog(context),
child: const Icon(Icons.feedback),
)
void showFeedbackDialog(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => FeedbackForm(
onSubmit: (feedback) {
// 提交到服务器
FirebaseFirestore.instance.collection('feedback').add({
'message': feedback,
'timestamp': FieldValue.serverTimestamp(),
});
},
),
);
}
11. 项目总结与经验分享
在开发手语学习App的课程列表功能过程中,我积累了一些有价值的经验:
-
性能优先:对于高频使用的列表页面,性能优化应该从设计阶段就开始考虑,而不是后期补救。
-
状态管理:选择合适的状