mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Compass app (#2446)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../data/repositories/continent/continent_repository.dart';
|
||||
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
import '../../../domain/models/continent/continent.dart';
|
||||
import '../../../domain/models/itinerary_config/itinerary_config.dart';
|
||||
import '../../../utils/command.dart';
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
/// View model for the search form.
|
||||
///
|
||||
/// Contains the form selected options
|
||||
/// and the logic to load the list of regions.
|
||||
class SearchFormViewModel extends ChangeNotifier {
|
||||
SearchFormViewModel({
|
||||
required ContinentRepository continentRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _continentRepository = continentRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
updateItineraryConfig = Command0(_updateItineraryConfig);
|
||||
load = Command0(_load)..execute();
|
||||
}
|
||||
|
||||
final _log = Logger('SearchFormViewModel');
|
||||
final ContinentRepository _continentRepository;
|
||||
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||
List<Continent> _continents = [];
|
||||
String? _selectedContinent;
|
||||
DateTimeRange? _dateRange;
|
||||
int _guests = 0;
|
||||
|
||||
/// True if the form is valid and can be submitted
|
||||
bool get valid =>
|
||||
_guests > 0 && _selectedContinent != null && _dateRange != null;
|
||||
|
||||
/// List of continents.
|
||||
/// Loaded in [load] command.
|
||||
List<Continent> get continents => _continents;
|
||||
|
||||
/// Selected continent.
|
||||
/// Null means no continent is selected.
|
||||
String? get selectedContinent => _selectedContinent;
|
||||
|
||||
/// Set selected continent.
|
||||
/// Set to null to clear the selection.
|
||||
set selectedContinent(String? continent) {
|
||||
_selectedContinent = continent;
|
||||
_log.finest('Selected continent: $continent');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Date range.
|
||||
/// Null means no range is selected.
|
||||
DateTimeRange? get dateRange => _dateRange;
|
||||
|
||||
/// Set date range.
|
||||
/// Can be set to null to clear selection.
|
||||
set dateRange(DateTimeRange? dateRange) {
|
||||
_dateRange = dateRange;
|
||||
_log.finest('Selected date range: $dateRange');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Number of guests
|
||||
int get guests => _guests;
|
||||
|
||||
/// Set number of guests
|
||||
/// If the quantity is negative, it will be set to 0
|
||||
set guests(int quantity) {
|
||||
if (quantity < 0) {
|
||||
_guests = 0;
|
||||
} else {
|
||||
_guests = quantity;
|
||||
}
|
||||
_log.finest('Set guests number: $_guests');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Load the list of continents and current itinerary config.
|
||||
late final Command0 load;
|
||||
|
||||
/// Store ViewModel data into [ItineraryConfigRepository] before navigating.
|
||||
late final Command0 updateItineraryConfig;
|
||||
|
||||
Future<Result<void>> _load() async {
|
||||
final result = await _loadContinents();
|
||||
if (result is Error) {
|
||||
return result;
|
||||
}
|
||||
return await _loadItineraryConfig();
|
||||
}
|
||||
|
||||
Future<Result<void>> _loadContinents() async {
|
||||
final result = await _continentRepository.getContinents();
|
||||
switch (result) {
|
||||
case Ok():
|
||||
{
|
||||
_continents = result.value;
|
||||
_log.fine('Continents (${_continents.length}) loaded');
|
||||
}
|
||||
case Error():
|
||||
{
|
||||
_log.warning('Failed to load continents', result.asError.error);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Result<void>> _loadItineraryConfig() async {
|
||||
final result = await _itineraryConfigRepository.getItineraryConfig();
|
||||
switch (result) {
|
||||
case Ok<ItineraryConfig>():
|
||||
{
|
||||
final itineraryConfig = result.value;
|
||||
_selectedContinent = itineraryConfig.continent;
|
||||
if (itineraryConfig.startDate != null &&
|
||||
itineraryConfig.endDate != null) {
|
||||
_dateRange = DateTimeRange(
|
||||
start: itineraryConfig.startDate!,
|
||||
end: itineraryConfig.endDate!,
|
||||
);
|
||||
}
|
||||
_guests = itineraryConfig.guests ?? 0;
|
||||
_log.fine('ItineraryConfig loaded');
|
||||
notifyListeners();
|
||||
}
|
||||
case Error<ItineraryConfig>():
|
||||
{
|
||||
_log.warning(
|
||||
'Failed to load stored ItineraryConfig',
|
||||
result.asError.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Result<void>> _updateItineraryConfig() async {
|
||||
assert(valid, "called when valid was false");
|
||||
final result = await _itineraryConfigRepository.setItineraryConfig(
|
||||
ItineraryConfig(
|
||||
continent: _selectedContinent,
|
||||
startDate: _dateRange!.start,
|
||||
endDate: _dateRange!.end,
|
||||
guests: _guests,
|
||||
),
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<void>():
|
||||
_log.fine('ItineraryConfig saved');
|
||||
case Error<void>():
|
||||
_log.warning('Failed to store ItineraryConfig', result.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../../domain/models/continent/continent.dart';
|
||||
import '../../../utils/image_error_listener.dart';
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/colors.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/error_indicator.dart';
|
||||
import '../view_models/search_form_viewmodel.dart';
|
||||
|
||||
/// Continent selection carousel
|
||||
///
|
||||
/// Loads a list of continents in a horizontal carousel.
|
||||
/// Users can tap one item to select it.
|
||||
/// Tapping again the same item will deselect it.
|
||||
class SearchFormContinent extends StatelessWidget {
|
||||
const SearchFormContinent({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 140,
|
||||
child: ListenableBuilder(
|
||||
listenable: viewModel.load,
|
||||
builder: (context, child) {
|
||||
if (viewModel.load.running) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (viewModel.load.error) {
|
||||
return Center(
|
||||
child: ErrorIndicator(
|
||||
title: AppLocalization.of(context).errorWhileLoadingContinents,
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: viewModel.load.execute,
|
||||
),
|
||||
);
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, child) {
|
||||
return ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: viewModel.continents.length,
|
||||
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final Continent(:imageUrl, :name) = viewModel.continents[index];
|
||||
return _CarouselItem(
|
||||
key: ValueKey(name),
|
||||
imageUrl: imageUrl,
|
||||
name: name,
|
||||
viewModel: viewModel,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const SizedBox(width: 8);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CarouselItem extends StatelessWidget {
|
||||
const _CarouselItem({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
required this.name,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final String name;
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
bool _selected() =>
|
||||
viewModel.selectedContinent == null ||
|
||||
viewModel.selectedContinent == name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 140,
|
||||
height: 140,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorListener: imageErrorListener,
|
||||
errorWidget: (context, url, error) {
|
||||
// NOTE: Getting "invalid image data" error for some of the images
|
||||
// e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey3,
|
||||
),
|
||||
child: SizedBox(width: 140, height: 140),
|
||||
);
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.openSans(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.white1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Overlay when other continent is selected
|
||||
Positioned.fill(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _selected() ? 0 : 0.7,
|
||||
duration: kThemeChangeDuration,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
// Support dark-mode
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Handle taps
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (viewModel.selectedContinent == name) {
|
||||
viewModel.selectedContinent = null;
|
||||
} else {
|
||||
viewModel.selectedContinent = name;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/date_format_start_end.dart';
|
||||
import '../../core/themes/colors.dart';
|
||||
import '../view_models/search_form_viewmodel.dart';
|
||||
|
||||
/// Date selection form field.
|
||||
///
|
||||
/// Opens a date range picker dialog when tapped.
|
||||
class SearchFormDate extends StatelessWidget {
|
||||
const SearchFormDate({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.paddingVertical,
|
||||
left: Dimens.of(context).paddingScreenHorizontal,
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: () {
|
||||
showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
).then((dateRange) => viewModel.dateRange = dateRange);
|
||||
},
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grey1),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.paddingHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalization.of(context).when,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) {
|
||||
final dateRange = viewModel.dateRange;
|
||||
if (dateRange != null) {
|
||||
return Text(
|
||||
dateFormatStartEnd(dateRange),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
AppLocalization.of(context).addDates,
|
||||
style: Theme.of(context).inputDecorationTheme.hintStyle,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/themes/colors.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../view_models/search_form_viewmodel.dart';
|
||||
|
||||
const String removeGuestsKey = 'remove-guests';
|
||||
const String addGuestsKey = 'add-guests';
|
||||
|
||||
/// Number of guests selection form
|
||||
///
|
||||
/// Users can tap the Plus and Minus icons to increase or decrease
|
||||
/// the number of guests.
|
||||
class SearchFormGuests extends StatelessWidget {
|
||||
const SearchFormGuests({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.paddingVertical,
|
||||
left: Dimens.of(context).paddingScreenHorizontal,
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
),
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grey1),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.paddingHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Who',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
_QuantitySelector(viewModel),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuantitySelector extends StatelessWidget {
|
||||
const _QuantitySelector(this.viewModel);
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 90,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
key: const ValueKey(removeGuestsKey),
|
||||
onTap: () {
|
||||
viewModel.guests--;
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.remove_circle_outline,
|
||||
color: AppColors.grey3,
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) => Text(
|
||||
viewModel.guests.toString(),
|
||||
style: viewModel.guests == 0
|
||||
? Theme.of(context).inputDecorationTheme.hintStyle
|
||||
: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
key: const ValueKey(addGuestsKey),
|
||||
onTap: () {
|
||||
viewModel.guests++;
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: AppColors.grey3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../routing/routes.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/search_bar.dart';
|
||||
import '../../results/widgets/results_screen.dart';
|
||||
import '../view_models/search_form_viewmodel.dart';
|
||||
import 'search_form_date.dart';
|
||||
import 'search_form_guests.dart';
|
||||
import 'search_form_continent.dart';
|
||||
import 'search_form_submit.dart';
|
||||
|
||||
/// Search form screen
|
||||
///
|
||||
/// Displays a search form with continent, date and guests selection.
|
||||
/// Tapping on the submit button opens the [ResultsScreen] screen
|
||||
/// passing the search options as query parameters.
|
||||
class SearchFormScreen extends StatelessWidget {
|
||||
const SearchFormScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, r) {
|
||||
if (!didPop) context.go(Routes.home);
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.of(context).paddingScreenVertical,
|
||||
left: Dimens.of(context).paddingScreenHorizontal,
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
bottom: Dimens.paddingVertical,
|
||||
),
|
||||
child: const AppSearchBar(),
|
||||
),
|
||||
),
|
||||
SearchFormContinent(viewModel: viewModel),
|
||||
SearchFormDate(viewModel: viewModel),
|
||||
SearchFormGuests(viewModel: viewModel),
|
||||
const Spacer(),
|
||||
SearchFormSubmit(viewModel: viewModel),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../routing/routes.dart';
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../results/widgets/results_screen.dart';
|
||||
import '../view_models/search_form_viewmodel.dart';
|
||||
|
||||
const String searchFormSubmitButtonKey = 'submit-button';
|
||||
|
||||
/// Search form submit button
|
||||
///
|
||||
/// The button is disabled when the form is data is incomplete.
|
||||
/// When tapped, it navigates to the [ResultsScreen]
|
||||
/// passing the search options as query parameters.
|
||||
class SearchFormSubmit extends StatefulWidget {
|
||||
const SearchFormSubmit({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<SearchFormSubmit> createState() => _SearchFormSubmitState();
|
||||
}
|
||||
|
||||
class _SearchFormSubmitState extends State<SearchFormSubmit> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.updateItineraryConfig.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SearchFormSubmit oldWidget) {
|
||||
oldWidget.viewModel.updateItineraryConfig.removeListener(_onResult);
|
||||
widget.viewModel.updateItineraryConfig.addListener(_onResult);
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.updateItineraryConfig.removeListener(_onResult);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.paddingVertical,
|
||||
left: Dimens.of(context).paddingScreenHorizontal,
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
bottom: Dimens.of(context).paddingScreenVertical,
|
||||
),
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.viewModel,
|
||||
child: SizedBox(
|
||||
height: 52,
|
||||
child: Center(
|
||||
child: Text(AppLocalization.of(context).search),
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
return FilledButton(
|
||||
key: const ValueKey(searchFormSubmitButtonKey),
|
||||
onPressed: widget.viewModel.valid
|
||||
? widget.viewModel.updateItineraryConfig.execute
|
||||
: null,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onResult() {
|
||||
if (widget.viewModel.updateItineraryConfig.completed) {
|
||||
widget.viewModel.updateItineraryConfig.clearResult();
|
||||
context.go(Routes.results);
|
||||
}
|
||||
|
||||
if (widget.viewModel.updateItineraryConfig.error) {
|
||||
widget.viewModel.updateItineraryConfig.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileSavingItinerary),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.updateItineraryConfig.execute,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user