🎯 项目实战的重要性
为什么要做实战项目?
学习 Flutter 的过程中,仅仅阅读文档和看教程是远远不够的。实战项目是将零散知识融会贯通的最佳方式。通过构建完整的应用,你会遇到真实的技术挑战——状态管理的取舍、网络请求的异常处理、UI 的适配细节、性能的优化等等。这些经验是任何教程无法替代的。
实战项目还能帮助你建立技术作品集。在求职面试中,能展示一个完整的、可运行的应用,比任何口头描述都更有说服力。
如何正确地做一个项目?
📋 第一步:需求分析
在写代码之前,先明确要做什么。列出核心功能、次要功能,画出页面流程图。不要一上来就写代码。
🏗️ 第二步:架构设计
选择合适的架构模式(MVC / MVVM / Clean Architecture),确定状态管理方案和项目目录结构。
🔨 第三步:逐步实现
从最核心的功能开始,先实现 MVP(最小可行产品),再逐步迭代。不要试图一次做完所有功能。
🧪 第四步:测试与打磨
编写单元测试和集成测试,处理边界情况,打磨 UI 细节和动画过渡效果。
项目难度递进路线
| 难度 | 项目 | 核心技术点 | 预计耗时 |
|---|---|---|---|
| ⭐ 入门 | 计算器 App | 布局、setState、事件处理 | 1-2 天 |
| ⭐⭐ 初中级 | Todo 待办应用 | CRUD、Provider、本地存储 | 3-5 天 |
| ⭐⭐⭐ 中级 | 天气预报 App | 网络请求、API、JSON、定位 | 5-7 天 |
| ⭐⭐⭐ 中级 | 新闻阅读 App | 分页加载、WebView、收藏 | 5-7 天 |
| ⭐⭐⭐⭐ 进阶 | 电商 App | Clean Architecture、认证、购物车 | 2-3 周 |
| ⭐⭐⭐⭐⭐ 高级 | 社交聊天 App | Firebase、实时通信、推送 | 3-4 周 |
🔢 计算器 App
计算器是入门 Flutter 的经典项目。它涉及网格布局(GridView)、状态管理(setState)和基本的业务逻辑处理。完成这个项目后,你将对 Flutter 的 Widget 系统和事件处理有深入的理解。
功能需求
- 支持加、减、乘、除四则运算
- 支持小数点输入
- 显示当前表达式和计算结果
- 支持清除(C)和退格功能
- 漂亮的按钮网格布局
项目结构
Project lib/ ├── main.dart // 应用入口 ├── calculator_screen.dart // 主界面 ├── calculator_button.dart // 按钮组件 └── calculator_logic.dart // 计算逻辑
完整代码:计算逻辑
Dart // calculator_logic.dart class CalculatorLogic { String _expression = ''; String _result = '0'; double? _firstOperand; String? _operator; bool _shouldResetInput = false; String get expression => _expression; String get result => _result; void onButtonPressed(String value) { if (value == 'C') { _clear(); } else if (value == '⌫') { _backspace(); } else if (value == '=') { _calculate(); } else if ('+-×÷'.contains(value)) { _setOperator(value); } else { _appendDigit(value); } } void _clear() { _expression = ''; _result = '0'; _firstOperand = null; _operator = null; _shouldResetInput = false; } void _backspace() { if (_result.length > 1) { _result = _result.substring(0, _result.length - 1); } else { _result = '0'; } } void _setOperator(String op) { _firstOperand = double.tryParse(_result); _operator = op; _expression = '$_result $op '; _shouldResetInput = true; } void _appendDigit(String digit) { if (_shouldResetInput) { _result = digit == '.' ? '0.' : digit; _shouldResetInput = false; } else { if (digit == '.' && _result.contains('.')) return; _result = _result == '0' && digit != '.' ? digit : _result + digit; } } void _calculate() { if (_firstOperand == null || _operator == null) return; final second = double.tryParse(_result) ?? 0; double res; switch (_operator) { case '+': res = _firstOperand! + second; break; case '-': res = _firstOperand! - second; break; case '×': res = _firstOperand! * second; break; case '÷': if (second == 0) { _result = '错误'; return; } res = _firstOperand! / second; break; default: return; } _expression = '$_firstOperand $_operator $second ='; _result = res == res.toInt() ? res.toInt().toString() : res.toStringAsFixed(8); _firstOperand = null; _operator = null; } }
完整代码:主界面
Dart // calculator_screen.dart import 'package:flutter/material.dart'; import 'calculator_logic.dart'; class CalculatorScreen extends StatefulWidget { const CalculatorScreen({super.key}); @override State<CalculatorScreen> createState() => _CalculatorScreenState(); } class _CalculatorScreenState extends State<CalculatorScreen> { final _logic = CalculatorLogic(); final List<String> buttons = [ 'C', '⌫', '÷', '×', '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '=', '0', '.', ]; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Color(0xFF1E1E2C), body: SafeArea( child: Column( children: [ // 显示区域 Expanded( flex: 2, child: Container( padding: EdgeInsets.all(24), alignment: Alignment.bottomRight, child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(_logic.expression, style: TextStyle(color: Colors.white54, fontSize: 20)), SizedBox(height: 8), Text(_logic.result, style: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold)), ], ), ), ), // 按钮网格 Expanded( flex: 4, child: GridView.builder( padding: EdgeInsets.all(12), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: buttons.length, itemBuilder: (ctx, i) { return ElevatedButton( onPressed: () => setState(() => _logic.onButtonPressed(buttons[i])), style: ElevatedButton.styleFrom( backgroundColor: _getButtonColor(buttons[i]), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), ), child: Text(buttons[i], style: TextStyle(fontSize: 24, color: Colors.white)), ); }, ), ), ], ), ), ); } Color _getButtonColor(String btn) { if (btn == 'C' || btn == '⌫') return Color(0xFFEF4444); if ('+-×÷='.contains(btn)) return Color(0xFF6366F1); return Color(0xFF2D2D44); } }
🏋️ 扩展练习
- 添加百分比(%)按钮和正负号切换(±)按钮
- 支持连续运算(如 1 + 2 + 3)
- 添加计算历史记录功能,使用 ListView 展示
- 加入按钮点击动画效果
✅ Todo 待办应用
待办应用是学习 CRUD(增删改查)操作、状态管理和本地持久化存储的经典项目。我们将使用 Provider 管理状态,Hive 实现本地数据持久化。
功能需求
- 添加、编辑、删除待办事项
- 标记已完成 / 未完成
- 按分类(工作、生活、学习)筛选
- 数据本地持久化存储
- 滑动删除手势支持
项目结构
Project lib/ ├── main.dart ├── models/ │ └── todo.dart // 数据模型 ├── providers/ │ └── todo_provider.dart // 状态管理 ├── screens/ │ ├── todo_list_screen.dart // 列表页 │ └── add_todo_screen.dart // 添加/编辑页 ├── widgets/ │ └── todo_tile.dart // 列表项组件 └── services/ └── hive_service.dart // 本地存储服务
数据模型
Dart import 'package:hive/hive.dart'; part 'todo.g.dart'; @HiveType(typeId: 0) enum TodoCategory { @HiveField(0) work, @HiveField(1) life, @HiveField(2) study, } @HiveType(typeId: 1) class Todo extends HiveObject { @HiveField(0) String title; @HiveField(1) String description; @HiveField(2) bool isCompleted; @HiveField(3) TodoCategory category; @HiveField(4) DateTime createdAt; Todo({ required this.title, this.description = '', this.isCompleted = false, this.category = TodoCategory.life, DateTime? createdAt, }) : createdAt = createdAt ?? DateTime.now(); }
Provider 状态管理
Dart import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../models/todo.dart'; class TodoProvider extends ChangeNotifier { late Box<Todo> _todoBox; TodoCategory? _filterCategory; List<Todo> _todos = []; List<Todo> get todos { if (_filterCategory == null) return _todos; return _todos.where((t) => t.category == _filterCategory).toList(); } int get completedCount => _todos.where((t) => t.isCompleted).length; // 初始化 Hive 并加载数据 Future<void> init() async { _todoBox = await Hive.openBox<Todo>('todos'); _todos = _todoBox.values.toList(); notifyListeners(); } // 添加待办 Future<void> addTodo(Todo todo) async { await _todoBox.add(todo); _todos = _todoBox.values.toList(); notifyListeners(); } // 切换完成状态 Future<void> toggleTodo(int index) async { final todo = _todos[index]; todo.isCompleted = !todo.isCompleted; await todo.save(); notifyListeners(); } // 删除待办 Future<void> deleteTodo(int index) async { await _todos[index].delete(); _todos = _todoBox.values.toList(); notifyListeners(); } // 筛选分类 void setFilter(TodoCategory? category) { _filterCategory = category; notifyListeners(); } }
列表页面(带滑动删除)
Dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/todo_provider.dart'; class TodoListScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('我的待办')), floatingActionButton: FloatingActionButton( onPressed: () => Navigator.pushNamed(context, '/add'), child: Icon(Icons.add), ), body: Consumer<TodoProvider>( builder: (ctx, provider, _) { final todos = provider.todos; if (todos.isEmpty) { return Center(child: Text('暂无待办事项,点击 + 添加')); } return ListView.builder( itemCount: todos.length, itemBuilder: (ctx, i) { final todo = todos[i]; return Dismissible( key: ValueKey(todo.key), direction: DismissDirection.endToStart, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), onDismissed: (_) => provider.deleteTodo(i), child: ListTile( leading: Checkbox( value: todo.isCompleted, onChanged: (_) => provider.toggleTodo(i), ), title: Text(todo.title, style: TextStyle( decoration: todo.isCompleted ? TextDecoration.lineThrough : null, )), subtitle: Text(todo.description), ), ); }, ); }, ), ); } }
main.dart 中调用 Hive.initFlutter(),并注册 TypeAdapter:Hive.registerAdapter(TodoAdapter())。运行 flutter pub run build_runner build 生成 .g.dart 文件。
🏋️ 扩展练习
- 添加截止日期功能,使用 DatePicker
- 添加优先级标签(高 / 中 / 低),并支持按优先级排序
- 实现搜索功能
- 添加统计页面,展示完成率图表
🌤️ 天气预报 App
天气预报应用是学习网络请求、API 接口对接、JSON 解析、GPS 定位和动画效果的综合练习。我们将使用 Dio 进行网络请求,OpenWeatherMap API 获取天气数据,Geolocator 获取定位,Lottie 展示天气动画。
功能需求
- 自动获取当前位置天气
- 支持手动搜索城市
- 显示当前温度、湿度、风速、天气状况
- 展示未来 5 天天气预报
- Lottie 天气动画效果
- 下拉刷新
依赖配置
YAML # pubspec.yaml dependencies: dio: ^5.4.0 geolocator: ^11.0.0 geocoding: ^3.0.0 lottie: ^3.1.0 intl: ^0.19.0 flutter_riverpod: ^2.5.0
天气数据模型
Dart class Weather { final String cityName; final double temperature; final double feelsLike; final int humidity; final double windSpeed; final String description; final String icon; final DateTime dateTime; const Weather({ required this.cityName, required this.temperature, required this.feelsLike, required this.humidity, required this.windSpeed, required this.description, required this.icon, required this.dateTime, }); factory Weather.fromJson(Map<String, dynamic> json, String city) { return Weather( cityName: city, temperature: (json['main']['temp'] as num).toDouble(), feelsLike: (json['main']['feels_like'] as num).toDouble(), humidity: json['main']['humidity'] as int, windSpeed: (json['wind']['speed'] as num).toDouble(), description: json['weather'][0]['description'] as String, icon: json['weather'][0]['icon'] as String, dateTime: DateTime.fromMillisecondsSinceEpoch( json['dt'] * 1000), ); } }
API 服务封装
Dart import 'package:dio/dio.dart'; import '../models/weather.dart'; class WeatherService { static const _apiKey = 'YOUR_API_KEY'; static const _baseUrl = 'https://api.openweathermap.org/data/2.5'; final Dio _dio = Dio(BaseOptions( baseUrl: _baseUrl, queryParameters: { 'appid': _apiKey, 'units': 'metric', 'lang': 'zh_cn', }, connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 10), )); // 根据经纬度获取当前天气 Future<Weather> getCurrentWeather(double lat, double lon) async { try { final response = await _dio.get('/weather', queryParameters: {'lat': lat, 'lon': lon}); return Weather.fromJson(response.data, response.data['name']); } on DioException catch (e) { throw Exception('获取天气失败: ${e.message}'); } } // 获取 5 天预报 Future<List<Weather>> getForecast(double lat, double lon) async { final response = await _dio.get('/forecast', queryParameters: {'lat': lat, 'lon': lon}); final list = response.data['list'] as List; final cityName = response.data['city']['name'] as String; return list.map((e) => Weather.fromJson(e, cityName)).toList(); } // 根据城市名搜索 Future<Weather> getWeatherByCity(String city) async { final response = await _dio.get('/weather', queryParameters: {'q': city}); return Weather.fromJson(response.data, response.data['name']); } }
Lottie 天气动画
Dart import 'package:lottie/lottie.dart'; class WeatherAnimation extends StatelessWidget { final String iconCode; const WeatherAnimation({required this.iconCode, super.key}); String get _assetPath { switch (iconCode) { case '01d': return 'assets/lottie/sunny.json'; case '01n': return 'assets/lottie/night.json'; case '02d': case '02n': return 'assets/lottie/cloudy.json'; case '09d': case '10d': return 'assets/lottie/rainy.json'; case '13d': return 'assets/lottie/snow.json'; case '11d': return 'assets/lottie/thunder.json'; default: return 'assets/lottie/cloudy.json'; } } @override Widget build(BuildContext context) { return Lottie.asset(_assetPath, width: 200, height: 200); } }
https://openweathermap.org/api 注册获取。免费套餐支持每分钟 60 次请求,足够开发和测试使用。
🏋️ 扩展练习
- 添加城市收藏功能,支持管理多个城市
- 根据天气条件动态切换背景颜色 / 渐变
- 添加空气质量指数(AQI)展示
- 实现生活指数建议(穿衣、出行、运动等)
📰 新闻阅读 App
新闻阅读应用将帮助你掌握分页加载(Pagination)、WebView 网页内嵌、收藏/书签功能以及Tab 分类导航。这是一个非常接近真实产品的中级项目。
功能需求
- 新闻分类 Tab(头条、科技、体育、娱乐等)
- 下拉刷新 + 上拉加载更多(分页)
- 新闻详情页(WebView 内嵌浏览)
- 收藏 / 取消收藏
- 搜索新闻
- 离线缓存已读新闻
项目架构
Project lib/ ├── main.dart ├── core/ │ ├── api_client.dart // Dio 封装 │ ├── constants.dart // API 地址、分类常量 │ └── extensions.dart // 日期格式化等扩展 ├── models/ │ └── article.dart // 新闻文章模型 ├── providers/ │ ├── news_provider.dart // 新闻列表状态 │ └── bookmark_provider.dart // 收藏状态 ├── screens/ │ ├── home_screen.dart // 首页(Tab 分类) │ ├── detail_screen.dart // WebView 详情页 │ ├── search_screen.dart // 搜索页 │ └── bookmark_screen.dart // 收藏页 └── widgets/ ├── news_card.dart // 新闻卡片 └── shimmer_loading.dart // 骨架屏加载
分页加载核心逻辑
Dart class NewsProvider extends ChangeNotifier { final ApiClient _api; List<Article> _articles = []; int _currentPage = 1; bool _isLoading = false; bool _hasMore = true; String _category = 'general'; NewsProvider(this._api); List<Article> get articles => _articles; bool get isLoading => _isLoading; bool get hasMore => _hasMore; // 切换分类 void changeCategory(String category) { _category = category; _articles.clear(); _currentPage = 1; _hasMore = true; fetchNews(); } // 下拉刷新 Future<void> refresh() async { _currentPage = 1; _hasMore = true; _articles.clear(); await fetchNews(); } // 获取新闻(支持分页) Future<void> fetchNews() async { if (_isLoading || !_hasMore) return; _isLoading = true; notifyListeners(); try { final result = await _api.getTopHeadlines( category: _category, page: _currentPage, pageSize: 20, ); if (result.isEmpty) { _hasMore = false; } else { _articles.addAll(result); _currentPage++; } } catch (e) { // 错误处理 } finally { _isLoading = false; notifyListeners(); } } }
WebView 详情页
Dart import 'package:webview_flutter/webview_flutter.dart'; class DetailScreen extends StatefulWidget { final String url; final String title; const DetailScreen({required this.url, required this.title, super.key}); @override State<DetailScreen> createState() => _DetailScreenState(); } class _DetailScreenState extends State<DetailScreen> { late final WebViewController _controller; double _progress = 0; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( onProgress: (p) => setState(() => _progress = p / 100), )) ..loadRequest(Uri.parse(widget.url)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), actions: [ IconButton( icon: Icon(Icons.bookmark_border), onPressed: () { /* 收藏逻辑 */ }, ), IconButton( icon: Icon(Icons.share), onPressed: () { /* 分享逻辑 */ }, ), ], bottom: _progress < 1 ? PreferredSize( preferredSize: Size.fromHeight(2), child: LinearProgressIndicator(value: _progress), ) : null, ), body: WebViewWidget(controller: _controller), ); } }
NewsAPI.org 的免费接口(开发者版每天 100 次请求),也可以使用国内的聚合数据等 API 服务。
🏋️ 扩展练习
- 实现骨架屏(Shimmer Loading)加载效果
- 添加夜间模式切换
- 实现新闻图片缓存(cached_network_image)
- 添加文章阅读时长估算功能
🛒 电商 App
电商应用是进阶级别的综合项目,涉及Clean Architecture 架构、Riverpod 状态管理、用户认证、购物车、订单结算等多个复杂模块。这个项目可以直接作为面试作品集展示。
功能模块
🔐 用户认证
- 注册 / 登录 / 退出
- JWT Token 管理
- 自动刷新 Token
- 个人资料编辑
🏠 商品浏览
- 商品分类与列表
- 商品详情页
- 搜索与筛选
- 轮播图展示
🛒 购物车
- 添加 / 移除商品
- 修改数量
- 实时价格计算
- 本地持久化存储
💳 订单结算
- 收货地址管理
- 支付方式选择
- 订单确认与提交
- 订单历史记录
Clean Architecture 项目结构
Project lib/ ├── core/ │ ├── error/ │ │ ├── failures.dart // 统一错误类型 │ │ └── exceptions.dart // 异常定义 │ ├── network/ │ │ ├── api_client.dart // Dio + 拦截器 │ │ └── network_info.dart // 网络状态检查 │ ├── usecase/ │ │ └── usecase.dart // UseCase 基类 │ └── router/ │ └── app_router.dart // GoRouter 路由 ├── features/ │ ├── auth/ │ │ ├── data/ │ │ │ ├── datasources/ // Remote + Local │ │ │ ├── models/ // DTO │ │ │ └── repositories/ // 仓库实现 │ │ ├── domain/ │ │ │ ├── entities/ // 实体 │ │ │ ├── repositories/ // 仓库接口 │ │ │ └── usecases/ // 用例 │ │ └── presentation/ │ │ ├── providers/ // Riverpod Providers │ │ ├── screens/ // 页面 │ │ └── widgets/ // 组件 │ ├── product/ // 同上结构 │ ├── cart/ // 同上结构 │ └── order/ // 同上结构 └── main.dart
认证模块:Token 拦截器
Dart import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class AuthInterceptor extends Interceptor { final Ref _ref; AuthInterceptor(this._ref); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final token = _ref.read(authTokenProvider); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401) { // 尝试刷新 Token try { final newToken = await _ref.read(authProvider.notifier).refreshToken(); err.requestOptions.headers['Authorization'] = 'Bearer $newToken'; final response = await Dio().fetch(err.requestOptions); handler.resolve(response); return; } catch (_) { _ref.read(authProvider.notifier).logout(); } } handler.next(err); } }
购物车模块:Riverpod Provider
Dart import 'package:flutter_riverpod/flutter_riverpod.dart'; class CartItem { final String productId; final String name; final double price; final String imageUrl; int quantity; CartItem({ required this.productId, required this.name, required this.price, required this.imageUrl, this.quantity = 1, }); double get totalPrice => price * quantity; } class CartNotifier extends StateNotifier<List<CartItem>> { CartNotifier() : super([]); double get totalAmount => state.fold(0, (sum, item) => sum + item.totalPrice); int get itemCount => state.length; void addItem(CartItem item) { final index = state.indexWhere((e) => e.productId == item.productId); if (index != -1) { state[index].quantity++; state = [...state]; // 触发更新 } else { state = [...state, item]; } } void removeItem(String productId) { state = state.where((e) => e.productId != productId).toList(); } void updateQuantity(String productId, int quantity) { if (quantity <= 0) { removeItem(productId); return; } final index = state.indexWhere((e) => e.productId == productId); if (index != -1) { state[index].quantity = quantity; state = [...state]; } } void clear() => state = []; } final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>( (ref) => CartNotifier(), );
结算流程关键代码
Dart class CheckoutScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final cartItems = ref.watch(cartProvider); final cart = ref.read(cartProvider.notifier); final address = ref.watch(selectedAddressProvider); return Scaffold( appBar: AppBar(title: Text('确认订单')), body: ListView( padding: EdgeInsets.all(16), children: [ // 收货地址卡片 _AddressCard(address: address), SizedBox(height: 16), // 商品清单 ...cartItems.map((item) => _OrderItemTile(item: item)), Divider(), // 价格明细 _PriceSummary( subtotal: cart.totalAmount, shipping: 10.0, discount: 0, ), ], ), bottomNavigationBar: SafeArea( child: Padding( padding: EdgeInsets.all(16), child: ElevatedButton( onPressed: () async { final order = await ref .read(orderProvider.notifier) .placeOrder(cartItems, address!); cart.clear(); Navigator.pushReplacementNamed( context, '/order-success', arguments: order.id, ); }, style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 16), ), child: Text('提交订单 ¥${(cart.totalAmount + 10).toStringAsFixed(2)}'), ), ), ), ); } }
💬 社交聊天 App
社交聊天应用是高级项目,涉及Firebase 全家桶(Authentication、Cloud Firestore、Cloud Messaging、Storage)、实时数据同步和推送通知。这个项目将让你掌握 BaaS(Backend as a Service)的完整使用方式。
功能需求
- 邮箱 / Google 第三方登录
- 联系人列表与在线状态
- 一对一实时聊天
- 发送文字、图片、语音消息
- 消息已读 / 未读状态
- 推送通知(FCM)
- 用户头像与个人资料
Firebase 初始化
Dart import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'firebase_options.dart'; // 后台消息处理(必须是顶层函数) @pragma('vm:entry-point') Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage msg) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); print('后台收到消息: ${msg.messageId}'); } Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); runApp(MyApp()); }
聊天服务:实时消息
Dart import 'package:cloud_firestore/cloud_firestore.dart'; class ChatService { final _firestore = FirebaseFirestore.instance; // 获取或创建聊天房间 ID String getChatRoomId(String uid1, String uid2) { return uid1.compareTo(uid2) < 0 ? '${uid1}_$uid2' : '${uid2}_$uid1'; } // 发送消息 Future<void> sendMessage({ required String chatRoomId, required String senderId, required String content, String type = 'text', }) async { final message = { 'senderId': senderId, 'content': content, 'type': type, 'timestamp': FieldValue.serverTimestamp(), 'isRead': false, }; await _firestore .collection('chatRooms') .doc(chatRoomId) .collection('messages') .add(message); // 更新最后一条消息(用于列表预览) await _firestore.collection('chatRooms').doc(chatRoomId).update({ 'lastMessage': content, 'lastMessageTime': FieldValue.serverTimestamp(), 'lastSenderId': senderId, }); } // 实时监听消息流 Stream<QuerySnapshot> getMessages(String chatRoomId) { return _firestore .collection('chatRooms') .doc(chatRoomId) .collection('messages') .orderBy('timestamp', descending: true) .snapshots(); } // 标记消息已读 Future<void> markAsRead(String chatRoomId, String currentUserId) async { final unread = await _firestore .collection('chatRooms') .doc(chatRoomId) .collection('messages') .where('isRead', isEqualTo: false) .where('senderId', isNotEqualTo: currentUserId) .get(); final batch = _firestore.batch(); for (final doc in unread.docs) { batch.update(doc.reference, {'isRead': true}); } await batch.commit(); } }
推送通知配置
Dart class NotificationService { final _messaging = FirebaseMessaging.instance; Future<void> initialize() async { // 请求通知权限 final settings = await _messaging.requestPermission( alert: true, badge: true, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { // 获取 FCM Token final token = await _messaging.getToken(); print('FCM Token: $token'); // 监听前台消息 FirebaseMessaging.onMessage.listen((message) { _showLocalNotification(message); }); // 用户点击通知打开 App FirebaseMessaging.onMessageOpenedApp.listen((message) { _navigateToChatRoom(message); }); } } }
flutterfire configure 命令自动生成配置文件。
🏋️ 扩展练习
- 实现群聊功能
- 添加语音消息发送(使用 record 插件)
- 实现消息撤回功能
- 添加"对方正在输入..."状态提示
🚀 完整上架指南
开发完应用只是第一步,将它成功发布到应用商店才算真正完成。本章将系统地介绍从 MVP 到上架的完整流程。
MVP 方法论
MVP(Minimum Viable Product,最小可行产品)的核心思想是:用最小的成本验证你的产品想法。不要试图一次做出完美的产品,而是先做出一个能跑通核心流程的版本,尽快推向市场收集反馈。
✅ MVP 应该包含的
- 1-3 个核心功能
- 基本的 UI/UX(不需要完美)
- 稳定不崩溃
- 完整的用户流程闭环
❌ MVP 可以不包含的
- 社交分享、第三方登录
- 复杂的设置选项
- 数据分析统计面板
- 多语言国际化
上架前检查清单
| 类别 | 检查项 | 说明 |
|---|---|---|
| 应用信息 | 应用名称与图标 | 1024x1024 图标,名称简洁不超过 30 字符 |
| 应用信息 | 截图和预览视频 | 至少 3-5 张高质量截图,涵盖核心功能 |
| 应用信息 | 应用描述 | 简明扼要,突出核心卖点,包含关键词 |
| 技术 | 签名配置 | Android Keystore / iOS 证书 + Provisioning Profile |
| 技术 | 版本号 | 遵循语义化版本(1.0.0),build number 递增 |
| 技术 | 混淆与压缩 | Android 开启 ProGuard / R8,减小包体积 |
| 合规 | 隐私政策 | 必须有可访问的隐私政策页面 URL |
| 合规 | 权限说明 | iOS 的 Info.plist 权限描述要清晰准确 |
| 测试 | 全面测试 | 多设备测试、弱网测试、异常流程测试 |
| 测试 | 性能检查 | 启动时间 < 3s,无明显卡顿,内存无泄漏 |
发布到 Google Play
- 注册 Google Play 开发者账号(一次性 $25)
- 在 Google Play Console 创建应用
- 上传
app-release.aab(App Bundle 格式) - 填写商品详情(标题、描述、截图、分类)
- 完成内容分级问卷
- 设置定价和分发国家
- 提交审核(通常 1-3 天)
发布到 App Store
- 注册 Apple Developer Program(每年 $99)
- 在 App Store Connect 创建应用
- 使用 Xcode 或
flutter build ipa打包 - 通过 Transporter 或 Xcode 上传构建
- 填写应用信息、截图、隐私政策
- 提交 App Review 审核(通常 1-7 天)
上架后监控
应用发布后,持续监控是保持应用质量的关键:
- 崩溃监控:使用 Firebase Crashlytics 或 Sentry 追踪崩溃日志
- 用户行为分析:使用 Firebase Analytics 了解用户使用习惯
- 性能监控:使用 Firebase Performance Monitoring 追踪启动时间、网络请求耗时
- 用户反馈:关注应用商店评论,及时回复和修复问题
- 版本迭代:根据数据和反馈定期发布更新
📚 学习总结
学习路径回顾
恭喜你走到了 Flutter 学习之旅的最后!让我们回顾一下整个学习过程中所掌握的技能:
| 阶段 | 核心技能 | 对应项目 |
|---|---|---|
| 基础入门 | Dart 语言、Widget 树、布局系统、setState | 计算器 App |
| 状态管理 | Provider、Riverpod、状态提升 | Todo 待办应用 |
| 网络与数据 | Dio、REST API、JSON 序列化、本地存储 | 天气预报 / 新闻阅读 |
| 架构设计 | Clean Architecture、依赖注入、Repository 模式 | 电商 App |
| 后端服务 | Firebase、实时数据库、推送通知、文件存储 | 社交聊天 App |
| 工程化 | CI/CD、测试、国际化、应用发布 | 完整上架流程 |
下一步学习方向
🎨 高级 UI
- CustomPainter 自定义绘制
- 复杂动画与 Rive 集成
- Sliver 高级滚动效果
- Platform Views 原生嵌入
🔌 平台集成
- Platform Channel 原生通信
- 编写 Flutter Plugin
- Flutter Web 与 Desktop
- 嵌入式设备开发
⚡ 性能优化
- DevTools 性能分析
- 渲染优化与 RepaintBoundary
- 内存泄漏排查
- 包体积优化
🤖 前沿技术
- AI 集成(TensorFlow Lite)
- AR/VR 增强现实
- Dart FFI 与 Rust 集成
- Impeller 渲染引擎
职业发展建议
Flutter 开发者在市场上需求量持续增长。以下是一些职业发展建议:
- 建立技术博客:记录学习过程和项目经验,不仅能加深理解,还能建立个人品牌
- 参与开源社区:为 Flutter 生态贡献代码,提交 PR 到流行的开源库
- 上架个人应用:至少有一个完整的、上架的个人应用,这是最好的简历加分项
- 持续关注更新:Flutter 迭代速度很快,关注官方博客和 Release Notes
- 拓展全栈能力:了解后端开发(Dart Shelf/Serverpod、Node.js)和 DevOps 基础
🎯 终极挑战
选择一个你真正感兴趣的项目创意,从零开始独立完成,并发布到 Google Play 或 App Store。记住:
- 从 MVP 开始,先上线再迭代
- 写好 README 和项目文档
- 将源码发布到 GitHub,展示你的代码质量
- 收集用户反馈,持续改进
- 把整个过程写成博客分享给社区