Skip to content

第14章:进阶实战

实战4:简易新闻APP(综合核心功能)

14.1 需求分析

功能需求

  • 实现首页新闻列表,显示新闻标题、摘要和图片
  • 实现新闻详情页,显示完整新闻内容
  • 实现路由跳转,从列表页跳转到详情页
  • 实现网络请求,获取新闻数据
  • 实现本地存储,保存收藏的新闻
  • 实现收藏功能,用户可以收藏/取消收藏新闻

技术要点

  • 路由导航:MaterialApp、Navigator.push
  • 网络请求:Dio
  • 列表组件:ListView.builder
  • 状态管理:StatefulWidget、setState()
  • 本地存储:shared_preferences
  • 布局组件:Column、Row、Card
  • 图片组件:Image.network
  • 文本组件:Text
  • 按钮组件:IconButton

14.2 核心实现

步骤 1:创建数据模型

dart
class News {
  final int id;
  final String title;
  final String description;
  final String content;
  final String imageUrl;
  final String author;
  final String publishedAt;
  bool isFavorite;

  News({
    required this.id,
    required this.title,
    required this.description,
    required this.content,
    required this.imageUrl,
    required this.author,
    required this.publishedAt,
    this.isFavorite = false,
  });

  factory News.fromJson(Map<String, dynamic> json) {
    return News(
      id: json['id'],
      title: json['title'],
      description: json['description'],
      content: json['content'],
      imageUrl: json['urlToImage'] ?? 'https://via.placeholder.com/400x200',
      author: json['author'] ?? 'Unknown',
      publishedAt: json['publishedAt'] ?? '',
    );
  }
}

步骤 2:创建新闻服务

dart
import 'package:dio/dio.dart';
import 'news_model.dart';

class NewsService {
  final Dio _dio = Dio();

  Future<List<News>> fetchNews() async {
    try {
      // 这里使用 NewsAPI 作为示例,实际使用时需要替换为真实的 API key
      final response = await _dio.get(
        'https://newsapi.org/v2/top-headlines',
        queryParameters: {
          'country': 'us',
          'apiKey': 'YOUR_API_KEY',
        },
      );

      final List<dynamic> articles = response.data['articles'];
      return articles
          .asMap()
          .entries
          .map((entry) => News.fromJson({
                'id': entry.key,
                ...entry.value,
              }))
          .toList();
    } catch (e) {
      // 模拟数据,实际开发中应该处理错误
      return List.generate(10, (index) => News(
            id: index,
            title: 'News Title $index',
            description: 'This is a sample news description for news $index',
            content: 'This is the full content of news $index. It contains more details about the news story.',
            imageUrl: 'https://picsum.photos/400/200?random=$index',
            author: 'Author $index',
            publishedAt: '2024-01-01T00:00:00Z',
          ));
    }
  }
}

步骤 3:创建首页

dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'news_service.dart';
import 'news_detail_page.dart';
import 'news_model.dart';

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

  @override
  State<NewsListPage> createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  List<News> _newsList = [];
  bool _isLoading = true;
  final NewsService _newsService = NewsService();
  Set<int> _favoriteIds = {};

  @override
  void initState() {
    super.initState();
    _loadFavorites();
    _fetchNews();
  }

  Future<void> _loadFavorites() async {
    final prefs = await SharedPreferences.getInstance();
    final favoriteIds = prefs.getStringList('favoriteNews') ?? [];
    setState(() {
      _favoriteIds = favoriteIds.map(int.parse).toSet();
    });
  }

  Future<void> _saveFavorites() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setStringList(
      'favoriteNews',
      _favoriteIds.map((id) => id.toString()).toList(),
    );
  }

  Future<void> _fetchNews() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final news = await _newsService.fetchNews();
      setState(() {
        _newsList = news.map((item) {
          item.isFavorite = _favoriteIds.contains(item.id);
          return item;
        }).toList();
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      // 处理错误
    }
  }

  void _toggleFavorite(int id) {
    setState(() {
      if (_favoriteIds.contains(id)) {
        _favoriteIds.remove(id);
      } else {
        _favoriteIds.add(id);
      }
      _saveFavorites();
      // 更新新闻列表中的收藏状态
      for (var news in _newsList) {
        if (news.id == id) {
          news.isFavorite = _favoriteIds.contains(id);
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('News App'),
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _fetchNews,
              child: ListView.builder(
                itemCount: _newsList.length,
                itemBuilder: (context, index) {
                  final news = _newsList[index];
                  return Card(
                    margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    child: GestureDetector(
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => NewsDetailPage(news: news),
                          ),
                        );
                      },
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          if (news.imageUrl.isNotEmpty)
                            Image.network(
                              news.imageUrl,
                              width: double.infinity,
                              height: 200,
                              fit: BoxFit.cover,
                            ),
                          Padding(
                            padding: const EdgeInsets.all(16.0),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Row(
                                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                  children: [
                                    Expanded(
                                      child: Text(
                                        news.title,
                                        style: const TextStyle(
                                          fontSize: 18,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                    ),
                                    IconButton(
                                      icon: Icon(
                                        news.isFavorite
                                            ? Icons.favorite
                                            : Icons.favorite_border,
                                        color: news.isFavorite ? Colors.red : null,
                                      ),
                                      onPressed: () {
                                        _toggleFavorite(news.id);
                                      },
                                    ),
                                  ],
                                ),
                                const SizedBox(height: 8),
                                Text(
                                  news.description,
                                  maxLines: 2,
                                  overflow: TextOverflow.ellipsis,
                                  style: const TextStyle(color: Colors.grey),
                                ),
                                const SizedBox(height: 8),
                                Row(
                                  children: [
                                    Text(
                                      news.author,
                                      style: const TextStyle(
                                        fontSize: 12,
                                        color: Colors.grey,
                                      ),
                                    ),
                                    const SizedBox(width: 16),
                                    Text(
                                      news.publishedAt.substring(0, 10),
                                      style: const TextStyle(
                                        fontSize: 12,
                                        color: Colors.grey,
                                      ),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
    );
  }
}

步骤 4:创建详情页

dart
import 'package:flutter/material.dart';
import 'news_model.dart';

class NewsDetailPage extends StatefulWidget {
  final News news;

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

  @override
  State<NewsDetailPage> createState() => _NewsDetailPageState();
}

class _NewsDetailPageState extends State<NewsDetailPage> {
  late bool _isFavorite;

  @override
  void initState() {
    super.initState();
    _isFavorite = widget.news.isFavorite;
  }

  void _toggleFavorite() {
    setState(() {
      _isFavorite = !_isFavorite;
      widget.news.isFavorite = _isFavorite;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('News Detail'),
        actions: [
          IconButton(
            icon: Icon(
              _isFavorite ? Icons.favorite : Icons.favorite_border,
              color: _isFavorite ? Colors.red : null,
            ),
            onPressed: _toggleFavorite,
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (widget.news.imageUrl.isNotEmpty)
              Image.network(
                widget.news.imageUrl,
                width: double.infinity,
                height: 300,
                fit: BoxFit.cover,
              ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    widget.news.title,
                    style: const TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 16),
                  Row(
                    children: [
                      Text(
                        widget.news.author,
                        style: const TextStyle(color: Colors.grey),
                      ),
                      const SizedBox(width: 16),
                      Text(
                        widget.news.publishedAt.substring(0, 10),
                        style: const TextStyle(color: Colors.grey),
                      ),
                    ],
                  ),
                  const SizedBox(height: 24),
                  Text(
                    widget.news.content,
                    style: const TextStyle(fontSize: 16),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

14.3 实操:搭建完整项目结构,完成各页面开发与交互,优化用户体验

步骤 1:创建项目

  1. 打开 Android Studio 或 VS Code
  2. 创建一个新的 Flutter 项目
  3. pubspec.yaml 中添加必要的依赖

步骤 2:添加依赖

yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.3+1
  shared_preferences: ^2.2.3

步骤 3:创建文件结构

lib/
  models/
    news_model.dart
  services/
    news_service.dart
  pages/
    news_list_page.dart
    news_detail_page.dart
  main.dart

步骤 4:实现代码

  1. 创建 news_model.dart 文件,定义 News 模型
  2. 创建 news_service.dart 文件,实现网络请求
  3. 创建 news_list_page.dart 文件,实现新闻列表页
  4. 创建 news_detail_page.dart 文件,实现新闻详情页
  5. 修改 main.dart 文件,配置路由

步骤 5:运行应用

  1. 启动模拟器或连接真机
  2. 运行项目
  3. 测试新闻列表加载
  4. 测试新闻详情页
  5. 测试收藏功能
  6. 测试下拉刷新

实战5:个人中心页面(样式+本地存储+状态管理)

14.4 需求分析

功能需求

  • 展示用户信息(头像、用户名、邮箱)
  • 实现主题切换功能(亮色/暗色)
  • 实现保存用户配置功能
  • 实现退出登录功能
  • 提供设置选项(如通知设置、隐私设置等)

技术要点

  • 状态管理:Provider
  • 本地存储:shared_preferences
  • 主题配置:ThemeData
  • 布局组件:Column、Row、Card、ListTile
  • 图片组件:CircleAvatar
  • 文本组件:Text
  • 按钮组件:Switch、ElevatedButton

14.5 核心实现

步骤 1:创建用户模型

dart
class User {
  final String name;
  final String email;
  final String avatar;

  User({
    required this.name,
    required this.email,
    required this.avatar,
  });
}

步骤 2:创建主题管理类

dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThemeModel extends ChangeNotifier {
  bool _isDarkMode = false;
  bool _notificationsEnabled = true;

  bool get isDarkMode => _isDarkMode;
  bool get notificationsEnabled => _notificationsEnabled;

  // 初始化:从本地存储加载设置
  Future<void> init() async {
    final prefs = await SharedPreferences.getInstance();
    _isDarkMode = prefs.getBool('isDarkMode') ?? false;
    _notificationsEnabled = prefs.getBool('notificationsEnabled') ?? true;
    notifyListeners();
  }

  // 切换主题
  Future<void> toggleTheme() async {
    _isDarkMode = !_isDarkMode;
    
    // 保存到本地存储
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isDarkMode', _isDarkMode);
    
    notifyListeners();
  }

  // 切换通知
  Future<void> toggleNotifications() async {
    _notificationsEnabled = !_notificationsEnabled;
    
    // 保存到本地存储
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('notificationsEnabled', _notificationsEnabled);
    
    notifyListeners();
  }

  // 获取当前主题
  ThemeData get themeData => _isDarkMode ? darkTheme : lightTheme;

  // 亮色主题
  static final lightTheme = ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.blue,
    primarySwatch: Colors.blue,
    accentColor: Colors.orange,
    backgroundColor: Colors.grey[100],
    cardColor: Colors.white,
  );

  // 暗色主题
  static final darkTheme = ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.blue[700],
    primarySwatch: Colors.blue,
    accentColor: Colors.orange[700],
    backgroundColor: Colors.grey[900],
    cardColor: Colors.grey[800],
  );
}

步骤 3:创建用户管理类

dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'user_model.dart';

class UserModel extends ChangeNotifier {
  User? _user;
  bool _isLoggedIn = false;

  User? get user => _user;
  bool get isLoggedIn => _isLoggedIn;

  // 初始化:从本地存储加载用户信息
  Future<void> init() async {
    final prefs = await SharedPreferences.getInstance();
    final name = prefs.getString('userName');
    final email = prefs.getString('userEmail');
    final avatar = prefs.getString('userAvatar');
    
    if (name != null && email != null) {
      _user = User(
        name: name,
        email: email,
        avatar: avatar ?? 'https://picsum.photos/200/200',
      );
      _isLoggedIn = true;
    }
    
    notifyListeners();
  }

  // 登录
  Future<void> login(String name, String email) async {
    _user = User(
      name: name,
      email: email,
      avatar: 'https://picsum.photos/200/200?random=1',
    );
    _isLoggedIn = true;
    
    // 保存到本地存储
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('userName', name);
    await prefs.setString('userEmail', email);
    await prefs.setString('userAvatar', _user!.avatar);
    
    notifyListeners();
  }

  // 登出
  Future<void> logout() async {
    _user = null;
    _isLoggedIn = false;
    
    // 从本地存储删除
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('userName');
    await prefs.remove('userEmail');
    await prefs.remove('userAvatar');
    
    notifyListeners();
  }
}

步骤 4:创建个人中心页面

dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_model.dart';
import 'user_model.dart';

class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userModel = Provider.of<UserModel>(context);
    final themeModel = Provider.of<ThemeModel>(context);

    if (!userModel.isLoggedIn) {
      return Scaffold(
        appBar: AppBar(title: const Text('Profile')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('Please login to view profile'),
              ElevatedButton(
                onPressed: () {
                  // 模拟登录
                  userModel.login('John Doe', 'john.doe@example.com');
                },
                child: const Text('Login'),
              ),
            ],
          ),
        ),
      );
    }

    final user = userModel.user!;

    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: ListView(
        children: [
          // 用户信息
          Container(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                CircleAvatar(
                  radius: 60,
                  backgroundImage: NetworkImage(user.avatar),
                ),
                const SizedBox(height: 20),
                Text(
                  user.name,
                  style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                Text(
                  user.email,
                  style: const TextStyle(color: Colors.grey),
                ),
              ],
            ),
          ),

          // 设置选项
          Card(
            margin: const EdgeInsets.all(16),
            child: Column(
              children: [
                ListTile(
                  title: const Text('Dark Mode'),
                  trailing: Switch(
                    value: themeModel.isDarkMode,
                    onChanged: (value) {
                      themeModel.toggleTheme();
                    },
                  ),
                ),
                const Divider(),
                ListTile(
                  title: const Text('Notifications'),
                  trailing: Switch(
                    value: themeModel.notificationsEnabled,
                    onChanged: (value) {
                      themeModel.toggleNotifications();
                    },
                  ),
                ),
                const Divider(),
                ListTile(
                  title: const Text('Privacy Settings'),
                  trailing: const Icon(Icons.arrow_forward),
                  onTap: () {
                    // 导航到隐私设置页面
                  },
                ),
                const Divider(),
                ListTile(
                  title: const Text('About'),
                  trailing: const Icon(Icons.arrow_forward),
                  onTap: () {
                    // 导航到关于页面
                  },
                ),
              ],
            ),
          ),

          // 退出登录
          Padding(
            padding: const EdgeInsets.all(16),
            child: ElevatedButton(
              onPressed: () {
                userModel.logout();
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red,
              ),
              child: const Text('Logout'),
            ),
          ),
        ],
      ),
    );
  }
}

步骤 5:配置主应用

dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_model.dart';
import 'user_model.dart';
import 'profile_page.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => ThemeModel()..init(),
        ),
        ChangeNotifierProvider(
          create: (context) => UserModel()..init(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeModel>(
      builder: (context, themeModel, child) {
        return MaterialApp(
          title: 'Profile App',
          theme: themeModel.themeData,
          home: ProfilePage(),
        );
      },
    );
  }
}

14.6 实操:完成个人中心开发,实现状态持久化、主题切换功能

步骤 1:创建项目

  1. 打开 Android Studio 或 VS Code
  2. 创建一个新的 Flutter 项目
  3. pubspec.yaml 中添加必要的依赖

步骤 2:添加依赖

yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.1
  shared_preferences: ^2.2.3

步骤 3:创建文件结构

lib/
  models/
    user_model.dart
  providers/
    theme_model.dart
    user_model.dart
  pages/
    profile_page.dart
  main.dart

步骤 4:实现代码

  1. 创建 user_model.dart 文件,定义 User 模型
  2. 创建 theme_model.dart 文件,实现主题管理
  3. 创建 user_model.dart 文件,实现用户管理
  4. 创建 profile_page.dart 文件,实现个人中心页面
  5. 修改 main.dart 文件,配置 Provider 和路由

步骤 5:运行应用

  1. 启动模拟器或连接真机
  2. 运行项目
  3. 测试登录功能
  4. 测试主题切换功能
  5. 测试通知设置功能
  6. 测试退出登录功能
  7. 重启应用,验证状态是否持久化

14.7 小结

本章介绍了两个进阶实战项目:简易新闻APP和个人中心页面。通过这些实战项目,我们综合应用了之前学习的核心知识点,包括:

  • 路由导航:实现页面之间的跳转
  • 网络请求:获取新闻数据
  • 状态管理:使用 Provider 管理全局状态
  • 本地存储:保存用户配置和收藏状态
  • 主题配置:实现亮色/暗色主题切换
  • 布局组件:构建复杂的页面布局
  • 列表组件:展示新闻列表
  • 图片组件:显示新闻图片和用户头像
  • 表单组件:实现设置选项

这些进阶实战项目涵盖了 Flutter 开发中的常见场景,通过实际动手实践,你可以更好地理解和掌握 Flutter 的核心概念和技术,提升你的 Flutter 开发技能。

在接下来的章节中,我们将学习 Flutter 应用的打包和发布,以及常见问题的解决方案。

© 2026 编程马·菜鸟教程 版权所有