refactor: scroll

This commit is contained in:
mso7nueh
2024-09-09 12:58:25 +03:00
parent f9c10047b2
commit 3ccf5b6d92
31 changed files with 610 additions and 608 deletions

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
class CommentsCubit extends Cubit<CommentsState> {
@@ -9,79 +10,60 @@ class CommentsCubit extends Cubit<CommentsState> {
required this.feedRepository,
}) : super(CommentsState());
getBlogPostComments(int id) async {
emit(CommentsState.loading());
setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
}
getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
switch (commentSourceType) {
case CommentSourceType.blogPost:
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
break;
case CommentSourceType.diet:
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
break;
case CommentSourceType.recipe:
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
break;
}
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
getRecipesComments(String id) async {
emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
getDietsComments(int id) async {
emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendBlogPostComment({
required int id,
required String commentText,
int? parentCommentId,
}) async {
try {
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendRecipesComment({
sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
try {
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendDietsComment({
required int id,
required String commentText,
int? parentCommentId,
}) async {
try {
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
switch (commentSourceType) {
case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.diet:
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.recipe:
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
}
getComments(id, commentSourceType);
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}

View File

@@ -11,6 +11,7 @@ abstract class CommentsState with _$CommentsState {
const factory CommentsState.loaded({
required CommentModel comment,
int? replyId,
}) = CommentsStateLoaded;
const factory CommentsState.error({

View File

@@ -20,7 +20,7 @@ mixin _$CommentsState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) =>
throw _privateConstructorUsedError;
@@ -28,7 +28,7 @@ mixin _$CommentsState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) =>
throw _privateConstructorUsedError;
@@ -36,7 +36,7 @@ mixin _$CommentsState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) =>
@@ -126,7 +126,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return $default();
@@ -137,7 +137,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return $default?.call();
@@ -148,7 +148,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -241,7 +241,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return loading();
@@ -252,7 +252,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return loading?.call();
@@ -263,7 +263,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -321,7 +321,7 @@ abstract class _$$CommentsStateLoadedImplCopyWith<$Res> {
$Res Function(_$CommentsStateLoadedImpl) then) =
__$$CommentsStateLoadedImplCopyWithImpl<$Res>;
@useResult
$Res call({CommentModel comment});
$Res call({CommentModel comment, int? replyId});
$CommentModelCopyWith<$Res> get comment;
}
@@ -338,12 +338,17 @@ class __$$CommentsStateLoadedImplCopyWithImpl<$Res>
@override
$Res call({
Object? comment = null,
Object? replyId = freezed,
}) {
return _then(_$CommentsStateLoadedImpl(
comment: null == comment
? _value.comment
: comment // ignore: cast_nullable_to_non_nullable
as CommentModel,
replyId: freezed == replyId
? _value.replyId
: replyId // ignore: cast_nullable_to_non_nullable
as int?,
));
}
@@ -359,14 +364,16 @@ class __$$CommentsStateLoadedImplCopyWithImpl<$Res>
/// @nodoc
class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
const _$CommentsStateLoadedImpl({required this.comment});
const _$CommentsStateLoadedImpl({required this.comment, this.replyId});
@override
final CommentModel comment;
@override
final int? replyId;
@override
String toString() {
return 'CommentsState.loaded(comment: $comment)';
return 'CommentsState.loaded(comment: $comment, replyId: $replyId)';
}
@override
@@ -374,11 +381,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CommentsStateLoadedImpl &&
(identical(other.comment, comment) || other.comment == comment));
(identical(other.comment, comment) || other.comment == comment) &&
(identical(other.replyId, replyId) || other.replyId == replyId));
}
@override
int get hashCode => Object.hash(runtimeType, comment);
int get hashCode => Object.hash(runtimeType, comment, replyId);
@JsonKey(ignore: true)
@override
@@ -392,10 +400,10 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return loaded(comment);
return loaded(comment, replyId);
}
@override
@@ -403,10 +411,10 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return loaded?.call(comment);
return loaded?.call(comment, replyId);
}
@override
@@ -414,12 +422,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(comment);
return loaded(comment, replyId);
}
return orElse();
}
@@ -463,10 +471,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
}
abstract class CommentsStateLoaded implements CommentsState {
const factory CommentsStateLoaded({required final CommentModel comment}) =
_$CommentsStateLoadedImpl;
const factory CommentsStateLoaded(
{required final CommentModel comment,
final int? replyId}) = _$CommentsStateLoadedImpl;
CommentModel get comment;
int? get replyId;
@JsonKey(ignore: true)
_$$CommentsStateLoadedImplCopyWith<_$CommentsStateLoadedImpl> get copyWith =>
throw _privateConstructorUsedError;
@@ -540,7 +550,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return error(errorMessage);
@@ -551,7 +561,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return error?.call(errorMessage);
@@ -562,7 +572,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {

View File

@@ -4,8 +4,8 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
class FeedCubit extends Cubit<FeedState> {
final FeedRepository feedRepository;
int _currentPage = 1;
final int _lastPage = 7;
int currentPage = 1;
late int lastPage;
bool isLoading = false;
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
@@ -13,6 +13,7 @@ class FeedCubit extends Cubit<FeedState> {
loadFeedBlogPostList() async {
try {
final feedBlogPosts = await feedRepository.getFeedBlogPosts();
lastPage = feedBlogPosts.meta.lastPage;
emit(
FeedState.loaded(
feedBlogPostList: feedBlogPosts.feedBlogPostList,
@@ -25,26 +26,29 @@ class FeedCubit extends Cubit<FeedState> {
}
loadNextPage() async {
print('start');
try {
if (_lastPage < _currentPage) {
_currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: _currentPage);
emit(
FeedState.loaded(
feedBlogPostList: [
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: false,
),
);
if (currentPage < lastPage && !isLoading) {
isLoading = true;
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
try {
if (currentPage < lastPage) {
currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage);
isLoading = false;
emit(
FeedState.loaded(
feedBlogPostList: [
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: isLoading,
),
);
}
} catch (e) {
emit(FeedState.error(errorMessage: e.toString()));
}
} catch (e) {
print('error bro');
print(e.toString());
} finally {
print('end');
}
}

View File

@@ -26,6 +26,27 @@ class _FeedScreenState extends State<FeedScreen> {
final searchController = TextEditingController();
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
int _selectedChipIndex = 0;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
final dietsScrollController = ScrollController();
@override
void initState() {
blogPostsScrollController.addListener(_onScroll);
super.initState();
}
_onScroll() {
if (blogPostsScrollController.offset >= blogPostsScrollController.position.maxScrollExtent && !blogPostsScrollController.position.outOfRange) {
context.read<FeedCubit>().loadNextPage();
}
}
@override
void dispose() {
blogPostsScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -68,6 +89,7 @@ class _FeedScreenState extends State<FeedScreen> {
),
backgroundColor: Colors.grey.shade200,
body: NestedScrollView(
controller: blogPostsScrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
@@ -108,47 +130,6 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
_buildDiets() {
return RefreshIndicator(
onRefresh: () => context.read<DietsCubit>().loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state is DietsStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.diets.data[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.diets.data.length,
);
} else if (state is DietsStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Center(
child: CupertinoActivityIndicator(),
),
);
}
},
),
);
}
_buildAllBlogPosts() {
return RefreshIndicator(
onRefresh: () => context.read<FeedCubit>().loadFeedBlogPostList(),
@@ -158,14 +139,17 @@ class _FeedScreenState extends State<FeedScreen> {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == state.feedBlogPostList.length) {
return CupertinoActivityIndicator();
}
return BlogPostWidget(feedBlogPostModel: state.feedBlogPostList[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.feedBlogPostList.length,
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
);
} else if (state is FeedStateError) {
return Scaffold(
@@ -231,6 +215,47 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
_buildDiets() {
return RefreshIndicator(
onRefresh: () => context.read<DietsCubit>().loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state is DietsStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.diets.data[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.diets.data.length,
);
} else if (state is DietsStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Center(
child: CupertinoActivityIndicator(),
),
);
}
},
),
);
}
_chipGroup() {
return PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),

View File

@@ -124,6 +124,8 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
child: Hero(
tag: baseUrl + '/storage/' + widget.feedBlogPostModel.image,
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
imageUrl: baseUrl + '/storage/' + widget.feedBlogPostModel.image,
errorWidget: (context, str, object) => SizedBox.shrink(),
),

View File

@@ -0,0 +1,119 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:flutter/material.dart';
enum CommentSourceType { blogPost, recipe, diet }
class CommentsWidget extends StatefulWidget {
final String id;
final CommentSourceType commentSourceType;
final CommentModel comment;
final int? replyId;
const CommentsWidget({
super.key,
required this.id,
required this.commentSourceType,
required this.comment,
this.replyId,
});
@override
State<CommentsWidget> createState() => _CommentsWidgetState();
}
class _CommentsWidgetState extends State<CommentsWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [
for (int i = 0; i < widget.comment.data.length; i++) ...[
_comment(widget.comment.data[i]),
if (widget.comment.data[i].childrenComments.isNotEmpty) SizedBox(height: 8),
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
Padding(
padding: EdgeInsets.only(left: 40),
child: _comment(widget.comment.data[i].childrenComments[j], 12, true),
),
SizedBox(height: 8),
],
],
);
}
_comment(Comment comment, [double radius = 16, bool isChild = false]) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: widget.replyId == comment.id ? appTheme.blueA400.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon),
radius: radius,
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
Text(
comment.body,
style: TextStyle(
height: 1,
color: Colors.black,
),
),
SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(width: 16),
if (!isChild)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(comment.id);
},
child: Text(
'Ответить',
style: TextStyle(
height: 1,
fontSize: 12,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
],
),
),
);
}
}

View File

@@ -34,6 +34,8 @@ class _DietsWidgetState extends State<DietsWidget> {
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + widget.diet.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
Positioned(

View File

@@ -34,6 +34,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + widget.recipes.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
Positioned(