refactor: move dialog state management to BLoC and make client hubs page stateless.

This commit is contained in:
Achintha Isuru
2026-01-21 19:55:56 -05:00
parent 12dfde0551
commit 0599e9b351
8 changed files with 108 additions and 30 deletions

View File

@@ -31,6 +31,26 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
on<ClientHubsDeleteRequested>(_onDeleteRequested); on<ClientHubsDeleteRequested>(_onDeleteRequested);
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested); on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
on<ClientHubsMessageCleared>(_onMessageCleared); on<ClientHubsMessageCleared>(_onMessageCleared);
on<ClientHubsAddDialogToggled>(_onAddDialogToggled);
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
}
void _onAddDialogToggled(
ClientHubsAddDialogToggled event,
Emitter<ClientHubsState> emit,
) {
emit(state.copyWith(showAddHubDialog: event.visible));
}
void _onIdentifyDialogToggled(
ClientHubsIdentifyDialogToggled event,
Emitter<ClientHubsState> emit,
) {
if (event.hub == null) {
emit(state.copyWith(clearHubToIdentify: true));
} else {
emit(state.copyWith(hubToIdentify: event.hub));
}
} }
Future<void> _onFetched( Future<void> _onFetched(
@@ -66,6 +86,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
status: ClientHubsStatus.actionSuccess, status: ClientHubsStatus.actionSuccess,
hubs: hubs, hubs: hubs,
successMessage: 'Hub created successfully', successMessage: 'Hub created successfully',
showAddHubDialog: false,
), ),
); );
} catch (e) { } catch (e) {
@@ -118,6 +139,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
status: ClientHubsStatus.actionSuccess, status: ClientHubsStatus.actionSuccess,
hubs: hubs, hubs: hubs,
successMessage: 'NFC tag assigned successfully', successMessage: 'NFC tag assigned successfully',
clearHubToIdentify: true,
), ),
); );
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base class for all client hubs events. /// Base class for all client hubs events.
abstract class ClientHubsEvent extends Equatable { abstract class ClientHubsEvent extends Equatable {
@@ -52,3 +53,23 @@ class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
class ClientHubsMessageCleared extends ClientHubsEvent { class ClientHubsMessageCleared extends ClientHubsEvent {
const ClientHubsMessageCleared(); 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<Object?> 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<Object?> get props => [hub];
}

View File

@@ -19,11 +19,20 @@ class ClientHubsState extends Equatable {
final String? errorMessage; final String? errorMessage;
final String? successMessage; 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({ const ClientHubsState({
this.status = ClientHubsStatus.initial, this.status = ClientHubsStatus.initial,
this.hubs = const [], this.hubs = const [],
this.errorMessage, this.errorMessage,
this.successMessage, this.successMessage,
this.showAddHubDialog = false,
this.hubToIdentify,
}); });
ClientHubsState copyWith({ ClientHubsState copyWith({
@@ -31,15 +40,29 @@ class ClientHubsState extends Equatable {
List<Hub>? hubs, List<Hub>? hubs,
String? errorMessage, String? errorMessage,
String? successMessage, String? successMessage,
bool? showAddHubDialog,
Hub? hubToIdentify,
bool clearHubToIdentify = false,
}) { }) {
return ClientHubsState( return ClientHubsState(
status: status ?? this.status, status: status ?? this.status,
hubs: hubs ?? this.hubs, hubs: hubs ?? this.hubs,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage, successMessage: successMessage ?? this.successMessage,
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
hubToIdentify: clearHubToIdentify
? null
: (hubToIdentify ?? this.hubToIdentify),
); );
} }
@override @override
List<Object?> get props => [status, hubs, errorMessage, successMessage]; List<Object?> get props => [
status,
hubs,
errorMessage,
successMessage,
showAddHubDialog,
hubToIdentify,
];
} }

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import '../blocs/client_hubs_bloc.dart'; import '../blocs/client_hubs_bloc.dart';
import '../blocs/client_hubs_event.dart'; import '../blocs/client_hubs_event.dart';
@@ -15,18 +14,13 @@ import '../widgets/hub_info_card.dart';
import '../widgets/identify_nfc_dialog.dart'; import '../widgets/identify_nfc_dialog.dart';
/// The main page for the client hubs feature. /// 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]. /// Creates a [ClientHubsPage].
const ClientHubsPage({super.key}); const ClientHubsPage({super.key});
@override
State<ClientHubsPage> createState() => _ClientHubsPageState();
}
class _ClientHubsPageState extends State<ClientHubsPage> {
bool _showAddHub = false;
Hub? _hubToIdentify;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<ClientHubsBloc>( return BlocProvider<ClientHubsBloc>(
@@ -68,14 +62,22 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
else if (state.hubs.isEmpty) else if (state.hubs.isEmpty)
HubEmptyState( HubEmptyState(
onAddPressed: () => onAddPressed: () =>
setState(() => _showAddHub = true), BlocProvider.of<ClientHubsBloc>(context).add(
const ClientHubsAddDialogToggled(
visible: true,
),
),
) )
else ...[ else ...[
...state.hubs.map( ...state.hubs.map(
(hub) => HubCard( (hub) => HubCard(
hub: hub, hub: hub,
onNfcPressed: () => onNfcPressed: () =>
setState(() => _hubToIdentify = hub), BlocProvider.of<ClientHubsBloc>(
context,
).add(
ClientHubsIdentifyDialogToggled(hub: hub),
),
onDeletePressed: () => onDeletePressed: () =>
BlocProvider.of<ClientHubsBloc>( BlocProvider.of<ClientHubsBloc>(
context, context,
@@ -90,33 +92,35 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
), ),
], ],
), ),
if (_showAddHub) if (state.showAddHubDialog)
AddHubDialog( AddHubDialog(
onCreate: (name, address) { onCreate: (name, address) {
BlocProvider.of<ClientHubsBloc>(context).add( BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsAddRequested(name: name, address: address), ClientHubsAddRequested(name: name, address: address),
); );
setState(() => _showAddHub = false);
}, },
onCancel: () => setState(() => _showAddHub = false), onCancel: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsAddDialogToggled(visible: false)),
), ),
if (_hubToIdentify != null) if (state.hubToIdentify != null)
IdentifyNfcDialog( IdentifyNfcDialog(
hub: _hubToIdentify!, hub: state.hubToIdentify!,
onAssign: (tagId) { onAssign: (tagId) {
BlocProvider.of<ClientHubsBloc>(context).add( BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsNfcTagAssignRequested( ClientHubsNfcTagAssignRequested(
hubId: _hubToIdentify!.id, hubId: state.hubToIdentify!.id,
nfcTagId: tagId, nfcTagId: tagId,
), ),
); );
setState(() => _hubToIdentify = null);
}, },
onCancel: () => setState(() => _hubToIdentify = null), onCancel: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsIdentifyDialogToggled()),
), ),
if (state.status == ClientHubsStatus.actionInProgress) if (state.status == ClientHubsStatus.actionInProgress)
Container( Container(
color: Colors.black.withOpacity(0.1), color: Colors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()), child: const Center(child: CircularProgressIndicator()),
), ),
], ],
@@ -152,7 +156,7 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -187,7 +191,9 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
], ],
), ),
UiButton.primary( UiButton.primary(
onPressed: () => setState(() => _showAddHub = true), onPressed: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsAddDialogToggled(visible: true)),
text: t.client_hubs.add_hub, text: t.client_hubs.add_hub,
leadingIcon: LucideIcons.plus, leadingIcon: LucideIcons.plus,
), ),

View File

@@ -42,7 +42,7 @@ class _AddHubDialogState extends State<AddHubDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withValues(alpha: 0.5),
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Container( child: Container(
@@ -52,7 +52,10 @@ class _AddHubDialogState extends State<AddHubDialog> {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20), BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
),
], ],
), ),
child: Column( child: Column(

View File

@@ -33,7 +33,7 @@ class HubCard extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.04), color: Colors.black.withValues(alpha: 0.04),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),

View File

@@ -20,7 +20,7 @@ class HubEmptyState extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.04), color: Colors.black.withValues(alpha: 0.04),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),

View File

@@ -40,7 +40,7 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withValues(alpha: 0.5),
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Container( child: Container(
@@ -50,7 +50,10 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20), BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
),
], ],
), ),
child: Column( child: Column(