From 0599e9b351c6838ce62f0d51b829a600eddd7532 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Wed, 21 Jan 2026 19:55:56 -0500 Subject: [PATCH] refactor: move dialog state management to BLoC and make client hubs page stateless. --- .../presentation/blocs/client_hubs_bloc.dart | 22 ++++++++ .../presentation/blocs/client_hubs_event.dart | 21 ++++++++ .../presentation/blocs/client_hubs_state.dart | 25 ++++++++- .../presentation/pages/client_hubs_page.dart | 52 +++++++++++-------- .../presentation/widgets/add_hub_dialog.dart | 7 ++- .../src/presentation/widgets/hub_card.dart | 2 +- .../presentation/widgets/hub_empty_state.dart | 2 +- .../widgets/identify_nfc_dialog.dart | 7 ++- 8 files changed, 108 insertions(+), 30 deletions(-) diff --git a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index 623c7b30..a874e690 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -31,6 +31,26 @@ class ClientHubsBloc extends Bloc on(_onDeleteRequested); on(_onNfcTagAssignRequested); on(_onMessageCleared); + on(_onAddDialogToggled); + on(_onIdentifyDialogToggled); + } + + void _onAddDialogToggled( + ClientHubsAddDialogToggled event, + Emitter emit, + ) { + emit(state.copyWith(showAddHubDialog: event.visible)); + } + + void _onIdentifyDialogToggled( + ClientHubsIdentifyDialogToggled event, + Emitter emit, + ) { + if (event.hub == null) { + emit(state.copyWith(clearHubToIdentify: true)); + } else { + emit(state.copyWith(hubToIdentify: event.hub)); + } } Future _onFetched( @@ -66,6 +86,7 @@ class ClientHubsBloc extends Bloc status: ClientHubsStatus.actionSuccess, hubs: hubs, successMessage: 'Hub created successfully', + showAddHubDialog: false, ), ); } catch (e) { @@ -118,6 +139,7 @@ class ClientHubsBloc extends Bloc status: ClientHubsStatus.actionSuccess, hubs: hubs, successMessage: 'NFC tag assigned successfully', + clearHubToIdentify: true, ), ); } catch (e) { diff --git a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart index 1007e395..bc3cdc79 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Base class for all client hubs events. abstract class ClientHubsEvent extends Equatable { @@ -52,3 +53,23 @@ class ClientHubsNfcTagAssignRequested extends ClientHubsEvent { class ClientHubsMessageCleared extends ClientHubsEvent { const ClientHubsMessageCleared(); } + +/// Event triggered to toggle the visibility of the "Add Hub" dialog. +class ClientHubsAddDialogToggled extends ClientHubsEvent { + final bool visible; + + const ClientHubsAddDialogToggled({required this.visible}); + + @override + List get props => [visible]; +} + +/// Event triggered to toggle the visibility of the "Identify NFC" dialog. +class ClientHubsIdentifyDialogToggled extends ClientHubsEvent { + final Hub? hub; + + const ClientHubsIdentifyDialogToggled({this.hub}); + + @override + List get props => [hub]; +} diff --git a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart index 90d3ee71..cc037f57 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart @@ -19,11 +19,20 @@ class ClientHubsState extends Equatable { final String? errorMessage; final String? successMessage; + /// Whether the "Add Hub" dialog should be visible. + final bool showAddHubDialog; + + /// The hub currently being identified/assigned an NFC tag. + /// If null, the identification dialog is closed. + final Hub? hubToIdentify; + const ClientHubsState({ this.status = ClientHubsStatus.initial, this.hubs = const [], this.errorMessage, this.successMessage, + this.showAddHubDialog = false, + this.hubToIdentify, }); ClientHubsState copyWith({ @@ -31,15 +40,29 @@ class ClientHubsState extends Equatable { List? hubs, String? errorMessage, String? successMessage, + bool? showAddHubDialog, + Hub? hubToIdentify, + bool clearHubToIdentify = false, }) { return ClientHubsState( status: status ?? this.status, hubs: hubs ?? this.hubs, errorMessage: errorMessage ?? this.errorMessage, successMessage: successMessage ?? this.successMessage, + showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog, + hubToIdentify: clearHubToIdentify + ? null + : (hubToIdentify ?? this.hubToIdentify), ); } @override - List get props => [status, hubs, errorMessage, successMessage]; + List get props => [ + status, + hubs, + errorMessage, + successMessage, + showAddHubDialog, + hubToIdentify, + ]; } diff --git a/apps/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart b/apps/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart index 590d6280..3fa9ead4 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import 'package:krow_domain/krow_domain.dart'; import 'package:core_localization/core_localization.dart'; import '../blocs/client_hubs_bloc.dart'; import '../blocs/client_hubs_event.dart'; @@ -15,18 +14,13 @@ import '../widgets/hub_info_card.dart'; import '../widgets/identify_nfc_dialog.dart'; /// The main page for the client hubs feature. -class ClientHubsPage extends StatefulWidget { +/// +/// This page follows the KROW Clean Architecture by being a [StatelessWidget] +/// and delegating all state management to the [ClientHubsBloc]. +class ClientHubsPage extends StatelessWidget { /// Creates a [ClientHubsPage]. const ClientHubsPage({super.key}); - @override - State createState() => _ClientHubsPageState(); -} - -class _ClientHubsPageState extends State { - bool _showAddHub = false; - Hub? _hubToIdentify; - @override Widget build(BuildContext context) { return BlocProvider( @@ -68,14 +62,22 @@ class _ClientHubsPageState extends State { else if (state.hubs.isEmpty) HubEmptyState( onAddPressed: () => - setState(() => _showAddHub = true), + BlocProvider.of(context).add( + const ClientHubsAddDialogToggled( + visible: true, + ), + ), ) else ...[ ...state.hubs.map( (hub) => HubCard( hub: hub, onNfcPressed: () => - setState(() => _hubToIdentify = hub), + BlocProvider.of( + context, + ).add( + ClientHubsIdentifyDialogToggled(hub: hub), + ), onDeletePressed: () => BlocProvider.of( context, @@ -90,33 +92,35 @@ class _ClientHubsPageState extends State { ), ], ), - if (_showAddHub) + if (state.showAddHubDialog) AddHubDialog( onCreate: (name, address) { BlocProvider.of(context).add( ClientHubsAddRequested(name: name, address: address), ); - setState(() => _showAddHub = false); }, - onCancel: () => setState(() => _showAddHub = false), + onCancel: () => BlocProvider.of( + context, + ).add(const ClientHubsAddDialogToggled(visible: false)), ), - if (_hubToIdentify != null) + if (state.hubToIdentify != null) IdentifyNfcDialog( - hub: _hubToIdentify!, + hub: state.hubToIdentify!, onAssign: (tagId) { BlocProvider.of(context).add( ClientHubsNfcTagAssignRequested( - hubId: _hubToIdentify!.id, + hubId: state.hubToIdentify!.id, nfcTagId: tagId, ), ); - setState(() => _hubToIdentify = null); }, - onCancel: () => setState(() => _hubToIdentify = null), + onCancel: () => BlocProvider.of( + context, + ).add(const ClientHubsIdentifyDialogToggled()), ), if (state.status == ClientHubsStatus.actionInProgress) Container( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), child: const Center(child: CircularProgressIndicator()), ), ], @@ -152,7 +156,7 @@ class _ClientHubsPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -187,7 +191,9 @@ class _ClientHubsPageState extends State { ], ), UiButton.primary( - onPressed: () => setState(() => _showAddHub = true), + onPressed: () => BlocProvider.of( + context, + ).add(const ClientHubsAddDialogToggled(visible: true)), text: t.client_hubs.add_hub, leadingIcon: LucideIcons.plus, ), diff --git a/apps/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart b/apps/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart index c618c650..3ae46ce0 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart @@ -42,7 +42,7 @@ class _AddHubDialogState extends State { @override Widget build(BuildContext context) { return Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), child: Center( child: SingleChildScrollView( child: Container( @@ -52,7 +52,10 @@ class _AddHubDialogState extends State { color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20), + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + ), ], ), child: Column( diff --git a/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart b/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart index e8d9673b..67157f3a 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart @@ -33,7 +33,7 @@ class HubCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.04), + color: Colors.black.withValues(alpha: 0.04), blurRadius: 10, offset: const Offset(0, 4), ), diff --git a/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart b/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart index 3836bdb5..a82fb2f0 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart @@ -20,7 +20,7 @@ class HubEmptyState extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.04), + color: Colors.black.withValues(alpha: 0.04), blurRadius: 10, offset: const Offset(0, 4), ), diff --git a/apps/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart b/apps/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart index ed5362f5..09d21e61 100644 --- a/apps/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart +++ b/apps/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart @@ -40,7 +40,7 @@ class _IdentifyNfcDialogState extends State { @override Widget build(BuildContext context) { return Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), child: Center( child: SingleChildScrollView( child: Container( @@ -50,7 +50,10 @@ class _IdentifyNfcDialogState extends State { color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20), + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + ), ], ), child: Column(