mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +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()) {
|
super(const CommunitiesState()) {
|
||||||
on<LoadCommunities>(_onLoadCommunities);
|
on<LoadCommunities>(_onLoadCommunities);
|
||||||
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
||||||
on<SearchCommunities>(_onSearchCommunities);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final CommunitiesService _communitiesService;
|
final CommunitiesService _communitiesService;
|
||||||
@ -39,19 +38,9 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
emit(
|
_onApiException(e, emit);
|
||||||
state.copyWith(
|
|
||||||
status: CommunitiesStatus.failure,
|
|
||||||
errorMessage: e.message,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
_onError(e, emit);
|
||||||
state.copyWith(
|
|
||||||
status: CommunitiesStatus.failure,
|
|
||||||
errorMessage: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,59 +72,30 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
emit(
|
_onApiException(e, emit);
|
||||||
state.copyWith(
|
|
||||||
isLoadingMore: false,
|
|
||||||
errorMessage: e.message,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
_onError(e, emit);
|
||||||
state.copyWith(
|
|
||||||
isLoadingMore: false,
|
|
||||||
errorMessage: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSearchCommunities(
|
void _onApiException(
|
||||||
SearchCommunities event,
|
APIException e,
|
||||||
Emitter<CommunitiesState> emit,
|
Emitter<CommunitiesState> emit,
|
||||||
) async {
|
) {
|
||||||
try {
|
emit(
|
||||||
emit(state.copyWith(status: CommunitiesStatus.loading));
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
errorMessage: e.message,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final param = LoadCommunitiesParam(
|
void _onError(Object e, Emitter<CommunitiesState> emit) {
|
||||||
page: 1,
|
emit(
|
||||||
search: event.searchQuery,
|
state.copyWith(
|
||||||
);
|
isLoadingMore: false,
|
||||||
|
errorMessage: e.toString(),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,12 +22,3 @@ class LoadMoreCommunities extends CommunitiesEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [];
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/common/widgets/search_bar.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/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/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/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/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/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_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_management_sidebar_header.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
|
||||||
import 'package:syncrow_web/utils/style.dart';
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
class SpaceManagementCommunitiesTree extends StatelessWidget {
|
class SpaceManagementCommunitiesTree extends StatefulWidget {
|
||||||
const SpaceManagementCommunitiesTree({super.key});
|
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) {
|
bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) {
|
||||||
final selectedSpace =
|
final selectedSpace =
|
||||||
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
||||||
@ -25,6 +41,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget {
|
|||||||
return isSpaceSelected || anySubSpaceIsSelected;
|
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;
|
static const _width = 300.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -39,18 +67,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget {
|
|||||||
SpaceManagementSidebarHeader(
|
SpaceManagementSidebarHeader(
|
||||||
onAddCommunity: () => _onAddCommunity(context),
|
onAddCommunity: () => _onAddCommunity(context),
|
||||||
),
|
),
|
||||||
CustomSearchBar(onSearchChanged: (value) {}),
|
CustomSearchBar(
|
||||||
|
onSearchChanged: _onSearchChanged,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
switch (state.status) {
|
switch (state.status) {
|
||||||
CommunitiesStatus.initial =>
|
CommunitiesStatus.initial =>
|
||||||
const Center(child: CircularProgressIndicator()),
|
const Center(child: CircularProgressIndicator()),
|
||||||
CommunitiesStatus.loading =>
|
CommunitiesStatus.loading => state.communities.isEmpty
|
||||||
const Center(child: CircularProgressIndicator()),
|
? const Center(child: CircularProgressIndicator())
|
||||||
CommunitiesStatus.success =>
|
: _buildCommunitiesTree(context, state),
|
||||||
_buildCommunitiesTree(context, state.communities),
|
CommunitiesStatus.success => _buildCommunitiesTree(context, state),
|
||||||
CommunitiesStatus.failure => Center(
|
CommunitiesStatus.failure => _buildErrorState(context, state),
|
||||||
child: Text(state.errorMessage ?? 'Something went wrong'),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -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(
|
Widget _buildCommunitiesTree(
|
||||||
BuildContext context,
|
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(
|
return Expanded(
|
||||||
child: SpaceManagementSidebarCommunitiesList(
|
child: SpaceManagementSidebarCommunitiesList(
|
||||||
communities: communities,
|
communities: state.communities,
|
||||||
|
onLoadMore: state.hasNext ? _onLoadMore : null,
|
||||||
|
isLoadingMore: state.isLoadingMore,
|
||||||
|
hasNext: state.hasNext,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _buildCommunityTile(context, communities[index]);
|
return _buildCommunityTile(context, state.communities[index]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,17 @@ class SpaceManagementSidebarCommunitiesList extends StatefulWidget {
|
|||||||
const SpaceManagementSidebarCommunitiesList({
|
const SpaceManagementSidebarCommunitiesList({
|
||||||
required this.communities,
|
required this.communities,
|
||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
|
this.onLoadMore,
|
||||||
|
this.isLoadingMore = false,
|
||||||
|
this.hasNext = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<CommunityModel> communities;
|
final List<CommunityModel> communities;
|
||||||
final Widget Function(BuildContext context, int index) itemBuilder;
|
final Widget Function(BuildContext context, int index) itemBuilder;
|
||||||
|
final VoidCallback? onLoadMore;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
final bool hasNext;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SpaceManagementSidebarCommunitiesList> createState() =>
|
State<SpaceManagementSidebarCommunitiesList> createState() =>
|
||||||
@ -25,12 +31,26 @@ class _SpaceManagementSidebarCommunitiesListState
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController = ScrollController();
|
_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) {
|
bool _onNotification(ScrollEndNotification notification) {
|
||||||
final hasReachedEnd = notification.metrics.extentAfter == 0;
|
final hasReachedEnd = notification.metrics.extentAfter == 0;
|
||||||
if (hasReachedEnd) {
|
if (hasReachedEnd &&
|
||||||
// Call data from API.
|
widget.hasNext &&
|
||||||
|
!widget.isLoadingMore &&
|
||||||
|
widget.onLoadMore != null) {
|
||||||
|
widget.onLoadMore!();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,13 +60,16 @@ class _SpaceManagementSidebarCommunitiesListState
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController
|
_scrollController
|
||||||
..removeListener(() {})
|
..removeListener(_onScroll)
|
||||||
..dispose();
|
..dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Calculate item count including loading indicator
|
||||||
|
final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@ -60,9 +83,20 @@ class _SpaceManagementSidebarCommunitiesListState
|
|||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||||
itemCount: widget.communities.length,
|
itemCount: itemCount,
|
||||||
controller: _scrollController,
|
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