我试图通过单击图像从timeline_details
页转到activity_feed
页,但出现此错误:
I/flutter (28907): The following NoSuchMethodError was thrown building FutureBuilder<DocumentSnapshot>(dirty, state:
I/flutter (28907): _FutureBuilderState<DocumentSnapshot>#ddf5c):
I/flutter (28907): The method '[]' was called on null.
I/flutter (28907): Receiver: null
I/flutter (28907): Tried calling: []("id")
I/flutter (28907):
I/flutter (28907): The relevant error-causing widget was:
I/flutter (28907): FutureBuilder<DocumentSnapshot>
I/flutter (28907): file:///F:/COURS%20DE%20PROGRAMMATION/PROJETS/MOBILE/minisocialnetwork/lib/pages/timeline_details.dart:272:12
I/flutter (28907):
I/flutter (28907): When the exception was thrown, this was the stack:
I/flutter (28907): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
I/flutter (28907): #1 DocumentSnapshot.[] (package:cloud_firestore/src/document_snapshot.dart:31:42)
I/flutter (28907): #2 new User.fromDocument (package:minisocialnetwork/models/user.dart:26:16)
I/flutter (28907): #3 _TimelineDetailsState.buildPostHeader.<anonymous closure> (package:minisocialnetwork/pages/timeline_details.dart:278:26)
I/flutter (28907): #4 _FutureBuilderState.build (package:flutter/src/widg**
以下是参考图片:
activity_feed页面: activity_feed page
timeline_details页面: timeline_details page
这是我的完整代码:
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:minisocialnetwork/models/user.dart';
import 'package:minisocialnetwork/pages/post_screen.dart';
import 'package:minisocialnetwork/pages/timeline.dart';
import 'package:minisocialnetwork/pages/timeline_details.dart';
import 'package:minisocialnetwork/widgets/header.dart';
import 'package:minisocialnetwork/widgets/progress.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'home.dart';
import 'profile.dart';
class ActivityFeed extends StatefulWidget {
@override
_ActivityFeedState createState() => _ActivityFeedState();
}
class _ActivityFeedState extends State<ActivityFeed> {
getActivityFeed() async {
QuerySnapshot snapshot = await activityFeedRef
.document(currentUser.id)
.collection('feedItems')
.orderBy('timestamp', descending: true)
.limit(50)
.getDocuments();
List<ActivityFeedItem> feedItems = [];
snapshot.documents.forEach((doc) {
feedItems.add(ActivityFeedItem.fromDocument(doc));
// print('Activity Feed Item: ${doc.data}');
});
return feedItems;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: header(context, titleText: "Notifications"),
body: Container(
child: FutureBuilder(
future: getActivityFeed(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
return ListView(
children: snapshot.data,
);
},
)),
);
}
}
Widget mediaPreview;
String activityItemText;
class ActivityFeedItem extends StatelessWidget {
final User currentUser;
final String profileId;
final String username;
final String userId;
final String type; // 'like', 'follow', 'comment'
final String mediaUrl;
final String postId;
final String title;
final String content;
final String category;
final String ownerId;
bool isLiked;
int likeCount;
Map likes;
final String userProfileImg;
final String commentData;
final Timestamp timestamp;
ActivityFeedItem({
this.currentUser,
this.profileId,
this.username,
this.userId,
this.type,
this.mediaUrl,
this.postId,
this.title,
this.content,
this.category,
this.ownerId,
this.isLiked,
this.likeCount,
this.likes,
this.userProfileImg,
this.commentData,
this.timestamp,
});
factory ActivityFeedItem.fromDocument(DocumentSnapshot doc) {
return ActivityFeedItem(
username: doc['username'],
userId: doc['userId'],
ownerId: doc['ownerId'],
title: doc['title'],
content: doc['content'],
category: doc['selectedCategory'],
likes: doc['likes'],
type: doc['type'],
postId: doc['postId'],
userProfileImg: doc['userProfileImg'],
commentData: doc['commentData'],
timestamp: doc['timestamp'],
mediaUrl: doc['mediaUrl'],
);
}
showTimelineDetails(BuildContext context,
{User currentUser,
String profileId,
String postId,
String ownerId,
String userId,
String mediaUrl,
String username,
String title,
String content,
String category,
Map likes,
int likeCount,
Timestamp timestamp,
bool isLiked}) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return TimelineDetails(
currentUser: currentUser,
profileId: profileId,
postId: postId,
ownerId: ownerId,
username: username,
title: title,
content: content,
category: category,
mediaUrl: mediaUrl,
likes: likes,
likeCount: likeCount,
isLiked: isLiked,
timestamp: timestamp,
);
}));
}
configureMediaPreview(context) {
if (type == "like" || type == 'comment') {
mediaPreview = GestureDetector(
onTap: () => showTimelineDetails(context),
child: Container(
height: 50.0,
width: 50.0,
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(mediaUrl),
),
),
)),
),
);
} else {
mediaPreview = Text('');
}
if (type == 'like') {
activityItemText = "a aimé votre article";
} else if (type == 'follow') {
activityItemText = "vous suit";
} else if (type == 'comment') {
activityItemText = 'a écrit: $commentData';
} else {
activityItemText = "Error: Unknown type '$type'";
}
}
@override
Widget build(BuildContext context) {
configureMediaPreview(context);
return Padding(
padding: EdgeInsets.only(bottom: 2.0),
child: Container(
color: Colors.white54,
child: ListTile(
leading: GestureDetector(
onTap: () => showProfile(context, profileId: userId),
child: CircleAvatar(
backgroundImage: CachedNetworkImageProvider(userProfileImg),
),
),
title: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
fontSize: 14.0,
color: Colors.black,
),
children: [
TextSpan(
text: username,
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: ' $activityItemText',
),
]),
),
subtitle: Text(
timeago.format(timestamp.toDate()),
overflow: TextOverflow.ellipsis,
),
trailing: mediaPreview,
),
),
);
}
}
showProfile(BuildContext context, {String profileId}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Profile(
profileId: profileId,
),
),
);
}
和
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:minisocialnetwork/models/user.dart';
import 'package:minisocialnetwork/widgets/custom_image.dart';
import 'package:minisocialnetwork/widgets/post.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:share/share.dart';
import '../widgets/progress.dart';
import '../models/user.dart';
import 'activity_feed.dart';
import 'home.dart';
//final usersRef = Firestore.instance.collection('users');
class TimelineDetails extends StatefulWidget {
final User currentUser;
final String profileId;
final postId;
final ownerId;
final username;
final title;
final content;
final category;
final mediaUrl;
final likes;
final likeCount;
bool isLiked;
final Timestamp timestamp;
TimelineDetails(
{this.currentUser,
this.profileId,
this.postId,
this.ownerId,
this.username,
this.title,
this.content,
this.category,
this.mediaUrl,
this.likes,
this.likeCount,
this.isLiked,
this.timestamp});
factory TimelineDetails.fromDocument(DocumentSnapshot doc) {
return TimelineDetails(
postId: doc['postId'],
ownerId: doc['ownerId'],
username: doc['username'],
title: doc['title'],
content: doc['content'],
category: doc['selectedCategory'],
mediaUrl: doc['mediaUrl'],
likes: doc['likes'],
);
}
int getLikeCount(likes) {
// if no likes, return 0
if (likes == null) {
return 0;
}
int count = 0;
// if the key is explicitly set to true, add a like
likes.values.forEach((val) {
if (val == true) {
count += 1;
}
});
return count;
}
@override
_TimelineDetailsState createState() => _TimelineDetailsState(
postId: this.postId,
ownerId: this.ownerId,
username: this.username,
title: this.title,
content: this.content,
category: this.category,
mediaUrl: this.mediaUrl,
likes: this.likes,
likeCount: getLikeCount(this.likes),
timestamp: this.timestamp,
);
}
class _TimelineDetailsState extends State<TimelineDetails> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final String currentUserId = currentUser?.id;
List<Post> posts;
bool isFollowing = false;
bool isLoading = false;
int postCount = 0;
int followerCount = 0;
int followingCount = 0;
List<String> followingList = [];
final String postId;
final String ownerId;
final String username;
final String title;
final String content;
final String category;
final String mediaUrl;
final Timestamp timestamp;
bool showHeart = false;
bool isLiked = false;
int likeCount;
Map likes;
_TimelineDetailsState({
this.postId,
this.ownerId,
this.username,
this.title,
this.content,
this.category,
this.mediaUrl,
this.likes,
this.likeCount,
this.timestamp,
});
@override
void initState() {
super.initState();
}
handleDeletePost(BuildContext parentContext) {
return showDialog(
context: parentContext,
builder: (context) {
return SimpleDialog(
title: Text("Supprimer cet article?"),
children: <Widget>[
SimpleDialogOption(
onPressed: () {
Navigator.pop(context);
deletePost();
},
child: Text(
'Supprimer',
style: TextStyle(color: Colors.red, fontSize: 18.0),
)),
SimpleDialogOption(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: TextStyle(fontSize: 18.0),
)),
],
);
});
}
// Note: To delete post, ownerId and currentUserId must be equal, so they can be used interchangeably
deletePost() async {
// delete post itself
postsRef
.document(ownerId)
.collection('userPosts')
.document(postId)
.get()
.then((doc) {
if (doc.exists) {
doc.reference.delete();
}
});
// delete uploaded image for thep ost
storageRef.child("post_$postId.jpg").delete();
// then delete all activity feed notifications
QuerySnapshot activityFeedSnapshot = await activityFeedRef
.document(ownerId)
.collection("feedItems")
.where('postId', isEqualTo: postId)
.getDocuments();
activityFeedSnapshot.documents.forEach((doc) {
if (doc.exists) {
doc.reference.delete();
}
});
// then delete all comments
QuerySnapshot commentsSnapshot = await commentsRef
.document(postId)
.collection('comments')
.getDocuments();
commentsSnapshot.documents.forEach((doc) {
if (doc.exists) {
doc.reference.delete();
}
});
}
handleLikePost() {
bool _isLiked = likes[currentUserId] == true;
if (_isLiked) {
postsRef
.document(ownerId)
.collection('userPosts')
.document(postId)
.updateData({'likes.$currentUserId': false});
removeLikeFromActivityFeed();
setState(() {
likeCount -= 1;
isLiked = false;
likes[currentUserId] = false;
});
} else if (!_isLiked) {
postsRef
.document(ownerId)
.collection('userPosts')
.document(postId)
.updateData({'likes.$currentUserId': true});
addLikeToActivityFeed();
setState(() {
likeCount += 1;
isLiked = true;
likes[currentUserId] = true;
showHeart = true;
});
Timer(Duration(milliseconds: 500), () {
setState(() {
showHeart = false;
});
});
}
}
addLikeToActivityFeed() {
// add a notification to the postOwner's activity feed only if comment made by OTHER user (to avoid getting notification for our own like)
bool isNotPostOwner = currentUserId != ownerId;
if (isNotPostOwner) {
activityFeedRef
.document(ownerId)
.collection("feedItems")
.document(postId)
.setData({
"type": "like",
"username": currentUser.username,
"userId": currentUser.id,
"userProfileImg": currentUser.photoUrl,
"postId": postId,
"mediaUrl": mediaUrl,
"timestamp": timestamp,
});
}
}
removeLikeFromActivityFeed() {
bool isNotPostOwner = currentUserId != ownerId;
if (isNotPostOwner) {
activityFeedRef
.document(ownerId)
.collection("feedItems")
.document(postId)
.get()
.then((doc) {
if (doc.exists) {
doc.reference.delete();
}
});
}
}
buildPostHeader() {
return FutureBuilder(
future: usersRef.document(ownerId).get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
User user = User.fromDocument(snapshot.data);
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
left: 8.0,
top: 12.0,
right: 8.0,
bottom: 8.0,
),
child: Text(
title,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 22.0,
),
),
),
ListTile(
leading: GestureDetector(
onTap: () => showProfile(context, profileId: user.id),
child: CircleAvatar(
backgroundImage: CachedNetworkImageProvider(user.photoUrl),
backgroundColor: Colors.grey,
),
),
title: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
username,
),
),
// Container(
// width: 100,
// height: 27,
// child: buildProfileButton(),
// ),
],
),
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
timeago.format(timestamp.toDate(), locale: 'fr'),
overflow: TextOverflow.ellipsis,
),
Text(category),
],
),
// trailing: IconButton(
// onPressed: () => handleDeletePost(context),
// icon: Icon(Icons.more_vert),
// ),
),
Container(child: cachedNetworkImage(mediaUrl)),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(content, style: TextStyle(fontSize: 18.0)),
),
],
);
},
);
}
void goBack() {
Navigator.pop(context);
}
@override
Widget build(context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: GestureDetector(
onTap: goBack,
child: Icon(
Icons.arrow_back,
color: Colors.white,
)),
title: Text(
'MiaKoz',
style: TextStyle(color: Colors.white),
),
),
body: Container(child: buildPostHeader()),
bottomNavigationBar: BottomAppBar(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: handleLikePost,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Image(
image: isLiked
? AssetImage("assets/images/clap.png")
: AssetImage("assets/images/no_clap.png"),
color: Colors.cyan,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
width: 28,
height: 28,
),
),
),
// GestureDetector(
// onTap: handleLikePost,
// child: Icon(
// isLiked ? Icons.favorite : Icons.favorite_border,
// size: 28.0,
// color: Colors.cyan,
// ),
// ),
Container(
margin: EdgeInsets.only(left: 8.0),
child: Text(
"$likeCount claps",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
],
),
GestureDetector(
onTap: () => showComments(
context,
postId: postId,
ownerId: ownerId,
mediaUrl: mediaUrl,
),
child: Icon(
Icons.chat,
size: 28.0,
color: Colors.cyan,
),
),
IconButton(
icon: Icon(
Icons.share,
color: Theme.of(context).primaryColor,
),
onPressed: () {
final RenderBox box = context.findRenderObject();
Share.share('${title} - ${mediaUrl}',
subject: content,
sharePositionOrigin:
box.localToGlobal(Offset.zero) & box.size);
}),
],
),
),
),
);
}
}