使用 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 中查询特定的活动并返回其详细信息。
  • getActivitydeleteActivity 和 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 模拟器的示例。

注意:由于数据模式发生了更改,您需要从仿真器或模拟器中删除应用程序及其内容。

结果

在本模块中,您对应用程序中的行程活动引入了创建、读取、更新和删除 (CRUD) 功能。您还更新了 Amplify API 以检索并保存您的行程活动数据。

此页内容对您是否有帮助?

添加个人资料功能