Skip to content

Commit 0c88289

Browse files
[Compass App] Booking screen (#2380)
This PR adds the Booking screen at the end of the main app flow. After the user selects `Activity`s, these get stored in the `ItineraryConfig` and then the user navigates to the `BookingScreen`. In the `BookingScreen`, a `Booking` is generated with the `BookingCreateComponent`. This component communicates with multiple repositories, and it is a bit more complex than the average view model, something that we want to show as discussed in the previous PRs. <details> <summary>Screenshots</summary> ![image](https://github.com/user-attachments/assets/6a9d8d5b-0d2c-4724-8aca-d750186651b7) ![image](https://github.com/user-attachments/assets/0ef4d00e-e67b-4ec6-9ea3-28511ed4c2b8) </details> In the `BookingScreen`, the user can tap on "share trip" which displays the OS share sheet functionality. This uses the plugin `share_plus`, but the functionality is also wrapped in the `BookingShareComponent`, which takes a `Booking` object and creates a shareable string, then calls to the `Share.share()` method from `share_plus`. But the `share_plus` dependency is also injected into the `BookingShareComponent`, allowing us to unit test without plugin dependencies. This is an example of a shared booking to instant messaging: ![image](https://github.com/user-attachments/assets/5a559080-4f9a-45e6-a736-ab849a7adc39) **TODO** - I want to take a look at the whole experience on mobile, as I noticed some inconsistent UI and navigation issues that I couldn't see on Desktop. I will submit those in a new PR. - We also talked about user authentication in the design document. I will work on that once we are happy with the main app flow. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
1 parent 0305894 commit 0c88289

File tree

64 files changed

+1613
-343
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1613
-343
lines changed

compass_app/app/android/settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pluginManagement {
1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
2121
id "com.android.application" version "7.3.0" apply false
22-
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
22+
id "org.jetbrains.kotlin.android" version "1.8.0" apply false
2323
}
2424

2525
include ":app"

compass_app/app/lib/config/dependencies.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ import '../data/repositories/destination/destination_repository_remote.dart';
1313
import '../data/repositories/itinerary_config/itinerary_config_repository.dart';
1414
import '../data/repositories/itinerary_config/itinerary_config_repository_memory.dart';
1515
import '../data/services/api_client.dart';
16+
import '../ui/booking/components/booking_create_component.dart';
17+
import '../ui/booking/components/booking_share_component.dart';
18+
19+
/// Shared providers for all configurations.
20+
List<SingleChildWidget> _sharedProviders = [
21+
Provider(
22+
lazy: true,
23+
create: (context) => BookingCreateComponent(
24+
destinationRepository: context.read(),
25+
activityRepository: context.read(),
26+
),
27+
),
28+
Provider(
29+
lazy: true,
30+
create: (context) => BookingShareComponent.withSharePlus(),
31+
),
32+
];
1633

1734
/// Configure dependencies for remote data.
1835
/// This dependency list uses repositories that connect to a remote server.
@@ -38,6 +55,7 @@ List<SingleChildWidget> get providersRemote {
3855
Provider.value(
3956
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
4057
),
58+
..._sharedProviders,
4159
];
4260
}
4361

@@ -57,5 +75,6 @@ List<SingleChildWidget> get providersLocal {
5775
Provider.value(
5876
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
5977
),
78+
..._sharedProviders,
6079
];
6180
}

compass_app/app/lib/routing/router.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
33

44
import '../ui/activities/view_models/activities_viewmodel.dart';
55
import '../ui/activities/widgets/activities_screen.dart';
6+
import '../ui/booking/widgets/booking_screen.dart';
7+
import '../ui/booking/view_models/booking_viewmodel.dart';
68
import '../ui/results/view_models/results_viewmodel.dart';
79
import '../ui/results/widgets/results_screen.dart';
810
import '../ui/search_form/view_models/search_form_viewmodel.dart';
@@ -47,6 +49,19 @@ final router = GoRouter(
4749
);
4850
},
4951
),
52+
GoRoute(
53+
path: 'booking',
54+
builder: (context, state) {
55+
final viewModel = BookingViewModel(
56+
itineraryConfigRepository: context.read(),
57+
bookingComponent: context.read(),
58+
shareComponent: context.read(),
59+
);
60+
return BookingScreen(
61+
viewModel: viewModel,
62+
);
63+
},
64+
),
5065
],
5166
),
5267
],

