使用 AWS Amplify 构建 Flutter 移动应用程序 - 第 1 部分

创建适用于 iOS 和 Android 的行程规划器应用程序

模块 4:添加 Amplify 存储

概述

在本模块中,您将添加可为每个行程上传图片的功能。您将添加 Amplify 存储以开启图片上传和渲染功能。

Amplify Storage 类别默认支持 Amazon Simple Storage Service (Amazon S3)。Amplify CLI 可帮助您创建和配置应用程序的存储桶。

要完成的目标

  • 向应用程序中添加 Amplify 存储
  • 向应用程序中添加行程详情页面
  • 实现图片上传功能

 最短完成时间

15 分钟

操作步骤

向应用程序中添加 Amplify 存储

步骤 1:前往应用程序的根文件夹,并在终端运行以下命令来设置存储源。

amplify add storage

步骤 2:若要创建 Storage 类别,请在系统提示时输入以下内容:

? Select from one of the below mentioned services: Content (Images, audio, video, etc.)
✔ Provide a friendly name for your resource that will be used to label this category in the project: · s3cf3f0a40
✔ Provide bucket name: · amplifytripsplannerstorage
✔ Who should have access: · Auth and guest users
✔ What kind of access do you want for Authenticated users? · create/update, read, delete
✔ What kind of access do you want for Guest users? · read
✔ Do you want to add a Lambda Trigger for your S3 Bucket? (y/N) · no

步骤 3:运行 amplify push 命令,在云上创建资源。 

步骤 4:按下 Enter 键。Amplify CLI 将部署资源并显示确认消息。

步骤 5:对于 iOS 系统,打开 ios/Runner/info.plist 文件。而 Android 系统无需任何配置即可访问手机相机和照片库。

加入以下键值对。

    <key>NSCameraUsageDescription</key>
    <string>Some Description</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Some Description</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Some Description</string>

ios/Runner/info.plist 文件将如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>Amplify Trips Planner</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>amplify_trips_planner</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>$(FLUTTER_BUILD_NAME)</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>$(FLUTTER_BUILD_NUMBER)</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UIViewControllerBasedStatusBarAppearance</key>
    <false/>
    <key>CADisableMinimumFrameDurationOnPhone</key>
    <true/>
    <key>UIApplicationSupportsIndirectInputEvents</key>
    <true/>
    <key>NSCameraUsageDescription</key>
    <string>Some Description</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Some Description</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Some Description</string>
</dict>
</plist>

步骤 6:打开 main.dart 文件并更新 _configureAmplify() 函数(如以下代码所示),以添加 Amplify 存储插件。

Future<void> _configureAmplify() async {
  await Amplify.addPlugins([
    AmplifyAuthCognito(),
    AmplifyAPI(modelProvider: ModelProvider.instance),
     AmplifyStorageS3()
  ]);
  await Amplify.configure(amplifyconfig);
}

现在 main.dart 文件应如下所示。

import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:amplify_trips_planner/trips_planner_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';

import 'amplifyconfiguration.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await _configureAmplify();
  } on AmplifyAlreadyConfiguredException {
    debugPrint('Amplify configuration failed.');
  }

  runApp(
    const ProviderScope(
      child: TripsPlannerApp(),
    ),
  );
}

Future<void> _configureAmplify() async {
  await Amplify.addPlugins([
    AmplifyAuthCognito(),
    AmplifyAPI(modelProvider: ModelProvider.instance),
     AmplifyStorageS3()
  ]);
  await Amplify.configure(amplifyconfig);
}

步骤 7:在 lib/common/services 文件夹中新建一个 dart 文件并将其命名为 storage_service.dart

步骤 8:打开 storage_service.dart 文件并使用以下代码更新该文件,以创建 StorageService。在该服务中,您可以找到 uploadFile 函数,它使用 Amplify 存储库将图片上传到 Amazon S3 存储桶中。此外,该服务还提供了一个 ValueNotifier 对象,用于跟踪图片上传的进度。

