refactor: move dialog state management to BLoC and make client hubs page stateless.
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user