feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/staff/full_address_model.dart';
|
||||
import 'package:krow/features/profile/address/data/gql.dart';
|
||||
|
||||
@injectable
|
||||
class AddressApiProvider {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
AddressApiProvider(this._apiClient);
|
||||
|
||||
Stream<FullAddress?> getStaffAddress() async* {
|
||||
await for (var response
|
||||
in _apiClient.queryWithCache(schema: staffAddress)) {
|
||||
if (response == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.hasException) {
|
||||
throw Exception(response.exception.toString());
|
||||
}
|
||||
|
||||
final address =
|
||||
FullAddress.fromJson(response.data?['me']?['full_address'] ?? {});
|
||||
yield address;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> putAddress(FullAddress address) async {
|
||||
final Map<String, dynamic> variables = {
|
||||
'input': address.toJson(),
|
||||
};
|
||||
|
||||
var result =
|
||||
await _apiClient.mutate(schema: saveFullAddress, body: variables);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:krow/core/data/models/staff/full_address_model.dart';
|
||||
|
||||
abstract class AddressRepository {
|
||||
Stream<FullAddress?> getStaffAddress();
|
||||
|
||||
Future<void> putAddress(FullAddress address);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const String staffAddress = r'''
|
||||
query staffAddress {
|
||||
me {
|
||||
id
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String saveFullAddress = r'''
|
||||
mutation saveFullAddress($input: AddressInput!) {
|
||||
update_staff_address(input: $input) {
|
||||
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/full_address_model.dart';
|
||||
import 'package:krow/features/profile/address/data/address_api_provider.dart';
|
||||
import 'package:krow/features/profile/address/data/address_repository.dart';
|
||||
|
||||
@Singleton(as: AddressRepository)
|
||||
class AddressRepositoryImpl implements AddressRepository {
|
||||
final AddressApiProvider _apiProvider;
|
||||
|
||||
AddressRepositoryImpl({required AddressApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Stream<FullAddress?> getStaffAddress() {
|
||||
return _apiProvider.getStaffAddress();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putAddress(FullAddress address) {
|
||||
return _apiProvider.putAddress(address);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
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/models/staff/full_address_model.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/features/profile/address/data/address_repository.dart';
|
||||
import 'package:krow/features/profile/address/domain/google_places_service.dart';
|
||||
|
||||
|
||||
part 'address_event.dart';
|
||||
part 'address_state.dart';
|
||||
|
||||
class AddressBloc extends Bloc<AddressEvent, AddressState> {
|
||||
AddressBloc() : super(const AddressState()) {
|
||||
on<InitializeAddressEvent>(_onInitialize);
|
||||
on<SubmitAddressEvent>(_onSubmit);
|
||||
on<AddressQueryChangedEvent>(_onQueryChanged);
|
||||
on<AddressSelectEvent>(_onSelect);
|
||||
}
|
||||
|
||||
void _onInitialize(
|
||||
InitializeAddressEvent event, Emitter<AddressState> emit) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
await for (var address in getIt<AddressRepository>().getStaffAddress()) {
|
||||
emit(state.copyWith(
|
||||
fullAddress: address,
|
||||
status: StateStatus.idle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onQueryChanged(
|
||||
AddressQueryChangedEvent event, Emitter<AddressState> emit) async {
|
||||
try {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final suggestions =
|
||||
await googlePlacesService.fetchSuggestions(event.query);
|
||||
emit(state.copyWith(suggestions: suggestions));
|
||||
} catch (e) {
|
||||
if (kDebugMode) print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelect(AddressSelectEvent event, Emitter<AddressState> emit) async {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final fullAddress =
|
||||
await googlePlacesService.getPlaceDetails(event.place.placeId);
|
||||
FullAddress address = FullAddress.fromGoogle(fullAddress);
|
||||
emit(state.copyWith(suggestions: [], fullAddress: address));
|
||||
}
|
||||
|
||||
void _onSubmit(SubmitAddressEvent event, Emitter<AddressState> emit) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
try {
|
||||
await getIt<AddressRepository>().putAddress(state.fullAddress!);
|
||||
emit(state.copyWith(status: StateStatus.success));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: StateStatus.error));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
part of 'address_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class AddressEvent {}
|
||||
|
||||
class InitializeAddressEvent extends AddressEvent {
|
||||
InitializeAddressEvent();
|
||||
}
|
||||
|
||||
class AddressQueryChangedEvent extends AddressEvent {
|
||||
final String query;
|
||||
|
||||
AddressQueryChangedEvent(this.query);
|
||||
}
|
||||
|
||||
class SubmitAddressEvent extends AddressEvent {
|
||||
SubmitAddressEvent();
|
||||
}
|
||||
|
||||
class AddressSelectEvent extends AddressEvent {
|
||||
final MapPlace place;
|
||||
|
||||
AddressSelectEvent(this.place);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
part of 'address_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class AddressState {
|
||||
final StateStatus status;
|
||||
final FullAddress? fullAddress;
|
||||
final List<MapPlace> suggestions;
|
||||
|
||||
const AddressState({
|
||||
this.status = StateStatus.idle,
|
||||
this.fullAddress,
|
||||
this.suggestions = const [],
|
||||
});
|
||||
|
||||
AddressState copyWith({
|
||||
StateStatus? status,
|
||||
FullAddress? fullAddress,
|
||||
List<MapPlace>? suggestions,
|
||||
}) {
|
||||
return AddressState(
|
||||
status: status ?? this.status,
|
||||
suggestions: suggestions ?? this.suggestions,
|
||||
fullAddress: fullAddress ?? this.fullAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class GooglePlacesService {
|
||||
|
||||
Future<List<MapPlace>> fetchSuggestions(String query) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
|
||||
const String baseUrl =
|
||||
'https://maps.googleapis.com/maps/api/place/autocomplete/json';
|
||||
final Uri uri =
|
||||
Uri.parse('$baseUrl?input=$query&key=$apiKey&types=geocode');
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final List<dynamic> predictions = data['predictions'];
|
||||
|
||||
return predictions.map((prediction) {
|
||||
return MapPlace.fromJson(prediction);
|
||||
}).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch place suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getPlaceDetails(String placeId) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
final String url =
|
||||
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&key=$apiKey';
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final result = data['result'];
|
||||
final location = result['geometry']['location'];
|
||||
|
||||
Map<String, dynamic> addressDetails = {
|
||||
'lat': location['lat'], // Latitude
|
||||
'lng': location['lng'], // Longitude
|
||||
'formatted_address': result['formatted_address'], // Full Address
|
||||
};
|
||||
|
||||
for (var component in result['address_components']) {
|
||||
List types = component['types'];
|
||||
if (types.contains('street_number')) {
|
||||
addressDetails['street_number'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('route')) {
|
||||
addressDetails['street'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('locality')) {
|
||||
addressDetails['city'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('administrative_area_level_1')) {
|
||||
addressDetails['state'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('country')) {
|
||||
addressDetails['country'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('postal_code')) {
|
||||
addressDetails['postal_code'] = component['long_name'];
|
||||
}
|
||||
}
|
||||
|
||||
return addressDetails;
|
||||
} else {
|
||||
throw Exception('Failed to fetch place details');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapPlace {
|
||||
final String description;
|
||||
final String placeId;
|
||||
|
||||
MapPlace({required this.description, required this.placeId});
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'description': description,
|
||||
'place_id': placeId,
|
||||
};
|
||||
}
|
||||
|
||||
factory MapPlace.fromJson(Map<String, dynamic> json) {
|
||||
return MapPlace(
|
||||
description: json['description'],
|
||||
placeId: json['place_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/enums/state_status.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/scroll_layout_helper.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/core/presentation/widgets/ui_kit/kw_suggestion_input.dart';
|
||||
import 'package:krow/features/profile/address/domain/bloc/address_bloc.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AddressScreen extends StatefulWidget implements AutoRouteWrapper {
|
||||
final bool isInEditMode;
|
||||
|
||||
const AddressScreen({super.key, this.isInEditMode = true});
|
||||
|
||||
@override
|
||||
State<AddressScreen> createState() => _AddressScreenState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => AddressBloc()..add(InitializeAddressEvent()),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddressScreenState extends State<AddressScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
showNotification: widget.isInEditMode,
|
||||
titleText: 'location_and_availability'.tr(),
|
||||
),
|
||||
body: BlocConsumer<AddressBloc, AddressState>(
|
||||
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(
|
||||
WorkingAreaRoute(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.status == StateStatus.loading,
|
||||
child: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.all(16),
|
||||
upperWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!widget.isInEditMode) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'what_is_your_address'.tr(),
|
||||
style: AppTextStyles.headingH1,
|
||||
),
|
||||
Text(
|
||||
'let_us_know_your_home_base'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(24),
|
||||
]
|
||||
else
|
||||
const Gap(8),
|
||||
KwSuggestionInput(
|
||||
title: 'address'.tr(),
|
||||
hintText: 'select_address'.tr(),
|
||||
horizontalPadding: 16,
|
||||
items: state.suggestions,
|
||||
onQueryChanged: (query) {
|
||||
context
|
||||
.read<AddressBloc>()
|
||||
.add(AddressQueryChangedEvent(query));
|
||||
},
|
||||
itemToStringBuilder: (item) => item.description,
|
||||
onSelected: (item) {
|
||||
context.read<AddressBloc>().add(AddressSelectEvent(item));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
..._textBlock('country'.tr(), state.fullAddress?.country),
|
||||
..._textBlock('state'.tr(), state.fullAddress?.region),
|
||||
..._textBlock('city'.tr(), state.fullAddress?.city),
|
||||
..._textBlock('apt_suite_building'.tr(),
|
||||
state.fullAddress?.streetNumber),
|
||||
..._textBlock(
|
||||
'street_address'.tr(), state.fullAddress?.street),
|
||||
..._textBlock('zip_code'.tr(), state.fullAddress?.zipCode,
|
||||
hasNext: false),
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
lowerWidget: KwButton.primary(
|
||||
disabled: state.fullAddress == null,
|
||||
label: widget.isInEditMode
|
||||
? 'save_changes'.tr()
|
||||
: 'save_and_continue'.tr(),
|
||||
onPressed: () {
|
||||
if (widget.isInEditMode) {
|
||||
context.read<AddressBloc>().add(SubmitAddressEvent());
|
||||
} else {
|
||||
context.router.push(WorkingAreaRoute(isInEditMode: false));
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _textBlock(String key, String? value, {bool hasNext = true}) {
|
||||
return [
|
||||
...[
|
||||
Text(
|
||||
key,
|
||||
style: AppTextStyles.captionReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
value ?? '',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
hasNext ? const Gap(24) : const Gap(0),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user