← 返回学习路线
💡

实战项目

从入门到进阶,通过真实项目巩固 Flutter 开发技能,涵盖计算器、待办、天气、新闻、电商、聊天等完整应用

🎯 项目实战的重要性

为什么要做实战项目?

学习 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. 支持连续运算(如 1 + 2 + 3)
  3. 添加计算历史记录功能,使用 ListView 展示
  4. 加入按钮点击动画效果

✅ 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),
                ),
              );
            },
          );
        },
      ),
    );
  }
}
注意:使用 Hive 前需要在 main.dart 中调用 Hive.initFlutter(),并注册 TypeAdapter:Hive.registerAdapter(TodoAdapter())。运行 flutter pub run build_runner build 生成 .g.dart 文件。

🏋️ 扩展练习

  1. 添加截止日期功能,使用 DatePicker
  2. 添加优先级标签(高 / 中 / 低),并支持按优先级排序
  3. 实现搜索功能
  4. 添加统计页面,展示完成率图表

🌤️ 天气预报 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);
  }
}
提示:OpenWeatherMap 提供免费 API Key,访问 https://openweathermap.org/api 注册获取。免费套餐支持每分钟 60 次请求,足够开发和测试使用。

🏋️ 扩展练习

  1. 添加城市收藏功能,支持管理多个城市
  2. 根据天气条件动态切换背景颜色 / 渐变
  3. 添加空气质量指数(AQI)展示
  4. 实现生活指数建议(穿衣、出行、运动等)

📰 新闻阅读 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),
    );
  }
}
推荐 API:可以使用 NewsAPI.org 的免费接口(开发者版每天 100 次请求),也可以使用国内的聚合数据等 API 服务。

🏋️ 扩展练习

  1. 实现骨架屏(Shimmer Loading)加载效果
  2. 添加夜间模式切换
  3. 实现新闻图片缓存(cached_network_image)
  4. 添加文章阅读时长估算功能

🛒 电商 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)}'),
          ),
        ),
      ),
    );
  }
}
架构建议:电商项目代码量较大,务必严格遵循 Clean Architecture 的分层原则。Data 层不要依赖 Domain 层以外的内容,Presentation 层只通过 UseCase 与 Domain 交互。

💬 社交聊天 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);
      });
    }
  }
}
提示:Firebase 提供 Spark(免费)套餐,包含 1GB Firestore 存储、10GB 每月数据传输、50K 每日消息推送,足够个人项目使用。使用 flutterfire configure 命令自动生成配置文件。

🏋️ 扩展练习

  1. 实现群聊功能
  2. 添加语音消息发送(使用 record 插件)
  3. 实现消息撤回功能
  4. 添加"对方正在输入..."状态提示

🚀 完整上架指南

开发完应用只是第一步,将它成功发布到应用商店才算真正完成。本章将系统地介绍从 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

  1. 注册 Google Play 开发者账号(一次性 $25)
  2. 在 Google Play Console 创建应用
  3. 上传 app-release.aab(App Bundle 格式)
  4. 填写商品详情(标题、描述、截图、分类)
  5. 完成内容分级问卷
  6. 设置定价和分发国家
  7. 提交审核(通常 1-3 天)

发布到 App Store

  1. 注册 Apple Developer Program(每年 $99)
  2. 在 App Store Connect 创建应用
  3. 使用 Xcode 或 flutter build ipa 打包
  4. 通过 Transporter 或 Xcode 上传构建
  5. 填写应用信息、截图、隐私政策
  6. 提交 App Review 审核(通常 1-7 天)
注意:Apple 审核较严格,常见被拒原因包括:隐私政策不完整、权限用途说明不清、使用了私有 API、UI 不符合 Human Interface Guidelines、应用过于简单("demo-like")等。建议提交前仔细阅读 App Store Review Guidelines。

上架后监控

应用发布后,持续监控是保持应用质量的关键:

  • 崩溃监控:使用 Firebase Crashlytics 或 Sentry 追踪崩溃日志
  • 用户行为分析:使用 Firebase Analytics 了解用户使用习惯
  • 性能监控:使用 Firebase Performance Monitoring 追踪启动时间、网络请求耗时
  • 用户反馈:关注应用商店评论,及时回复和修复问题
  • 版本迭代:根据数据和反馈定期发布更新
黄金法则:上线后的第一周是最关键的。密切关注崩溃率(目标 < 1%)、用户留存率和评分。如果崩溃率过高,立即发布修复版本。

📚 学习总结

学习路径回顾

恭喜你走到了 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 基础
最后的话:编程学习是一场马拉松,不是短跑。遇到困难不要气馁,每个优秀的开发者都是从新手一步步成长起来的。保持好奇心,持续练习,你一定能成为出色的 Flutter 开发者!

🎯 终极挑战

选择一个你真正感兴趣的项目创意,从零开始独立完成,并发布到 Google Play 或 App Store。记住:

  1. 从 MVP 开始,先上线再迭代
  2. 写好 README 和项目文档
  3. 将源码发布到 GitHub,展示你的代码质量
  4. 收集用户反馈,持续改进
  5. 把整个过程写成博客分享给社区