import 'dart:io';

import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart';

final storageServiceProvider = Provider<StorageService>((ref) {
  return StorageService(ref: ref);
});

class StorageService {
  StorageService({
    required Ref ref,
  });

  ValueNotifier<double> uploadProgress = ValueNotifier<double>(0);
  Future<String> getImageUrl(String key) async {
    final result = await Amplify.Storage.getUrl(
      key: key,
      options: const StorageGetUrlOptions(
        pluginOptions: S3GetUrlPluginOptions(
          validateObjectExistence: true,
          expiresIn: Duration(days: 1),
        ),
      ),
    ).result;
    return result.url.toString();
  }

  ValueNotifier<double> getUploadProgress() {
    return uploadProgress;
  }

  Future<String?> uploadFile(File file) async {
    try {
      final extension = p.extension(file.path);
      final key = const Uuid().v1() + extension;
      final awsFile = AWSFile.fromPath(file.path);

      await Amplify.Storage.uploadFile(
        localFile: awsFile,
        key: key,
        onProgress: (progress) {
          uploadProgress.value = progress.fractionCompleted;
        },
      ).result;

      return key;
    } on Exception catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }

  void resetUploadProgress() {
    uploadProgress.value = 0;
  }
}

步骤 9:lib/common/ui 文件夹中新建一个 dart 文件并将其命名为 upload_progress_dialog.dart

步骤 10:打开 upload_progress_dialog.dart 文件并使用以下代码更新该文件,以创建一个显示图片上传进度指示器的对话框。

注意:VSCode 会提示缺少 trip_controller.dart 文件的错误。在后续步骤中,您将修复这个问题。

