社交App是Flutter能力的集大成者。Feed流需要高效列表渲染和下拉刷新,关注系统需要实时状态同步,通知推送需要后台服务,图片分享需要选择/压缩/上传。本课将整合前面所有知识,构建一个功能丰富的社交App。
// lib/models/user_profile.dart
class UserProfile {
final String uid;
final String username;
final String displayName;
final String? avatarUrl;
final String? bio;
final int followersCount;
final int followingCount;
final int postsCount;
final bool isVerified;
final DateTime joinedAt;
const UserProfile({
required this.uid, required this.username, required this.displayName,
this.avatarUrl, this.bio,
this.followersCount = 0, this.followingCount = 0, this.postsCount = 0,
this.isVerified = false, required this.joinedAt,
});
}
// 关注关系
class FollowRelation {
final String followerId; // 关注者
final String followingId; // 被关注者
final DateTime followedAt;
const FollowRelation({
required this.followerId, required this.followingId,
required this.followedAt,
});
}
// 动态(帖子)
class Post {
final String id;
final String authorId;
final String authorName;
final String? authorAvatar;
final String content;
final List<String> imageUrls;
final int likeCount;
final int commentCount;
final int shareCount;
final bool isLiked;
final DateTime createdAt;
const Post({
required this.id, required this.authorId, required this.authorName,
this.authorAvatar, required this.content,
this.imageUrls = const [],
this.likeCount = 0, this.commentCount = 0, this.shareCount = 0,
this.isLiked = false, required this.createdAt,
});
Post copyWith({
int? likeCount, int? commentCount, int? shareCount, bool? isLiked,
}) => Post(
id: id, authorId: authorId, authorName: authorName,
authorAvatar: authorAvatar, content: content, imageUrls: imageUrls,
likeCount: likeCount ?? this.likeCount,
commentCount: commentCount ?? this.commentCount,
shareCount: shareCount ?? this.shareCount,
isLiked: isLiked ?? this.isLiked,
createdAt: createdAt,
);
}
// 评论
class Comment {
final String id;
final String postId;
final String authorId;
final String authorName;
final String? authorAvatar;
final String content;
final String? replyToId; // 回复某条评论
final DateTime createdAt;
const Comment({
required this.id, required this.postId, required this.authorId,
required this.authorName, this.authorAvatar, required this.content,
this.replyToId, required this.createdAt,
});
}
// 通知
class AppNotification {
final String id;
final NotificationType type;
final String fromUserId;
final String fromUserName;
final String? fromUserAvatar;
final String? referenceId; // 关联的帖子/评论ID
final String message;
final bool isRead;
final DateTime createdAt;
const AppNotification({
required this.id, required this.type, required this.fromUserId,
required this.fromUserName, this.fromUserAvatar, this.referenceId,
required this.message, this.isRead = false, required this.createdAt,
});
}
enum NotificationType { like, comment, follow, mention, system }
// lib/providers/feed_provider.dart
class FeedProvider extends ChangeNotifier {
final PostRepository _repo;
List<Post> _posts = [];
String? _nextCursor; // 游标分页
bool _isLoading = false;
bool _hasMore = true;
List<Post> get posts => _posts;
bool get isLoading => _isLoading;
bool get hasMore => _hasMore;
FeedProvider(this._repo);
// 首次加载/下拉刷新
Future<void> refresh() async {
_nextCursor = null;
_hasMore = true;
_isLoading = true;
notifyListeners();
final result = await _repo.getFeed(cursor: null, limit: 20);
_posts = result.items;
_nextCursor = result.nextCursor;
_hasMore = result.hasMore;
_isLoading = false;
notifyListeners();
}
// 加载更多
Future<void> loadMore() async {
if (_isLoading || !_hasMore) return;
_isLoading = true;
notifyListeners();
final result = await _repo.getFeed(cursor: _nextCursor, limit: 20);
_posts.addAll(result.items);
_nextCursor = result.nextCursor;
_hasMore = result.hasMore;
_isLoading = false;
notifyListeners();
}
// 点赞/取消点赞(乐观更新)
Future<void> toggleLike(String postId) async {
final index = _posts.indexWhere((p) => p.id == postId);
if (index == -1) return;
final post = _posts[index];
final wasLiked = post.isLiked;
// 乐观更新UI
_posts[index] = post.copyWith(
isLiked: !wasLiked,
likeCount: post.likeCount + (wasLiked ? -1 : 1),
);
notifyListeners();
// 网络请求
try {
if (wasLiked) {
await _repo.unlikePost(postId);
} else {
await _repo.likePost(postId);
}
} catch (e) {
// 回滚
_posts[index] = post;
notifyListeners();
}
}
// 发布新动态
Future<void> createPost(String content, {List<String>? imagePaths}) async {
final post = await _repo.createPost(content, imagePaths: imagePaths);
_posts.insert(0, post);
notifyListeners();
}
}
// lib/pages/feed_page.dart
class FeedPage extends StatefulWidget {
@override
State<FeedPage> createState() => _FeedPageState();
}
class _FeedPageState extends State<FeedPage> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
context.read<FeedProvider>().refresh();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 300) {
context.read<FeedProvider>().loadMore();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<FeedProvider>(
builder: (context, feed, _) {
return RefreshIndicator(
onRefresh: () => feed.refresh(),
child: ListView.builder(
controller: _scrollController,
itemCount: feed.posts.length + (feed.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == feed.posts.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return PostCard(
post: feed.posts[index],
onLike: () => feed.toggleLike(feed.posts[index].id),
onComment: () => _navigateToComments(feed.posts[index]),
onShare: () => _sharePost(feed.posts[index]),
onProfile: () => _navigateToProfile(feed.posts[index].authorId),
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _showCreatePostSheet,
child: const Icon(Icons.edit),
),
);
}
}
// lib/providers/follow_provider.dart
class FollowProvider extends ChangeNotifier {
final FollowRepository _repo;
final Set<String> _followingIds = {}; // 已关注的用户ID集合
bool _isLoading = false;
bool isFollowing(String userId) => _followingIds.contains(userId);
Future<void> loadFollowing() async {
_isLoading = true;
notifyListeners();
final ids = await _repo.getFollowingIds();
_followingIds.clear();
_followingIds.addAll(ids);
_isLoading = false;
notifyListeners();
}
Future<void> toggleFollow(String userId) async {
final wasFollowing = _followingIds.contains(userId);
// 乐观更新
if (wasFollowing) {
_followingIds.remove(userId);
} else {
_followingIds.add(userId);
}
notifyListeners();
try {
if (wasFollowing) {
await _repo.unfollow(userId);
} else {
await _repo.follow(userId);
}
} catch (e) {
// 回滚
if (wasFollowing) {
_followingIds.add(userId);
} else {
_followingIds.remove(userId);
}
notifyListeners();
}
}
}
// 关注按钮组件
class FollowButton extends StatelessWidget {
final String userId;
const FollowButton({required this.userId});
@override
Widget build(BuildContext context) {
final isFollowing = context.select<FollowProvider, bool>(
(p) => p.isFollowing(userId),
);
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: isFollowing
? OutlinedButton(
onPressed: () => context.read<FollowProvider>().toggleFollow(userId),
child: const Text('已关注'),
)
: FilledButton(
onPressed: () => context.read<FollowProvider>().toggleFollow(userId),
child: const Text('关注'),
),
);
}
}
// lib/providers/notification_provider.dart
class NotificationProvider extends ChangeNotifier {
final NotificationRepository _repo;
List<AppNotification> _notifications = [];
int _unreadCount = 0;
int get unreadCount => _unreadCount;
List<AppNotification> get notifications => _notifications;
// WebSocket推送更新
void onNewNotification(AppNotification notification) {
_notifications.insert(0, notification);
_unreadCount++;
notifyListeners();
// 显示本地通知
_showLocalNotification(notification);
}
Future<void> _showLocalNotification(AppNotification n) async {
final plugin = FlutterLocalNotificationsPlugin();
await plugin.show(
n.id.hashCode,
n.fromUserName,
n.message,
const NotificationDetails(
android: AndroidNotificationDetails(
'social_channel', '社交通知',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
),
);
}
// 标记已读
Future<void> markAsRead(String notificationId) async {
final index = _notifications.indexWhere((n) => n.id == notificationId);
if (index != -1) {
_notifications[index] = AppNotification(
id: n.id, type: n.type, fromUserId: n.fromUserId,
fromUserName: n.fromUserName, fromUserAvatar: n.fromUserAvatar,
referenceId: n.referenceId, message: n.message,
isRead: true, createdAt: n.createdAt,
);
_unreadCount = _notifications.where((n) => !n.isRead).length;
notifyListeners();
}
await _repo.markAsRead(notificationId);
}
}
// 通知徽章
class NotificationBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final count = context.select<NotificationProvider, int>(
(p) => p.unreadCount,
);
return Stack(
children: [
const Icon(Icons.notifications),
if (count > 0)
Positioned(right: 0, top: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red, shape: BoxShape.circle),
constraints: const BoxConstraints(minWidth: 14, minHeight: 14),
child: Text('$count', textAlign: TextAlign.center,
style: const TextStyle(fontSize: 9, color: Colors.white)),
)),
],
);
}
}
// lib/services/image_service.dart
class ImageService {
final ImagePicker _picker = ImagePicker();
Future<List<File>> pickImages({int maxCount = 9}) async {
final images = await _picker.pickMultiImage(imageQuality: 80);
if (images.length > maxCount) {
throw '最多选择$maxCount张图片';
}
return images.map((x) => File(x.path)).toList();
}
Future<File> takePhoto() async {
final photo = await _picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
imageQuality: 80,
);
if (photo == null) throw '取消拍照';
return File(photo.path);
}
// 压缩图片
Future<File> compressImage(File file) async {
final tempDir = await getTemporaryDirectory();
final targetPath = '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path, targetPath,
quality: 75,
minWidth: 1080,
rotate: 0,
);
return File(result!.path);
}
}
你已构建了一个社交App!综合运用了:
下一课预告:Hero动画与页面转场——让你的App拥有丝滑的共享元素过渡效果!