compass_app/app/lib/ui/activities/view_models/activities_viewmodel.dart

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,7 @@ class ActivitiesViewModel extends ChangeNotifier {
1414
}) : _activityRepository = activityRepository,
1515
_itineraryConfigRepository = itineraryConfigRepository {
1616
loadActivities = Command0(_loadActivities)..execute();
17-
saveActivities = Command0(() async {
18-
_log.shout(
19-
'Save activities not implemented',
20-
null,
21-
StackTrace.current,
22-
);
23-
return Result.error(Exception('Not implemented'));
24-
});
17+
saveActivities = Command0(_saveActivities);
2518
}
2619

2720
final _log = Logger('ActivitiesViewModel');
@@ -62,6 +55,8 @@ class ActivitiesViewModel extends ChangeNotifier {
6255
return Result.error(Exception('Destination not found'));
6356
}
6457

58+
_selectedActivities.addAll(result.asOk.value.activities);
59+
6560
final resultActivities =
6661
await _activityRepository.getByDestination(destinationRef);
6762
switch (resultActivities) {
@@ -118,4 +113,26 @@ class ActivitiesViewModel extends ChangeNotifier {
118113
_log.finest('Activity $activityRef removed');
119114
notifyListeners();
120115
}
116+
117+
Future<Result<void>> _saveActivities() async {
118+
final resultConfig = await _itineraryConfigRepository.getItineraryConfig();
119+
if (resultConfig is Error) {
120+
_log.warning(
121+
'Failed to load stored ItineraryConfig',
122+
resultConfig.asError.error,
123+
);
124+
return resultConfig;
125+
}
126+
127+
final itineraryConfig = resultConfig.asOk.value;
128+
final result = await _itineraryConfigRepository.setItineraryConfig(
129+
itineraryConfig.copyWith(activities: _selectedActivities.toList()));
130+
if (result is Error) {
131+
_log.warning(
132+
'Failed to store ItineraryConfig',
133+
result.asError.error,
134+
);
135+
}
136+
return result;
137+
}
121138
}

