feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -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(),
),
),
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}