debounce and refactored CommunitiesBloc.

This commit is contained in:
Faris Armoush
2025-06-22 12:01:32 +03:00
parent 51c088d998
commit 65ed94eb08
5 changed files with 204 additions and 88 deletions

View File

@ -0,0 +1,60 @@
import 'dart:async';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
class DebouncedCommunitiesService implements CommunitiesService {
DebouncedCommunitiesService({
required CommunitiesService communitiesService,
this.debounceDuration = const Duration(milliseconds: 400),
}) : _communitiesService = communitiesService;
final CommunitiesService _communitiesService;
final Duration debounceDuration;
Timer? _debounceTimer;
String _lastSearchQuery = '';
@override
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
if (param.search.isNotEmpty) {
return _getDebouncedCommunity(param);
}
return _communitiesService.getCommunity(param);
}
Future<CommunitiesPaginationModel> _getDebouncedCommunity(
LoadCommunitiesParam param,
) async {
final completer = Completer<CommunitiesPaginationModel>();
_debounceTimer?.cancel();
_lastSearchQuery = param.search;
_debounceTimer = Timer(debounceDuration, () async {
try {
if (_lastSearchQuery == param.search) {
final result = await _communitiesService.getCommunity(param);
if (!completer.isCompleted) {
completer.complete(result);
}
} else {
if (!completer.isCompleted) {
completer.complete(const CommunitiesPaginationModel.empty());
}
}
} catch (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
}
});
return completer.future;
}
}

View File

@ -15,7 +15,6 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
super(const CommunitiesState()) {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<SearchCommunities>(_onSearchCommunities);
}
final CommunitiesService _communitiesService;
@ -39,19 +38,9 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
),
);
} on APIException catch (e) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
errorMessage: e.message,
),
);
_onApiException(e, emit);
} catch (e) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
errorMessage: e.toString(),
),
);
_onError(e, emit);
}
}
@ -83,59 +72,30 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
),
);
} on APIException catch (e) {
emit(
state.copyWith(
isLoadingMore: false,
errorMessage: e.message,
),
);
_onApiException(e, emit);
} catch (e) {
emit(
state.copyWith(
isLoadingMore: false,
errorMessage: e.toString(),
),
);
_onError(e, emit);
}
}
Future<void> _onSearchCommunities(
SearchCommunities event,
void _onApiException(
APIException e,
Emitter<CommunitiesState> emit,
) async {
try {
emit(state.copyWith(status: CommunitiesStatus.loading));
final param = LoadCommunitiesParam(
page: 1,
search: event.searchQuery,
);
final paginationResponse = await _communitiesService.getCommunity(param);
emit(
CommunitiesState(
status: CommunitiesStatus.success,
communities: paginationResponse.communities,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
searchQuery: event.searchQuery,
),
);
} on APIException catch (e) {
) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.message,
),
);
} catch (e) {
}
void _onError(Object e, Emitter<CommunitiesState> emit) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.toString(),
),
);
}
}
}

View File

@ -22,12 +22,3 @@ class LoadMoreCommunities extends CommunitiesEvent {
@override
List<Object?> get props => [];
}
class SearchCommunities extends CommunitiesEvent {
const SearchCommunities(this.searchQuery);
final String searchQuery;
@override
List<Object?> get props => [searchQuery];
}

View File

