05-Flutter实战项目

摘要:本文通过完整的实战项目,综合运用Flutter核心知识,从零到一构建一个新闻阅读App。涵盖项目架构设计、网络请求、状态管理、本地存储、主题切换等核心功能,帮助你将理论知识转化为实际开发能力。

关键词:Flutter、实战项目、新闻App、Provider、网络请求、本地缓存、主题切换

适合人群:已掌握Flutter基础、状态管理、网络存储的开发者

阅读时间:约 60 分钟

版本信息:Flutter 3.x | Dart 3.x | http 1.x | provider 6.x


文章目录

  • [1. 项目概述](#1. 项目概述)
    • [1.1 功能需求](#1.1 功能需求)
    • [1.2 技术栈](#1.2 技术栈)
    • [1.3 项目结构](#1.3 项目结构)
  • [2. 项目初始化](#2. 项目初始化)
    • [2.1 创建项目](#2.1 创建项目)
    • [2.2 添加依赖](#2.2 添加依赖)
  • [3. 数据层](#3. 数据层)
    • [3.1 数据模型](#3.1 数据模型)
    • [3.2 网络服务](#3.2 网络服务)
    • [3.3 本地缓存](#3.3 本地缓存)
  • [4. 状态管理](#4. 状态管理)
    • [4.1 新闻状态](#4.1 新闻状态)
    • [4.2 主题状态](#4.2 主题状态)
  • [5. UI 层](#5. UI 层)
    • [5.1 首页](#5.1 首页)
    • [5.2 新闻详情](#5.2 新闻详情)
    • [5.3 设置页](#5.3 设置页)
  • [6. 路由配置](#6. 路由配置)
  • [7. 完整代码](#7. 完整代码)
  • [❓ 常见问题 FAQ](#❓ 常见问题 FAQ)
    • [1. 如何添加更多页面?](#1. 如何添加更多页面?)
    • [2. 如何实现分页加载?](#2. 如何实现分页加载?)
    • [3. 如何优化图片加载?](#3. 如何优化图片加载?)
    • [4. 如何添加搜索功能?](#4. 如何添加搜索功能?)
  • [📝 总结](#📝 总结)
  • [📚 参考资料](#📚 参考资料)

1. 项目概述

1.1 功能需求

  构建一个新闻阅读App,包含以下功能:

  • 新闻列表展示(支持分页)
  • 新闻详情查看
  • 收藏功能
  • 主题切换(亮色/暗色)
  • 离线缓存

1.2 技术栈

技术 用途
Provider 状态管理
http 网络请求
SharedPreferences 本地存储
cached_network_image 图片缓存

1.3 项目结构

复制代码
lib/
├── main.dart
├── models/
│   └── news.dart
├── services/
│   ├── api_service.dart
│   └── storage_service.dart
├── providers/
│   ├── news_provider.dart
│   └── theme_provider.dart
└── screens/
    ├── home_screen.dart
    ├── detail_screen.dart
    └── settings_screen.dart

2. 项目初始化

2.1 创建项目

bash 复制代码
flutter create news_app
cd news_app

2.2 添加依赖

  编辑 pubspec.yaml

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  provider: ^6.0.0
  shared_preferences: ^2.2.0
  cached_network_image: ^3.3.0
  intl: ^0.18.0

3. 数据层

3.1 数据模型

models/news.dart

dart 复制代码
class NewsModel {
  final String id;
  final String title;
  final String summary;
  final String imageUrl;
  final String content;
  final String author;
  final DateTime publishDate;
  final bool isFavorite;

  NewsModel({
    required this.id,
    required this.title,
    required this.summary,
    required this.imageUrl,
    required this.content,
    required this.author,
    required this.publishDate,
    this.isFavorite = false,
  });

  factory NewsModel.fromJson(Map<String, dynamic> json) {
    return NewsModel(
      id: json['id']?.toString() ?? '',
      title: json['title'] ?? '',
      summary: json['summary'] ?? '',
      imageUrl: json['image_url'] ?? '',
      content: json['content'] ?? '',
      author: json['author'] ?? '',
      publishDate: DateTime.parse(json['publish_date']),
      isFavorite: json['is_favorite'] ?? false,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'summary': summary,
      'image_url': imageUrl,
      'content': content,
      'author': author,
      'publish_date': publishDate.toIso8601String(),
      'is_favorite': isFavorite,
    };
  }
}

3.2 网络服务

services/api_service.dart

dart 复制代码
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/news.dart';

class ApiService {
  static const String baseUrl = 'https://newsapi.example.com';

  // 获取新闻列表
  static Future<List<NewsModel>> getNewsList({int page = 1, int pageSize = 20}) async {
    final response = await http.get(
      Uri.parse('$baseUrl/news?page=$page&pageSize=$pageSize'),
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return (data['news'] as List)
          .map((item) => NewsModel.fromJson(item))
          .toList();
    } else {
      throw Exception('获取新闻失败');
    }
  }

  // 获取新闻详情
  static Future<NewsModel> getNewsDetail(String id) async {
    final response = await http.get(
      Uri.parse('$baseUrl/news/$id'),
    );

    if (response.statusCode == 200) {
      return NewsModel.fromJson(json.decode(response.body));
    } else {
      throw Exception('获取详情失败');
    }
  }
}

3.3 本地缓存

services/storage_service.dart

dart 复制代码
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/news.dart';

class StorageService {
  static const String _favoritesKey = 'favorites';
  static const String _themeKey = 'isDarkTheme';

  // 获取收藏列表
  static Future<List<NewsModel>> getFavorites() async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getStringList(_favoritesKey) ?? [];
    return data.map((item) => NewsModel.fromJson(json.decode(item))).toList();
  }

  // 保存收藏列表
  static Future<void> saveFavorites(List<NewsModel> favorites) async {
    final prefs = await SharedPreferences.getInstance();
    final data = favorites.map((item) => json.encode(item.toJson())).toList();
    await prefs.setStringList(_favoritesKey, data);
  }

  // 获取主题设置
  static Future<bool> getTheme() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_themeKey) ?? false;
  }

  // 保存主题设置
  static Future<void> saveTheme(bool isDark) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_themeKey, isDark);
  }
}

4. 状态管理

4.1 新闻状态

providers/news_provider.dart

dart 复制代码
import 'package:flutter/material.dart';
import '../models/news.dart';
import '../services/api_service.dart';
import '../services/storage_service.dart';

class NewsProvider with ChangeNotifier {
  List<NewsModel> _newsList = [];
  List<NewsModel> _favorites = [];
  bool _isLoading = false;
  String? _error;

  List<NewsModel> get newsList => _newsList;
  List<NewsModel> get favorites => _favorites;
  bool get isLoading => _isLoading;
  String? get error => _error;

  // 加载新闻
  Future<void> loadNews() async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _newsList = await ApiService.getNewsList();
      _favorites = await StorageService.getFavorites();
      _updateFavorites();
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  // 收藏/取消收藏
  Future<void> toggleFavorite(NewsModel news) async {
    final index = _newsList.indexWhere((n) => n.id == news.id);
    if (index != -1) {
      _newsList[index] = NewsModel(
        id: _newsList[index].id,
        title: _newsList[index].title,
        summary: _newsList[index].summary,
        imageUrl: _newsList[index].imageUrl,
        content: _newsList[index].content,
        author: _newsList[index].author,
        publishDate: _newsList[index].publishDate,
        isFavorite: !_newsList[index].isFavorite,
      );

      if (_newsList[index].isFavorite) {
        _favorites.add(_newsList[index]);
      } else {
        _favorites.removeWhere((n) => n.id == news.id);
      }

      await StorageService.saveFavorites(_favorites);
      notifyListeners();
    }
  }

  void _updateFavorites() {
    _newsList = _newsList.map((news) {
      final isFav = _favorites.any((f) => f.id == news.id);
      return NewsModel(
        id: news.id,
        title: news.title,
        summary: news.summary,
        imageUrl: news.imageUrl,
        content: news.content,
        author: news.author,
        publishDate: news.publishDate,
        isFavorite: isFav,
      );
    }).toList();
  }
}

4.2 主题状态

providers/theme_provider.dart

dart 复制代码
import 'package:flutter/material.dart';
import '../services/storage_service.dart';

class ThemeProvider with ChangeNotifier {
  bool _isDark = false;

  bool get isDark => _isDark;

  ThemeMode get themeMode => _isDark ? ThemeMode.dark : ThemeMode.light;

  Future<void> init() async {
    _isDark = await StorageService.getTheme();
    notifyListeners();
  }

  Future<void> toggleTheme() async {
    _isDark = !_isDark;
    await StorageService.saveTheme(_isDark);
    notifyListeners();
  }
}

5. UI 层

5.1 首页

screens/home_screen.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../providers/news_provider.dart';
import '../providers/theme_provider.dart';
import 'detail_screen.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  void initState() {
    super.initState();
    context.read<NewsProvider>().loadNews();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新闻阅读'),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () {
              Navigator.pushNamed(context, '/settings');
            },
          ),
        ],
      ),
      body: Consumer<NewsProvider>(
        builder: (context, provider, child) {
          if (provider.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }

          if (provider.error != null) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('加载失败: ${provider.error}'),
                  ElevatedButton(
                    onPressed: () => provider.loadNews(),
                    child: const Text('重试'),
                  ),
                ],
              ),
            );
          }

          return ListView.builder(
            itemCount: provider.newsList.length,
            itemBuilder: (context, index) {
              final news = provider.newsList[index];
              return Card(
                margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                child: ListTile(
                  leading: ClipRRect(
                    borderRadius: BorderRadius.circular(4),
                    child: CachedNetworkImage(
                      imageUrl: news.imageUrl,
                      width: 80,
                      height: 80,
                      fit: BoxFit.cover,
                      placeholder: (context, url) => const CircularProgressIndicator(),
                      errorWidget: (context, url, error) => const Icon(Icons.error),
                    ),
                  ),
                  title: Text(news.title, maxLines: 2, overflow: TextOverflow.ellipsis),
                  subtitle: Text('${news.author} · ${news.publishDate.toString().substring(0, 10)}'),
                  trailing: IconButton(
                    icon: Icon(
                      news.isFavorite ? Icons.favorite : Icons.favorite_border,
                      color: news.isFavorite ? Colors.red : null,
                    ),
                    onPressed: () => provider.toggleFavorite(news),
                  ),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => DetailScreen(news: news),
                      ),
                    );
                  },
                ),
              );
            },
          );
        },
      ),
    );
  }
}

5.2 新闻详情

screens/detail_screen.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/news.dart';

class DetailScreen extends StatelessWidget {
  final NewsModel news;

  const DetailScreen({super.key, required this.news});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(news.title)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            CachedNetworkImage(
              imageUrl: news.imageUrl,
              width: double.infinity,
              height: 200,
              fit: BoxFit.cover,
            ),
            const SizedBox(height: 16),
            Text(
              news.title,
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              '${news.author} · ${news.publishDate.toString().substring(0, 10)}',
              style: const TextStyle(color: Colors.grey),
            ),
            const SizedBox(height: 16),
            Text(
              news.content,
              style: const TextStyle(fontSize: 16, height: 1.6),
            ),
          ],
        ),
      ),
    );
  }
}

5.3 设置页

screens/settings_screen.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/theme_provider.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置')),
      body: Consumer<ThemeProvider>(
        builder: (context, themeProvider, child) {
          return ListView(
            children: [
              SwitchListTile(
                title: const Text('暗色主题'),
                value: themeProvider.isDark,
                onChanged: (value) => themeProvider.toggleTheme(),
              ),
              const Divider(),
              ListTile(
                title: const Text('清除缓存'),
                trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                onTap: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('缓存已清除')),
                  );
                },
              ),
              ListTile(
                title: const Text('关于我们'),
                trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                onTap: () {},
              ),
            ],
          );
        },
      ),
    );
  }
}

