From b0286b0e8ce74376439abac2963c52749d0640a1 Mon Sep 17 00:00:00 2001 From: sidhdhi canopas <122426509+cp-sidhdhi-p@users.noreply.github.com> Date: Thu, 9 May 2024 15:02:20 +0530 Subject: [PATCH] Match list with streams (#14) * match list with streams * remove todo comments and error handling * resource de-allocation * use resume detector --- .../ball_score/ball_score_service.dart | 49 +++-- data/lib/service/innings/inning_service.dart | 2 + data/lib/service/match/match_service.dart | 170 ++++++++++-------- data/lib/service/team/team_service.dart | 74 +++----- khelo/android/app/build.gradle | 2 - khelo/lib/components/resume_detector.dart | 40 +++++ .../domain/extensions/enum_extensions.dart | 16 +- khelo/lib/ui/flow/home/home_screen.dart | 53 +++++- khelo/lib/ui/flow/home/home_view_model.dart | 25 ++- khelo/lib/ui/flow/main/main_screen.dart | 33 +++- .../ui/flow/matches/match_list_screen.dart | 64 +++++-- .../flow/matches/match_list_view_model.dart | 37 +++- .../ui/flow/my_game/my_game_tab_screen.dart | 39 +++- .../ui/flow/stats/my_stats_tab_screen.dart | 39 +++- .../user_match/user_match_list_screen.dart | 62 ++++++- .../user_match_list_view_model.dart | 35 +++- .../stats/user_stat/user_stat_screen.dart | 65 +++++-- .../stats/user_stat/user_stat_view_model.dart | 35 +++- .../select_filter_option_sheet.dart | 2 - khelo/lib/ui/flow/team/team_list_screen.dart | 62 +++++-- .../ui/flow/team/team_list_view_model.dart | 79 +++++++- .../team/team_list_view_model.freezed.dart | 34 +++- khelo/pubspec.lock | 14 +- khelo/pubspec.yaml | 2 + 24 files changed, 765 insertions(+), 268 deletions(-) create mode 100644 khelo/lib/components/resume_detector.dart diff --git a/data/lib/service/ball_score/ball_score_service.dart b/data/lib/service/ball_score/ball_score_service.dart index a0f75361..23d46e56 100644 --- a/data/lib/service/ball_score/ball_score_service.dart +++ b/data/lib/service/ball_score/ball_score_service.dart @@ -79,35 +79,48 @@ class BallScoreService { controller.add(ballScores); } catch (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); } }, onError: (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); }); return controller.stream; } - Future> getCurrentUserRelatedBalls() async { + Stream> getCurrentUserRelatedBalls() { if (_currentUserId == null) { - return []; + return Stream.value([]); } - try { - final QuerySnapshot> snapshot = await _firestore - .collection(_collectionName) - .where(Filter.or( - Filter("bowler_id", isEqualTo: _currentUserId), - Filter("batsman_id", isEqualTo: _currentUserId), - Filter("wicket_taker_id", isEqualTo: _currentUserId), - Filter("player_out_id", isEqualTo: _currentUserId))) - .get(); + StreamController> controller = + StreamController>(); + _firestore + .collection(_collectionName) + .where(Filter.or( + Filter("bowler_id", isEqualTo: _currentUserId), + Filter("batsman_id", isEqualTo: _currentUserId), + Filter("wicket_taker_id", isEqualTo: _currentUserId), + Filter("player_out_id", isEqualTo: _currentUserId))) + .snapshots() + .listen((snapshot) { + try { + final ballList = snapshot.docs.map((doc) { + final data = doc.data(); + return BallScoreModel.fromJson(data).copyWith(id: doc.id); + }).toList(); - return snapshot.docs.map((doc) { - final data = doc.data(); - return BallScoreModel.fromJson(data).copyWith(id: doc.id); - }).toList(); - } catch (error, stack) { - throw AppError.fromError(error, stack); - } + controller.add(ballList); + } catch (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); + } + }, onError: (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); + }); + + return controller.stream; } Future deleteBall(String ballId) async { diff --git a/data/lib/service/innings/inning_service.dart b/data/lib/service/innings/inning_service.dart index 85790632..eff81b4f 100644 --- a/data/lib/service/innings/inning_service.dart +++ b/data/lib/service/innings/inning_service.dart @@ -86,9 +86,11 @@ class InningsService { controller.add(innings); } catch (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); } }, onError: (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); }); return controller.stream; diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index caa9052e..34ad405c 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -76,94 +76,112 @@ class MatchService { } } - Future> getCurrentUserPlayedMatches() async { + Stream> getCurrentUserPlayedMatches() { if (_currentUserId == null) { - return []; + return Stream.value([]); } - try { - QuerySnapshot snapshot = - await _firestore.collection(_collectionName).get(); + + StreamController> controller = + StreamController>(); + + _firestore.collection(_collectionName).snapshots().listen( + (QuerySnapshot> snapshot) async { List matches = []; - for (QueryDocumentSnapshot mainDoc in snapshot.docs) { - Map mainDocData = - mainDoc.data() as Map; - AddEditMatchRequest match = AddEditMatchRequest.fromJson(mainDocData); - if (match.teams - .map((e) => e.squad.map((e) => e.id).contains(_currentUserId)) - .contains(true) && - match.match_status == MatchStatus.finish) { - List teams = await getTeamsList(match.teams); - matches.add(MatchModel( - id: match.id, - teams: teams, - match_type: match.match_type, - number_of_over: match.number_of_over, - over_per_bowler: match.over_per_bowler, - city: match.city, - ground: match.ground, - start_time: match.start_time, - created_by: match.created_by, - ball_type: match.ball_type, - pitch_type: match.pitch_type, - match_status: match.match_status, - toss_winner_id: match.toss_winner_id, - toss_decision: match.toss_decision, - current_playing_team_id: match.current_playing_team_id, - )); + try { + for (QueryDocumentSnapshot mainDoc in snapshot.docs) { + Map mainDocData = + mainDoc.data() as Map; + AddEditMatchRequest match = AddEditMatchRequest.fromJson(mainDocData); + if (match.teams + .map((e) => e.squad.map((e) => e.id).contains(_currentUserId)) + .contains(true) && + match.match_status == MatchStatus.finish) { + List teams = await getTeamsList(match.teams); + matches.add(MatchModel( + id: match.id, + teams: teams, + match_type: match.match_type, + number_of_over: match.number_of_over, + over_per_bowler: match.over_per_bowler, + city: match.city, + ground: match.ground, + start_time: match.start_time, + created_by: match.created_by, + ball_type: match.ball_type, + pitch_type: match.pitch_type, + match_status: match.match_status, + toss_winner_id: match.toss_winner_id, + toss_decision: match.toss_decision, + current_playing_team_id: match.current_playing_team_id, + )); + } } + controller.add(matches); + } catch (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); } + }, onError: (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); + }); - return matches; - } catch (error, stack) { - throw AppError.fromError(error, stack); - } + return controller.stream; } - Future> getCurrentUserRelatedMatches() async { + Stream> getCurrentUserRelatedMatches() { if (_currentUserId == null) { - return []; + return Stream.value([]); } - try { - QuerySnapshot snapshot = - await _firestore.collection(_collectionName).get(); + + StreamController> controller = + StreamController>(); + + _firestore.collection(_collectionName).snapshots().listen( + (QuerySnapshot> snapshot) async { List matches = []; - for (QueryDocumentSnapshot mainDoc in snapshot.docs) { - Map mainDocData = - mainDoc.data() as Map; - AddEditMatchRequest match = AddEditMatchRequest.fromJson(mainDocData); - List teams = await getTeamsList(match.teams); - if (teams - .map((e) => - e.team.players?.map((e) => e.id).contains(_currentUserId)) - .contains(true) || - teams - .map((e) => e.team.created_by == _currentUserId) - .contains(true)) { - matches.add(MatchModel( - id: match.id, - teams: teams, - match_type: match.match_type, - number_of_over: match.number_of_over, - over_per_bowler: match.over_per_bowler, - city: match.city, - ground: match.ground, - start_time: match.start_time, - created_by: match.created_by, - ball_type: match.ball_type, - pitch_type: match.pitch_type, - match_status: match.match_status, - toss_winner_id: match.toss_winner_id, - toss_decision: match.toss_decision, - current_playing_team_id: match.current_playing_team_id, - )); + try { + for (QueryDocumentSnapshot mainDoc in snapshot.docs) { + Map mainDocData = + mainDoc.data() as Map; + AddEditMatchRequest match = AddEditMatchRequest.fromJson(mainDocData); + List teams = await getTeamsList(match.teams); + if (teams.any((team) => + team.team.players + ?.map((player) => player.id) + .contains(_currentUserId) ?? + false || team.team.created_by == _currentUserId)) { + matches.add(MatchModel( + id: match.id, + teams: teams, + match_type: match.match_type, + number_of_over: match.number_of_over, + over_per_bowler: match.over_per_bowler, + city: match.city, + ground: match.ground, + start_time: match.start_time, + created_by: match.created_by, + ball_type: match.ball_type, + pitch_type: match.pitch_type, + match_status: match.match_status, + toss_winner_id: match.toss_winner_id, + toss_decision: match.toss_decision, + current_playing_team_id: match.current_playing_team_id, + )); + } } + controller.add(matches); + } catch (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); } + }, onError: (error, stack) { + controller.addError(AppError.fromError(error, stack)); + controller.close(); + }); - return matches; - } catch (error, stack) { - throw AppError.fromError(error, stack); - } + return controller.stream; } Future> getMatchesByTeamId(String teamId) async { @@ -248,9 +266,11 @@ class MatchService { controller.add(matches); } catch (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); } }, onError: (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); }); return controller.stream; @@ -310,6 +330,8 @@ class MatchService { controller.add(matchModel); } catch (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); + subscription?.cancel(); } } else { controller.close(); @@ -317,6 +339,8 @@ class MatchService { } }, onError: (error, stack) { controller.addError(AppError.fromError(error, stack)); + controller.close(); + subscription?.cancel(); }); return controller.stream; diff --git a/data/lib/service/team/team_service.dart b/data/lib/service/team/team_service.dart index a523f558..9a1170f1 100644 --- a/data/lib/service/team/team_service.dart +++ b/data/lib/service/team/team_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:data/api/team/team_model.dart'; import 'package:data/api/user/user_models.dart'; @@ -76,38 +77,23 @@ class TeamService { } } - Future> getUserRelatedTeams({ - TeamFilterOption option = TeamFilterOption.all, - }) async { - try { - QuerySnapshot mainCollectionSnapshot; - - switch (option) { - case TeamFilterOption.all: - mainCollectionSnapshot = await _firestore - .collection(_collectionName) - .where( - Filter.or( - Filter('created_by', isEqualTo: _currentUserId), - Filter('players', arrayContains: _currentUserId), - ), - ) - .get(); - case TeamFilterOption.createdByMe: - mainCollectionSnapshot = await _firestore - .collection(_collectionName) - .where('created_by', isEqualTo: _currentUserId) - .get(); - case TeamFilterOption.memberMe: - mainCollectionSnapshot = await _firestore - .collection(_collectionName) - .where('players', arrayContains: _currentUserId) - .get(); - } + Stream> getUserRelatedTeams() { + if (_currentUserId == null) { + return Stream.value([]); + } + return _firestore + .collection(_collectionName) + .where( + Filter.or( + Filter('created_by', isEqualTo: _currentUserId), + Filter('players', arrayContains: _currentUserId), + ), + ) + .snapshots() + .asyncMap((snapshot) async { List teams = []; - - for (QueryDocumentSnapshot mainDoc in mainCollectionSnapshot.docs) { + for (QueryDocumentSnapshot mainDoc in snapshot.docs) { AddTeamRequestModel teamRequestModel = AddTeamRequestModel.fromJson( mainDoc.data() as Map); @@ -116,22 +102,22 @@ class TeamService { : null; final team = TeamModel( - name: teamRequestModel.name, - name_lowercase: teamRequestModel.name_lowercase, - id: teamRequestModel.id, - city: teamRequestModel.city, - created_at: teamRequestModel.created_at, - created_by: teamRequestModel.created_by, - profile_img_url: teamRequestModel.profile_img_url, - players: member); + name: teamRequestModel.name, + name_lowercase: teamRequestModel.name_lowercase, + id: teamRequestModel.id, + city: teamRequestModel.city, + created_at: teamRequestModel.created_at, + created_by: teamRequestModel.created_by, + profile_img_url: teamRequestModel.profile_img_url, + players: member, + ); teams.add(team); } - return teams; - } catch (error, stack) { + }).handleError((error, stack) { throw AppError.fromError(error, stack); - } + }); } Future> getUserOwnedTeams() async { @@ -264,9 +250,3 @@ class TeamService { } } } - -enum TeamFilterOption { - all, - createdByMe, - memberMe; -} diff --git a/khelo/android/app/build.gradle b/khelo/android/app/build.gradle index c96b35e0..0da73e3b 100644 --- a/khelo/android/app/build.gradle +++ b/khelo/android/app/build.gradle @@ -42,7 +42,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.canopas.khelo" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. @@ -70,7 +69,6 @@ android { } } else { release { - // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. keyAlias 'androiddebugkey' keyPassword 'android' diff --git a/khelo/lib/components/resume_detector.dart b/khelo/lib/components/resume_detector.dart new file mode 100644 index 00000000..1a6a150d --- /dev/null +++ b/khelo/lib/components/resume_detector.dart @@ -0,0 +1,40 @@ +import 'package:flutter/cupertino.dart'; +import 'package:uuid/uuid.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +class ResumeDetector extends StatefulWidget { + final Function() onResume; + final Widget child; + + const ResumeDetector({ + super.key, + required this.onResume, + required this.child, + }); + + @override + State createState() => _ResumeDetectorState(); +} + +class _ResumeDetectorState extends State { + final String _key = const Uuid().v4(); + + var _lastNotifiedTime = DateTime.now(); + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: Key(_key), + child: widget.child, + onVisibilityChanged: (info) { + if (info.visibleFraction == 1) { + if (DateTime.now().difference(_lastNotifiedTime).inMilliseconds > + 1000) { + widget.onResume(); + _lastNotifiedTime = DateTime.now(); + } + } + }, + ); + } +} \ No newline at end of file diff --git a/khelo/lib/domain/extensions/enum_extensions.dart b/khelo/lib/domain/extensions/enum_extensions.dart index 1a9c2a6f..7cb96e1c 100644 --- a/khelo/lib/domain/extensions/enum_extensions.dart +++ b/khelo/lib/domain/extensions/enum_extensions.dart @@ -1,7 +1,6 @@ import 'package:data/api/ball_score/ball_score_model.dart'; import 'package:data/api/match/match_model.dart'; import 'package:data/api/user/user_models.dart'; -import 'package:data/service/team/team_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:style/extensions/context_extensions.dart'; @@ -218,17 +217,4 @@ extension ExtrasTypeString on ExtrasType { return context.l10n.score_board_leg_bye_short_text; } } -} - -extension TeamFilterOptionString on TeamFilterOption { - String getString(BuildContext context) { - switch (this) { - case TeamFilterOption.all: - return context.l10n.team_list_all_teams_title; - case TeamFilterOption.createdByMe: - return context.l10n.team_list_created_by_me_title; - case TeamFilterOption.memberMe: - return context.l10n.team_list_me_as_member_title; - } - } -} +} \ No newline at end of file diff --git a/khelo/lib/ui/flow/home/home_screen.dart b/khelo/lib/ui/flow/home/home_screen.dart index 864855e0..b7223067 100644 --- a/khelo/lib/ui/flow/home/home_screen.dart +++ b/khelo/lib/ui/flow/home/home_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/app_page.dart'; import 'package:khelo/components/error_screen.dart'; import 'package:khelo/components/image_avatar.dart'; +import 'package:khelo/components/resume_detector.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/enum_extensions.dart'; import 'package:khelo/domain/formatter/date_formatter.dart'; @@ -15,17 +16,56 @@ import 'package:style/indicator/progress_indicator.dart'; import 'package:style/page_views/expandable_page_view.dart'; import 'package:style/text/app_text_style.dart'; -class HomeScreen extends ConsumerWidget { +class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + late HomeViewNotifier notifier; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); final state = ref.watch(homeViewStateProvider); - final notifier = ref.watch(homeViewStateProvider.notifier); + notifier = ref.watch(homeViewStateProvider.notifier); return AppPage( title: context.l10n.home_screen_title, - body: _body(context, notifier, state), + body: ResumeDetector( + onResume: notifier.onResume, + child: _body(context, notifier, state), + ), ); } @@ -40,7 +80,7 @@ class HomeScreen extends ConsumerWidget { if (state.error != null) { return ErrorScreen( error: state.error, - onRetryTap: notifier.loadMatches, + onRetryTap: notifier.onResume, ); } @@ -63,8 +103,7 @@ class HomeScreen extends ConsumerWidget { return ExpandablePageView( itemCount: state.matches.length, itemBuilder: (context, index) { - return _matchCell( - context, state.matches[index]); + return _matchCell(context, state.matches[index]); }, ); } diff --git a/khelo/lib/ui/flow/home/home_view_model.dart b/khelo/lib/ui/flow/home/home_view_model.dart index 41be2cd0..b18a0a28 100644 --- a/khelo/lib/ui/flow/home/home_view_model.dart +++ b/khelo/lib/ui/flow/home/home_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:data/api/match/match_model.dart'; import 'package:data/service/match/match_service.dart'; import 'package:flutter/cupertino.dart'; @@ -7,21 +8,22 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_view_model.freezed.dart'; final homeViewStateProvider = - StateNotifierProvider.autoDispose( + StateNotifierProvider( (ref) => HomeViewNotifier(ref.read(matchServiceProvider)), ); class HomeViewNotifier extends StateNotifier { final MatchService _matchService; + late StreamSubscription _streamSubscription; HomeViewNotifier(this._matchService) : super(const HomeViewState()) { - loadMatches(); + _loadMatches(); } - void loadMatches() async { + void _loadMatches() async { state = state.copyWith(loading: state.matches.isEmpty); - _matchService.getRunningMatches().listen( + _streamSubscription = _matchService.getRunningMatches().listen( (matches) { state = state.copyWith(matches: matches, loading: false, error: null); }, @@ -31,6 +33,21 @@ class HomeViewNotifier extends StateNotifier { }, ); } + + _cancelStreamSubscription() async { + await _streamSubscription.cancel(); + } + + onResume() { + _cancelStreamSubscription(); + _loadMatches(); + } + + @override + void dispose() { + _cancelStreamSubscription(); + super.dispose(); + } } @freezed diff --git a/khelo/lib/ui/flow/main/main_screen.dart b/khelo/lib/ui/flow/main/main_screen.dart index 6e5f0aac..70afe233 100644 --- a/khelo/lib/ui/flow/main/main_screen.dart +++ b/khelo/lib/ui/flow/main/main_screen.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,7 +18,8 @@ class MainScreen extends ConsumerStatefulWidget { ConsumerState createState() => _MainScreenState(); } -class _MainScreenState extends ConsumerState { +class _MainScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { static final List _widgets = [ const HomeScreen(), const MyGameTabScreen(), @@ -29,9 +29,38 @@ class _MainScreenState extends ConsumerState { final _materialPageController = PageController(); final _cupertinoTabController = CupertinoTabController(); + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + _materialPageController.dispose(); + _cupertinoTabController.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } @override Widget build(BuildContext context) { + super.build(context); if (Platform.isIOS) { return _cupertinoTabs(context); } diff --git a/khelo/lib/ui/flow/matches/match_list_screen.dart b/khelo/lib/ui/flow/matches/match_list_screen.dart index dab2490e..c8e7a108 100644 --- a/khelo/lib/ui/flow/matches/match_list_screen.dart +++ b/khelo/lib/ui/flow/matches/match_list_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/error_screen.dart'; import 'package:khelo/components/match_status_tag.dart'; +import 'package:khelo/components/resume_detector.dart'; import 'package:khelo/components/won_by_message_text.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/data_model_extensions/match_model_extension.dart'; @@ -17,22 +18,61 @@ import 'package:style/text/app_text_style.dart'; import 'match_list_view_model.dart'; -class MatchListScreen extends ConsumerWidget { +class MatchListScreen extends ConsumerStatefulWidget { const MatchListScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _MatchListScreenState(); +} + +class _MatchListScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + late MatchListViewNotifier notifier; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); final state = ref.watch(matchListStateProvider); - final notifier = ref.watch(matchListStateProvider.notifier); + notifier = ref.watch(matchListStateProvider.notifier); - return Column( - children: [ - _topStartMatchView(context), - const SizedBox( - height: 24, - ), - _matchList(context, notifier, state), - ], + return ResumeDetector( + onResume: notifier.onResume, + child: Column( + children: [ + _topStartMatchView(context), + const SizedBox( + height: 24, + ), + _matchList(context, notifier, state), + ], + ), ); } @@ -73,7 +113,7 @@ class MatchListScreen extends ConsumerWidget { if (state.error != null) { return ErrorScreen( error: state.error, - onRetryTap: notifier.loadMatches, + onRetryTap: notifier.onResume, ); } diff --git a/khelo/lib/ui/flow/matches/match_list_view_model.dart b/khelo/lib/ui/flow/matches/match_list_view_model.dart index 5eb57327..ce7c21fa 100644 --- a/khelo/lib/ui/flow/matches/match_list_view_model.dart +++ b/khelo/lib/ui/flow/matches/match_list_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:data/api/match/match_model.dart'; import 'package:data/service/match/match_service.dart'; import 'package:data/storage/app_preferences.dart'; @@ -7,8 +8,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'match_list_view_model.freezed.dart'; -final matchListStateProvider = StateNotifierProvider.autoDispose< - MatchListViewNotifier, MatchListViewState>((ref) { +final matchListStateProvider = + StateNotifierProvider((ref) { final notifier = MatchListViewNotifier( ref.read(matchServiceProvider), ref.read(currentUserPod)?.id, @@ -21,29 +22,49 @@ final matchListStateProvider = StateNotifierProvider.autoDispose< class MatchListViewNotifier extends StateNotifier { final MatchService _matchService; + late StreamSubscription _matchesStreamSubscription; MatchListViewNotifier(this._matchService, String? userId) : super(MatchListViewState( currentUserId: userId, )) { - loadMatches(); + _loadMatches(); } void setUserId(String? userId) { state = state.copyWith(currentUserId: userId); } - Future loadMatches() async { + Future _loadMatches() async { state = state.copyWith(loading: true); try { - final matches = await _matchService.getCurrentUserRelatedMatches(); - state = state.copyWith(matches: matches, loading: false); + _matchesStreamSubscription = + _matchService.getCurrentUserRelatedMatches().listen((matches) { + state = state.copyWith(matches: matches, loading: false, error: null); + }, onError: (e) { + state = state.copyWith(loading: false, error: e); + debugPrint("MatchListViewNotifier: error while load matches -> $e"); + }); } catch (e) { state = state.copyWith(loading: false, error: e); - debugPrint( - "MatchListViewNotifier: error while load matches -> $e"); + debugPrint("MatchListViewNotifier: error while load matches -> $e"); } } + + _cancelStreamSubscription() async { + await _matchesStreamSubscription.cancel(); + } + + onResume(){ + _cancelStreamSubscription(); + _loadMatches(); + } + + @override + void dispose() { + _cancelStreamSubscription(); + super.dispose(); + } } @freezed diff --git a/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart b/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart index 55fc7825..6defb4e0 100644 --- a/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart +++ b/khelo/lib/ui/flow/my_game/my_game_tab_screen.dart @@ -18,7 +18,8 @@ class MyGameTabScreen extends ConsumerStatefulWidget { ConsumerState createState() => _MyGameTabScreenState(); } -class _MyGameTabScreenState extends ConsumerState { +class _MyGameTabScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final List _tabs = [ const MatchListScreen(), const TeamListScreen(), @@ -26,34 +27,56 @@ class _MyGameTabScreenState extends ConsumerState { late PageController _controller; - late MyGameTabViewNotifier notifier; - int get _selectedTab => _controller.hasClients ? _controller.page?.round() ?? 0 : _controller.initialPage; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; @override void initState() { super.initState(); - notifier = ref.read(myGameTabViewStateProvider.notifier); + WidgetsBinding.instance.addObserver(this); + _controller = PageController( initialPage: ref.read(myGameTabViewStateProvider).selectedTab, ); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + _controller.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + @override Widget build(BuildContext context) { - notifier = ref.watch(myGameTabViewStateProvider.notifier); + super.build(context); + final notifier = ref.watch(myGameTabViewStateProvider.notifier); + return AppPage( body: Builder( builder: (context) { - return _content(context, ref); + return _content(context, notifier); }, ), ); } - Widget _content(BuildContext context, WidgetRef ref) { + Widget _content(BuildContext context, MyGameTabViewNotifier notifier) { return SafeArea( child: Column( children: [ @@ -130,4 +153,4 @@ class _MyGameTabScreenState extends ConsumerState { ), ); } -} +} \ No newline at end of file diff --git a/khelo/lib/ui/flow/stats/my_stats_tab_screen.dart b/khelo/lib/ui/flow/stats/my_stats_tab_screen.dart index 31b6c66f..e1d53fca 100644 --- a/khelo/lib/ui/flow/stats/my_stats_tab_screen.dart +++ b/khelo/lib/ui/flow/stats/my_stats_tab_screen.dart @@ -16,7 +16,8 @@ class MyStatsTabScreen extends ConsumerStatefulWidget { ConsumerState createState() => _MyStatsTabScreenState(); } -class _MyStatsTabScreenState extends ConsumerState { +class _MyStatsTabScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final List _tabs = [ const UserMatchListScreen(), const UserStatScreen(), @@ -24,35 +25,57 @@ class _MyStatsTabScreenState extends ConsumerState { late PageController _controller; - late MyStatsTabViewNotifier notifier; - int get _selectedTab => _controller.hasClients ? _controller.page?.round() ?? 0 : _controller.initialPage; + bool _wantKeepAlive = true; + @override + bool get wantKeepAlive => _wantKeepAlive; + @override void initState() { super.initState(); - notifier = ref.read(myStatsTabStateProvider.notifier); + WidgetsBinding.instance.addObserver(this); + _controller = PageController( initialPage: ref.read(myStatsTabStateProvider).selectedTab, ); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + _controller.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + @override Widget build(BuildContext context) { - notifier = ref.watch(myStatsTabStateProvider.notifier); + super.build(context); + final notifier = ref.watch(myStatsTabStateProvider.notifier); + return AppPage( title: context.l10n.my_stat_screen_title, body: Builder( builder: (context) { - return _content(context, ref); + return _content(context, notifier); }, ), ); } - Widget _content(BuildContext context, WidgetRef ref) { + Widget _content(BuildContext context, MyStatsTabViewNotifier notifier) { return SafeArea( child: Column( children: [ @@ -119,4 +142,4 @@ class _MyStatsTabScreenState extends ConsumerState { ), ); } -} +} \ No newline at end of file diff --git a/khelo/lib/ui/flow/stats/user_match/user_match_list_screen.dart b/khelo/lib/ui/flow/stats/user_match/user_match_list_screen.dart index 916b703e..fc2f16ff 100644 --- a/khelo/lib/ui/flow/stats/user_match/user_match_list_screen.dart +++ b/khelo/lib/ui/flow/stats/user_match/user_match_list_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/error_screen.dart'; import 'package:khelo/components/image_avatar.dart'; +import 'package:khelo/components/resume_detector.dart'; import 'package:khelo/components/won_by_message_text.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/data_model_extensions/match_model_extension.dart'; @@ -16,28 +17,73 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; -class UserMatchListScreen extends ConsumerWidget { +class UserMatchListScreen extends ConsumerStatefulWidget { const UserMatchListScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _UserMatchListScreenState(); +} + +class _UserMatchListScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + late UserMatchListViewNotifier notifier; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + final state = ref.watch(userMatchListStateProvider); - final notifier = ref.watch(userMatchListStateProvider.notifier); + notifier = ref.watch(userMatchListStateProvider.notifier); + + return ResumeDetector( + onResume: notifier.onResume, + child: _body(context, notifier, state), + ); + } + Widget _body( + BuildContext context, + UserMatchListViewNotifier notifier, + UserMatchListState state, + ) { if (state.loading) { return const AppProgressIndicator(); } + if (state.error != null) { return ErrorScreen( error: state.error, - onRetryTap: notifier.loadUserMatches, + onRetryTap: notifier.onResume, ); } - return _body(context, state); - } - - Widget _body(BuildContext context, UserMatchListState state) { if (state.matches.isNotEmpty) { return ListView.separated( padding: const EdgeInsets.only(bottom: 50, top: 24), diff --git a/khelo/lib/ui/flow/stats/user_match/user_match_list_view_model.dart b/khelo/lib/ui/flow/stats/user_match/user_match_list_view_model.dart index 17bfa802..90a8f836 100644 --- a/khelo/lib/ui/flow/stats/user_match/user_match_list_view_model.dart +++ b/khelo/lib/ui/flow/stats/user_match/user_match_list_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:data/api/match/match_model.dart'; import 'package:data/service/match/match_service.dart'; import 'package:flutter/cupertino.dart'; @@ -6,8 +7,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'user_match_list_view_model.freezed.dart'; -final userMatchListStateProvider = StateNotifierProvider.autoDispose< - UserMatchListViewNotifier, UserMatchListState>((ref) { +final userMatchListStateProvider = + StateNotifierProvider((ref) { return UserMatchListViewNotifier( ref.read(matchServiceProvider), ); @@ -15,23 +16,45 @@ final userMatchListStateProvider = StateNotifierProvider.autoDispose< class UserMatchListViewNotifier extends StateNotifier { final MatchService _matchService; + late StreamSubscription _matchesStreamSubscription; UserMatchListViewNotifier(this._matchService) : super(const UserMatchListState()) { - loadUserMatches(); + _loadUserMatches(); } - Future loadUserMatches() async { + Future _loadUserMatches() async { state = state.copyWith(loading: true); try { - final matches = await _matchService.getCurrentUserPlayedMatches(); - state = state.copyWith(matches: matches, loading: false); + _matchesStreamSubscription = + _matchService.getCurrentUserPlayedMatches().listen((matches) { + state = state.copyWith(matches: matches, loading: false, error: null); + }, onError: (e) { + state = state.copyWith(error: e, loading: false); + debugPrint( + "UserMatchListViewNotifier: error while loading user matches -> $e"); + }); } catch (e) { state = state.copyWith(error: e, loading: false); debugPrint( "UserMatchListViewNotifier: error while loading user matches -> $e"); } } + + _cancelStreamSubscription() async { + await _matchesStreamSubscription.cancel(); + } + + onResume() { + _cancelStreamSubscription(); + _loadUserMatches(); + } + + @override + Future dispose() async { + _cancelStreamSubscription(); + super.dispose(); + } } @freezed diff --git a/khelo/lib/ui/flow/stats/user_stat/user_stat_screen.dart b/khelo/lib/ui/flow/stats/user_stat/user_stat_screen.dart index c4a49718..d7d163af 100644 --- a/khelo/lib/ui/flow/stats/user_stat/user_stat_screen.dart +++ b/khelo/lib/ui/flow/stats/user_stat/user_stat_screen.dart @@ -2,20 +2,65 @@ import 'package:data/api/ball_score/ball_score_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/error_screen.dart'; +import 'package:khelo/components/resume_detector.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/ui/flow/stats/user_stat/user_stat_view_model.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; -class UserStatScreen extends ConsumerWidget { +class UserStatScreen extends ConsumerStatefulWidget { const UserStatScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _UserStatScreenState(); +} + +class _UserStatScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + late UserStatViewNotifier notifier; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); final state = ref.watch(userStatViewStateProvider); - final notifier = ref.watch(userStatViewStateProvider.notifier); + notifier = ref.watch(userStatViewStateProvider.notifier); + return ResumeDetector( + onResume: notifier.onResume, + child: _body(context, notifier, state), + ); + } + + Widget _body(BuildContext context, UserStatViewNotifier notifier, + UserStatViewState state) { if (state.loading) { return const AppProgressIndicator(); } @@ -23,14 +68,10 @@ class UserStatScreen extends ConsumerWidget { if (state.error != null) { return ErrorScreen( error: state.error, - onRetryTap: notifier.getUserRelatedBalls, + onRetryTap: notifier.onResume, ); } - return _body(context, state); - } - - Widget _body(BuildContext context, UserStatViewState state) { return ListView( padding: const EdgeInsets.only(left: 16, right: 16, bottom: 50), children: [ @@ -151,9 +192,7 @@ class UserStatScreen extends ConsumerWidget { .fold( 0, (sum, element) => - sum + - element.runs_scored + - (element.extras_awarded ?? 0)); + sum + element.runs_scored + (element.extras_awarded ?? 0)); return ( _calculateBowlingAverage(runsConceded, wicketTaken), @@ -232,9 +271,7 @@ class UserStatScreen extends ConsumerWidget { .fold( 0, (sum, element) => - sum + - element.runs_scored + - (element.extras_awarded ?? 0)); + sum + element.runs_scored + (element.extras_awarded ?? 0)); if (bowledBallCount == 0) { return 0.0; } diff --git a/khelo/lib/ui/flow/stats/user_stat/user_stat_view_model.dart b/khelo/lib/ui/flow/stats/user_stat/user_stat_view_model.dart index 961e2285..1a507830 100644 --- a/khelo/lib/ui/flow/stats/user_stat/user_stat_view_model.dart +++ b/khelo/lib/ui/flow/stats/user_stat/user_stat_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:data/api/ball_score/ball_score_model.dart'; import 'package:data/service/ball_score/ball_score_service.dart'; import 'package:data/storage/app_preferences.dart'; @@ -8,8 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'user_stat_view_model.freezed.dart'; final userStatViewStateProvider = - StateNotifierProvider.autoDispose( - (ref) { + StateNotifierProvider((ref) { final notifier = UserStatViewNotifier( ref.read(ballScoreServiceProvider), ref.read(currentUserPod)?.id, @@ -22,27 +22,50 @@ final userStatViewStateProvider = class UserStatViewNotifier extends StateNotifier { final BallScoreService _ballScoreService; + late StreamSubscription _ballScoreStreamSubscription; UserStatViewNotifier(this._ballScoreService, String? userId) : super(UserStatViewState(currentUserId: userId)) { - getUserRelatedBalls(); + _getUserRelatedBalls(); } void setUserId(String? userId) { state = state.copyWith(currentUserId: userId); } - Future getUserRelatedBalls() async { + Future _getUserRelatedBalls() async { state = state.copyWith(loading: true); try { - final ballScores = await _ballScoreService.getCurrentUserRelatedBalls(); - state = state.copyWith(ballList: ballScores, loading: false); + _ballScoreStreamSubscription = + _ballScoreService.getCurrentUserRelatedBalls().listen((ballScores) { + state = + state.copyWith(ballList: ballScores, loading: false, error: null); + }, onError: (e) { + state = state.copyWith(error: e, loading: false); + debugPrint( + "UserStatViewNotifier: error while getting user related balls -> $e"); + }); } catch (e) { state = state.copyWith(error: e, loading: false); debugPrint( "UserStatViewNotifier: error while getting user related balls -> $e"); } } + + _cancelStreamSubscription() { + _ballScoreStreamSubscription.cancel(); + } + + onResume() { + _cancelStreamSubscription(); + _getUserRelatedBalls(); + } + + @override + void dispose() { + _cancelStreamSubscription(); + super.dispose(); + } } @freezed diff --git a/khelo/lib/ui/flow/team/components/select_filter_option_sheet.dart b/khelo/lib/ui/flow/team/components/select_filter_option_sheet.dart index 74166781..6ffdab81 100644 --- a/khelo/lib/ui/flow/team/components/select_filter_option_sheet.dart +++ b/khelo/lib/ui/flow/team/components/select_filter_option_sheet.dart @@ -1,8 +1,6 @@ -import 'package:data/service/team/team_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:khelo/domain/extensions/enum_extensions.dart'; import 'package:khelo/ui/flow/team/team_list_view_model.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; diff --git a/khelo/lib/ui/flow/team/team_list_screen.dart b/khelo/lib/ui/flow/team/team_list_screen.dart index 611e49ed..1ab75a9c 100644 --- a/khelo/lib/ui/flow/team/team_list_screen.dart +++ b/khelo/lib/ui/flow/team/team_list_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/error_screen.dart'; import 'package:khelo/components/image_avatar.dart'; +import 'package:khelo/components/resume_detector.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; -import 'package:khelo/domain/extensions/enum_extensions.dart'; import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/team/components/select_filter_option_sheet.dart'; import 'package:khelo/ui/flow/team/team_list_view_model.dart'; @@ -14,9 +14,44 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; -class TeamListScreen extends ConsumerWidget { +class TeamListScreen extends ConsumerStatefulWidget { const TeamListScreen({super.key}); + @override + ConsumerState createState() => _TeamListScreenState(); +} + +class _TeamListScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + late TeamListViewNotifier notifier; + bool _wantKeepAlive = true; + + @override + bool get wantKeepAlive => _wantKeepAlive; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + setState(() { + _wantKeepAlive = false; + }); + } else if (state == AppLifecycleState.resumed) { + setState(() { + _wantKeepAlive = true; + }); + } else if (state == AppLifecycleState.detached) { + // deallocate resources + notifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + } + void _observeShowFilterOptionSheet( BuildContext context, WidgetRef ref, @@ -29,12 +64,15 @@ class TeamListScreen extends ConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { - final notifier = ref.watch(teamListViewStateProvider.notifier); + Widget build(BuildContext context) { + super.build(context); + notifier = ref.watch(teamListViewStateProvider.notifier); final state = ref.watch(teamListViewStateProvider); _observeShowFilterOptionSheet(context, ref); - return _teamList(context, notifier, state); + return ResumeDetector( + onResume: notifier.onResume, + child: _teamList(context, notifier, state)); } Widget _teamList( @@ -48,17 +86,19 @@ class TeamListScreen extends ConsumerWidget { if (state.error != null) { return ErrorScreen( error: state.error, - onRetryTap: notifier.loadTeamList, + onRetryTap: notifier.onResume, ); } return Stack( children: [ ListView.separated( + itemCount: state.filteredTeams.length, padding: context.mediaQueryPadding + const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 60), + separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { - final team = state.teams[index]; + final team = state.filteredTeams[index]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -81,12 +121,6 @@ class TeamListScreen extends ConsumerWidget { ], ); }, - itemCount: state.teams.length, - separatorBuilder: (context, index) { - return const SizedBox( - height: 16, - ); - }, ), _floatingAddButton(context, notifier), ], @@ -137,7 +171,6 @@ class TeamListScreen extends ConsumerWidget { } else if (value == context.l10n.team_list_edit_team_title) { await AppRoute.addTeam(team: team).push(context); } - notifier.loadTeamList(); }, ) ] @@ -184,7 +217,6 @@ class TeamListScreen extends ConsumerWidget { backgroundColor: context.colorScheme.primary, onTap: () async { await AppRoute.addTeam().push(context); - notifier.loadTeamList(); }, icon: Icon( Icons.add_rounded, diff --git a/khelo/lib/ui/flow/team/team_list_view_model.dart b/khelo/lib/ui/flow/team/team_list_view_model.dart index 178549f2..688619a1 100644 --- a/khelo/lib/ui/flow/team/team_list_view_model.dart +++ b/khelo/lib/ui/flow/team/team_list_view_model.dart @@ -1,15 +1,16 @@ +import 'dart:async'; import 'package:data/api/team/team_model.dart'; import 'package:data/service/team/team_service.dart'; import 'package:data/storage/app_preferences.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; part 'team_list_view_model.freezed.dart'; final teamListViewStateProvider = - StateNotifierProvider.autoDispose( - (ref) { + StateNotifierProvider((ref) { final notifier = TeamListViewNotifier( ref.read(teamServiceProvider), ref.read(currentUserPod)?.id, @@ -22,38 +23,82 @@ final teamListViewStateProvider = class TeamListViewNotifier extends StateNotifier { final TeamService _teamService; + late StreamSubscription _teamsStreamSubscription; TeamListViewNotifier(this._teamService, String? userId) : super(TeamListViewState(currentUserId: userId)) { - loadTeamList(); + _loadTeamList(); } void setUserId(String? userId) { state = state.copyWith(currentUserId: userId); } - Future loadTeamList() async { + Future _loadTeamList() async { state = state.copyWith(loading: state.teams.isEmpty); try { - final res = - await _teamService.getUserRelatedTeams(option: state.selectedFilter); - state = state.copyWith(teams: res, loading: false); + _teamsStreamSubscription = + _teamService.getUserRelatedTeams().listen((teams) { + state = state.copyWith(teams: teams, loading: false, error: null); + _filterTeamList(); + }, onError: (e) { + state = state.copyWith(loading: false, error: e); + debugPrint("TeamListViewNotifier: error while loading team list -> $e"); + }); } catch (e) { state = state.copyWith(loading: false, error: e); debugPrint("TeamListViewNotifier: error while loading team list -> $e"); } } + void _filterTeamList() { + List list = []; + switch (state.selectedFilter) { + case TeamFilterOption.createdByMe: + list = state.teams + .where((element) => element.created_by == state.currentUserId) + .toList(); + case TeamFilterOption.memberMe: + list = state.teams + .where((element) => + element.created_by == state.currentUserId || + (element.players + ?.map((e) => e.id) + .contains(state.currentUserId) ?? + false)) + .toList(); + default: + list = state.teams; + } + + state = state.copyWith(filteredTeams: list); + } + void onFilterOptionSelect(TeamFilterOption filter) { if (filter != state.selectedFilter) { state = state.copyWith(selectedFilter: filter); - loadTeamList(); + _filterTeamList(); } } void onFilterButtonTap() { state = state.copyWith(showFilterOptionSheet: DateTime.now()); } + + _cancelStreamSubscription() { + _teamsStreamSubscription.cancel(); + } + + onResume() { + _cancelStreamSubscription(); + _loadTeamList(); + } + + @override + void dispose() { + _cancelStreamSubscription(); + super.dispose(); + } } @freezed @@ -63,7 +108,25 @@ class TeamListViewState with _$TeamListViewState { DateTime? showFilterOptionSheet, String? currentUserId, @Default([]) List teams, + @Default([]) List filteredTeams, @Default(true) bool loading, @Default(TeamFilterOption.all) TeamFilterOption selectedFilter, }) = _TeamListViewState; } + +enum TeamFilterOption { + all, + createdByMe, + memberMe; + + String getString(BuildContext context) { + switch (this) { + case TeamFilterOption.all: + return context.l10n.team_list_all_teams_title; + case TeamFilterOption.createdByMe: + return context.l10n.team_list_created_by_me_title; + case TeamFilterOption.memberMe: + return context.l10n.team_list_me_as_member_title; + } + } +} diff --git a/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart b/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart index 35b8c7dd..26314019 100644 --- a/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart +++ b/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart @@ -20,6 +20,7 @@ mixin _$TeamListViewState { DateTime? get showFilterOptionSheet => throw _privateConstructorUsedError; String? get currentUserId => throw _privateConstructorUsedError; List get teams => throw _privateConstructorUsedError; + List get filteredTeams => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; TeamFilterOption get selectedFilter => throw _privateConstructorUsedError; @@ -39,6 +40,7 @@ abstract class $TeamListViewStateCopyWith<$Res> { DateTime? showFilterOptionSheet, String? currentUserId, List teams, + List filteredTeams, bool loading, TeamFilterOption selectedFilter}); } @@ -60,6 +62,7 @@ class _$TeamListViewStateCopyWithImpl<$Res, $Val extends TeamListViewState> Object? showFilterOptionSheet = freezed, Object? currentUserId = freezed, Object? teams = null, + Object? filteredTeams = null, Object? loading = null, Object? selectedFilter = null, }) { @@ -77,6 +80,10 @@ class _$TeamListViewStateCopyWithImpl<$Res, $Val extends TeamListViewState> ? _value.teams : teams // ignore: cast_nullable_to_non_nullable as List, + filteredTeams: null == filteredTeams + ? _value.filteredTeams + : filteredTeams // ignore: cast_nullable_to_non_nullable + as List, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -102,6 +109,7 @@ abstract class _$$TeamListViewStateImplCopyWith<$Res> DateTime? showFilterOptionSheet, String? currentUserId, List teams, + List filteredTeams, bool loading, TeamFilterOption selectedFilter}); } @@ -121,6 +129,7 @@ class __$$TeamListViewStateImplCopyWithImpl<$Res> Object? showFilterOptionSheet = freezed, Object? currentUserId = freezed, Object? teams = null, + Object? filteredTeams = null, Object? loading = null, Object? selectedFilter = null, }) { @@ -138,6 +147,10 @@ class __$$TeamListViewStateImplCopyWithImpl<$Res> ? _value._teams : teams // ignore: cast_nullable_to_non_nullable as List, + filteredTeams: null == filteredTeams + ? _value._filteredTeams + : filteredTeams // ignore: cast_nullable_to_non_nullable + as List, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -158,9 +171,11 @@ class _$TeamListViewStateImpl implements _TeamListViewState { this.showFilterOptionSheet, this.currentUserId, final List teams = const [], + final List filteredTeams = const [], this.loading = true, this.selectedFilter = TeamFilterOption.all}) - : _teams = teams; + : _teams = teams, + _filteredTeams = filteredTeams; @override final Object? error; @@ -177,6 +192,15 @@ class _$TeamListViewStateImpl implements _TeamListViewState { return EqualUnmodifiableListView(_teams); } + final List _filteredTeams; + @override + @JsonKey() + List get filteredTeams { + if (_filteredTeams is EqualUnmodifiableListView) return _filteredTeams; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_filteredTeams); + } + @override @JsonKey() final bool loading; @@ -186,7 +210,7 @@ class _$TeamListViewStateImpl implements _TeamListViewState { @override String toString() { - return 'TeamListViewState(error: $error, showFilterOptionSheet: $showFilterOptionSheet, currentUserId: $currentUserId, teams: $teams, loading: $loading, selectedFilter: $selectedFilter)'; + return 'TeamListViewState(error: $error, showFilterOptionSheet: $showFilterOptionSheet, currentUserId: $currentUserId, teams: $teams, filteredTeams: $filteredTeams, loading: $loading, selectedFilter: $selectedFilter)'; } @override @@ -200,6 +224,8 @@ class _$TeamListViewStateImpl implements _TeamListViewState { (identical(other.currentUserId, currentUserId) || other.currentUserId == currentUserId) && const DeepCollectionEquality().equals(other._teams, _teams) && + const DeepCollectionEquality() + .equals(other._filteredTeams, _filteredTeams) && (identical(other.loading, loading) || other.loading == loading) && (identical(other.selectedFilter, selectedFilter) || other.selectedFilter == selectedFilter)); @@ -212,6 +238,7 @@ class _$TeamListViewStateImpl implements _TeamListViewState { showFilterOptionSheet, currentUserId, const DeepCollectionEquality().hash(_teams), + const DeepCollectionEquality().hash(_filteredTeams), loading, selectedFilter); @@ -229,6 +256,7 @@ abstract class _TeamListViewState implements TeamListViewState { final DateTime? showFilterOptionSheet, final String? currentUserId, final List teams, + final List filteredTeams, final bool loading, final TeamFilterOption selectedFilter}) = _$TeamListViewStateImpl; @@ -241,6 +269,8 @@ abstract class _TeamListViewState implements TeamListViewState { @override List get teams; @override + List get filteredTeams; + @override bool get loading; @override TeamFilterOption get selectedFilter; diff --git a/khelo/pubspec.lock b/khelo/pubspec.lock index 6ef0f310..47fe0ac9 100644 --- a/khelo/pubspec.lock +++ b/khelo/pubspec.lock @@ -1281,13 +1281,13 @@ packages: source: hosted version: "1.3.2" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_math: dependency: transitive description: @@ -1296,6 +1296,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/khelo/pubspec.yaml b/khelo/pubspec.yaml index c8651b85..4f71bf20 100644 --- a/khelo/pubspec.yaml +++ b/khelo/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: path: ../data shared_preferences: ^2.0.18 + uuid: ^4.4.0 + visibility_detector: ^0.4.0+2 # UI cupertino_icons: ^1.0.2