使用 AWS Amplify 构建 Flutter 移动应用程序 - 第 2 部分
使用嵌套数据和 Amplify 函数为 iOS 和 Android 创建行程规划器应用程序
模块 4:添加个人资料功能
概述
在本模块中,您将在应用程序中添加展示和编辑用户个人资料的功能。您将添加一个用于在用户完成注册后创建个人资料的 Amplify 函数。
您将创建一种 GraphQL API,该 API 使用由 Amazon DynamoDB(一种 NoSQL 数据库)提供支持的 AWS AppSync(一种 GraphQL 托管服务)。
要完成的目标
在本模块中,您将:
- 向应用程序中添加行程数据模型
- 添加创建用户个人资料的 Amplify 函数
- 实现用于显示和编辑个人资料的 UI
最短完成时间
45 分钟
操作步骤
向应用程序中添加行程数据模型
步骤 1:您需要为 AWS Identity and Access Management (IAM) 身份验证配置 API,使 AWS Lambda 函数获得访问 GraphQL API 的权限。在应用程序的根目录中运行以下命令,更新 API 的授权模式。
amplify update api
步骤 2:选择 GraphQL 选项,然后选择 Authorization modes(验证模式),对其进行编辑。接受 Amazon Cognito User Pool 作为默认的验证类型。在 Configure additional auth types(配置额外验证类型)提示出现后,选择 Y。最后,从 additional authorization types(额外验证类型)中选择 IAM。
? Select from one of the below mentioned services: GraphQL
General information
- Name: amplifytripsplanner
- API endpoint: https://qmrokxalkng4xdaegffe4g2nsq.appsync-api.us-west-1.amazonaws.com/graphql
Authorization modes
- Default: Amazon Cognito User Pool
Conflict detection (required for DataStore)
- Disabled
? Select a setting to edit Authorization modes
? 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? Yes
? Choose the additional authorization types you want to configure for the API IAM
步骤 3:打开 amplify/backend/api/amplifytripsplanner/schema.graphql 文件,添加个人资料的数据模型
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!
}
type Profile
@model
@auth(
rules: [
{ allow: private, provider: iam }
{ allow: owner, operations: [read, update, create] }
]
) {
id: ID!
email: String!
firstName: String
lastName: String
homeCity: String
owner: String!
}
enum ActivityCategory { Flight, Lodging, Meeting, Restaurant }
步骤 4:在应用程序的根文件夹中运行以下命令以生成数据模型文件。
amplify codegen models
Amplify CLI 将在 lib/models 文件夹中生成 dart 文件。
步骤 5:运行 amplify push 命令,在云中创建资源。
步骤 6:按下 Enter(回车)键 Amplify CLI 将部署资源并显示确认消息,如屏幕截图中所示。
创建 Auth(验证)服务
步骤 1:在 lib/common/services 文件夹中创建一个新 dart 文件,并将其命名为 auth_service.dart。
步骤 2:打开 auth_service.dart 文件,使用以下代码更新该文件,创建 AuthService。服务将使用 Amplify 登出用户。
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService();
});
class AuthService {
AuthService();
Future<void> signOut() async {
try {
await Amplify.Auth.signOut();
} on Exception catch (e) {
debugPrint(e.toString());
}
}
}
步骤 3:打开 lib/common/navigation/ui/navigation_drawer.dart 文件。使用以下代码更新该文件,添加登出应用程序的选项。
ListTile(
leading: const Icon(Icons.exit_to_app),
title: const Text('Logout'),
onTap: () => ref.read(authServiceProvider).signOut(),
),
the_navigation_drawer.dart 文件应如下所示。
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/services/auth_service.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,
);
},
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: const Text('Logout'),
onTap: () => ref.read(authServiceProvider).signOut(),
),
],
),
);
}
}
添加创建用户个人资料的 Amplify 函数
步骤 1:进入应用程序的根目录,并在终端中运行以下命令,更新身份验证配置。
amplify update auth
步骤 2:浏览配置,对 Lambda 触发器的配置选择 Yes,并启用 Post Confirmation(确认之后的)触发器:
Please note that certain attributes may not be overwritten if you choose to use defaults settings.
You have configured resources that might depend on this Cognito resource. Updating this Cognito resource could have unintended side effects.
Using service: Cognito, provided by: awscloudformation
What do you want to do? Walkthrough all the auth configurations
Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)
Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
Do you want to enable 3rd party authentication providers in your identity pool? No
Do you want to add User Pool Groups? No
Do you want to add an admin queries API? No
Multifactor authentication (MFA) user login options: OFF
Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
Specify an email verification subject: Your verification code
Specify an email verification message: Your verification code is {####}
Do you want to override the default password policy for this User Pool? No
Specify the app's refresh token expiration period (in days): 30
Do you want to specify the user attributes this app can read and write? No
Do you want to enable any of the following capabilities?
Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? Yes
? Which triggers do you want to enable for Cognito Post Confirmation
? What functionality do you want to use for Post Confirmation Create your own module
Successfully added resource amplifytripsplannere57d0b5cPostConfirmation locally.
Amplify CLI 将添加一个新的文件夹用于存放函数,其中包含 Post Confirmation 触发器,您将在该触发器中编写函数来创建用户的个人资料。
步骤 3:打开 function 文件夹中的 src 文件夹中的 package.json 文件。
步骤 4:按下方所示更新 package.json 文件即可安装所需依赖库。
"dependencies": {
"axios": "latest",
"@aws-crypto/sha256-js": "^2.0.1",
"@aws-sdk/credential-provider-node": "^3.76.0",
"@aws-sdk/protocol-http": "^3.58.0",
"@aws-sdk/signature-v4": "^3.58.0",
"node-fetch": "2"
},
文件 package.json 应如下所示:
{
"name": "amplifytripsplannere57d0b5cPostConfirmation",
"version": "2.0.0",
"description": "Lambda function generated by Amplify",
"main": "index.js",
"license": "Apache-2.0",
"dependencies": {
"axios": "latest",
"@aws-crypto/sha256-js": "^2.0.1",
"@aws-sdk/credential-provider-node": "^3.76.0",
"@aws-sdk/protocol-http": "^3.58.0",
"@aws-sdk/signature-v4": "^3.58.0",
"node-fetch": "2"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.92"
}
}
步骤 5:打开 function 文件夹中的 src 文件夹中的 custom.js 文件。
步骤 6:按照以下示例更新 custom.js 文件即可运行一次 GraphQL mutation(变更)操作,将创建个人资料记录所需的变量作为参数传回。
const { Sha256 } = require("@aws-crypto/sha256-js");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { SignatureV4 } = require("@aws-sdk/signature-v4");
const { HttpRequest } = require("@aws-sdk/protocol-http");
const { default: fetch, Request } = require("node-fetch");
const GRAPHQL_ENDPOINT = process.env.API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIENDPOINTOUTPUT;
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
const query = /* GraphQL */ `
mutation createProfile($email: String!,$owner: String!) {
createProfile(input: {
email: $email,
owner: $owner,
}) {
email
}
}
`;
/**
* @type {import('@types/aws-lambda').PostConfirmationTriggerHandler}
*/
exports.handler = async (event) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
const variables = {
email: event.request.userAttributes.email,
owner: `${event.request.userAttributes.sub}::${event.userName}`
};
const endpoint = new URL(GRAPHQL_ENDPOINT);
const signer = new SignatureV4({
credentials: defaultProvider(),
region: AWS_REGION,
service: 'appsync',
sha256: Sha256
});
const requestToBeSigned = new HttpRequest({
method: 'POST',
headers: {
'Content-Type': 'application/json',
host: endpoint.host
},
hostname: endpoint.host,
body: JSON.stringify({ query, variables }),
path: endpoint.pathname
});
const signed = await signer.sign(requestToBeSigned);
const request = new Request(endpoint, signed);
let statusCode = 200;
let body;
let response;
try {
response = await fetch(request);
body = await response.json();
if (body.errors) statusCode = 400;
} catch (error) {
statusCode = 500;
body = {
errors: [
{
message: error.message
}
]
};
}
console.log(`statusCode: ${statusCode}`);
console.log(`body: ${JSON.stringify(body)}`);
return {
statusCode,
body: JSON.stringify(body)
};
};
步骤 7:导航到应用程序的根目录,并在终端中运行以下命令,更新上面创建的函数的配置。
amplify update function
步骤 8:选择上面已创建的函数,并更新 Resource access permissions(资源访问权限),使函数能够访问 API 的 Query(查询)、Mutation(变更)和 Subscription(订阅)功能。
? Select the Lambda function you want to update amplifytripsplannere57d0b5cPostConfi
rmation
General information
- Name: amplifytripsplannere57d0b5cPostConfirmation
- Runtime: nodejs
Resource access permission
- Not configured
Scheduled recurring invocation
- Not configured
Lambda layers
- Not configured
Environment variables:
- Not configured
Secrets configuration
- Not configured
? Which setting do you want to update? Resource access permissions
? Select the categories you want this function to have access to. api
? Select the operations you want to permit on amplifytripsplanner Query, Mutation, S
ubscription
You can access the following resource attributes as environment variables from your Lambda function
API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIENDPOINTOUTPUT
API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIIDOUTPUT
? Do you want to edit the local lambda function now? No
步骤 9:运行 amplify push 命令,在云中创建资源。
步骤 10:按下 Enter 键。Amplify CLI 将部署资源并显示确认消息,如屏幕截图中所示。
创建个人资料文件夹
步骤 1:在 lib/features 中创建一个新文件夹,命名为 profile。
步骤 2:在 profile 文件夹中创建以下新文件夹:
- 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,
profile,
}
步骤 4:在 lib/features/profile/services 文件夹中创建一个新的 dart 文件并将其命名为 profile_api_service.dart。
步骤 5:打开 profile_api_service.dart 文件并使用以下代码段更新该文件,以创建包含如下函数的 ProfileAPIService:
- getProfile:向 Amplify API 查询用户个人资料,返回其详细信息。
- updateProfile:使用 Amplify API 更新用户个人资料。
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 profileAPIServiceProvider = Provider<ProfileAPIService>((ref) {
return ProfileAPIService();
});
class ProfileAPIService {
ProfileAPIService();
Future<Profile> getProfile() async {
try {
final request = ModelQueries.list(Profile.classType);
final response = await Amplify.API.query(request: request).response;
final profile = response.data!.items.first;
return profile!;
} on Exception catch (error) {
safePrint('getProfile failed: $error');
rethrow;
}
}
Future<void> updateProfile(Profile updatedProfile) async {
try {
await Amplify.API
.mutate(
request: ModelMutations.update(updatedProfile),
)
.response;
} on Exception catch (error) {
safePrint('updateProfile failed: $error');
}
}
}
步骤 7:打开 profile_repository.dart 文件,使用以下代码更新该文件:
import 'package:amplify_trips_planner/features/profile/service/profile_api_service.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
final profileAPIService = ref.read(profileAPIServiceProvider);
return ProfileRepository(profileAPIService);
});
class ProfileRepository {
ProfileRepository(this.profileAPIService);
final ProfileAPIService profileAPIService;
Future<Profile> getProfile() {
return profileAPIService.getProfile();
}
Future<void> update(Profile updatedProfile) async {
return profileAPIService.updateProfile(updatedProfile);
}
}
实现用于显示和编辑个人资料的 UI
步骤 2:打开 profile_controller.dart 文件,使用以下代码更新该文件。UI 将会使用该控件编辑个人资料详细信息。
请注意:VSCode 将会因为缺失 profile_controller.g.dart 而提示错误。我们将在下一步骤中修复该问题。
import 'package:amplify_trips_planner/features/profile/data/profile_repository.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'profile_controller.g.dart';
@riverpod
class ProfileController extends _$ProfileController {
Future<Profile> _fetchProfile() async {
final profileRepository = ref.read(profileRepositoryProvider);
return profileRepository.getProfile();
}
@override
FutureOr<Profile> build() async {
return _fetchProfile();
}
Future<void> updateProfile(Profile profile) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final profileRepository = ref.read(profileRepositoryProvider);
await profileRepository.update(profile);
return _fetchProfile();
});
}
}
dart run build_runner build -d
import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart';
import 'package:amplify_trips_planner/features/profile/controller/profile_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 EditProfileBottomSheet extends ConsumerWidget {
EditProfileBottomSheet({
required this.profile,
super.key,
});
final Profile profile;
final formGlobalKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context, WidgetRef ref) {
final firstNameController = TextEditingController(
text: profile.firstName != null ? profile.firstName! : '',
);
final lastNameController = TextEditingController(
text: profile.lastName != null ? profile.lastName! : '',
);
final homeCityController = TextEditingController(
text: profile.homeCity != null ? profile.homeCity! : '',
);
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: 'First Name',
controller: firstNameController,
keyboardType: TextInputType.name,
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Last Name',
controller: lastNameController,
keyboardType: TextInputType.name,
),
const SizedBox(
height: 20,
),
BottomSheetTextFormField(
labelText: 'Home City',
controller: homeCityController,
keyboardType: TextInputType.name,
),
const SizedBox(
height: 20,
),
TextButton(
child: const Text('OK'),
onPressed: () async {
final currentState = formGlobalKey.currentState;
if (currentState == null) {
return;
}
if (currentState.validate()) {
final updatedProfile = profile.copyWith(
firstName: firstNameController.text,
lastName: lastNameController.text,
homeCity: homeCityController.text,
);
await ref
.watch(profileControllerProvider.notifier)
.updateProfile(updatedProfile);
if (context.mounted) {
context.pop();
}
}
}, //,
),
],
),
),
);
}
}
import 'package:amplify_trips_planner/common/utils/colors.dart' as constants;
import 'package:amplify_trips_planner/features/profile/controller/profile_controller.dart';
import 'package:amplify_trips_planner/features/profile/ui/profile_page/edit_profile_bottomsheet.dart';
import 'package:amplify_trips_planner/models/ModelProvider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProfileListView extends ConsumerWidget {
const ProfileListView({
required this.profile,
super.key,
});
final AsyncValue<Profile> profile;
void editProfile(BuildContext context, Profile profile) async {
await showModalBottomSheet<void>(
isScrollControlled: true,
elevation: 5,
context: context,
builder: (BuildContext context) {
return EditProfileBottomSheet(
profile: profile,
);
},
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
switch (profile) {
case AsyncData(:final value):
return ListView(
children: [
Card(
child: ListTile(
leading: const Icon(
Icons.verified_user,
size: 50,
color: Color(constants.primaryColorDark),
),
title: Text(
value.firstName != null ? value.firstName! : 'Add your name',
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(value.email),
),
),
ListTile(
dense: true,
title: Text(
'Home',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
tileColor: Colors.grey,
),
Card(
child: ListTile(
title: Text(
value.firstName != null ? value.homeCity! : 'Add your city',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
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: () {
editProfile(context, value);
},
child: const Text('Edit'),
),
],
),
)
],
);
case AsyncError():
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text(
'Error',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () async {
ref.invalidate(profileControllerProvider);
},
child: const Text('Try again'),
),
],
);
case AsyncLoading():
return const Center(
child: CircularProgressIndicator(),
);
case _:
return const Center(
child: Text('Error'),
);
}
}
}
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/profile/controller/profile_controller.dart';
import 'package:amplify_trips_planner/features/profile/ui/profile_page/profile_listview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProfilePage extends ConsumerWidget {
const ProfilePage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileValue = ref.watch(profileControllerProvider);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Amplify Trips Planner',
),
backgroundColor: const Color(constants.primaryColorDark),
),
drawer: const TheNavigationDrawer(),
body: ProfileListView(
profile: profileValue,
),
);
}
}
GoRoute(
path: '/profile',
name: AppRoute.profile.name,
builder: (context, state) => const ProfilePage(),
),
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/profile/ui/profile_page/profile_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,
);
},
),
GoRoute(
path: '/profile',
name: AppRoute.profile.name,
builder: (context, state) => const ProfilePage(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () {
context.goNamed(
AppRoute.profile.name,
);
},
),
import 'package:amplify_trips_planner/common/navigation/router/routes.dart';
import 'package:amplify_trips_planner/common/services/auth_service.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,
);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () {
context.goNamed(
AppRoute.profile.name,
);
},
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: const Text('Logout'),
onTap: () => ref.read(authServiceProvider).signOut(),
),
],
),
);
}
}
步骤 12:在模拟器或仿真器中运行应用程序,创建一个新用户,然后导航到 settings(设置)屏幕并更新用户个人资料。下面是一个使用 iPhone 模拟器的示例。
请注意:
- 由于数据模式发生更改,您需要从模拟器或仿真器中删除应用程序及其内容。
- 如果在设置页面遇到旋转加载图标和流错误,可能是因为 VTL 解析器未正确更新。要解决此问题,请在 schema.graphql 文件中添加一行空行,并运行 amplify push 命令。
结果
在本模块中,您向应用程序中添加了展示和编辑用户个人资料的功能。您使用了一个 Amplify 函数来创建用户的个人资料。您还实现了用于显示个人资料详细信息和更新资料的 UI。