6. 路由配置

main.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/news_provider.dart';
import 'providers/theme_provider.dart';
import 'screens/home_screen.dart';
import 'screens/settings_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final themeProvider = ThemeProvider();
  await themeProvider.init();

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => NewsProvider()),
        ChangeNotifierProvider.value(value: themeProvider),
      ],
      child: const NewsApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: '新闻阅读',
          themeMode: themeProvider.themeMode,
          theme: ThemeData(
            primarySwatch: Colors.blue,
            brightness: Brightness.light,
          ),
          darkTheme: ThemeData(
            primarySwatch: Colors.blue,
            brightness: Brightness.dark,
          ),
          initialRoute: '/',
          routes: {
            '/': (context) => const HomeScreen(),
            '/settings': (context) => const SettingsScreen(),
          },
        );
      },
    );
  }
}

7. 完整代码

  将以上所有文件整合后,即可运行项目:

bash 复制代码
flutter run

运行效果

  • 首页展示新闻列表,支持加载状态和错误处理
  • 点击新闻卡片进入详情页
  • 点击收藏图标切换收藏状态
  • 设置页支持切换暗色主题
  • 所有数据持久化到本地

❓ 常见问题 FAQ

1. 如何添加更多页面?

解答

  在 main.dartroutes 中添加路由映射,然后使用 Navigator.pushNamed(context, '/route') 跳转。