import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class UploadProgressDialog extends ConsumerWidget {
  const UploadProgressDialog({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Dialog(
      backgroundColor: Colors.white,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20),
        child: ValueListenableBuilder(
          valueListenable:
              ref.read(tripControllerProvider('').notifier).uploadProgress(),
          builder: (context, value, child) {
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const CircularProgressIndicator(),
                const SizedBox(
                  height: 15,
                ),
                Text('${(double.parse(value.toString()) * 100).toInt()} %'),
                Container(
                  alignment: Alignment.topCenter,
                  margin: const EdgeInsets.all(20),
                  child: LinearProgressIndicator(
                    value: double.parse(value.toString()),
                    backgroundColor: Colors.grey,
                    color: Colors.purple,
                    minHeight: 10,
                  ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

向应用程序中添加行程详情页面

步骤 1:在 lib/features/trip/controller 文件夹中新建一个 dart 文件并将其命名为 trip_controller.dart

步骤 2:打开 trip_controller.dart 文件并使用以下代码更新该文件。UI 将使用该控件的 ID 编辑和删除行程。UI 还将使用该控件上传行程图片。

注意:VSCode 会因缺少 trip_controller.g.dart 文件而报错。在下个步骤中,您将修复这个问题。

import 'dart:async';
import 'dart:io';

import 'package:amplify_trips_planner/common/services/storage_service.dart';
import 'package:amplify_trips_planner/features/trip/data/trips_repository.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'trip_controller.g.dart';

@riverpod
class TripController extends _$TripController {
  Future<Trip> _fetchTrip(String tripId) async {
    final tripsRepository = ref.read(tripsRepositoryProvider);
    return tripsRepository.getTrip(tripId);
  }

  @override
  FutureOr<Trip> build(String tripId) async {
    return _fetchTrip(tripId);
  }

  Future<void> updateTrip(Trip trip) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final tripsRepository = ref.read(tripsRepositoryProvider);
      await tripsRepository.update(trip);
      return _fetchTrip(trip.id);
    });
  }

  Future<void> uploadFile(File file, Trip trip) async {
    final fileKey = await ref.read(storageServiceProvider).uploadFile(file);
    if (fileKey != null) {
      final imageUrl =
          await ref.read(storageServiceProvider).getImageUrl(fileKey);
      final updatedTrip =
          trip.copyWith(tripImageKey: fileKey, tripImageUrl: imageUrl);
      await ref.read(tripsRepositoryProvider).update(updatedTrip);
      ref.read(storageServiceProvider).resetUploadProgress();
    }
  }

  ValueNotifier<double> uploadProgress() {
    return ref.read(storageServiceProvider).getUploadProgress();
  }
}

步骤 3:前往应用程序的根文件夹,并在终端运行以下命令。 

dart run build_runner build -d

此操作会在 lib/feature/trip/controller 文件夹中生成 trip_controller.g.dart 文件。

步骤 4:在  lib/features/trip/ui 文件夹中新建一个文件夹并将其命名为  trip_page,然后在其中创建  delete_trip_dialog.dart 文件。

步骤 5:打开 delete_trip_dialog.dart 文件并使用以下代码更新该文件。系统将显示一个对话框,让用户确认是否删除所选行程。

import 'package:flutter/material.dart';

class DeleteTripDialog extends StatelessWidget {
  const DeleteTripDialog({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Please Confirm'),
      content: const Text('Delete this trip?'),
      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/trip/ui/trip_page 文件夹中新建一个 dart 文件并将其命名为 selected_trip_card.dart

步骤 7:打开 selected_trip_card.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/colors.dart' as constants;
import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart';
import 'package:amplify_trips_planner/features/trip/controller/trips_list_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/delete_trip_dialog.dart';
import 'package:amplify_trips_planner/models/Trip.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';

class SelectedTripCard extends ConsumerWidget {
  const SelectedTripCard({
    required this.trip,
    super.key,
  });

  final Trip trip;

  Future<bool> uploadImage({
    required BuildContext context,
    required WidgetRef ref,
    required Trip trip,
  }) async {
    final picker = ImagePicker();
    final pickedFile = await picker.pickImage(source: ImageSource.gallery);
    if (pickedFile == null) {
      return false;
    }
    final file = File(pickedFile.path);
    if (context.mounted) {
      showDialog<String>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return const UploadProgressDialog();
        },
      );

      await ref
          .watch(tripControllerProvider(trip.id).notifier)
          .uploadFile(file, trip);
    }

    return true;
  }

  Future<bool> deleteTrip(
    BuildContext context,
    WidgetRef ref,
    Trip trip,
  ) async {
    var value = await showDialog<bool>(
      context: context,
      builder: (BuildContext context) {
        return const DeleteTripDialog();
      },
    );
    value ??= false;

    if (value) {
      await ref.watch(tripsListControllerProvider.notifier).removeTrip(trip);
    }
    return value;
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(15),
      ),
      elevation: 5,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            trip.tripName,
            textAlign: TextAlign.center,
            style: const TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(
            height: 8,
          ),
          Container(
            alignment: Alignment.center,
            color: const Color(constants.primaryColorDark), //Color(0xffE1E5E4),
            height: 150,

            child: trip.tripImageUrl != null
                ? Stack(
                    children: [
                      const Center(child: CircularProgressIndicator()),
                      CachedNetworkImage(
                        cacheKey: trip.tripImageKey,
                        imageUrl: trip.tripImageUrl!,
                        width: double.maxFinite,
                        height: 500,
                        alignment: Alignment.topCenter,
                        fit: BoxFit.fill,
                      ),
                    ],
                  )
                : Image.asset(
                    'images/amplify.png',
                    fit: BoxFit.contain,
                  ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              IconButton(
                onPressed: () {
                  context.goNamed(
                    AppRoute.editTrip.name,
                    pathParameters: {'id': trip.id},
                    extra: trip,
                  );
                },
                icon: const Icon(Icons.edit),
              ),
              IconButton(
                onPressed: () {
                  uploadImage(
                    context: context,
                    trip: trip,
                    ref: ref,
                  ).then((value) {
                    if (value) {
                      Navigator.of(context, rootNavigator: true).pop();
                      ref.invalidate(tripControllerProvider(trip.id));
                    }
                  });
                },
                icon: const Icon(Icons.camera_enhance_sharp),
              ),
              IconButton(
                onPressed: () {
                  deleteTrip(context, ref, trip).then((value) {
                    if (value) {
                      context.goNamed(
                        AppRoute.home.name,
                      );
                    }
                  });
                },
                icon: const Icon(Icons.delete),
              ),
            ],
          )
        ],
      ),
    );
  }
}

步骤 8: lib/features/trip/ui/trip_page 文件夹中新建一个 dart 文件并将其命名为 trip_details.dart

步骤 9:打开 trip_details.dart 文件并使用以下代码更新该文件,以创建一个列,该列使用您创建的 SelectedTripCard 小部件,用于展示行程详细信息。

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,
            ),
          ],
        );

      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'),
        );
    }
  }
}