@ -1,20 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatelessWidget {
class SpaceManagementCommunitiesTree extends StatefulWidget {
const SpaceManagementCommunitiesTree({super.key});
@override
State<SpaceManagementCommunitiesTree> createState() =>
_SpaceManagementCommunitiesTreeState();
}
class _SpaceManagementCommunitiesTreeState
extends State<SpaceManagementCommunitiesTree> {
@override
void initState() {
context.read<CommunitiesBloc>().add(
const LoadCommunities(LoadCommunitiesParam()),
);
super.initState();
}
bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) {
final selectedSpace =
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
@ -25,6 +41,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget {
return isSpaceSelected || anySubSpaceIsSelected;
}
void _onSearchChanged(String searchQuery) {
context.read<CommunitiesBloc>().add(
LoadCommunities(LoadCommunitiesParam(
search: searchQuery.trim(),
)),
);
}
void _onLoadMore() {
context.read<CommunitiesBloc>().add(const LoadMoreCommunities());
}
static const _width = 300.0;
@override
@ -39,18 +67,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget {
SpaceManagementSidebarHeader(
onAddCommunity: () => _onAddCommunity(context),
),
CustomSearchBar(onSearchChanged: (value) {}),
CustomSearchBar(
onSearchChanged: _onSearchChanged,
),
const SizedBox(height: 16),
switch (state.status) {
CommunitiesStatus.initial =>
const Center(child: CircularProgressIndicator()),
CommunitiesStatus.loading =>
const Center(child: CircularProgressIndicator()),
CommunitiesStatus.success =>
_buildCommunitiesTree(context, state.communities),
CommunitiesStatus.failure => Center(
child: Text(state.errorMessage ?? 'Something went wrong'),
),
CommunitiesStatus.loading => state.communities.isEmpty
? const Center(child: CircularProgressIndicator())
: _buildCommunitiesTree(context, state),
CommunitiesStatus.success => _buildCommunitiesTree(context, state),
CommunitiesStatus.failure => _buildErrorState(context, state),
},
],
),
@ -59,15 +87,58 @@ class SpaceManagementCommunitiesTree extends StatelessWidget {
);
}
Widget _buildErrorState(BuildContext context, CommunitiesState state) {
return Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
state.errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<CommunitiesBloc>().add(
LoadCommunities(LoadCommunitiesParam(
search: state.searchQuery,
)),
);
},
child: const Text('Retry'),
),
],
),
),
);
}
Widget _buildCommunitiesTree(
BuildContext context,
List<CommunityModel> communities,
CommunitiesState state,
) {
if (state.communities.isEmpty && state.status == CommunitiesStatus.success) {
return Expanded(
child: Center(
child: Text(
state.searchQuery.isEmpty
? 'No communities found'
: 'No communities found for "${state.searchQuery}"',
textAlign: TextAlign.center,
),
),
);
}
return Expanded(
child: SpaceManagementSidebarCommunitiesList(
communities: communities,
communities: state.communities,
onLoadMore: state.hasNext ? _onLoadMore : null,
isLoadingMore: state.isLoadingMore,
hasNext: state.hasNext,
itemBuilder: (context, index) {
return _buildCommunityTile(context, communities[index]);
return _buildCommunityTile(context, state.communities[index]);
},
),
);

View File

@ -6,11 +6,17 @@ class SpaceManagementSidebarCommunitiesList extends StatefulWidget {
const SpaceManagementSidebarCommunitiesList({
required this.communities,
required this.itemBuilder,
this.onLoadMore,
this.isLoadingMore = false,
this.hasNext = false,
super.key,
});
final List<CommunityModel> communities;
final Widget Function(BuildContext context, int index) itemBuilder;
final VoidCallback? onLoadMore;
final bool isLoadingMore;
final bool hasNext;
@override
State<SpaceManagementSidebarCommunitiesList> createState() =>
@ -25,12 +31,26 @@ class _SpaceManagementSidebarCommunitiesListState
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
// Trigger pagination when user is close to the bottom
if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) {
widget.onLoadMore!();
}
}
}
bool _onNotification(ScrollEndNotification notification) {
final hasReachedEnd = notification.metrics.extentAfter == 0;
if (hasReachedEnd) {
// Call data from API.
if (hasReachedEnd &&
widget.hasNext &&
!widget.isLoadingMore &&
widget.onLoadMore != null) {
widget.onLoadMore!();
return true;
}
@ -40,13 +60,16 @@ class _SpaceManagementSidebarCommunitiesListState
@override
void dispose() {
_scrollController
..removeListener(() {})
..removeListener(_onScroll)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Calculate item count including loading indicator
final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
@ -60,9 +83,20 @@ class _SpaceManagementSidebarCommunitiesListState
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsetsDirectional.only(start: 16),
itemCount: widget.communities.length,
itemCount: itemCount,
controller: _scrollController,
itemBuilder: widget.itemBuilder,
itemBuilder: (context, index) {
if (index == widget.communities.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
return widget.itemBuilder(context, index);
},
),
),
),