2. 如何实现分页加载?

解答

  在 NewsProvider 中添加 currentPage 变量,在 ListView 底部添加 LoadMore 组件触发下一页加载。

3. 如何优化图片加载?

解答

  使用 cached_network_image 包自动缓存图片,避免重复下载。

4. 如何添加搜索功能?

解答

  在 NewsProvider 中添加 searchKeyword 变量,过滤 _newsList 并调用 notifyListeners()


📝 总结

项目亮点

  1. 分层架构:数据层、状态层、UI层分离
  2. 状态管理:使用 Provider 管理全局状态
  3. 本地缓存:SharedPreferences 持久化数据
  4. 主题切换:支持亮色/暗色主题
  5. 错误处理:完善的加载状态和错误提示

后续扩展

  • 添加 SQLite 存储更多数据
  • 实现 WebSocket 实时推送
  • 添加动画效果
  • 支持多语言

📚 参考资料

📚 系列导航入门指南 | 进阶开发 | 状态管理 | 网络与存储 | 实战项目

相关推荐
涛涛ing1 小时前
React 19.3 发布,但真正的赢家是 StyleX:当 AI 成为框架的“第一用户”
前端
zhangminghuan2 小时前
Vue vs React 实战拓展面试题|摒弃基础八股,聚焦业务开发核心难点(2026前端进阶)
前端
IT_陈寒2 小时前
Redis主从切换竟让业务卡了3秒?这个坑我替你踩了
前端·人工智能·后端
天若有情6732 小时前
独立开发复盘:我用 Node.js 做了个在线工具箱
前端·json·工具
xieliyu.2 小时前
前端基础:常见CSS选择器用法
前端·css·笔记·vscode
zttbee2 小时前
前端测试内容
前端
传奇开心果编程2 小时前
【Flutter入门练中学】第1课:从零开始
学习·flutter·ui
2603_965896623 小时前
async/await|JS同步写法解决异步终极方案
前端·javascript
A24207349305 小时前
TCP 三次握手与四次挥手:从原理到状态机一次讲清
前端