步骤 10:在 lib/features/trip/ui/trip_page 文件夹中新建一个 dart 文件并将其命名为 trip_page.dart

步骤 11:打开 trip_page.dart 文件并使用以下代码更新该文件,以创建 TripPage,该页面将使用 tripId 获取行程详细信息。TripPage 将使用您之前创建的 TripDetails 来显示数据。

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/trip/controller/trip_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_details.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),
      ),
      body: TripDetails(
        tripId: tripId,
        trip: tripValue,
      ),
    );
  }
}

步骤 12:打开 lib/common/navigation/router/router.dart 文件并更新该文件,以添加 TripPage 路由。

    GoRoute(
      path: '/trip/:id',
      name: AppRoute.trip.name,
      builder: (context, state) {
        final tripId = state.pathParameters['id']!;
        return TripPage(tripId: tripId);
      },
    ),

router.dart 文件应当如以下代码段所示。

import 'package:amplify_trips_planner/common/navigation/router/routes.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: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);
      },
    ),
  ],
  errorBuilder: (context, state) => Scaffold(
    body: Center(
      child: Text(state.error.toString()),
    ),
  ),
);

向应用程序中添加编辑行程页面

步骤 1:在 lib/features/trip/ui 文件夹中新建一个文件夹并将其命名为 edit_trip_page,然后在其中创建 edit_trip_page.dart 文件。

步骤 2:打开 edit_trip_page.dart 文件并使用以下代码更新该文件,创建可供用户编辑所选行程的 UI。

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/trip/controller/trip_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 EditTripPage extends ConsumerStatefulWidget {
  const EditTripPage({
    required this.trip,
    super.key,
  });

  final Trip trip;

  @override
  EditTripPageState createState() => EditTripPageState();
}

class EditTripPageState extends ConsumerState<EditTripPage> {
  @override
  void initState() {
    tripNameController.text = widget.trip.tripName;
    destinationController.text = widget.trip.destination;

    startDateController.text =
        widget.trip.startDate.getDateTime().format('yyyy-MM-dd');

    endDateController.text =
        widget.trip.endDate.getDateTime().format('yyyy-MM-dd');

    super.initState();
  }

  final formGlobalKey = GlobalKey<FormState>();
  final tripNameController = TextEditingController();
  final destinationController = TextEditingController();
  final startDateController = TextEditingController();

