Appearance
第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:创建项目
- 打开 Android Studio 或 VS Code
- 创建一个新的 Flutter 项目
- 在
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:实现代码
- 创建
news_model.dart文件,定义 News 模型 - 创建
news_service.dart文件,实现网络请求 - 创建
news_list_page.dart文件,实现新闻列表页 - 创建
news_detail_page.dart文件,实现新闻详情页 - 修改
main.dart文件,配置路由
步骤 5:运行应用
- 启动模拟器或连接真机
- 运行项目
- 测试新闻列表加载
- 测试新闻详情页
- 测试收藏功能
- 测试下拉刷新
实战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:创建项目
- 打开 Android Studio 或 VS Code
- 创建一个新的 Flutter 项目
- 在
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:实现代码
- 创建
user_model.dart文件,定义 User 模型 - 创建
theme_model.dart文件,实现主题管理 - 创建
user_model.dart文件,实现用户管理 - 创建
profile_page.dart文件,实现个人中心页面 - 修改
main.dart文件,配置 Provider 和路由
步骤 5:运行应用
- 启动模拟器或连接真机
- 运行项目
- 测试登录功能
- 测试主题切换功能
- 测试通知设置功能
- 测试退出登录功能
- 重启应用,验证状态是否持久化
14.7 小结
本章介绍了两个进阶实战项目:简易新闻APP和个人中心页面。通过这些实战项目,我们综合应用了之前学习的核心知识点,包括:
- 路由导航:实现页面之间的跳转
- 网络请求:获取新闻数据
- 状态管理:使用 Provider 管理全局状态
- 本地存储:保存用户配置和收藏状态
- 主题配置:实现亮色/暗色主题切换
- 布局组件:构建复杂的页面布局
- 列表组件:展示新闻列表
- 图片组件:显示新闻图片和用户头像
- 表单组件:实现设置选项
这些进阶实战项目涵盖了 Flutter 开发中的常见场景,通过实际动手实践,你可以更好地理解和掌握 Flutter 的核心概念和技术,提升你的 Flutter 开发技能。
在接下来的章节中,我们将学习 Flutter 应用的打包和发布,以及常见问题的解决方案。
