使用 AWS Amplify 构建 Flutter 移动应用程序 - 第 2 部分
使用嵌套数据和 Amplify 函数创建 iOS 版和 Android 版行程规划应用程序
模块 3:添加活动功能
概述
在本模块中,将更新 Amplify API 以检索并保存您的行程活动数据。GraphQL API 使用由 Amazon DynamoDB(一种 NoSQL 数据库)提供支持的 AWS AppSync(一种 GraphQL 托管服务)。
要完成的目标
在本模块中,您将:
- 向应用程序添加活动数据模型
- 实现活动功能的创建、读取、更新和删除 (CRUD) 操作和工作流
- 实现活动列表 UI
- 向应用程序中添加“活动详情”页面
完成所需最短时间
45 分钟
操作步骤
向应用程序中添加活动数据模型
步骤 1:打开 amplify/backend/api/amplifytripsplanner/schema.graphql 文件,并按如下所示更新该文件:
- 创建活动数据模型
- 引入活动类别 enum 值
- 更新 Trip 模型,与 Activity 建立 1:n 关系
type Trip @model @auth(rules: [{ allow: owner }]) {
id: ID!
tripName: String!
destination: String!
startDate: AWSDate!
endDate: AWSDate!
tripImageUrl: String
tripImageKey: String
Activities: [Activity] @hasMany(indexName: "byTrip", fields: ["id"])
}
type Activity @model @auth(rules: [{allow: owner}]) {
id: ID!
activityName: String!
tripID: ID! @index(name: "byTrip", sortKeyFields: ["activityName"])
trip: Trip! @belongsTo(fields: ["tripID"])
activityImageUrl: String
activityImageKey: String
activityDate: AWSDate!
activityTime: AWSTime
category: ActivityCategory!
}
enum ActivityCategory { Flight, Lodging, Meeting, Restaurant }
步骤 2:在应用程序的根文件夹中运行以下命令以生成行程模型文件。
amplify codegen models
Amplify CLI 将在 lib/models 文件夹中生成 dart 文件。
步骤 3:运行 amplify push 命令,在云中创建资源。
步骤 4:按下 Enter(回车键)。Amplify CLI 将部署资源并显示确认消息,如屏幕截图中所示。
为活动创建文件夹
步骤 1:在 lib/features 文件夹中创建一个新文件夹,并将其命名为 activity。
步骤 2:在 activity 文件夹中创建以下新文件夹:
- service:与 Amplify 后端连接的层。
- data:提取网络代码(特别是 services)的存储库层。
- controller:这是连接 UI 和存储库的域层。
- ui:在此处创建应用程序将呈现给用户的小部件和页面。
步骤 3:打开文件 lib/common/navigation/router/routes.dart。更新该文件,在其中添加活动功能的 enum 值。routes.dart 文件应如下所示:
enum AppRoute {
home,
trip,
editTrip,
pastTrips,
pastTrip,
activity,
addActivity,
editActivity,
}
对活动功能执行 CRUD 操作和工作流
步骤 1:在 lib/features/trip/services 文件夹内部创建一个新的 dart 文件并将其命名为 activities_api_service.dart。
步骤 2:打开 activities_api_service.dart 文件并使用以下代码段更新该文件,以创建包含如下函数的 ActivitiesAPIService:
- getActivitiesForTrip:在 Amplify API 中查询特定行程的活动,并返回活动列表。
- getActivity:在 Amplify API 中查询特定的活动并返回其详细信息。
- getActivity、deleteActivity 和 updateActivity:在 Amplify API 中添加、删除或更新活动。
import 'dart:async';
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final activitiesAPIServiceProvider = Provider<ActivitiesAPIService>((ref) {
final service = ActivitiesAPIService();
return service;
});
class ActivitiesAPIService {
ActivitiesAPIService();
Future<List<Activity>> getActivitiesForTrip(String tripId) async {
try {
final request = ModelQueries.list(
Activity.classType,
where: Activity.TRIP.eq(tripId),
);
final response = await Amplify.API.query(request: request).response;
final activites = response.data?.items;
if (activites == null) {
safePrint('errors: ${response.errors}');
return const [];
}
activites.sort(
(a, b) => a!.activityDate
.getDateTime()
.compareTo(b!.activityDate.getDateTime()),
);
return activites.map((e) => e as Activity).toList();
} on Exception catch (error) {
safePrint('getActivitiesForTrip failed: $error');
return const [];
}
}
Future<void> addActivity(Activity activity) async {
try {
final request = ModelMutations.create(activity);
final response = await Amplify.API.mutate(request: request).response;
final createdActivity = response.data;
if (createdActivity == null) {
safePrint('errors: ${response.errors}');
return;
}
} on Exception catch (error) {
safePrint('addActivity failed: $error');
}
}
Future<void> deleteActivity(Activity activity) async {
try {
await Amplify.API
.mutate(
request: ModelMutations.delete(activity),
)
.response;
} on Exception catch (error) {
safePrint('deleteActivity failed: $error');
}
}
Future<void> updateActivity(Activity updatedActivity) async {
try {
await Amplify.API
.mutate(
request: ModelMutations.update(updatedActivity),
)
.response;
} on Exception catch (error) {
safePrint('updateActivity failed: $error');
}
}
Future<Activity> getActivity(String activityId) async {
try {
final request = ModelQueries.get(
Activity.classType,
ActivityModelIdentifier(id: activityId),
);
final response = await Amplify.API.query(request: request).response;
final activity = response.data!;
return activity;
} on Exception catch (error) {
safePrint('getActivity failed: $error');
rethrow;
}
}
}
步骤 3:在 lib/features/activity/data 文件夹中创建一个新的 dart 文件,并将其命名为 activities_repository.dart。
步骤 4:打开 activities_repository.dart 文件,在其中添加以下代码,更新该文件:
import 'package:amplify_trips_planner/features/activity/service/activities_api_service.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final activitiesRepositoryProvider = Provider<ActivitiesRepository>((ref) {
final activitiesAPIService = ref.read(activitiesAPIServiceProvider);
return ActivitiesRepository(activitiesAPIService);
});
class ActivitiesRepository {
ActivitiesRepository(
this.activitiesAPIService,
);
final ActivitiesAPIService activitiesAPIService;
Future<List<Activity>> getActivitiesForTrip(String tripId) {
return activitiesAPIService.getActivitiesForTrip(tripId);
}
Future<Activity> getActivity(String activityId) {
return activitiesAPIService.getActivity(activityId);
}
Future<void> add(Activity activity) async {
return activitiesAPIService.addActivity(activity);
}
Future<void> delete(Activity activity) async {
return activitiesAPIService.deleteActivity(activity);
}
Future<void> update(Activity activity) async {
return activitiesAPIService.updateActivity(activity);
}
}
实现活动列表 UI
步骤 1:在 lib/features/activity/controller 文件夹中创建一个新的 dart 文件,并将其命名为 activities_list_controller.dart。
步骤 2:打开 activities_list_controller.dart 文件,在其中添加以下代码,更新该文件。UI 将使用控制器获取行程的活动、添加新活动和删除活动。
注意:VSCode 将显示由于缺少 activities_list_controller.g.dart 文件而导致出现的错误。您将在后面的步骤中修复此错误。
import 'dart:async';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart';
import 'package:amplify_trips_planner/features/activity/data/activities_repository.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activities_list_controller.g.dart';
@riverpod
class ActivitiesListController extends _$ActivitiesListController {
Future<List<Activity>> _fetchActivities(String tripId) async {
final activitiesRepository = ref.read(activitiesRepositoryProvider);
final activities = await activitiesRepository.getActivitiesForTrip(tripId);
return activities;
}
@override
FutureOr<List<Activity>> build(String tripId) async {
return _fetchActivities(tripId);
}
Future<void> removeActivity(Activity activity) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final activitiesRepository = ref.read(activitiesRepositoryProvider);
await activitiesRepository.delete(activity);
return _fetchActivities(activity.trip.id);
});
}
Future<void> add({
required String name,
required String activityDate,
required TimeOfDay activityTime,
required ActivityCategory category,
required Trip trip,
}) async {
final now = DateTime.now();
final time = DateTime(
now.year,
now.month,
now.day,
activityTime.hour,
activityTime.minute,
);
final activity = Activity(
activityName: name,
activityDate: TemporalDate(DateTime.parse(activityDate)),
activityTime: TemporalTime.fromString(time.format('HH:mm:ss.sss')),
trip: trip,
category: category,
);
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final activitiesRepository = ref.read(activitiesRepositoryProvider);
await activitiesRepository.add(activity);
return _fetchActivities(trip.id);
});
}
}
步骤 3:前往应用程序的根文件夹,然后在终端运行以下命令。
dart run build_runner build -d
这将在 lib/feature/activity/controller 文件夹中生成 activities_list.g.dart 文件。
步骤 4:在 lib/features/activity/ui 文件夹中创建一个新的 dart 文件,并将其命名为 activity_category_icon.dart。
步骤 5:打开 activity_category_icon.dart 文件,在其中添加以下代码,更新该文件。这将允许应用程序显示一个表示活动类别的图标。
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
class ActivityCategoryIcon extends StatelessWidget {
const ActivityCategoryIcon({
required this.activityCategory,
super.key,
});
final ActivityCategory activityCategory;
@override
Widget build(BuildContext context) {
switch (activityCategory) {
case ActivityCategory.Flight:
return const Icon(
Icons.flight,
size: 50,
color: Color(constants.primaryColorDark),
);
case ActivityCategory.Lodging:
return const Icon(
Icons.hotel,
size: 50,
color: Color(constants.primaryColorDark),
);
case ActivityCategory.Meeting:
return const Icon(
Icons.computer,
size: 50,
color: Color(constants.primaryColorDark),
);
case ActivityCategory.Restaurant:
return const Icon(
Icons.restaurant,
size: 50,
color: Color(constants.primaryColorDark),
);
default:
ActivityCategory.Flight;
}
return const Icon(
Icons.flight,
size: 50,
color: Color(constants.primaryColorDark),
);
}
}
步骤 6:在 lib/features/activity/ui 文件夹中创建一个新文件夹并将其命名为 activities_list,然后在其中创建文件 activities_timeline.dart。
步骤 7:打开 activities_timeline.dart 文件,在其中添加以下代码,更新该文件。这将显示行程活动的时间表。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_category_icon.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:timelines/timelines.dart';
class ActivitiesTimeline extends StatelessWidget {
const ActivitiesTimeline({
super.key,
required this.activities,
});
final List<Activity> activities;
@override
Widget build(BuildContext context) {
return Column(
children: [
Flexible(
child: Timeline.tileBuilder(
builder: TimelineTileBuilder.fromStyle(
oppositeContentsBuilder: (context, index) {
return InkWell(
onTap: () => context.goNamed(
AppRoute.activity.name,
pathParameters: {'id': activities[index].id},
),
child: Padding(
padding: const EdgeInsets.only(bottom: 15),
child: ActivityCategoryIcon(
activityCategory: activities[index].category,
),
),
);
},
contentsAlign: ContentsAlign.alternating,
contentsBuilder: (context, index) => InkWell(
onTap: () => context.goNamed(
AppRoute.activity.name,
pathParameters: {'id': activities[index].id},
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
activities[index].activityName,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 5,
),
Text(
activities[index]
.activityDate
.getDateTime()
.format('yyyy-MM-dd'),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
activities[index]
.activityTime!
.getDateTime()
.format('hh:mm a'),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
itemCount: activities.length,
),
),
),
],
);
}
}
步骤 8:在 lib/features/activity/ui/activities_list 文件夹中创建文件 activities_list.dart。
步骤 9:打开 activities_list.dart 文件,在其中添加以下代码,更新该文件,以使用前面创建的 ActivitiesTimeline 小部件显示行程活动的时间表。
import 'package:amplify_trips_planner/features/activity/controller/activities_list_controller.dart';
import 'package:amplify_trips_planner/features/activity/ui/activities_list/activities_timeline.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ActivitiesList extends ConsumerWidget {
const ActivitiesList({
required this.trip,
super.key,
});
final Trip trip;
@override
Widget build(BuildContext context, WidgetRef ref) {
final activitiesListValue = ref.watch(activitiesListControllerProvider(trip.id));
switch (activitiesListValue) {
case AsyncData(:final value):
return value.isEmpty
? const Center(
child: Text('No Activities'),
)
: ActivitiesTimeline(activities: value);
case AsyncError():
return const Center(
child: Text('Error'),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
实现用于添加活动的 UI
步骤 1:在 lib/features/activity/ui 文件夹中创建一个新文件夹并将其命名为 add_activity,然后在其中创建文件 add_activity_form.dart。
步骤 2:打开 add_activity_form.dart 文件,在其中添加以下代码,更新该文件。这样,我们就可以向用户提供一张表单,以便用户提交创建新行程的活动所需的详细信息。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart';
import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart';
import 'package:amplify_trips_planner/features/activity/controller/activities_list_controller.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class AddActivityForm extends ConsumerStatefulWidget {
const AddActivityForm({
required this.trip,
super.key,
});
final AsyncValue<Trip> trip;
@override
AddActivityFormState createState() => AddActivityFormState();
}
class AddActivityFormState extends ConsumerState<AddActivityForm> {
final formGlobalKey = GlobalKey<FormState>();
final activityNameController = TextEditingController();
final activityDateController = TextEditingController();
final activityTimeController = TextEditingController();
var activityCategory = ActivityCategory.Flight;
var activityTime = TimeOfDay.now();
@override
Widget build(BuildContext context) {
final activityNameController = TextEditingController();
final activityDateController = TextEditingController();
final activityTimeController = TextEditingController();
var activityCategory = ActivityCategory.Flight;
var activityTime = TimeOfDay.now();
switch (widget.trip) {
case AsyncData(:final value):
return SingleChildScrollView(
child: Form(
key: formGlobalKey,
child: Container(
padding: EdgeInsets.only(
top: 15,
left: 15,
right: 15,
bottom: MediaQuery.of(context).viewInsets.bottom + 15,
),
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BottomSheetTextFormField(
labelText: 'Activity Name',
controller: activityNameController,
keyboardType: TextInputType.name,
),
const SizedBox(
height: 20,
),
DropdownButtonFormField<ActivityCategory>(
onChanged: (value) {
activityCategory = value!;
},
value: activityCategory,
decoration: const InputDecoration(
labelText: 'Category',
),
items: [
for (var category in ActivityCategory.values)
DropdownMenuItem(
value: category,
child: Text(category.name),
),
],
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Activity Date',
controller: activityDateController,
keyboardType: TextInputType.datetime,
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.parse(value.startDate.toString()),
firstDate: DateTime.parse(value.startDate.toString()),
lastDate: DateTime.parse(value.endDate.toString()),
);
if (pickedDate != null) {
activityDateController.text =
pickedDate.format('yyyy-MM-dd');
} else {}
},
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Activity Time',
controller: activityTimeController,
keyboardType: TextInputType.datetime,
onTap: () async {
await showTimePicker(
context: context,
initialTime: activityTime,
initialEntryMode: TimePickerEntryMode.dial,
).then((timeOfDay) {
if (timeOfDay != null) {
final localizations =
MaterialLocalizations.of(context);
final formattedTimeOfDay =
localizations.formatTimeOfDay(timeOfDay);
activityTimeController.text = formattedTimeOfDay;
activityTime = timeOfDay;
}
});
},
),
const SizedBox(
height: 20,
),
TextButton(
child: const Text('OK'),
onPressed: () async {
final currentState = formGlobalKey.currentState;
if (currentState == null) {
return;
}
if (currentState.validate()) {
await ref
.watch(activitiesListControllerProvider(value.id)
.notifier)
.add(
name: activityNameController.text,
activityDate: activityDateController.text,
activityTime: activityTime,
category: activityCategory,
trip: value,
);
if (context.mounted) {
context.goNamed(
AppRoute.trip.name,
pathParameters: {'id': value.id},
);
}
}
}, //,
),
],
),
),
),
);
case AsyncError():
return const Center(
child: Text('Error'),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 3:在 lib/features/activity/ui/add_activity 文件夹中创建一个新文件夹,并将其命名为 add_activity_page.dart。
步骤 4:打开 add_activity_page.dart 文件,在其中添加以下代码,更新该文件,以使用上面创建的 AddActivityForm 为所选行程创建新活动。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_form.dart';
import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class AddActivityPage extends ConsumerWidget {
AddActivityPage({
required this.tripId,
super.key,
});
final String tripId;
final formGlobalKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tripValue = ref.watch(tripControllerProvider(tripId));
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
leading: IconButton(
onPressed: () {
context.goNamed(
AppRoute.trip.name,
pathParameters: {'id': tripId},
);
},
icon: const Icon(Icons.arrow_back),
),
backgroundColor: const Color(constants.primaryColorDark),
),
body: AddActivityForm(
trip: tripValue,
),
);
}
}
步骤 5:在 lib/features/trip/ui/trip_page 文件夹中创建一个新文件,并将其命名为 trip_page_floating_button.dart。
步骤 6:打开 lib/features/trip/ui/trip_page/trip_page_floating_button.dart 文件,将其更新为使用 floatingActionButton 打开 AddActivityForm。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class TripPageFloatingButton extends StatelessWidget {
const TripPageFloatingButton({
required this.trip,
super.key,
});
final AsyncValue<Trip> trip;
@override
Widget build(BuildContext context) {
switch (trip) {
case AsyncData(:final value):
return FloatingActionButton(
onPressed: () {
context.goNamed(
AppRoute.addActivity.name,
pathParameters: {'id': value.id},
);
},
backgroundColor: const Color(constants.primaryColorDark),
child: const Icon(Icons.add),
);
case AsyncError():
return const Placeholder();
case AsyncLoading():
return const SizedBox();
case _:
return const SizedBox();
}
}
}
步骤 7:打开 lib/features/trip/ui/trip_page/trip_page.dart 文件,将其更新为使用 TripPageFloatingButton 为行程添加活动。
floatingActionButton: TripPageFloatingButton(
trip: tripValue,
),
trip_page.dart 文件应如下所示。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart';
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_details.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page_floating_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class TripPage extends ConsumerWidget {
const TripPage({
required this.tripId,
super.key,
});
final String tripId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final tripValue = ref.watch(tripControllerProvider(tripId));
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
actions: [
IconButton(
onPressed: () {
context.goNamed(
AppRoute.home.name,
);
},
icon: const Icon(Icons.home),
),
],
backgroundColor: const Color(constants.primaryColorDark),
),
drawer: const TheNavigationDrawer(),
floatingActionButton: TripPageFloatingButton(
trip: tripValue,
),
body: TripDetails(
tripId: tripId,
trip: tripValue,
),
);
}
}
步骤 8:打开 lib/features/trip/ui/trip_page/trip_details.dart 文件,将其更新为如下所示的内容,以显示行程的活动列表。
Expanded(
child: ActivitiesList(
trip: value,
),
)
trip_details.dart 文件应如下所示。
import 'package:amplify_trips_planner/features/activity/ui/activities_list/activities_list.dart';
import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/selected_trip_card.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TripDetails extends ConsumerWidget {
const TripDetails({
required this.trip,
required this.tripId,
super.key,
});
final AsyncValue<Trip> trip;
final String tripId;
@override
Widget build(BuildContext context, WidgetRef ref) {
switch (trip) {
case AsyncData(:final value):
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 8,
),
SelectedTripCard(trip: value),
const SizedBox(
height: 20,
),
const Divider(
height: 20,
thickness: 5,
indent: 20,
endIndent: 20,
),
const Text(
'Your Activities',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 8,
),
Expanded(
child: ActivitiesList(
trip: value,
),
)
],
);
case AsyncError():
return Center(
child: Column(
children: [
const Text('Error'),
TextButton(
onPressed: () async {
ref.invalidate(tripControllerProvider(tripId));
},
child: const Text('Try again'),
),
],
),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 9:打开 lib/common/navigation/router/router.dart 文件,在其中添加 AddActivityPage 路线,更新该文件。
GoRoute(
path: '/addActivity/:id',
name: AppRoute.addActivity.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return AddActivityPage(tripId: tripId);
},
),
router.dart 文件应如下所示。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/edit_trip_page/edit_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/past_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trips/past_trips_list.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_list/trips_list_page.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
name: AppRoute.home.name,
builder: (context, state) => const TripsListPage(),
),
GoRoute(
path: '/trip/:id',
name: AppRoute.trip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return TripPage(tripId: tripId);
},
),
GoRoute(
path: '/edittrip/:id',
name: AppRoute.editTrip.name,
builder: (context, state) {
return EditTripPage(
trip: state.extra! as Trip,
);
},
),
GoRoute(
path: '/pasttrip/:id',
name: AppRoute.pastTrip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return PastTripPage(tripId: tripId);
},
),
GoRoute(
path: '/pasttrips',
name: AppRoute.pastTrips.name,
builder: (context, state) => const PastTripsList(),
),
GoRoute(
path: '/addActivity/:id',
name: AppRoute.addActivity.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return AddActivityPage(tripId: tripId);
},
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
实现表示“活动详情”页面的 UI
步骤 1:在 lib/features/activity/controller 文件夹中创建一个新的 dart 文件,并将其命名为 activity_controller.dart。
步骤 2:打开 activity_controller.dart 文件,在其中添加以下代码,更新该文件。UI 将使用此控制器通过 ID 编辑和删除活动。UI 还将使用此控制器上传活动的文件。
注意:VSCode 将显示由于缺少 activity_controller.g.dart 文件而导致出现的错误。您将在后面的步骤中修复此错误。
import 'dart:io';
import 'package:amplify_trips_planner/common/services/storage_service.dart';
import 'package:amplify_trips_planner/features/activity/data/activities_repository.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_controller.g.dart';
@riverpod
class ActivityController extends _$ActivityController {
Future<Activity> _fetchActivity(String activityId) async {
final activitiesRepository = ref.read(activitiesRepositoryProvider);
return activitiesRepository.getActivity(activityId);
}
@override
FutureOr<Activity> build(String activityId) async {
return _fetchActivity(activityId);
}
Future<void> uploadFile(File file, Activity activity) async {
final fileKey = await ref.read(storageServiceProvider).uploadFile(file);
if (fileKey != null) {
final imageUrl =
await ref.read(storageServiceProvider).getImageUrl(fileKey);
final updatedActivity = activity.copyWith(
activityImageKey: fileKey,
activityImageUrl: imageUrl,
);
await updateActivity(updatedActivity);
ref.read(storageServiceProvider).resetUploadProgress();
}
}
Future<String> getFileUrl(Activity activity) async {
final fileKey = activity.activityImageKey;
return ref.read(storageServiceProvider).getImageUrl(fileKey!);
}
ValueNotifier<double> uploadProgress() {
return ref.read(storageServiceProvider).getUploadProgress();
}
Future<void> updateActivity(Activity activity) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final activitiesRepository = ref.read(activitiesRepositoryProvider);
await activitiesRepository.update(activity);
return _fetchActivity(activity.id);
});
}
}
步骤 3:前往应用程序的根文件夹,然后在终端运行以下命令。
dart run build_runner build -d
这将在 lib/feature/activity/controller 文件夹中生成 activity_controller.g.dart 文件。
步骤 4:在 lib/features/activity/ui 文件夹中创建一个新文件夹并将其命名为 activity_page,然后在其中创建文件 delete_activity_dialog.dart。
步骤 5:打开 delete_activity_dialog.dart 文件,在其中添加以下代码,更新该文件。这将显示一个对话框,以便用户确认删除所选活动。
import 'package:flutter/material.dart';
class DeleteActivityDialog extends StatelessWidget {
const DeleteActivityDialog({
super.key,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Please Confirm'),
content: const Text('Delete this activity?'),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
child: const Text('Yes'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('No'),
)
],
);
}
}
步骤 6:在 lib/features/activity/ui/activity_page 文件夹中创建一个新的 dart 文件,并将其命名为 activity_page_appbar_icon.dart。
步骤 7:打开 activity_page_appbar_icon.dart 文件,在其中添加以下代码,更新该文件,以创建 IconButton,用于返回活动的行程页面。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class ActivityPageAppBarIcon extends StatelessWidget {
const ActivityPageAppBarIcon({
super.key,
required this.activity,
});
final AsyncValue<Activity> activity;
@override
Widget build(BuildContext context) {
switch (activity) {
case AsyncData(:final value):
return IconButton(
onPressed: () {
context.goNamed(
AppRoute.trip.name,
pathParameters: {'id': value.trip.id},
);
},
icon: const Icon(Icons.arrow_back),
);
case AsyncError():
return const Placeholder();
case AsyncLoading():
return const SizedBox();
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 8:在 lib/features/activity/ui/activity_page 文件夹中创建一个新的 dart 文件,并将其命名为 activity_listview.dart。
步骤 9:打开 activity_listview.dart 文件,在其中添加以下代码,更新该文件,以显示活动详情,并使用户能够上传和打开活动的文件。
import 'dart:io';
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/ui/upload_progress_dialog.dart';
import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart';
import 'package:amplify_trips_planner/features/activity/controller/activities_list_controller.dart';
import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_category_icon.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_page/delete_activity_dialog.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
class ActivityListView extends ConsumerWidget {
const ActivityListView({
required this.activity,
super.key,
});
final AsyncValue<Activity> activity;
Future<bool> deleteActivity(
BuildContext context,
WidgetRef ref,
Activity activity,
) async {
var value = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return const DeleteActivityDialog();
},
);
value ??= false;
if (value) {
await ref
.watch(activitiesListControllerProvider(activity.trip.id).notifier)
.removeActivity(activity);
}
return value;
}
Future<void> openFile({
required BuildContext context,
required WidgetRef ref,
required Activity activity,
}) async {
final fileUrl = await ref
.watch(activityControllerProvider(activity.id).notifier)
.getFileUrl(activity);
final url = Uri.parse(fileUrl);
await launchUrl(url);
}
Future<bool> uploadFile({
required BuildContext context,
required WidgetRef ref,
required Activity activity,
}) async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'pdf', 'png'],
);
if (result == null) {
return false;
}
final platformFile = result.files.first;
final file = File(platformFile.path!);
if (context.mounted) {
await showDialog<String>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const UploadProgressDialog();
},
);
await ref
.watch(activityControllerProvider(activity.id).notifier)
.uploadFile(file, activity);
}
return true;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
switch (activity) {
case AsyncData(:final value):
return ListView(
children: [
Card(
child: ListTile(
leading: ActivityCategoryIcon(activityCategory: value.category),
title: Text(
value.activityName,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(value.category.name),
),
),
ListTile(
dense: true,
title: Text(
'Activity Date',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
tileColor: Colors.grey,
),
Card(
child: ListTile(
title: Text(
value.activityDate.getDateTime().format('EE MMMM dd'),
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(
value.activityTime!.getDateTime().format('hh:mm a'),
),
),
),
ListTile(
dense: true,
title: Text(
'Documents',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
tileColor: Colors.grey,
),
Card(
child: value.activityImageUrl != null
? Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
openFile(
context: context,
ref: ref,
activity: value,
);
},
child: const Text('Open'),
),
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
uploadFile(
context: context,
activity: value,
ref: ref,
).then(
(isUploaded) => isUploaded ? context.pop() : null,
);
},
child: const Text('Replace'),
),
],
)
: ListTile(
title: TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
uploadFile(
context: context,
activity: value,
ref: ref,
).then(
(isUploaded) => isUploaded ? context.pop() : null,
);
// Navigator.of(context, rootNavigator: true)
// .pop());
},
child: const Text('Attach a PDF or photo'),
),
),
),
const ListTile(
dense: true,
tileColor: Colors.grey,
visualDensity: VisualDensity(vertical: -4),
),
Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
context.goNamed(
AppRoute.editActivity.name,
pathParameters: {'id': value.id},
extra: value,
);
},
child: const Text('Edit'),
),
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
deleteActivity(context, ref, value).then(
(isDeleted) {
if (isDeleted) {
context.goNamed(
AppRoute.trip.name,
pathParameters: {'id': value.trip.id},
);
}
},
);
},
child: const Text('Delete'),
),
],
),
)
],
);
case AsyncError():
return const Center(
child: Text('Error'),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 10:在 lib/features/activity/ui/activity_page 文件夹中创建一个新的 dart 文件,并将其命名为 activity_page.dart。
步骤 11:打开 activity_page.dart 文件,在其中添加以下代码,更新该文件,以创建 ActivityPage,将在其中使用上面创建的 ActivityListView 显示活动详情。
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_listview.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_page_appbar_icon.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ActivityPage extends ConsumerWidget {
const ActivityPage({
required this.activityId,
super.key,
});
final String activityId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final activityValue = ref.watch(activityControllerProvider(activityId));
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
leading: ActivityPageAppBarIcon(
activity: activityValue,
),
backgroundColor: const Color(constants.primaryColorDark),
),
body: ActivityListView(
activity: activityValue,
),
);
}
}
步骤 12:打开 /lib/common/navigation/router/router.dart 文件,在其中添加 AddActivityPage 路线,更新该文件。
GoRoute(
path: '/activity/:id',
name: AppRoute.activity.name,
builder: (context, state) {
final activityId = state.pathParameters['id']!;
return ActivityPage(activityId: activityId);
},
),
router.dart 文件应如下所示。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_page.dart';
import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/edit_trip_page/edit_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/past_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trips/past_trips_list.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_list/trips_list_page.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
name: AppRoute.home.name,
builder: (context, state) => const TripsListPage(),
),
GoRoute(
path: '/trip/:id',
name: AppRoute.trip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return TripPage(tripId: tripId);
},
),
GoRoute(
path: '/edittrip/:id',
name: AppRoute.editTrip.name,
builder: (context, state) {
return EditTripPage(
trip: state.extra! as Trip,
);
},
),
GoRoute(
path: '/pasttrip/:id',
name: AppRoute.pastTrip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return PastTripPage(tripId: tripId);
},
),
GoRoute(
path: '/pasttrips',
name: AppRoute.pastTrips.name,
builder: (context, state) => const PastTripsList(),
),
GoRoute(
path: '/addActivity/:id',
name: AppRoute.addActivity.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return AddActivityPage(tripId: tripId);
},
),
GoRoute(
path: '/activity/:id',
name: AppRoute.activity.name,
builder: (context, state) {
final activityId = state.pathParameters['id']!;
return ActivityPage(activityId: activityId);
},
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
实现用于编辑活动的 UI
步骤 1:在 lib/features/activity/ui 文件夹中创建一个新文件夹并将其命名为 edit_activity,然后在其中创建文件 edit_activity_page.dart。
步骤 2:打开 edit_activity_page.dart 文件,在其中添加以下代码,更新该文件。这样,就可以向用户呈现一张表单,以便用户更新所选活动的详情。
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart';
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart';
import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
class EditActivityPage extends ConsumerStatefulWidget {
const EditActivityPage({
required this.activity,
super.key,
});
final Activity activity;
@override
EditActivityPageState createState() => EditActivityPageState();
}
class EditActivityPageState extends ConsumerState<EditActivityPage> {
@override
void initState() {
activityNameController.text = widget.activity.activityName;
activityDateController.text =
widget.activity.activityDate.getDateTime().format('yyyy-MM-dd');
activityTime =
TimeOfDay.fromDateTime(widget.activity.activityTime!.getDateTime());
activityTimeController.text =
widget.activity.activityTime!.getDateTime().format('hh:mm a');
activityCategoryController.text = widget.activity.category.name;
activityCategory = widget.activity.category;
super.initState();
}
final formGlobalKey = GlobalKey<FormState>();
final activityNameController = TextEditingController();
final activityDateController = TextEditingController();
var activityTime = TimeOfDay.now();
final activityTimeController = TextEditingController();
final activityCategoryController = TextEditingController();
var activityCategory = ActivityCategory.Flight;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
leading: IconButton(
onPressed: () {
context.goNamed(
AppRoute.activity.name,
pathParameters: {'id': widget.activity.id},
);
},
icon: const Icon(Icons.arrow_back),
),
backgroundColor: const Color(constants.primaryColorDark),
),
body: SingleChildScrollView(
child: Form(
key: formGlobalKey,
child: Container(
padding: EdgeInsets.only(
top: 15,
left: 15,
right: 15,
bottom: MediaQuery.of(context).viewInsets.bottom + 15,
),
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BottomSheetTextFormField(
labelText: 'Activity Name',
controller: activityNameController,
keyboardType: TextInputType.name,
),
const SizedBox(
height: 20,
),
DropdownButtonFormField<ActivityCategory>(
onChanged: (value) {
activityCategoryController.text = value!.name;
activityCategory = value;
},
value: activityCategory,
decoration: const InputDecoration(
labelText: 'Category',
),
items: [
for (var category in ActivityCategory.values)
DropdownMenuItem(
value: category,
child: Text(category.name),
),
],
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Activity Date',
controller: activityDateController,
keyboardType: TextInputType.datetime,
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.parse(
widget.activity.activityDate.toString()),
firstDate: DateTime.parse(
widget.activity.trip.startDate.toString()),
lastDate: DateTime.parse(
widget.activity.trip.endDate.toString()),
);
if (pickedDate != null) {
activityDateController.text =
pickedDate.format('yyyy-MM-dd');
} else {}
},
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Activity Time',
controller: activityTimeController,
keyboardType: TextInputType.datetime,
onTap: () async {
await showTimePicker(
context: context,
initialTime: activityTime,
initialEntryMode: TimePickerEntryMode.dial,
).then((timeOfDay) {
if (timeOfDay != null) {
final localizations = MaterialLocalizations.of(context);
final formattedTimeOfDay =
localizations.formatTimeOfDay(timeOfDay);
activityTimeController.text = formattedTimeOfDay;
activityTime = timeOfDay;
}
});
},
),
const SizedBox(
height: 20,
),
TextButton(
child: const Text('OK'),
onPressed: () async {
final currentState = formGlobalKey.currentState;
if (currentState == null) {
return;
}
if (currentState.validate()) {
final format = DateFormat.jm();
activityTime = TimeOfDay.fromDateTime(
format.parse(activityTimeController.text),
);
final now = DateTime.now();
final time = DateTime(
now.year,
now.month,
now.day,
activityTime.hour,
activityTime.minute,
);
final updatedActivity = widget.activity.copyWith(
category: ActivityCategory.values
.byName(activityCategoryController.text),
activityName: activityNameController.text,
activityDate: TemporalDate(
DateTime.parse(activityDateController.text),
),
activityTime: TemporalTime.fromString(
time.format('HH:mm:ss.sss'),
),
);
await ref
.watch(
activityControllerProvider(widget.activity.id)
.notifier,
)
.updateActivity(updatedActivity);
if (context.mounted) {
context.goNamed(
AppRoute.activity.name,
pathParameters: {'id': widget.activity.id},
);
}
}
},
),
],
),
),
),
),
);
}
}
步骤 3:打开 lib/common/navigation/router/router.dart 文件,在其中添加 EditActivityPage 路线,更新该文件。
GoRoute(
path: '/editactivity/:id',
name: AppRoute.editActivity.name,
builder: (context, state) {
return EditActivityPage(
activity: state.extra! as Activity,
);
},
),
router.dart 文件应如下所示。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_page.dart';
import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_page.dart';
import 'package:amplify_trips_planner/features/activity/ui/edit_activity/edit_activity_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/edit_trip_page/edit_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/past_trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/past_trips/past_trips_list.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_list/trips_list_page.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
name: AppRoute.home.name,
builder: (context, state) => const TripsListPage(),
),
GoRoute(
path: '/trip/:id',
name: AppRoute.trip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return TripPage(tripId: tripId);
},
),
GoRoute(
path: '/edittrip/:id',
name: AppRoute.editTrip.name,
builder: (context, state) {
return EditTripPage(
trip: state.extra! as Trip,
);
},
),
GoRoute(
path: '/pasttrip/:id',
name: AppRoute.pastTrip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return PastTripPage(tripId: tripId);
},
),
GoRoute(
path: '/pasttrips',
name: AppRoute.pastTrips.name,
builder: (context, state) => const PastTripsList(),
),
GoRoute(
path: '/addActivity/:id',
name: AppRoute.addActivity.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return AddActivityPage(tripId: tripId);
},
),
GoRoute(
path: '/activity/:id',
name: AppRoute.activity.name,
builder: (context, state) {
final activityId = state.pathParameters['id']!;
return ActivityPage(activityId: activityId);
},
),
GoRoute(
path: '/editactivity/:id',
name: AppRoute.editActivity.name,
builder: (context, state) {
return EditActivityPage(
activity: state.extra! as Activity,
);
},
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
步骤 4:在仿真器或模拟器中运行应用程序并创建行程,然后在其中添加一些活动。下面是一个使用 iPhone 模拟器的示例。
注意:由于数据模式发生了更改,您需要从仿真器或模拟器中删除应用程序及其内容。