feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/gql_schemas.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
|
||||
@injectable
|
||||
class EmergencyContactApiProvider {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
EmergencyContactApiProvider(this._apiClient);
|
||||
|
||||
Stream<List<EmergencyContactModel>> getStaffEmergencyContacts() async* {
|
||||
await for (var response in _apiClient.queryWithCache(
|
||||
schema: getEmergencyContactsQuerySchema,
|
||||
)) {
|
||||
if (response == null || response.data == null) continue;
|
||||
|
||||
if (response.data == null || response.data!.isEmpty) {
|
||||
if (response.source?.name == 'cache') continue;
|
||||
|
||||
if (response.hasException) {
|
||||
throw Exception(response.exception.toString());
|
||||
}
|
||||
}
|
||||
|
||||
final contactsData =
|
||||
(response.data?['me'] as Map<String, dynamic>?)?['emergency_contacts']
|
||||
as List<dynamic>? ??
|
||||
[];
|
||||
|
||||
yield [
|
||||
for (final contact in contactsData)
|
||||
EmergencyContactModel.fromJson(
|
||||
contact as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<EmergencyContactModel>?> updateEmergencyContacts(
|
||||
List<EmergencyContactModel> contacts,
|
||||
) async {
|
||||
var result = await _apiClient.mutate(
|
||||
schema: updateEmergencyContactsMutationSchema,
|
||||
body: {
|
||||
'input': [
|
||||
for (final contact in contacts) contact.toJson(),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!.isEmpty) return null;
|
||||
|
||||
//TODO(Heorhii) add contacts return
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
|
||||
abstract class EmergencyContactRepository {
|
||||
Stream<List<EmergencyContactModel>> getStaffEmergencyContacts();
|
||||
|
||||
Future<List<EmergencyContactModel>?> updateEmergencyContacts(
|
||||
List<EmergencyContactModel> contacts,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const String getEmergencyContactsQuerySchema = r'''
|
||||
query StaffEmergencyContacts {
|
||||
me {
|
||||
id
|
||||
emergency_contacts {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateEmergencyContactsMutationSchema = r'''
|
||||
mutation SaveEmergencyContacts ($input: [CreateStaffEmergencyContactsInput!]) {
|
||||
attach_staff_emergency_contacts(contacts: $input) {
|
||||
emergency_contacts {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
final class EmergencyContactModel {
|
||||
const EmergencyContactModel({
|
||||
this.contactId,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
|
||||
const EmergencyContactModel.empty({
|
||||
this.contactId,
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.phoneNumber = '',
|
||||
});
|
||||
|
||||
factory EmergencyContactModel.fromJson(Map<String, dynamic> json) {
|
||||
return EmergencyContactModel(
|
||||
contactId: json['id'] as String?,
|
||||
firstName: json['first_name'] as String? ?? '',
|
||||
lastName: json['last_name'] as String? ?? '',
|
||||
phoneNumber: json['phone'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
final String? contactId;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String phoneNumber;
|
||||
|
||||
bool get isFilled =>
|
||||
firstName.isNotEmpty && lastName.isNotEmpty && phoneNumber.isNotEmpty;
|
||||
|
||||
EmergencyContactModel copyWith({
|
||||
String? contactId,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? phoneNumber,
|
||||
bool? isSaved,
|
||||
}) {
|
||||
return EmergencyContactModel(
|
||||
contactId: contactId ?? this.contactId,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'phone': phoneNumber,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/emergency_contact_repository.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
|
||||
part 'emergency_contacts_event.dart';
|
||||
|
||||
part 'emergency_contacts_state.dart';
|
||||
|
||||
class EmergencyContactsBloc
|
||||
extends Bloc<EmergencyContactsEvent, EmergencyContactsState> {
|
||||
EmergencyContactsBloc() : super(const EmergencyContactsState()) {
|
||||
on<InitializeEmergencyContactsEvent>(_handleInitializeEvent);
|
||||
|
||||
on<AddNewContactEvent>(_handleAddNewContactEvent);
|
||||
|
||||
on<UpdateContactEvent>(_handleUpdateContactEvent);
|
||||
|
||||
on<DeleteContactEvent>(_handleDeleteContactEvent);
|
||||
|
||||
on<SaveContactsChanges>(_handleSaveContactsChanges);
|
||||
}
|
||||
|
||||
Future<void> _handleInitializeEvent(
|
||||
InitializeEmergencyContactsEvent event,
|
||||
Emitter<EmergencyContactsState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isInEditMode: event.isInEditMode,
|
||||
contacts: event.isInEditMode
|
||||
? state.contacts
|
||||
: [const EmergencyContactModel.empty()],
|
||||
status: event.isInEditMode ? StateStatus.loading : StateStatus.idle,
|
||||
),
|
||||
);
|
||||
if (!state.isInEditMode) return;
|
||||
|
||||
try {
|
||||
await for (final emergencyContacts
|
||||
in getIt<EmergencyContactRepository>().getStaffEmergencyContacts()) {
|
||||
if (emergencyContacts.isEmpty) continue;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
contacts: emergencyContacts,
|
||||
status: StateStatus.idle,
|
||||
isListReducible: emergencyContacts.length > 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (except) {
|
||||
log(except.toString());
|
||||
}
|
||||
|
||||
if (state.status == StateStatus.loading) {
|
||||
emit(state.copyWith(status: StateStatus.idle));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAddNewContactEvent(
|
||||
AddNewContactEvent event,
|
||||
Emitter<EmergencyContactsState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
contacts: [
|
||||
...state.contacts,
|
||||
const EmergencyContactModel.empty(),
|
||||
],
|
||||
isListReducible: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleUpdateContactEvent(
|
||||
UpdateContactEvent event,
|
||||
Emitter<EmergencyContactsState> emit,
|
||||
) {
|
||||
final updatedContact = state.contacts[event.index].copyWith(
|
||||
firstName: event.firstName,
|
||||
lastName: event.lastName,
|
||||
phoneNumber: event.phoneNumber,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
contacts: List.from(state.contacts)..[event.index] = updatedContact,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDeleteContactEvent(
|
||||
DeleteContactEvent event,
|
||||
Emitter<EmergencyContactsState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
contacts: List.from(state.contacts)..removeAt(event.index),
|
||||
isListReducible: state.contacts.length > 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleSaveContactsChanges(
|
||||
SaveContactsChanges event,
|
||||
Emitter<EmergencyContactsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
|
||||
try {
|
||||
await getIt<EmergencyContactRepository>().updateEmergencyContacts(
|
||||
state.contacts,
|
||||
);
|
||||
} catch (except) {
|
||||
log(except.toString());
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: StateStatus.success));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
part of 'emergency_contacts_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class EmergencyContactsEvent {}
|
||||
|
||||
class InitializeEmergencyContactsEvent extends EmergencyContactsEvent {
|
||||
InitializeEmergencyContactsEvent(this.isInEditMode);
|
||||
|
||||
final bool isInEditMode;
|
||||
}
|
||||
|
||||
class AddNewContactEvent extends EmergencyContactsEvent {}
|
||||
|
||||
class UpdateContactEvent extends EmergencyContactsEvent {
|
||||
UpdateContactEvent({
|
||||
required this.index,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String phoneNumber;
|
||||
}
|
||||
|
||||
class DeleteContactEvent extends EmergencyContactsEvent {
|
||||
DeleteContactEvent({required this.index});
|
||||
|
||||
final int index;
|
||||
}
|
||||
|
||||
class SaveContactsChanges extends EmergencyContactsEvent {}
|
||||
@@ -0,0 +1,32 @@
|
||||
part of 'emergency_contacts_bloc.dart';
|
||||
|
||||
@immutable
|
||||
final class EmergencyContactsState {
|
||||
const EmergencyContactsState({
|
||||
this.contacts = const [],
|
||||
this.status = StateStatus.idle,
|
||||
this.isInEditMode = true,
|
||||
this.isListReducible = false,
|
||||
});
|
||||
|
||||
final List<EmergencyContactModel> contacts;
|
||||
final StateStatus status;
|
||||
final bool isInEditMode;
|
||||
final bool isListReducible;
|
||||
|
||||
bool get isFilled => contacts.indexWhere((contact) => !contact.isFilled) < 0;
|
||||
|
||||
EmergencyContactsState copyWith({
|
||||
List<EmergencyContactModel>? contacts,
|
||||
StateStatus? status,
|
||||
bool? isInEditMode,
|
||||
bool? isListReducible,
|
||||
}) {
|
||||
return EmergencyContactsState(
|
||||
contacts: contacts ?? this.contacts,
|
||||
status: status ?? this.status,
|
||||
isInEditMode: isInEditMode ?? this.isInEditMode,
|
||||
isListReducible: isListReducible ?? this.isListReducible,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/emergency_contact_api_provider.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/emergency_contact_repository.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
|
||||
@LazySingleton(as: EmergencyContactRepository)
|
||||
class EmergencyContactRepositoryImpl implements EmergencyContactRepository {
|
||||
EmergencyContactRepositoryImpl({
|
||||
required EmergencyContactApiProvider apiProvider,
|
||||
}) : _apiProvider = apiProvider;
|
||||
|
||||
final EmergencyContactApiProvider _apiProvider;
|
||||
|
||||
@override
|
||||
Stream<List<EmergencyContactModel>> getStaffEmergencyContacts() {
|
||||
return _apiProvider.getStaffEmergencyContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<EmergencyContactModel>?> updateEmergencyContacts(
|
||||
List<EmergencyContactModel> contacts,
|
||||
) {
|
||||
return _apiProvider.updateEmergencyContacts(contacts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/domain/bloc/emergency_contacts_bloc.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/presentation/widgets/bottom_control_button.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/presentation/widgets/contacts_list_sliver.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EmergencyContactsScreen extends StatelessWidget
|
||||
implements AutoRouteWrapper {
|
||||
const EmergencyContactsScreen({
|
||||
super.key,
|
||||
this.isInEditMode = true,
|
||||
});
|
||||
|
||||
final bool isInEditMode;
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (BuildContext context) => EmergencyContactsBloc()
|
||||
..add(InitializeEmergencyContactsEvent(isInEditMode)),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
appBar: KwAppBar(
|
||||
titleText: 'emergency_contact'.tr(),
|
||||
centerTitle: true,
|
||||
showNotification: isInEditMode,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: CustomScrollView(
|
||||
primary: false,
|
||||
slivers: [
|
||||
if (!isInEditMode)
|
||||
SliverList.list(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'add_emergency_contacts'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Gap(8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'provide_emergency_contact'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' ${'must_have_one_contact'.tr()}',
|
||||
style: AppTextStyles.bodyMediumSmb,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Gap(16),
|
||||
),
|
||||
const ContactsListSliver(),
|
||||
SliverList.list(
|
||||
children: [
|
||||
KwButton.outlinedPrimary(
|
||||
fit: KwButtonFit.shrinkWrap,
|
||||
label: 'add_more'.tr(),
|
||||
leftIcon: Assets.images.icons.add,
|
||||
onPressed: () {
|
||||
context.read<EmergencyContactsBloc>().add(
|
||||
AddNewContactEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'add_additional_contact'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Gap(120),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: BottomControlButton(
|
||||
isInEditMode: isInEditMode,
|
||||
footerActionName:
|
||||
isInEditMode ? 'save_changes'.tr() : 'save_and_continue'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/domain/bloc/emergency_contacts_bloc.dart';
|
||||
|
||||
class BottomControlButton extends StatefulWidget {
|
||||
const BottomControlButton({
|
||||
super.key,
|
||||
required this.isInEditMode,
|
||||
required this.footerActionName,
|
||||
});
|
||||
|
||||
final bool isInEditMode;
|
||||
final String footerActionName;
|
||||
|
||||
static const _height = 52.0;
|
||||
|
||||
@override
|
||||
State<BottomControlButton> createState() => _BottomControlButtonState();
|
||||
}
|
||||
|
||||
class _BottomControlButtonState extends State<BottomControlButton>
|
||||
with WidgetsBindingObserver {
|
||||
bool isVisible = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
if (View.of(context).viewInsets.bottom > 0 && isVisible) {
|
||||
setState(() => isVisible = false);
|
||||
} else if (View.of(context).viewInsets.bottom == 0 && !isVisible) {
|
||||
setState(() => isVisible = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
duration: Durations.short2,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
child: AnimatedSize(
|
||||
duration: Durations.short2,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: BlocConsumer<EmergencyContactsBloc, EmergencyContactsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status ||
|
||||
previous.isFilled != current.isFilled,
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == StateStatus.success) {
|
||||
if (widget.isInEditMode) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
context.router.push(
|
||||
MobilityRoute(isInEditMode: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return KwLoadingOverlay(
|
||||
shouldShowLoading: state.status == StateStatus.loading,
|
||||
child: KwButton.primary(
|
||||
label: widget.footerActionName,
|
||||
height: BottomControlButton._height,
|
||||
disabled: !state.isFilled,
|
||||
onPressed: () {
|
||||
context.read<EmergencyContactsBloc>().add(
|
||||
SaveContactsChanges(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/int_extensions.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_phone_input.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
|
||||
class ContactFormWidget extends StatefulWidget {
|
||||
const ContactFormWidget({
|
||||
super.key,
|
||||
required this.contactModel,
|
||||
required this.index,
|
||||
this.onDelete,
|
||||
this.onContactUpdate,
|
||||
});
|
||||
|
||||
final EmergencyContactModel contactModel;
|
||||
final int index;
|
||||
final VoidCallback? onDelete;
|
||||
final void Function({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String phoneNumber,
|
||||
})? onContactUpdate;
|
||||
|
||||
@override
|
||||
State<ContactFormWidget> createState() => _ContactFormWidgetState();
|
||||
}
|
||||
|
||||
class _ContactFormWidgetState extends State<ContactFormWidget> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
Timer? _contactUpdateTimer;
|
||||
|
||||
void _scheduleContactUpdate() {
|
||||
if (widget.onContactUpdate == null) return;
|
||||
_contactUpdateTimer?.cancel();
|
||||
|
||||
_contactUpdateTimer = Timer(
|
||||
const Duration(microseconds: 500),
|
||||
() => widget.onContactUpdate!(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
phoneNumber: _phoneController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_firstNameController.text = widget.contactModel.firstName;
|
||||
_lastNameController.text = widget.contactModel.lastName;
|
||||
_phoneController.text = widget.contactModel.phoneNumber;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 24,
|
||||
),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Text(
|
||||
'contact_details'.tr(
|
||||
namedArgs: {
|
||||
'index': (widget.index + 1).toOrdinal(),
|
||||
},
|
||||
),
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -18,
|
||||
top: -18,
|
||||
child: _DeleteIconWidget(
|
||||
onDelete: widget.onDelete,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
KwTextInput(
|
||||
title: 'first_name'.tr(),
|
||||
hintText: '',
|
||||
keyboardType: TextInputType.name,
|
||||
controller: _firstNameController,
|
||||
onChanged: (_) => _scheduleContactUpdate(),
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'last_name'.tr(),
|
||||
hintText: '',
|
||||
keyboardType: TextInputType.name,
|
||||
controller: _lastNameController,
|
||||
onChanged: (_) => _scheduleContactUpdate(),
|
||||
),
|
||||
const Gap(8),
|
||||
KwPhoneInput(
|
||||
title: 'phone_number'.tr(),
|
||||
controller: _phoneController,
|
||||
onChanged: (_) => _scheduleContactUpdate(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contactUpdateTimer?.cancel();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteIconWidget extends StatelessWidget {
|
||||
const _DeleteIconWidget({this.onDelete});
|
||||
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
child: onDelete != null
|
||||
? IconButton(
|
||||
onPressed: onDelete,
|
||||
icon: Assets.images.icons.delete.svg(
|
||||
height: 16,
|
||||
width: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/data/models/emergency_contact_model.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/domain/bloc/emergency_contacts_bloc.dart';
|
||||
import 'package:krow/features/profile/emergency_contacts/presentation/widgets/contact_form_widget.dart';
|
||||
|
||||
class ContactsListSliver extends StatefulWidget {
|
||||
const ContactsListSliver({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContactsListSliver> createState() => _ContactsListSliverState();
|
||||
}
|
||||
|
||||
class _ContactsListSliverState extends State<ContactsListSliver> {
|
||||
final _listKey = GlobalKey<SliverAnimatedListState>();
|
||||
late final _contactsBloc = context.read<EmergencyContactsBloc>();
|
||||
|
||||
int _itemsCount = 0;
|
||||
|
||||
EmergencyContactsState get _contactsState => _contactsBloc.state;
|
||||
|
||||
bool _listenWhenHandler(
|
||||
EmergencyContactsState previous,
|
||||
EmergencyContactsState current,
|
||||
) =>
|
||||
previous.contacts != current.contacts;
|
||||
|
||||
void _addListItem(int index) {
|
||||
_listKey.currentState?.insertItem(
|
||||
index,
|
||||
duration: Durations.short3,
|
||||
);
|
||||
|
||||
_itemsCount++;
|
||||
}
|
||||
|
||||
void _addSingleListItem({required int index}) => _addListItem(index);
|
||||
|
||||
Future<void> _addMultipleListItems({required int addedItemsLength}) async {
|
||||
_listKey.currentState?.insertAllItems(
|
||||
_itemsCount,
|
||||
addedItemsLength,
|
||||
duration: Durations.short3,
|
||||
);
|
||||
_itemsCount += addedItemsLength;
|
||||
|
||||
await Future<void>.delayed(Durations.short2);
|
||||
}
|
||||
|
||||
Future<void> _listenHandler(
|
||||
BuildContext context,
|
||||
EmergencyContactsState state,
|
||||
) async {
|
||||
if (state.contacts.length <= _itemsCount) return;
|
||||
|
||||
if (state.contacts.length - _itemsCount == 1) {
|
||||
_addSingleListItem(index: state.contacts.length - 1);
|
||||
} else {
|
||||
await _addMultipleListItems(
|
||||
addedItemsLength: state.contacts.length - _itemsCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _removeListItem({
|
||||
required int index,
|
||||
required EmergencyContactModel contactData,
|
||||
}) {
|
||||
_listKey.currentState?.removeItem(
|
||||
index,
|
||||
(context, animation) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0, 0.5),
|
||||
),
|
||||
child: ScaleTransition(
|
||||
scale: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.1, 0.6),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: ContactFormWidget(
|
||||
contactModel: contactData,
|
||||
index: index,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
duration: Durations.short4,
|
||||
);
|
||||
|
||||
_itemsCount--;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_itemsCount = _contactsState.contacts.length;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<EmergencyContactsBloc, EmergencyContactsState>(
|
||||
bloc: _contactsBloc,
|
||||
listenWhen: _listenWhenHandler,
|
||||
listener: _listenHandler,
|
||||
child: SliverAnimatedList(
|
||||
key: _listKey,
|
||||
initialItemCount: _contactsState.contacts.length,
|
||||
itemBuilder: (
|
||||
BuildContext context,
|
||||
int index,
|
||||
Animation<double> animation,
|
||||
) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.3, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: ContactFormWidget(
|
||||
key: ValueKey(_contactsState.contacts[index]),
|
||||
contactModel: _contactsState.contacts[index],
|
||||
index: index,
|
||||
onContactUpdate: ({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String phoneNumber,
|
||||
}) {
|
||||
context.read<EmergencyContactsBloc>().add(
|
||||
UpdateContactEvent(
|
||||
index: index,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phoneNumber: phoneNumber,
|
||||
),
|
||||
);
|
||||
},
|
||||
onDelete: _contactsState.isListReducible
|
||||
? () {
|
||||
_removeListItem(
|
||||
index: index,
|
||||
contactData: _contactsState.contacts[index],
|
||||
);
|
||||
|
||||
context.read<EmergencyContactsBloc>().add(
|
||||
DeleteContactEvent(index: index),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user