feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -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