使用 AWS Amplify 构建 Flutter 移动应用程序 - 第 1 部分
创建适用于 iOS 和 Android 的行程规划器应用程序
模块 3:添加 API
概述
在本模块中,您将使用 Amplify CLI 向应用程序中添加一个用于检索行程数据和将此类数据持久化的 API。您将创建一种 GraphQL API,该 API 使用由 Amazon DynamoDB(一种 NoSQL 数据库)提供支持的 AWS AppSync(一种 GraphQL 托管服务)。
要完成的目标
- 向应用程序中添加 Amplify API
- 向应用程序中添加行程数据模型
- 对行程功能执行 CRUD 操作和工作流
- 实现行程列表 UI
最短完成时间
20 分钟
操作步骤
向应用程序中添加 Amplify API
步骤 1:前往应用程序的根文件夹,然后在终端运行以下命令来预配 Amplify API 资源。
amplify add api
步骤 2:若要创建 GraphQL API,请在系统提示时输入以下内容:
? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue A
uthorization modes: API key (default, expiration time: 7 days from now)
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Configure additional auth types? No
? Here is the GraphQL API that we will create. Select a setting to edit or continue C
ontinue
? Choose a schema template: Blank Schema
✅ GraphQL schema compiled successfully.
Amplify CLI 将为该 API 添加一个新文件夹,其中包含 schema.graphql 文件,您可以在该文件中定义应用程序模型。
步骤 3:打开 schema.graphql 文件并使用以下代码更新该文件,以定义行程模型。
type Trip @model @auth(rules: [{ allow: owner }]) {
id: ID!
tripName: String!
destination: String!
startDate: AWSDate!
endDate: AWSDate!
tripImageUrl: String
tripImageKey: String
}
向应用程序中添加行程数据模型
步骤 1:在应用程序的根文件夹中运行以下命令以生成行程模型文件。
amplify codegen models
Amplify CLI 将在 lib/models 文件夹中生成 dart 文件。
步骤 2:运行 amplify push 命令,在云中创建资源。
步骤 3:按下 Enter 键。Amplify CLI 将部署资源并显示确认消息,如屏幕截图中所示。
步骤 4:打开 main.dart 文件并更新 _configureAmplify() 函数(如代码中所示),以添加 Amplify DataStore 和 API 插件。
Future<void> _configureAmplify() async {
await Amplify.addPlugins([
AmplifyAuthCognito(),
AmplifyAPI(modelProvider: ModelProvider.instance),
]);
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 '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),
]);
await Amplify.configure(amplifyconfig);
}
对行程功能执行 CRUD 操作和工作流
步骤 1:在 lib/features/trip/services 文件夹中新建一个 dart 文件并将其命名为 trips_api_service.dart。
步骤 2:打开 trips_datastore_service.dart 文件并使用以下代码段更新该文件,以创建包含如下函数的 TripsAPIService:
- getTrips - 此函数将在 Amplify API 中查询正在进行和即将发生的行程并返回这些行程的列表。
- getPastTrips - 此函数将在 Amplify API 中查询过去的行程并返回这些行程的列表。
- getTrip - 此函数将在 Amplify API 中查询特定行程。
- addTrip、deleteTrip 和 updateTrip 分别用于在 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 tripsAPIServiceProvider = Provider<TripsAPIService>((ref) {
final service = TripsAPIService();
return service;
});
class TripsAPIService {
TripsAPIService();
Future<List<Trip>> getTrips() async {
try {
final request = ModelQueries.list(Trip.classType);
final response = await Amplify.API.query(request: request).response;
final trips = response.data?.items;
if (trips == null) {
safePrint('getTrips errors: ${response.errors}');
return const [];
}
trips.sort(
(a, b) =>
a!.startDate.getDateTime().compareTo(b!.startDate.getDateTime()),
);
return trips
.map((e) => e as Trip)
.where(
(element) => element.endDate.getDateTime().isAfter(DateTime.now()),
)
.toList();
} on Exception catch (error) {
safePrint('getTrips failed: $error');
return const [];
}
}
Future<List<Trip>> getPastTrips() async {
try {
final request = ModelQueries.list(Trip.classType);
final response = await Amplify.API.query(request: request).response;
final trips = response.data?.items;
if (trips == null) {
safePrint('getPastTrips errors: ${response.errors}');
return const [];
}
trips.sort(
(a, b) =>
a!.startDate.getDateTime().compareTo(b!.startDate.getDateTime()),
);
return trips
.map((e) => e as Trip)
.where(
(element) => element.endDate.getDateTime().isBefore(DateTime.now()),
)
.toList();
} on Exception catch (error) {
safePrint('getPastTrips failed: $error');
return const [];
}
}
Future<void> addTrip(Trip trip) async {
try {
final request = ModelMutations.create(trip);
final response = await Amplify.API.mutate(request: request).response;
final createdTrip = response.data;
if (createdTrip == null) {
safePrint('addTrip errors: ${response.errors}');
return;
}
} on Exception catch (error) {
safePrint('addTrip failed: $error');
}
}
Future<void> deleteTrip(Trip trip) async {
try {
await Amplify.API
.mutate(
request: ModelMutations.delete(trip),
)
.response;
} on Exception catch (error) {
safePrint('deleteTrip failed: $error');
}
}
Future<void> updateTrip(Trip updatedTrip) async {
try {
await Amplify.API
.mutate(
request: ModelMutations.update(updatedTrip),
)
.response;
} on Exception catch (error) {
safePrint('updateTrip failed: $error');
}
}
Future<Trip> getTrip(String tripId) async {
try {
final request = ModelQueries.get(
Trip.classType,
TripModelIdentifier(id: tripId),
);
final response = await Amplify.API.query(request: request).response;
final trip = response.data!;
return trip;
} on Exception catch (error) {
safePrint('getTrip failed: $error');
rethrow;
}
}
}
步骤 3:在 lib/features/trip/data 文件夹内部创建一个新的 dart 文件并将其命名为 trips_repository.dart。
步骤 4:打开 trips_repository.dart 文件并使用以下代码更新该文件:
import 'package:amplify_trips_planner/features/trip/service/trips_api_service.dart';
import 'package:amplify_trips_planner/models/Trip.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final tripsRepositoryProvider = Provider<TripsRepository>((ref) {
final tripsAPIService = ref.read(tripsAPIServiceProvider);
return TripsRepository(tripsAPIService);
});
class TripsRepository {
TripsRepository(this.tripsAPIService);
final TripsAPIService tripsAPIService;
Future<List<Trip>> getTrips() {
return tripsAPIService.getTrips();
}
Future<List<Trip>> getPastTrips() {
return tripsAPIService.getPastTrips();
}
Future<void> add(Trip trip) async {
return tripsAPIService.addTrip(trip);
}
Future<void> update(Trip updatedTrip) async {
return tripsAPIService.updateTrip(updatedTrip);
}
Future<void> delete(Trip deletedTrip) async {
return tripsAPIService.deleteTrip(deletedTrip);
}
Future<Trip> getTrip(String tripId) async {
return tripsAPIService.getTrip(tripId);
}
}
步骤 5:在 lib/feature/trip/controller 文件夹中新建一个 dart 文件并将其命名为 trips_list_controller.dart。
步骤 6:打开 trips_list_controller.dart 文件并使用以下代码更新该文件。该 UI 将使用控制器添加新行程,方法是创建行程条目并将其作为参数传递给 tripsRepository.add(trip) 函数。
注意:VSCode 会因缺少 trips_list_controller.g.dart 文件而报错。
您将在下一步中修复此错误。
import 'dart:async';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_trips_planner/features/trip/data/trips_repository.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'trips_list_controller.g.dart';
@riverpod
class TripsListController extends _$TripsListController {
Future<List<Trip>> _fetchTrips() async {
final tripsRepository = ref.read(tripsRepositoryProvider);
final trips = await tripsRepository.getTrips();
return trips;
}
@override
FutureOr<List<Trip>> build() async {
return _fetchTrips();
}
Future<void> addTrip({
required String name,
required String destination,
required String startDate,
required String endDate,
}) async {
final trip = Trip(
tripName: name,
destination: destination,
startDate: TemporalDate(DateTime.parse(startDate)),
endDate: TemporalDate(DateTime.parse(endDate)),
);
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final tripsRepository = ref.read(tripsRepositoryProvider);
await tripsRepository.add(trip);
return _fetchTrips();
});
}
Future<void> removeTrip(Trip trip) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final tripsRepository = ref.read(tripsRepositoryProvider);
await tripsRepository.delete(trip);
return _fetchTrips();
});
}
}
步骤 7:前往应用程序的根文件夹,然后在终端运行以下命令。
dart run build_runner build -d
这将在 lib/feature/trip/controller 文件夹中生成 trips_list_controller.g.dart 文件。
实现行程列表 UI
步骤 1:在 lib/common/ui 文件夹中新建一个 dart 文件并将其命名为 bottomsheet_text_form_field.dart。
步骤 2:打开 bottomsheet_text_form_field.dart 文件并使用以下代码段更新该文件,以创建 BottomSheetTextFormField 窗口部件,通过该窗口部件可以构建 TextFormField,该应用程序将在表单中使用 TextFormField 创建新行程。
import 'package:flutter/material.dart';
class BottomSheetTextFormField extends StatelessWidget {
const BottomSheetTextFormField({
required this.labelText,
required this.controller,
required this.keyboardType,
this.onTap,
super.key,
});
final String labelText;
final TextEditingController controller;
final TextInputType keyboardType;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
autofocus: true,
autocorrect: false,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a value';
}
return null;
},
decoration: InputDecoration(
labelText: labelText,
),
onTap: onTap,
);
}
}
步骤 3:在 lib/common/utils 文件夹中新建一个 dart 文件并将其命名为 date_time_formatter.dart。
步骤 4:打开 date_time_formatter.dart 文件并使用以下代码更新该文件,以创建 DateTimeFormatter 扩展来格式化 DateTime 值。
import 'package:intl/intl.dart';
extension DateTimeFormatter on DateTime {
String format(String format) {
return DateFormat(format).format(this);
}
}
步骤 5:在 lib/features/trip/ui/trips_list 文件夹中新建一个 dart 文件并将其命名为 add_trip_bottomsheet.dart。
步骤 6:打开 add_trip_bottomsheet.dart 文件并使用以下代码更新该文件。这样,我们就可以向用户提供一张表单,供用户提交创建新行程所需的详细信息。
现在,我们在表单中使用 BottomSheetTextFormField 窗口部件和 DateTimeFormatter 扩展。
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/trip/controller/trips_list_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class AddTripBottomSheet extends ConsumerStatefulWidget {
const AddTripBottomSheet({
super.key,
});
@override
AddTripBottomSheetState createState() => AddTripBottomSheetState();
}
class AddTripBottomSheetState extends ConsumerState<AddTripBottomSheet> {
final formGlobalKey = GlobalKey<FormState>();
final tripNameController = TextEditingController();
final destinationController = TextEditingController();
final startDateController = TextEditingController();
final endDateController = TextEditingController();
@override
Widget build(BuildContext context) {
return 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) {
print(pickedDate.format('yyyy-MM-dd'));
startDateController.text = pickedDate.format('yyyy-MM-dd');
print(startDateController.text);
}
},
),
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) {
print(pickedDate.format('yyyy-MM-dd'));
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()) {
await ref.watch(tripsListControllerProvider.notifier).addTrip(
name: tripNameController.text,
destination: destinationController.text,
startDate: startDateController.text,
endDate: endDateController.text,
);
if (context.mounted) {
context.pop();
}
}
}, //,
),
],
),
),
);
}
}
步骤 7:在 lib/features/trip/ui 文件夹中新建一个文件夹并将其命名为 trips_gridview,然后在这个文件夹中创建 trip_gridview_item_card.dart 文件。
步骤 8:打开 trip_gridview_item_card.dart 文件并使用以下代码更新该文件。这将创建一个 Card 窗口部件,用于在行程列表页面上显示行程详细信息。
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/models/ModelProvider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class TripGridViewItemCard extends StatelessWidget {
const TripGridViewItemCard({
required this.trip,
super.key,
});
final Trip trip;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Column(
children: [
Expanded(
child: Container(
height: 500,
alignment: Alignment.center,
color: const Color(constants.primaryColorDark),
child: Stack(
children: [
Positioned.fill(
child: trip.tripImageUrl != null
? Stack(
children: [
const Center(child: CircularProgressIndicator()),
CachedNetworkImage(
errorWidget: (context, url, dynamic error) =>
const Icon(Icons.error_outline_outlined),
imageUrl: trip.tripImageUrl!,
cacheKey: trip.tripImageKey,
width: double.maxFinite,
height: 500,
alignment: Alignment.topCenter,
fit: BoxFit.fill,
),
],
)
: Image.asset(
'images/amplify.png',
fit: BoxFit.contain,
),
),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
trip.destination,
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(color: Colors.white),
),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(2, 8, 8, 4),
child: DefaultTextStyle(
softWrap: false,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
trip.tripName,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Colors.black54),
),
),
Text(
trip.startDate.getDateTime().format('MMMM dd, yyyy'),
style: const TextStyle(fontSize: 12),
),
Text(
trip.endDate.getDateTime().format('MMMM dd, yyyy'),
style: const TextStyle(fontSize: 12),
),
],
),
),
),
],
),
);
}
}
步骤 9:在 lib/features/trip/ui/trips_gridview 文件夹中创建 trip_gridview_item.dart 文件。
步骤 10:打开 trip_gridview_item.dart 文件并使用以下代码更新该文件。该应用程序将使用该文件来显示行程列表网格。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trip_gridview_item_card.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class TripGridViewItem extends StatelessWidget {
const TripGridViewItem({
required this.trip,
super.key,
});
final Trip trip;
@override
Widget build(BuildContext context) {
return InkWell(
splashColor: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(15),
onTap: () {
context.goNamed(
AppRoute.trip.name,
pathParameters: {'id': trip.id},
extra: trip,
);
},
child: TripGridViewItemCard(
trip: trip,
),
);
}
}
步骤 11:在 lib/features/trip/ui/trips_gridview 文件夹中创建 trips_list_gridview.dart 文件。
步骤 12:打开 trips_list_gridview.dart 文件并使用以下代码更新该文件。这样网格视图组件会显示行程列表。
import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trip_gridview_item.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TripsListGridView extends StatelessWidget {
const TripsListGridView({
required this.tripsList,
super.key,
});
final AsyncValue<List<Trip>> tripsList;
@override
Widget build(BuildContext context) {
switch (tripsList) {
case AsyncData(:final value):
return value.isEmpty
? const Center(
child: Text('No Trips'),
)
: OrientationBuilder(
builder: (context, orientation) {
return GridView.count(
crossAxisCount:
(orientation == Orientation.portrait) ? 2 : 3,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
padding: const EdgeInsets.all(4),
childAspectRatio:
(orientation == Orientation.portrait) ? 0.9 : 1.4,
children: value.map((tripData) {
return TripGridViewItem(
trip: tripData,
);
}).toList(growable: false),
);
},
);
case AsyncError():
return const Center(
child: Text('Error'),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 13:打开 trips_list_page.dart 文件并使用以下代码更新该文件,以使用上面创建的 TripsListGridView 窗口部件显示行程。
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/features/trip/controller/trips_list_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trips_list_gridview.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_list/add_trip_bottomsheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TripsListPage extends ConsumerWidget {
const TripsListPage({
super.key,
});
Future<void> showAddTripDialog(BuildContext context) =>
showModalBottomSheet<void>(
isScrollControlled: true,
elevation: 5,
context: context,
builder: (sheetContext) {
return const AddTripBottomSheet();
},
);
@override
Widget build(BuildContext context, WidgetRef ref) {
final tripsListValue = ref.watch(tripsListControllerProvider);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
backgroundColor: const Color(constants.primaryColorDark),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showAddTripDialog(context);
},
backgroundColor: const Color(constants.primaryColorDark),
child: const Icon(Icons.add),
),
body: TripsListGridView(
tripsList: tripsListValue,
),
);
}
}
步骤 14:在模拟器中运行该应用程序并创建一个行程。下面是一个使用 iPhone 模拟器的示例。
结果
在本模块中,您已经使用 Amplify 添加了一个用于行程的 GraphQL API,并在您的应用程序中配置了用来创建、读取、更新和删除行程的功能。