compass_app/app/lib/ui/activities/widgets/activities_header.dart

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,32 @@ class ActivitiesHeader extends StatelessWidget {
1111

1212
@override
1313
Widget build(BuildContext context) {
14-
return Padding(
15-
padding: EdgeInsets.only(
16-
left: Dimens.of(context).paddingScreenHorizontal,
17-
right: Dimens.of(context).paddingScreenHorizontal,
18-
top: Dimens.of(context).paddingScreenVertical,
19-
bottom: Dimens.paddingVertical,
20-
),
21-
child: Row(
22-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
23-
children: [
24-
CustomBackButton(
25-
onTap: () {
26-
// Navigate to ResultsScreen and edit search
27-
context.go('/results');
28-
},
29-
),
30-
Text(
31-
AppLocalization.of(context).activities,
32-
style: Theme.of(context).textTheme.titleLarge,
33-
),
34-
const HomeButton(),
35-
],
14+
return SafeArea(
15+
top: true,
16+
bottom: false,
17+
child: Padding(
18+
padding: EdgeInsets.only(
19+
left: Dimens.of(context).paddingScreenHorizontal,
20+
right: Dimens.of(context).paddingScreenHorizontal,
21+
top: Dimens.of(context).paddingScreenVertical,
22+
bottom: Dimens.paddingVertical,
23+
),
24+
child: Row(
25+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
26+
children: [
27+
CustomBackButton(
28+
onTap: () {
29+
// Navigate to ResultsScreen and edit search
30+
context.go('/results');
31+
},
32+
),
33+
Text(
34+
AppLocalization.of(context).activities,
35+
style: Theme.of(context).textTheme.titleLarge,
36+
),
37+
const HomeButton(),
38+
],
39+
),
3640
),
3741
);
3842
}

compass_app/app/lib/ui/activities/widgets/activities_screen.dart

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:go_router/go_router.dart';
23

34
import '../../core/localization/applocalization.dart';
45
import '../../core/themes/dimens.dart';
@@ -43,67 +44,71 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
4344

4445
@override
4546
Widget build(BuildContext context) {
46-
return Scaffold(
47-
body: ListenableBuilder(
48-
listenable: widget.viewModel.loadActivities,
49-
builder: (context, child) {
50-
if (widget.viewModel.loadActivities.completed) {
51-
return child!;
52-
}
53-
return Column(
54-
children: [
55-
const ActivitiesHeader(),
56-
if (widget.viewModel.loadActivities.running)
57-
const Expanded(
58-
child: Center(child: CircularProgressIndicator())),
59-
if (widget.viewModel.loadActivities.error)
60-
Expanded(
61-
child: Center(
62-
child: ErrorIndicator(
63-
title: AppLocalization.of(context)
64-
.errorWhileLoadingActivities,
65-
label: AppLocalization.of(context).tryAgain,
66-
onPressed: widget.viewModel.loadActivities.execute,
67-
),
68-
),
69-
),
70-
],
71-
);
72-
},
73-
child: ListenableBuilder(
74-
listenable: widget.viewModel,
47+
return PopScope(
48+
canPop: false,
49+
onPopInvokedWithResult: (d, r) => context.go('/results'),
50+
child: Scaffold(
51+
body: ListenableBuilder(
52+
listenable: widget.viewModel.loadActivities,
7553
builder: (context, child) {
54+
if (widget.viewModel.loadActivities.completed) {
55+
return child!;
56+
}
7657
return Column(
7758
children: [
78-
Expanded(
79-
child: CustomScrollView(
80-
slivers: [
81-
const SliverToBoxAdapter(
82-
child: ActivitiesHeader(),
83-
),
84-
ActivitiesTitle(
85-
viewModel: widget.viewModel,
86-
activityTimeOfDay: ActivityTimeOfDay.daytime,
87-
),
88-
ActivitiesList(
89-
viewModel: widget.viewModel,
90-
activityTimeOfDay: ActivityTimeOfDay.daytime,
59+
const ActivitiesHeader(),
60+
if (widget.viewModel.loadActivities.running)
61+
const Expanded(
62+
child: Center(child: CircularProgressIndicator())),
63+
if (widget.viewModel.loadActivities.error)
64+
Expanded(
65+
child: Center(
66+
child: ErrorIndicator(
67+
title: AppLocalization.of(context)
68+
.errorWhileLoadingActivities,
69+
label: AppLocalization.of(context).tryAgain,
70+
onPressed: widget.viewModel.loadActivities.execute,
9171
),
92-
ActivitiesTitle(
93-
viewModel: widget.viewModel,
94-
activityTimeOfDay: ActivityTimeOfDay.evening,
95-
),
96-
ActivitiesList(
97-
viewModel: widget.viewModel,
98-
activityTimeOfDay: ActivityTimeOfDay.evening,
99-
),
100-
],
72+
),
10173
),
102-
),
103-
_BottomArea(viewModel: widget.viewModel),
10474
],
10575
);
10676
},
77+
child: ListenableBuilder(
78+
listenable: widget.viewModel,
79+
builder: (context, child) {
80+
return Column(
81+
children: [
82+
Expanded(
83+
child: CustomScrollView(
84+
slivers: [
85+
const SliverToBoxAdapter(
86+
child: ActivitiesHeader(),
87+
),
88+
ActivitiesTitle(
89+
viewModel: widget.viewModel,
90+
activityTimeOfDay: ActivityTimeOfDay.daytime,
91+
),
92+
ActivitiesList(
93+
viewModel: widget.viewModel,
94+
activityTimeOfDay: ActivityTimeOfDay.daytime,
95+
),
96+
ActivitiesTitle(
97+
viewModel: widget.viewModel,
98+
activityTimeOfDay: ActivityTimeOfDay.evening,
99+
),
100+
ActivitiesList(
101+
viewModel: widget.viewModel,
102+
activityTimeOfDay: ActivityTimeOfDay.evening,
103+
),
104+
],
105+
),
106+
),
107+
_BottomArea(viewModel: widget.viewModel),
108+
],
109+
);
110+
},
111+
),
107112
),
108113
),
109114
);
@@ -112,7 +117,7 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
112117
void _onResult() {
113118
if (widget.viewModel.saveActivities.completed) {
114119
widget.viewModel.saveActivities.clearResult();
115-
// TODO
120+
context.go('/booking');
116121
}
117122

118123
if (widget.viewModel.saveActivities.error) {
@@ -159,6 +164,7 @@ class _BottomArea extends StatelessWidget {
159164
style: Theme.of(context).textTheme.labelLarge,
160165
),
161166
FilledButton(
167+
key: const Key('confirm-button'),
162168
onPressed: viewModel.selectedActivities.isNotEmpty
163169
? viewModel.saveActivities.execute
164170
: null,

compass_app/app/lib/ui/activities/widgets/activity_entry.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ class ActivityEntry extends StatelessWidget {
4242
),
4343
Text(
4444
activity.name,
45-
maxLines: 1,
45+
maxLines: 2,
4646
overflow: TextOverflow.ellipsis,
4747
style: Theme.of(context).textTheme.bodyMedium,
4848
),
4949
],
5050
),
5151
),
52+
const SizedBox(width: 20),
5253
CustomCheckbox(
5354
key: ValueKey('${activity.ref}-checkbox'),
5455
value: selected,

0 commit comments

Comments
 (0)