  final endDateController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text(
          'Amplify Trips Planner',
        ),
        leading: IconButton(
          onPressed: () {
            context.goNamed(
              AppRoute.trip.name,
              pathParameters: {'id': widget.trip.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: 'Trip Name',
                  controller: tripNameController,
                  keyboardType: TextInputType.name,
                ),
                const SizedBox(
                  height: 20,
                ),
                BottomSheetTextFormField(
                  labelText: 'Trip Destination',
                  controller: destinationController,
                  keyboardType: TextInputType.name,
                ),
                const SizedBox(
                  height: 20,
                ),
                BottomSheetTextFormField(
                  labelText: 'Start Date',
                  controller: startDateController,
                  keyboardType: TextInputType.datetime,
                  onTap: () async {
                    final pickedDate = await showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime(2000),
                      lastDate: DateTime(2101),
                    );

                    if (pickedDate != null) {
                      startDateController.text =
                          pickedDate.format('yyyy-MM-dd');
                    }
                  },
                ),
                const SizedBox(
                  height: 20,
                ),
                BottomSheetTextFormField(
                  labelText: 'End Date',
                  controller: endDateController,
                  keyboardType: TextInputType.datetime,
                  onTap: () async {
                    if (startDateController.text.isNotEmpty) {
                      final pickedDate = await showDatePicker(
                        context: context,
                        initialDate: DateTime.parse(startDateController.text),
                        firstDate: DateTime.parse(startDateController.text),
                        lastDate: DateTime(2101),
                      );

                      if (pickedDate != null) {
                        endDateController.text =
                            pickedDate.format('yyyy-MM-dd');
                      }
                    }
                  },
                ),
                const SizedBox(
                  height: 20,
                ),
                TextButton(
                  child: const Text('OK'),
                  onPressed: () async {
                    final currentState = formGlobalKey.currentState;
                    if (currentState == null) {
                      return;
                    }
                    if (currentState.validate()) {
                      final updatedTrip = widget.trip.copyWith(
                        tripName: tripNameController.text,
                        destination: destinationController.text,
                        startDate: TemporalDate(
                          DateTime.parse(startDateController.text),
                        ),
                        endDate: TemporalDate(
                          DateTime.parse(endDateController.text),
                        ),
                      );

                      await ref
                          .watch(
                              tripControllerProvider(widget.trip.id).notifier)
                          .updateTrip(updatedTrip);
                      if (context.mounted) {
                        context.goNamed(
                          AppRoute.trip.name,
                          pathParameters: {'id': widget.trip.id},
                          extra: updatedTrip,
                        );
                      }
                    }
                  }, //,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

步骤 3:打开 lib/common/navigation/router/router.dart 文件并更新该文件,以添加 EditTripPage 路由。

    GoRoute(
      path: '/edittrip/:id',
      name: AppRoute.editTrip.name,
      builder: (context, state) {
        return EditTripPage(
          trip: state.extra! as Trip,
        );
      },
    ),  

router.dart 文件应当如以下代码段所示。

import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/features/trip/ui/edit_trip_page/edit_trip_page.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,
        );
      },
    ),    
  ],
  errorBuilder: (context, state) => Scaffold(
    body: Center(
      child: Text(state.error.toString()),
    ),
  ),
);

步骤 4:在模拟器中运行应用程序,并尝试以下操作:

  • 创建新行程
  • 编辑新创建的行程
  • 上传行程图片
  • 删除行程

下面是一个使用 iPhone 模拟器的示例。

结果

在本模块中,您已使用 AWS Amplify 将文件存储添加到使用 Amazon S3 的应用程序中,以便用户在应用程序中上传并查看图片。

恭喜您!

恭喜您!您使用 AWS Amplify 成功创建了一个跨平台 Flutter 移动应用程序!您为应用程序添加了身份验证功能,允许用户注册、登录和管理账户。应用程序中还添加了已配置 Amazon DynamoDB 数据库的可扩展 GraphQL API,从而允许用户创建、读取、更新和删除行程。另外,您也使用 Amazon S3 添加了云存储,这样用户就可以在应用程序中上传和查看图片。

清除资源

您现在已经完成了本教程,可以在应用程序的根文件夹中运行以下命令删除后台资源,以避免产生意外费用。

amplify delete

后续步骤

查看本系列中的第二个指南,为应用程序添加一些新功能。

您还可以查看下面这些推荐的后续步骤,了解有关 Amplify 的更多信息,提供反馈,并结识其他使用 Amplify 进行构建的开发人员。

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