使用 AWS Amplify 构建 Flutter 移动应用程序 - 第 2 部分
使用嵌套数据和 Amplify 函数创建 iOS 版和 Android 版行程规划应用程序
模块 2:添加过往行程功能
概述
在本模块中,我们将实现程序逻辑及 UI(用户界面),用以显示过往行程。您还将在应用程序中添加一个导航侧边栏,允许用户浏览您将在本教程中介绍的各个不同页面。
要完成的目标
- 在本模块中,您将:
- 在应用程序中添加导航侧边栏
- 添加行程 TripGridViewItem 小部件
- 实现过往行程列表页面
- 实现所选的过往行程详细信息页面
完成所需最短时间
40 分钟
操作步骤
在应用程序中添加导航侧边栏
步骤 1:打开文件 lib/common/navigation/router/routes.dart。在其中添加 enum 值 pastTrips 和 pastTrip,更新该文件。文件 routes.dart 应如下所示:
enum AppRoute {
home,
trip,
editTrip,
pastTrips,
pastTrip,
}
步骤 2:在 lib/common/ui 文件夹中创建一个新 dart 文件,并将其命名为 the_navigation_drawer.dart。
步骤 3:打开 navigation_drawer.dart 文件,在其中添加以下代码,更新该文件,以创建用于导航到行程路线和过往行程路线的选项。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class TheNavigationDrawer extends ConsumerWidget {
const TheNavigationDrawer({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: Color(constants.primaryColorDark),
),
padding: EdgeInsets.all(16),
child: Column(
children: [
SizedBox(height: 10),
Text(
'Amplify Trips Planner',
style: TextStyle(fontSize: 22, color: Colors.white),
),
],
),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Trips'),
onTap: () {
context.goNamed(
AppRoute.home.name,
);
},
),
ListTile(
leading: const Icon(Icons.category),
title: const Text('Past Trips'),
onTap: () {
context.goNamed(
AppRoute.pastTrips.name,
);
},
),
],
),
);
}
}
步骤 4:打开 lib/features/trip/ui/trips_list/trips_list_page.dart 文件,在其中添加导航侧边栏,更新该文件。
drawer: const TheNavigationDrawer(),
trips_list_page.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/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),
),
drawer: const TheNavigationDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: () {
showAddTripDialog(context);
},
backgroundColor: const Color(constants.primaryColorDark),
child: const Icon(Icons.add),
),
body: TripsListGridView(
tripsList: tripsListValue,
),
);
}
}
步骤 5: 打开 lib/features/trip/ui/trip_page/trip_page.dart 文件,在其中添加导航侧边栏,更新该文件。
drawer: const TheNavigationDrawer(),
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: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(),
body: TripDetails(
tripId: tripId,
trip: tripValue,
),
);
}
}
更新行程 TripGridViewItem 小部件
步骤 1:应用程序将通过让卡片组件变灰,突出显示过往行程。为了实现此功能,您需要使用滤镜变换颜色。打开 lib/common/utils/colors.dart 文件,将其更新为包含如下所示的内容,以便为 colorFilter 定义类型为 List 的常量。
const List<double> greyoutMatrix = [
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
];
colors.dart 文件现在看上去应当与下面的代码段相似。
import 'package:flutter/material.dart';
const Map<int, Color> primarySwatch = {
50: Color.fromRGBO(255, 207, 68, .1),
100: Color.fromRGBO(255, 207, 68, .2),
200: Color.fromRGBO(255, 207, 68, .3),
300: Color.fromRGBO(255, 207, 68, .4),
400: Color.fromRGBO(255, 207, 68, .5),
500: Color.fromRGBO(255, 207, 68, .6),
600: Color.fromRGBO(255, 207, 68, .7),
700: Color.fromRGBO(255, 207, 68, .8),
800: Color.fromRGBO(255, 207, 68, .9),
900: Color.fromRGBO(255, 207, 68, 1),
};
const MaterialColor primaryColor = MaterialColor(0xFFFFCF44, primarySwatch);
const int primaryColorDark = 0xFFFD9725;
const List<double> greyoutMatrix = [
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
];
步骤 2:打开 lib/features/trip/ui/trips_gridview/trip_gridview_item.dart 文件,在其中添加以下代码,更新该文件。应用程序将使用小部件在 GridView 中显示行程卡。如果行程为过往行程,将通过上述您所创建的颜色滤镜和常量将其变为灰色。
注意:VSCode 将在 trips_list_gridview.dart 文件中显示一个关于 TripGridViewItem 小部件缺少 isPast 参数的错误。您将在后面的步骤中修复此错误。
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/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,
required this.isPast,
super.key,
});
final Trip trip;
final bool isPast;
@override
Widget build(BuildContext context) {
return InkWell(
splashColor: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(15),
onTap: () {
context.goNamed(
isPast ? AppRoute.pastTrip.name : AppRoute.trip.name,
pathParameters: {'id': trip.id},
extra: trip,
);
},
child: isPast
? ColorFiltered(
colorFilter: const ColorFilter.matrix(constants.greyoutMatrix),
child: TripGridViewItemCard(
trip: trip,
),
)
: TripGridViewItemCard(
trip: trip,
),
);
}
}
步骤 3:打开 lib/features/trip/ui/trips_gridview/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,
required this.isPast,
super.key,
});
final AsyncValue<List<Trip>> tripsList;
final bool isPast;
@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,
isPast: isPast,
);
}).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'),
);
}
}
}
步骤 4:打开 lib/features/trip/ui/trips_list/trips_list_page.dart 文件,在其中设置 isPast 参数,更新该文件。
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/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),
),
drawer: const TheNavigationDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: () {
showAddTripDialog(context);
},
backgroundColor: const Color(constants.primaryColorDark),
child: const Icon(Icons.add),
),
body: TripsListGridView(
tripsList: tripsListValue,
isPast: false,
),
);
}
}
在应用程序中添加 PastTripsList 页面
步骤 1:在 lib/feature/trip/controller 文件夹内部创建一个新的 dart 文件并将其命名为 past_trips_list_controller.dart。
步骤 2:打开 past_trips_list_controller.dart 文件,在其中添加以下代码,更新该文件。UI 将使用控制器通过 tripsRepository.getPastTrips() 函数获取用户过去的行程。
注意:VSCode 将显示由于缺少 past_trips_list_controller.g.dart 文件而导致出现的错误。您将在后面的步骤中修复此错误。
import 'dart:async';
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 'past_trips_list_controller.g.dart';
@riverpod
class PastTripsListController extends _$PastTripsListController {
Future<List<Trip>> _fetchTrips() async {
final tripsRepository = ref.read(tripsRepositoryProvider);
final trips = await tripsRepository.getPastTrips();
return trips;
}
@override
FutureOr<List<Trip>> build() async {
return _fetchTrips();
}
}
步骤 3:前往应用程序的根目录,然后在终端运行以下命令。
dart run build_runner build -d
这将在 lib/feature/trip/controller 文件夹中生成 past_trips_list_controller.g.dart 文件。
步骤 4:在 lib/features/trip/ui 文件夹中创建一个新文件夹,将其命名为 past_trips,然后在其中创建文件 past_trips_list.dart。
步骤 5:打开 past_trips_list.dart 文件并使用以下代码更新该文件,以创建要用来显示行程的 GridView。
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/past_trips_list_controller.dart';
import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trips_list_gridview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PastTripsList extends ConsumerWidget {
const PastTripsList({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tripsListValue = ref.watch(pastTripsListControllerProvider);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
backgroundColor: const Color(constants.primaryColorDark),
),
drawer: const TheNavigationDrawer(),
body: TripsListGridView(
tripsList: tripsListValue,
isPast: true,
),
);
}
}
步骤 6:打开 lib/common/navigation/router/router.dart 文件,在其中添加 PastTripsList 路线,更新该文件。
GoRoute(
path: '/pasttrips',
name: AppRoute.pastTrips.name,
builder: (context, state) => const PastTripsList(),
),
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/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: '/pasttrips',
name: AppRoute.pastTrips.name,
builder: (context, state) => const PastTripsList(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
在应用程序中添加 PastTrips 详细信息页面
步骤 1:在 lib/features/trip/ui 文件夹中创建一个新文件夹,将其命名为 past_trip_page,然后在其中创建文件 selected_past_trip_card.dart。
步骤 2:打开 selected_past_trip_card.dart 文件,在其中添加以下代码,更新该文件。我们将在此处检查是否有过往行程图像,并将其显示在卡小部件中。如果没有图像,我们将使用应用程序资产中的占位图像。
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class SelectedPastTripCard extends StatelessWidget {
const SelectedPastTripCard({
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(
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),
height: 150,
child: trip.tripImageUrl != null
? Stack(
children: [
const Center(child: CircularProgressIndicator()),
CachedNetworkImage(
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,
),
),
],
),
);
}
}
步骤 3:在 lib/features/trip/ui/past_trip_page 文件夹内部创建一个新的 dart 文件并将其命名为 past_trip_details.dart。
步骤 4:打开 past_trip_details.dart 文件,在其中添加以下代码,更新该文件,以创建 PastTripDetails,将在其中使用您在上面创建的 SelectedPastTripCard 显示数据。
import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/selected_past_trip_card.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PastTripDetails extends ConsumerWidget {
const PastTripDetails({
required this.trip,
super.key,
});
final AsyncValue<Trip> trip;
@override
Widget build(BuildContext context, WidgetRef ref) {
switch (trip) {
case AsyncData(:final value):
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 8,
),
SelectedPastTripCard(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 const Center(
child: Text('Error'),
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
步骤 5:在 lib/features/trip/ui/past_trip_page 文件夹中创建一个新的 dart 文件,并将其命名为 past_trip_page.dart。
步骤 6:打开 past_trip_page.dart 文件,在其中添加以下代码,更新该文件,以创建 PastTripPage,将在其中使用 tripId 获取过往行程详细信息。PastTripPage 将变灰,并使用您在上面创建的 PastTripDetails 显示数据。
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/past_trip_page/past_trip_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class PastTripPage extends ConsumerWidget {
const PastTripPage({
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(),
body: ColorFiltered(
colorFilter: const ColorFilter.matrix(constants.greyoutMatrix),
child: PastTripDetails(
trip: tripValue,
),
),
);
}
}
步骤 7:打开 /lib/common/navigation/router/router.dart 文件,在其中添加 PastTripPage 路线,更新该文件。
GoRoute(
path: '/pasttrip/:id',
name: AppRoute.pastTrip.name,
builder: (context, state) {
final tripId = state.pathParameters['id']!;
return PastTripPage(tripId: tripId);
},
),
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/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(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
步骤 8:在模拟器中运行该应用程序并创建一个行程。将其开始和结束日期设置为过去。下面是一个使用 iPhone 模拟器的示例。