Skip to content

Commit d0f2006

Browse files
committed
Compass App: Add "server" dart shelf-app and "shared" dart package (#2359)
This PR introduces two new subprojects: - `compass_server` under `compass_app/server`. - `compass_model` under `compass_app/model`. **`compass_server`** - Dart server implemented using `shelf`. - Created with the `dart create -t server-shelf` template. - Implements two REST endpoints: - `GET /continent`: Returns the list of `Continent` as JSON. - `GET /destination`: Returns the list of `Destination` as JSON. - Generated Docker files have been removed. - Implemented tests. - TODO: Implement some basic auth. **`compass_model`** - Dart package to share data model classes between the `server` and `app`. - Contains the data model classes (`Continent`, `Destination`). - Generated JSON from/To methods and data classes using `freezed`. - The sole purpose of this package is to host the data model. Other shared code should go in a different package. **Other changes** - Created an API Client to connect to the local dart server. - Created "remote" repositories, which also implement a basic in-memory caching strategy. - Created two dependency configurations, one with local repositories and one with remote repos. - Created two application main targets to select configuration: - `lib/main_development.dart` which starts the app with the "local" data configuration. - `lib/main_staging.dart` which starts the app with the "remove" (local dart server) configuration. - `lib/main.dart` still works as default entry point. - Implemented tests for remote repositories. - [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 WIP implement activity repository local wip activity repository updates and iOS stuff implement navigation logic to activities implement tests fixing navigation with query parameters refactor search queries cleanup and comments cleanup add ItineraryConfig, refactor SearchScreen to use it fix tests refactor with Commands refactor commands add progress indicator to commands simplify command command tests cleanup simplify activity repository cleanup cleanup cleanup fix comment should be Command0 refactor assets, server, and add activities endpoint fix assets refactor wip activities screen add error state to commands Added comments handle errors in Command add comments WIP error indicator widget lifecycle wip activities and error handling Add random errors to repositories to simulate recovering from errors implement activites WIP tests create test activities screen finish Activities UI
1 parent 175195e commit d0f2006

33 files changed

+917
-192
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class Assets {
2+
static const activities = 'assets/activities.json';
3+
static const destinations = 'assets/destinations.json';
4+
}

compass_app/app/lib/data/repositories/activity/activity_repository_local.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import 'dart:convert';
2+
import 'dart:math';
23

34
import 'package:compass_model/model.dart';
5+
import 'package:flutter/material.dart';
46
import 'package:flutter/services.dart';
57

8+
import '../../../config/assets.dart';
69
import '../../../utils/result.dart';
710
import 'activity_repository.dart';
811

@@ -11,6 +14,12 @@ import 'activity_repository.dart';
1114
class ActivityRepositoryLocal implements ActivityRepository {
1215
@override
1316
Future<Result<List<Activity>>> getByDestination(String ref) async {
17+
// Simulate network loading and random errors
18+
// await Future.delayed(Durations.long1);
19+
// if (Random().nextBool()) {
20+
// return Result.error(Exception('Random error!'));
21+
// }
22+
1423
try {
1524
final localData = await _loadAsset();
1625
final list = _parse(localData);
@@ -25,7 +34,7 @@ class ActivityRepositoryLocal implements ActivityRepository {
2534
}
2635

2736
Future<String> _loadAsset() async {
28-
return await rootBundle.loadString('assets/activities.json');
37+
return await rootBundle.loadString(Assets.activities);
2938
}
3039

3140
List<Activity> _parse(String localData) {

compass_app/app/lib/data/repositories/continent/continent_repository_local.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1+
import 'dart:math';
2+
13
import 'package:compass_model/model.dart';
4+
import 'package:flutter/material.dart';
25

36
import '../../../utils/result.dart';
47
import 'continent_repository.dart';
58

69
/// Local data source with all possible continents.
710
class ContinentRepositoryLocal implements ContinentRepository {
811
@override
9-
Future<Result<List<Continent>>> getContinents() {
12+
Future<Result<List<Continent>>> getContinents() async {
13+
// Simulate network loading and random errors
14+
// await Future.delayed(Durations.long1);
15+
// if (Random().nextBool()) {
16+
// return Result.error(Exception('Random error!'));
17+
// }
18+
1019
return Future.value(
1120
Result.ok(
1221
[

compass_app/app/lib/data/repositories/destination/destination_repository_local.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import 'dart:convert';
2+
import 'dart:math';
23

34
import 'package:compass_model/model.dart';
5+
import 'package:flutter/material.dart';
46
import 'package:flutter/services.dart' show rootBundle;
57

8+
import '../../../config/assets.dart';
69
import '../../../utils/result.dart';
710
import 'destination_repository.dart';
811

@@ -12,6 +15,12 @@ class DestinationRepositoryLocal implements DestinationRepository {
1215
/// Obtain list of destinations from local assets
1316
@override
1417
Future<Result<List<Destination>>> getDestinations() async {
18+
// Simulate network loading and random errors
19+
// await Future.delayed(Durations.long1);
20+
// if (Random().nextBool()) {
21+
// return Result.error(Exception('Random error!'));
22+
// }
23+
1524
try {
1625
final localData = await _loadAsset();
1726
final list = _parse(localData);
@@ -22,7 +31,7 @@ class DestinationRepositoryLocal implements DestinationRepository {
2231
}
2332

2433
Future<String> _loadAsset() async {
25-
return await rootBundle.loadString('assets/destinations.json');
34+
return await rootBundle.loadString(Assets.destinations);
2635
}
2736

2837
List<Destination> _parse(String localData) {

compass_app/app/lib/data/repositories/itinerary_config/itinerary_config_repository_memory.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:compass_model/model.dart';
4+
import 'package:flutter/material.dart';
45

56
import '../../../utils/result.dart';
67
import 'itinerary_config_repository.dart';

compass_app/app/lib/main.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'package:flutter/services.dart';
2+
3+
import 'config/assets.dart';
14
import 'ui/core/themes/theme.dart';
25
import 'routing/router.dart';
36
import 'package:flutter/material.dart';
@@ -9,6 +12,13 @@ import 'main_development.dart' as development;
912
void main() {
1013
// Launch development config by default
1114
development.main();
15+
rootBundle.loadString('AssetManifest.json').then((assets) {
16+
print(assets);
17+
} );
18+
19+
rootBundle.loadString(Assets.destinations);
20+
rootBundle.loadString(Assets.activities);
21+
1222
}
1323

1424
class MainApp extends StatelessWidget {

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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,57 +13,76 @@ class ActivitiesViewModel extends ChangeNotifier {
1313
}) : _activityRepository = activityRepository,
1414
_itineraryConfigRepository = itineraryConfigRepository {
1515
loadActivities = Command0(_loadActivities)..execute();
16+
saveActivities =
17+
Command0(() async => Result.error(Exception('Not implemented')));
1618
}
1719

1820
final ActivityRepository _activityRepository;
1921
final ItineraryConfigRepository _itineraryConfigRepository;
20-
List<Activity> _activities = <Activity>[];
22+
List<Activity> _daytimeActivities = <Activity>[];
23+
List<Activity> _eveningActivities = <Activity>[];
2124
final Set<String> _selectedActivities = <String>{};
2225

23-
/// List of [Activity] per destination.
24-
List<Activity> get activities => _activities;
26+
/// List of daytime [Activity] per destination.
27+
List<Activity> get daytimeActivities => _daytimeActivities;
28+
29+
/// List of evening [Activity] per destination.
30+
List<Activity> get eveningActivities => _eveningActivities;
2531

2632
/// Selected [Activity] by ref.
2733
Set<String> get selectedActivities => _selectedActivities;
2834

2935
/// Load list of [Activity] for a [Destination] by ref.
3036
late final Command0 loadActivities;
3137

32-
Future<void> _loadActivities() async {
38+
/// Save list [selectedActivities] into itinerary configuration.
39+
late final Command0 saveActivities;
40+
41+
Future<Result<void>> _loadActivities() async {
3342
final result = await _itineraryConfigRepository.getItineraryConfig();
3443
if (result is Error) {
3544
// TODO: Handle error
3645
print(result.asError.error);
37-
return;
46+
return result;
3847
}
3948

4049
final destinationRef = result.asOk.value.destination;
4150
if (destinationRef == null) {
42-
// TODO: Error here
43-
return;
51+
return Result.error(Exception('Destination not found'));
4452
}
4553

4654
final resultActivities =
4755
await _activityRepository.getByDestination(destinationRef);
4856
switch (resultActivities) {
4957
case Ok():
5058
{
51-
_activities = resultActivities.value;
52-
print(_activities);
59+
_daytimeActivities = resultActivities.value
60+
.where((activity) =>
61+
['morning', 'afternoon'].contains(activity.timeOfDay))
62+
.toList();
63+
64+
_eveningActivities = resultActivities.value
65+
.where((activity) =>
66+
['evening', 'night'].contains(activity.timeOfDay))
67+
.toList();
68+
// print(_activities);
5369
}
5470
case Error():
5571
{
5672
// TODO: Handle error
5773
print(resultActivities.error);
5874
}
5975
}
76+
6077
notifyListeners();
78+
return resultActivities;
6179
}
6280

6381
/// Add [Activity] to selected list.
6482
void addActivity(String activityRef) {
6583
assert(
66-
activities.any((activity) => activity.ref == activityRef),
84+
(_daytimeActivities + _eveningActivities)
85+
.any((activity) => activity.ref == activityRef),
6786
"Activity $activityRef not found",
6887
);
6988
_selectedActivities.add(activityRef);
@@ -73,7 +92,8 @@ class ActivitiesViewModel extends ChangeNotifier {
7392
/// Remove [Activity] from selected list.
7493
void removeActivity(String activityRef) {
7594
assert(
76-
activities.any((activity) => activity.ref == activityRef),
95+
(_daytimeActivities + _eveningActivities)
96+
.any((activity) => activity.ref == activityRef),
7797
"Activity $activityRef not found",
7898
);
7999
_selectedActivities.remove(activityRef);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:go_router/go_router.dart';
3+
4+
import '../../core/ui/back_button.dart';
5+
import '../../core/ui/home_button.dart';
6+
7+
class ActivitiesHeader extends StatelessWidget {
8+
const ActivitiesHeader({super.key});
9+
10+
@override
11+
Widget build(BuildContext context) {
12+
return Padding(
13+
padding: const EdgeInsets.only(
14+
top: 24,
15+
bottom: 24,
16+
left: 20,
17+
right: 20,
18+
),
19+
child: Row(
20+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
21+
children: [
22+
CustomBackButton(
23+
onTap: () {
24+
// Navigate to ResultsScreen and edit search
25+
context.go('/results');
26+
},
27+
),
28+
Text(
29+
'Activities',
30+
style: Theme
31+
.of(context)
32+
.textTheme
33+
.titleLarge,
34+
),
35+
const HomeButton(),
36+
],
37+
),
38+
);
39+
}
40+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../view_models/activities_viewmodel.dart';
4+
import 'activity_entry.dart';
5+
import 'activity_time_of_day.dart';
6+
7+
class ActivitiesList extends StatelessWidget {
8+
const ActivitiesList({
9+
super.key,
10+
required this.viewModel,
11+
required this.activityTimeOfDay,
12+
});
13+
14+
final ActivitiesViewModel viewModel;
15+
final ActivityTimeOfDay activityTimeOfDay;
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
final list = switch (activityTimeOfDay) {
20+
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
21+
ActivityTimeOfDay.evening => viewModel.eveningActivities,
22+
};
23+
return SliverPadding(
24+
padding: const EdgeInsets.all(20),
25+
sliver: SliverList(
26+
delegate: SliverChildBuilderDelegate(
27+
(context, index) {
28+
final activity = list[index];
29+
return Padding(
30+
padding:
31+
EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
32+
child: ActivityEntry(
33+
key: ValueKey(activity.ref),
34+
activity: activity,
35+
selected: viewModel.selectedActivities.contains(activity.ref),
36+
onChanged: (value) {
37+
if (value!) {
38+
viewModel.addActivity(activity.ref);
39+
} else {
40+
viewModel.removeActivity(activity.ref);
41+
}
42+
},
43+
),
44+
);
45+
},
46+
childCount: list.length,
47+
),
48+
),
49+
);
50+
}
51+
}

0 commit comments

Comments
 (0)