mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-09 22:57:21 +00:00
debounce and refactored CommunitiesBloc
.
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommunitiesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
void _onError(Object e, Emitter<CommunitiesState> emit) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
Reference in New Issue
Block a user