mirror of https://github.com/flutter/samples.git
Compass App: Activities screen, error handling and logs (#2371)
This PR introduces the Activities screen, handling of errors in view models and commands, and logs using the dart `logging` package. **Activities** - The screen loads a list of activities, split in daytime and evening activities, and the user can select them. - Server adds the endpoint `/destination/<id>/activitity` which was missing before. Screencast provided: [Screencast from 2024-07-29 16-29-02.webm](https://github.com/user-attachments/assets/a56024d8-0a9c-49e7-8fd0-c895da15badc) **Error handling** _UI Error handling:_ In the screencast you can see a `SnackBar` appearing, since the "Confirm" button is not yet implemented. The `saveActivities` Command returns an error `Result.error()`, then the error state is exposed by the Command and consumed by the listener in the `ActivityScreen`, which displays a `SnackBar` and consumes the state. Functionality is similar to the one found in [UI events - Consuming events can trigger state updates](https://developer.android.com/topic/architecture/ui-layer/events#consuming-trigger-updates) from the Android architecture guide, as the command state is "consumed" and cleared. The Snackbar also includes an action to "try again". Tapping on it calls to the failed Command `execute()` so users can run the action again. For example, here the `saveActivities` command failed, so `error` is `true`. Then we call to `clearResult()` to remove the failed status, and show a `SnackBar`, with the `SnackBarAction` that runs `saveActivities` again when tapped. ```dart if (widget.viewModel.saveActivities.error) { widget.viewModel.saveActivities.clearResult(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Error while saving activities'), action: SnackBarAction( label: "Try again", onPressed: widget.viewModel.saveActivities.execute, ), ), ); } ``` Since commands expose `running`, `error` and `completed`, it is easy to implement loading and error indicator widgets: [Screencast from 2024-07-29 16-55-42.webm](https://github.com/user-attachments/assets/fb5772d0-7b9d-4ded-8fa2-9ce347f4d555) As side node, we can easily simulate that state by adding these lines in any of the repository implementations: ```dart await Future.delayed(Durations.extralong1); return Result.error(Exception('ERROR!')); ``` _In-code error handling:_ The project introduces the `logging` package. In the entry point `main_development.dart` the log level is configured. Then in code, a `Logger` is creaded in each View Model with the name of the class. Then the log calls are used depending on the `Result` response, some finer traces are also added. By default, they are printed to the IDE debug console, for example: ``` [SearchFormViewModel] Continents (7) loaded [SearchFormViewModel] ItineraryConfig loaded [SearchFormViewModel] Selected continent: Asia [SearchFormViewModel] Selected date range: 2024-07-30 00:00:00.000 - 2024-08-08 00:00:00.000 [SearchFormViewModel] Set guests number: 1 [SearchFormViewModel] ItineraryConfig saved ``` **Other changes** - The json files containing destinations and activities are moved into the `app/assets/` folders, and the server is querying those files instead of their own copy. This is done to avoid file duplication but we can make a copy of those assets files for the server if we decide to. **TODO Next** - I will implement the "book a trip" screen which would complete the main application flow, which should introduce a more complex "component/use case" outside a view model. ## 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.mdpull/2389/head
parent
175195eae6
commit
0305894b0e
@ -0,0 +1,4 @@
|
|||||||
|
class Assets {
|
||||||
|
static const activities = 'assets/activities.json';
|
||||||
|
static const destinations = 'assets/destinations.json';
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../core/localization/applocalization.dart';
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../../core/ui/back_button.dart';
|
||||||
|
import '../../core/ui/home_button.dart';
|
||||||
|
|
||||||
|
class ActivitiesHeader extends StatelessWidget {
|
||||||
|
const ActivitiesHeader({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
right: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
top: Dimens.of(context).paddingScreenVertical,
|
||||||
|
bottom: Dimens.paddingVertical,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
CustomBackButton(
|
||||||
|
onTap: () {
|
||||||
|
// Navigate to ResultsScreen and edit search
|
||||||
|
context.go('/results');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
AppLocalization.of(context).activities,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const HomeButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../view_models/activities_viewmodel.dart';
|
||||||
|
import 'activity_entry.dart';
|
||||||
|
import 'activity_time_of_day.dart';
|
||||||
|
|
||||||
|
class ActivitiesList extends StatelessWidget {
|
||||||
|
const ActivitiesList({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
required this.activityTimeOfDay,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ActivitiesViewModel viewModel;
|
||||||
|
final ActivityTimeOfDay activityTimeOfDay;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final list = switch (activityTimeOfDay) {
|
||||||
|
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
|
||||||
|
ActivityTimeOfDay.evening => viewModel.eveningActivities,
|
||||||
|
};
|
||||||
|
return SliverPadding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: Dimens.paddingVertical,
|
||||||
|
left: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
right: Dimens.of(context).paddingScreenHorizontal,
|
||||||
|
bottom: Dimens.paddingVertical,
|
||||||
|
),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final activity = list[index];
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
|
||||||
|
child: ActivityEntry(
|
||||||
|
key: ValueKey(activity.ref),
|
||||||
|
activity: activity,
|
||||||
|
selected: viewModel.selectedActivities.contains(activity.ref),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value!) {
|
||||||
|
viewModel.addActivity(activity.ref);
|
||||||
|
} else {
|
||||||
|
viewModel.removeActivity(activity.ref);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: list.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/localization/applocalization.dart';
|
||||||
|
import '../../core/themes/dimens.dart';
|
||||||
|
import '../view_models/activities_viewmodel.dart';
|
||||||
|
import 'activity_time_of_day.dart';
|
||||||
|
|
||||||
|
class ActivitiesTitle extends StatelessWidget {
|
||||||
|
const ActivitiesTitle({
|
||||||
|
super.key,
|
||||||
|
required this.activityTimeOfDay,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ActivitiesViewModel viewModel;
|
||||||
|
final ActivityTimeOfDay activityTimeOfDay;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final list = switch (activityTimeOfDay) {
|
||||||
|
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
|
||||||
|
ActivityTimeOfDay.evening => viewModel.eveningActivities,
|
||||||
|
};
|
||||||
|
if (list.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox());
|
||||||
|
}
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||||
|
child: Text(_label(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _label(BuildContext context) => switch (activityTimeOfDay) {
|
||||||
|
ActivityTimeOfDay.daytime => AppLocalization.of(context).daytime,
|
||||||
|
ActivityTimeOfDay.evening => AppLocalization.of(context).evening,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/ui/custom_checkbox.dart';
|
||||||
|
|
||||||
|
class ActivityEntry extends StatelessWidget {
|
||||||
|
const ActivityEntry({
|
||||||
|
super.key,
|
||||||
|
required this.activity,
|
||||||
|
required this.selected,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Activity activity;
|
||||||
|
final bool selected;
|
||||||
|
final ValueChanged<bool?> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: activity.imageUrl,
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
activity.timeOfDay.name.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
activity.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CustomCheckbox(
|
||||||
|
key: ValueKey('${activity.ref}-checkbox'),
|
||||||
|
value: selected,
|
||||||
|
onChanged: onChanged,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
enum ActivityTimeOfDay { daytime, evening }
|
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
sealed class Dimens {
|
||||||
|
const Dimens();
|
||||||
|
|
||||||
|
/// General horizontal padding used to separate UI items
|
||||||
|
static const paddingHorizontal = 20.0;
|
||||||
|
|
||||||
|
/// General vertical padding used to separate UI items
|
||||||
|
static const paddingVertical = 24.0;
|
||||||
|
|
||||||
|
/// Horizontal padding for screen edges
|
||||||
|
abstract final double paddingScreenHorizontal;
|
||||||
|
|
||||||
|
/// Vertical padding for screen edges
|
||||||
|
abstract final double paddingScreenVertical;
|
||||||
|
|
||||||
|
/// Horizontal symmetric padding for screen edges
|
||||||
|
EdgeInsets get edgeInsetsScreenHorizontal =>
|
||||||
|
EdgeInsets.symmetric(horizontal: paddingScreenHorizontal);
|
||||||
|
|
||||||
|
/// Symmetric padding for screen edges
|
||||||
|
EdgeInsets get edgeInsetsScreenSymmetric => EdgeInsets.symmetric(
|
||||||
|
horizontal: paddingScreenHorizontal, vertical: paddingScreenVertical);
|
||||||
|
|
||||||
|
static final dimensDesktop = DimensDesktop();
|
||||||
|
static final dimensMobile = DimensMobile();
|
||||||
|
|
||||||
|
/// Get dimensions definition based on screen size
|
||||||
|
factory Dimens.of(BuildContext context) =>
|
||||||
|
switch (MediaQuery.sizeOf(context).width) {
|
||||||
|
> 600 => dimensDesktop,
|
||||||
|
_ => dimensMobile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mobile dimensions
|
||||||
|
class DimensMobile extends Dimens {
|
||||||
|
@override
|
||||||
|
double paddingScreenHorizontal = Dimens.paddingHorizontal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double paddingScreenVertical = Dimens.paddingVertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Desktop/Web dimensions
|
||||||
|
class DimensDesktop extends Dimens {
|
||||||
|
@override
|
||||||
|
double paddingScreenHorizontal = 100.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double paddingScreenVertical = 64.0;
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../themes/colors.dart';
|
||||||
|
|
||||||
|
class CustomCheckbox extends StatelessWidget {
|
||||||
|
const CustomCheckbox({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool value;
|
||||||
|
final ValueChanged<bool?> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkResponse(
|
||||||
|
radius: 24,
|
||||||
|
onTap: () => onChanged(!value),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(color: AppColors.grey3),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: value
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: Visibility(
|
||||||
|
visible: value,
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../themes/colors.dart';
|
||||||
|
|
||||||
|
class ErrorIndicator extends StatelessWidget {
|
||||||
|
const ErrorIndicator({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IntrinsicWidth(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: const ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(AppColors.red1),
|
||||||
|
foregroundColor: WidgetStatePropertyAll(Colors.white),
|
||||||
|
),
|
||||||
|
child: Text(label),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:compass_app/ui/activities/view_models/activities_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/activities/widgets/activities_screen.dart';
|
||||||
|
import 'package:compass_app/ui/activities/widgets/activity_entry.dart';
|
||||||
|
import 'package:compass_app/ui/core/themes/theme.dart';
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||||
|
|
||||||
|
import '../../util/fakes/repositories/fake_activities_repository.dart';
|
||||||
|
import '../../util/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ResultsScreen widget tests', () {
|
||||||
|
late ActivitiesViewModel viewModel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = ActivitiesViewModel(
|
||||||
|
activityRepository: FakeActivityRepository(),
|
||||||
|
itineraryConfigRepository: FakeItineraryConfigRepository(
|
||||||
|
itineraryConfig: ItineraryConfig(
|
||||||
|
continent: 'Europe',
|
||||||
|
startDate: DateTime(2024, 01, 01),
|
||||||
|
endDate: DateTime(2024, 01, 31),
|
||||||
|
guests: 2,
|
||||||
|
destination: 'DESTINATION',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build and render the ResultsScreen widget
|
||||||
|
Future<void> loadScreen(WidgetTester tester) async {
|
||||||
|
// Load some data
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
home: ActivitiesScreen(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should load screen', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
expect(find.byType(ActivitiesScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should list activity', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
expect(find.byType(ActivityEntry), findsOneWidget);
|
||||||
|
expect(find.text('NAME'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should select activity', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await loadScreen(tester);
|
||||||
|
await tester.tap(find.byKey(const ValueKey('REF-checkbox')));
|
||||||
|
expect(viewModel.selectedActivities, contains('REF'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:compass_app/data/repositories/activity/activity_repository.dart';
|
||||||
|
import 'package:compass_app/utils/result.dart';
|
||||||
|
import 'package:compass_model/src/model/activity/activity.dart';
|
||||||
|
|
||||||
|
class FakeActivityRepository implements ActivityRepository {
|
||||||
|
Map<String, List<Activity>> activities = {
|
||||||
|
"DESTINATION": [
|
||||||
|
const Activity(
|
||||||
|
description: 'DESCRIPTION',
|
||||||
|
destinationRef: 'DESTINATION',
|
||||||
|
duration: 3,
|
||||||
|
familyFriendly: true,
|
||||||
|
imageUrl: 'http://example.com/img.png',
|
||||||
|
locationName: 'LOCATION NAME',
|
||||||
|
name: 'NAME',
|
||||||
|
price: 3,
|
||||||
|
ref: 'REF',
|
||||||
|
timeOfDay: TimeOfDay.afternoon,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<List<Activity>>> getByDestination(String ref) async {
|
||||||
|
return Result.ok(activities[ref]!);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,4 @@
|
|||||||
|
class Assets {
|
||||||
|
static const activities = '../app/assets/activities.json';
|
||||||
|
static const destinations = '../app/assets/destinations.json';
|
||||||
|
}
|
@ -1,9 +1,42 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:compass_model/model.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
|
||||||
Future<Response> destinationHandler(Request req) async {
|
import '../config/assets.dart';
|
||||||
final file = File('assets/destinations.json');
|
|
||||||
final jsonString = await file.readAsString();
|
class DestinationApi {
|
||||||
return Response.ok(jsonString);
|
final List<Destination> destinations =
|
||||||
|
(json.decode(File(Assets.destinations).readAsStringSync()) as List)
|
||||||
|
.map((element) => Destination.fromJson(element))
|
||||||
|
.toList();
|
||||||
|
final List<Activity> activities =
|
||||||
|
(json.decode(File(Assets.activities).readAsStringSync()) as List)
|
||||||
|
.map((element) => Activity.fromJson(element))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Router get router {
|
||||||
|
final router = Router();
|
||||||
|
|
||||||
|
router.get('/', (Request request) {
|
||||||
|
return Response.ok(
|
||||||
|
json.encode(destinations),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/<id>/activity', (Request request, String id) {
|
||||||
|
final list = activities
|
||||||
|
.where((activity) => activity.destinationRef == id)
|
||||||
|
.toList();
|
||||||
|
return Response.ok(
|
||||||
|
json.encode(list),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue