refactor: split the various parts of the post widget out into separate widgets

Makes the whole thing way more readable and is a preparation for reusing
code in TimelinePostScreen
This commit is contained in:
Bart Ribbers 2025-04-22 11:39:05 +02:00
parent 4f347634db
commit b0c0c22381

View file

@ -53,7 +53,6 @@ class TimelinePostWidget extends StatefulWidget {
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
return SizedBox(
@ -64,30 +63,99 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
_PostHeader(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
allowDeletion: widget.allowAllDeletion ||
widget.post.creator?.userId == widget.userId,
onUserTap: widget.onUserTap,
onPostDelete: widget.onPostDelete,
),
if (widget.post.imageUrl != null || widget.post.image != null) ...[
const SizedBox(height: 8.0),
_PostImage(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
),
],
const SizedBox(height: 8.0),
_PostLikeAndReactionsInformation(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
isLikedByUser: isLikedByUser,
onTapComment: widget.onTap,
),
const SizedBox(height: 8.0),
if (widget.options.itemInfoBuilder != null) ...[
widget.options.itemInfoBuilder!(post: widget.post),
] else ...[
_PostInfo(
options: widget.options,
post: widget.post,
onTap: widget.onTap,
),
],
if (widget.options.dividerBuilder != null) ...[
widget.options.dividerBuilder!(),
],
],
),
);
}
}
class _PostHeader extends StatelessWidget {
const _PostHeader({
required this.service,
required this.options,
required this.userId,
required this.post,
required this.allowDeletion,
required this.onUserTap,
required this.onPostDelete,
});
final TimelineService service;
final TimelineOptions options;
final String userId;
final TimelinePost post;
final bool allowDeletion;
final void Function(String userId)? onUserTap;
final VoidCallback onPostDelete;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
children: [
if (widget.post.creator != null) ...[
if (post.creator != null) ...[
InkWell(
onTap: widget.onUserTap != null
? () =>
widget.onUserTap?.call(widget.post.creator!.userId)
onTap: onUserTap != null
? () => onUserTap?.call(post.creator!.userId)
: null,
child: Row(
children: [
if (widget.post.creator!.imageUrl != null) ...[
widget.options.userAvatarBuilder?.call(
widget.post.creator!,
if (post.creator!.imageUrl != null) ...[
options.userAvatarBuilder?.call(
post.creator!,
28,
) ??
CircleAvatar(
radius: 14,
backgroundImage: CachedNetworkImageProvider(
widget.post.creator!.imageUrl!,
post.creator!.imageUrl!,
),
),
] else ...[
widget.options.anonymousAvatarBuilder?.call(
widget.post.creator!,
options.anonymousAvatarBuilder?.call(
post.creator!,
28,
) ??
const CircleAvatar(
@ -99,254 +167,243 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
],
const SizedBox(width: 10.0),
Text(
widget.options.nameBuilder?.call(widget.post.creator) ??
widget.post.creator?.fullName ??
widget.options.translations.anonymousUser,
style: widget.options.theme.textStyles
.listPostCreatorTitleStyle ??
theme.textTheme.titleSmall!.copyWith(
color: Colors.black,
),
options.nameBuilder?.call(post.creator) ??
post.creator?.fullName ??
options.translations.anonymousUser,
style: options.theme.textStyles.listPostCreatorTitleStyle ??
theme.textTheme.titleSmall!.copyWith(color: Colors.black),
),
],
),
),
],
const Spacer(),
if (widget.allowAllDeletion ||
widget.post.creator?.userId == widget.userId) ...[
if (allowDeletion) ...[
PopupMenuButton(
onSelected: (value) async {
if (value == 'delete') {
await showPostDeletionConfirmationDialog(
widget.options,
options,
context,
widget.onPostDelete,
onPostDelete,
);
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'delete',
child: Row(
children: [
Text(
widget.options.translations.deletePost,
style: widget
.options.theme.textStyles.deletePostStyle ??
options.translations.deletePost,
style: options.theme.textStyles.deletePostStyle ??
theme.textTheme.bodyMedium,
),
const SizedBox(width: 8.0),
widget.options.theme.deleteIcon ??
options.theme.deleteIcon ??
Icon(
Icons.delete,
color: widget.options.theme.iconColor,
color: options.theme.iconColor,
),
],
),
),
],
child: widget.options.theme.moreIcon ??
child: options.theme.moreIcon ??
Icon(
Icons.more_horiz_rounded,
color: widget.options.theme.iconColor,
color: options.theme.iconColor,
),
),
],
],
),
// image of the post
if (widget.post.imageUrl != null || widget.post.image != null) ...[
const SizedBox(height: 8.0),
Flexible(
flex: widget.options.postWidgetHeight != null ? 1 : 0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: widget.options.doubleTapTolike
? TappableImage(
likeAndDislikeIcon:
widget.options.likeAndDislikeIconsForDoubleTap,
post: widget.post,
userId: widget.userId,
onLike: ({required bool liked}) async {
var userId = widget.userId;
);
}
}
late TimelinePost result;
class _PostLikeAndReactionsInformation extends StatelessWidget {
const _PostLikeAndReactionsInformation({
required this.service,
required this.options,
required this.userId,
required this.post,
required this.isLikedByUser,
required this.onTapComment,
});
if (!liked) {
result = await widget.service.postService.likePost(
final TimelineService service;
final TimelineOptions options;
final String userId;
final TimelinePost post;
final bool isLikedByUser;
final VoidCallback onTapComment;
@override
Widget build(BuildContext context) => Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
if (!isLikedByUser) {
await service.postService.likePost(
userId,
widget.post,
post,
);
} else {
result =
await widget.service.postService.unlikePost(
await service.postService.unlikePost(
userId,
widget.post,
post,
);
}
},
icon: options.theme.likeIcon ??
Icon(
isLikedByUser
? Icons.favorite_rounded
: Icons.favorite_outline_outlined,
color: options.theme.iconColor,
size: options.iconSize,
),
),
const SizedBox(width: 4.0),
if (options.iconsWithValues) ...[
Text('${post.likes}'),
],
if (post.reactionEnabled) ...[
const SizedBox(width: 8.0),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: onTapComment,
icon: options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: options.theme.iconColor,
width: options.iconSize,
height: options.iconSize,
),
),
if (options.iconsWithValues) ...[
const SizedBox(width: 4.0),
Text('${post.reaction}'),
],
],
],
);
}
class _PostImage extends StatelessWidget {
const _PostImage({
required this.options,
required this.service,
required this.userId,
required this.post,
});
final TimelineOptions options;
final TimelineService service;
final String userId;
final TimelinePost post;
@override
Widget build(BuildContext context) => Flexible(
flex: options.postWidgetHeight != null ? 1 : 0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: options.doubleTapTolike
? TappableImage(
likeAndDislikeIcon: options.likeAndDislikeIconsForDoubleTap,
post: post,
userId: userId,
onLike: ({required bool liked}) async {
TimelinePost result;
if (!liked) {
result = await service.postService.likePost(
userId,
post,
);
} else {
result = await service.postService.unlikePost(
userId,
post,
);
}
return result.likedBy?.contains(userId) ?? false;
},
)
: widget.post.imageUrl != null
: post.imageUrl != null
? CachedNetworkImage(
width: double.infinity,
imageUrl: widget.post.imageUrl!,
imageUrl: post.imageUrl!,
fit: BoxFit.fitWidth,
)
: Image.memory(
width: double.infinity,
widget.post.image!,
post.image!,
fit: BoxFit.fitWidth,
),
),
),
],
const SizedBox(height: 8.0),
// post information
if (widget.options.iconsWithValues) ...[
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
var userId = widget.userId;
if (!isLikedByUser) {
await widget.service.postService.likePost(
userId,
widget.post,
);
} else {
await widget.service.postService.unlikePost(
userId,
widget.post,
);
}
},
icon: widget.options.theme.likeIcon ??
Icon(
isLikedByUser
? Icons.favorite_rounded
: Icons.favorite_outline_outlined,
color: widget.options.theme.iconColor,
size: widget.options.iconSize,
),
),
const SizedBox(width: 4.0),
Text('${widget.post.likes}'),
if (widget.post.reactionEnabled) ...[
const SizedBox(width: 8.0),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: widget.onTap,
icon: widget.options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: widget.options.theme.iconColor,
width: widget.options.iconSize,
height: widget.options.iconSize,
),
),
const SizedBox(width: 4.0),
Text('${widget.post.reaction}'),
],
],
),
] else ...[
Row(
}
class _PostInfo extends StatelessWidget {
const _PostInfo({
required this.options,
required this.post,
required this.onTap,
});
final TimelineOptions options;
final TimelinePost post;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Column(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed:
isLikedByUser ? widget.onTapUnlike : widget.onTapLike,
icon: (isLikedByUser
? widget.options.theme.likedIcon
: widget.options.theme.likeIcon) ??
Icon(
isLikedByUser
? Icons.favorite_rounded
: Icons.favorite_outline,
color: widget.options.theme.iconColor,
size: widget.options.iconSize,
),
),
const SizedBox(width: 8.0),
if (widget.post.reactionEnabled) ...[
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: widget.onTap,
icon: widget.options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: widget.options.theme.iconColor,
width: widget.options.iconSize,
height: widget.options.iconSize,
),
),
],
],
),
],
const SizedBox(height: 8),
if (widget.options.itemInfoBuilder != null) ...[
widget.options.itemInfoBuilder!(
post: widget.post,
),
] else ...[
_PostLikeCountText(
post: widget.post,
options: widget.options,
post: post,
options: options,
),
const SizedBox(height: 4.0),
Row(
children: [
Text(
widget.options.nameBuilder?.call(widget.post.creator) ??
widget.post.creator?.fullName ??
widget.options.translations.anonymousUser,
style: widget.options.theme.textStyles.listCreatorNameStyle ??
options.nameBuilder?.call(post.creator) ??
post.creator?.fullName ??
options.translations.anonymousUser,
style: options.theme.textStyles.listCreatorNameStyle ??
theme.textTheme.titleSmall!.copyWith(
color: Colors.black,
),
),
const SizedBox(width: 4.0),
Text(
widget.post.title,
style: widget.options.theme.textStyles.listPostTitleStyle ??
post.title,
style: options.theme.textStyles.listPostTitleStyle ??
theme.textTheme.bodySmall,
),
],
),
const SizedBox(height: 4.0),
InkWell(
onTap: widget.onTap,
onTap: onTap,
child: Text(
widget.options.translations.viewPost,
style: widget.options.theme.textStyles.viewPostStyle ??
options.translations.viewPost,
style: options.theme.textStyles.viewPostStyle ??
theme.textTheme.titleSmall!.copyWith(
color: const Color(0xFF8D8D8D),
),
),
),
],
if (widget.options.dividerBuilder != null) ...[
widget.options.dividerBuilder!(),
],
],
),
);
}
}