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<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
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(
@@ -66,6 +86,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub created successfully',
showAddHubDialog: false,
),
);
} catch (e) {
@@ -118,6 +139,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'NFC tag assigned successfully',
clearHubToIdentify: true,
),
);
} catch (e) {

View File

@@ -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<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? 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<Hub>? 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<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_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<ClientHubsPage> createState() => _ClientHubsPageState();
}
class _ClientHubsPageState extends State<ClientHubsPage> {
bool _showAddHub = false;
Hub? _hubToIdentify;
@override
Widget build(BuildContext context) {
return BlocProvider<ClientHubsBloc>(
@@ -68,14 +62,22 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () =>
setState(() => _showAddHub = true),
BlocProvider.of<ClientHubsBloc>(context).add(
const ClientHubsAddDialogToggled(
visible: true,
),
),
)
else ...[
...state.hubs.map(
(hub) => HubCard(
hub: hub,
onNfcPressed: () =>
setState(() => _hubToIdentify = hub),
BlocProvider.of<ClientHubsBloc>(
context,
).add(
ClientHubsIdentifyDialogToggled(hub: hub),
),
onDeletePressed: () =>
BlocProvider.of<ClientHubsBloc>(
context,
@@ -90,33 +92,35 @@ class _ClientHubsPageState extends State<ClientHubsPage> {
),
],
),
if (_showAddHub)
if (state.showAddHubDialog)
AddHubDialog(
onCreate: (name, address) {
BlocProvider.of<ClientHubsBloc>(context).add(
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(
hub: _hubToIdentify!,
hub: state.hubToIdentify!,
onAssign: (tagId) {
BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsNfcTagAssignRequested(
hubId: _hubToIdentify!.id,
hubId: state.hubToIdentify!.id,
nfcTagId: tagId,
),
);
setState(() => _hubToIdentify = null);
},
onCancel: () => setState(() => _hubToIdentify = null),
onCancel: () => BlocProvider.of<ClientHubsBloc>(
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<ClientHubsPage> {
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<ClientHubsPage> {
],
),
UiButton.primary(
onPressed: () => setState(() => _showAddHub = true),
onPressed: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsAddDialogToggled(visible: true)),
text: t.client_hubs.add_hub,
leadingIcon: LucideIcons.plus,
),

View File

@@ -42,7 +42,7 @@ class _AddHubDialogState extends State<AddHubDialog> {
@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<AddHubDialog> {
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(

View File

@@ -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),
),

View File

@@ -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),
),

View File

@@ -40,7 +40,7 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
@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<IdentifyNfcDialog> {
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(