I’m going to show you how you can use Flutter and AWS Amplify to quickly go from nothing to a working cross-platform mobile application with authentication and backend infrastructure. What would usually take a small dev team a week or so to setup can be achieved in a fraction of the time using this toolkit.
跟随本教程,预计您将在一小时内完成。虽然我自己在解决各种问题时花费了数小时,但愿我已详细记录这些问题,以便您能避免重蹈覆辙。
这是成品。若您希望获得“我之前做好的版本”,请按照README中的步骤操作,大约十五分钟即可运行起来。这里是GitHub链接。
本教程分为五个部分:
- 前置条件与代码库设置
- 添加身份验证
- 上传头像
- 存储用户详情
- 增添设计亮点
建议
Flutter作为一个成熟平台,已使用多年,拥有活跃的社区以及众多插件和扩展,能实现大多数功能。
Amplify同样实力强劲;然而,我发现其API功能难以驾驭,且Flutter库未能及时更新以匹配Amplify的最新公告和特性。特别是与AppSync GraphQL和DataStore(用于离线数据存储和同步)的协作相当脆弱(稍后可见)。
两者结合,对于加速移动应用原型开发极为有利,但当感觉是在强行让Amplify适应您的需求时,不妨考虑弃用它,转而直接使用AWS的抽象服务。
我构建的演示应用主要保存用户个人资料信息——这是许多应用的常见需求。用户可以创建账户并登录,上传个人头像,并提交一些个人信息。接下来,我们将深入探讨全栈开发——从使用Flutter和Dart编写应用代码,到利用DynamoDB等技术,全面覆盖你需要了解的内容。
第一部分:预备知识和搭建代码库
本教程假设你的机器上已配置以下内容:
Code Editor/ IDE | I use VSCode as it has a good set of Flutter and Dart plugins that speed up development, such as auto loading of dependencies, Dart linting, and intellisense. You’re free to use whatever IDE works for you though |
AWS Account | Create an AWS account if you don’t already have one. Visit AWS’ official page for steps to create an AWS account.
All of what we’ll use today is part of the free tier, so it shouldn’t cost you anything to follow this tutorial. |
AWS CLI and AWS Amplify CLI | Install AWS and Amplify CLI tooling.
Make sure you have an up-to-date version of Node and NPM (this is what the CLIs use). Visit Node.Js’ official website to download the up-to-date version. If you need to run multiple versions of Node, I recommend using NVM to manage and switch between them. To install AWS CLI, visit AWS’ official page. |
XCode (for iOS) | If you don’t have access to a Mac, you can deploy EC2 instances running MacOS in AWS these days, which you can use when you need to build iOS artifacts.
Download Xcode through the Mac App Store. Follow the rest of the steps here to set it up ready for iOS Flutter development. |
Android Studio (for Android) | Follow the steps here to be ready for Android Flutter development. |
Flutter SDK | Follow these steps to get Flutter and its dependencies up and running (if you’re on a Mac that is, other guides are available for other OSes). |
Flutter和Amplify提供脚手架工具来创建项目初始结构。按照特定顺序操作至关重要;否则,文件夹结构可能与工具预期不符,导致后续修正困难。
请确保首先使用Flutter创建代码库结构,然后在其内部初始化Amplify。
I used the official Flutter getting started documentation to kick things off for my demo.
我们先确认Flutter是否正确安装并已添加到PATH中,运行命令flutter doctor
进行检查。
如果你是首次涉足移动开发,可能需要处理几项配置。对我来说,包括:
- 安装Android Studio(及Android SDK CLI)。
- 安装XCode和CocoaPods。
- 同意Android和iOS工具的条款和条件。
创建应用代码库
当所有预备条件就绪后,你可以创建Flutter脚手架。这一步骤会生成我们将要工作的文件夹,因此请从父目录运行以下命令:
flutter create flutterapp --platforms=android,ios
I’ve specified Android and iOS as target platforms to remove the unnecessary config for other platforms (e.g. web, Windows, Linux).
在这个阶段,你可能想要重命名创建的顶级目录,以防你不想让它与你的应用程序名称一致。我将它从“flutterapp”更改为“flutter-amplify-tutorial”(我的git仓库名称)。
此时,Flutter已经为我们创建了七十三份文件。让我们来看看这些文件都是什么:
我们将花费大部分时间在ios/android
和lib/
文件夹上。在ios
和android
文件夹中,有可以分别用XCode和Android Studio打开的项目资源。这些项目作为平台无关的Dart代码与你的目标平台之间的互操作,你可以使用它们来针对各自的平台测试你的应用程序。现在让我们尝试在iOS上进行测试:
iOS设置
open -a Simulator
flutter run
在我的Mac上,使用最简化的XCode设置,这从无到有,直接运行了一个iPhone 14 Pro Max模拟器,上面运行着搭建好的Flutter应用,相当酷。
如果你看到以下内容:那么恭喜你,你已经成功生成了框架结构。
你也可以在XCode中打开ios/Runner.xcodeproj
项目,探索其内容,并在模拟器和物理设备上运行,就像处理任何其他XCode项目一样。
Android设置
Android的设置稍微复杂一些,因为你必须先在Android Studio中明确配置一个模拟器,然后才能运行它。首先在Android Studio中打开android/flutterapp_android.iml
项目,然后你可以配置并运行一个模拟器来测试应用程序。
给Android Studio一些时间,让它下载Gradle以及运行应用所需的所有依赖项——你可以在窗口右下角的进度条中跟踪这一过程。
当Android Studio准备就绪后,如果你已经在AVD中配置了模拟设备,你应该能够点击窗口右上角的播放按钮:
看哪,同样的应用出现在Android上:
这演示了创建新Flutter项目时提供的示例应用代码。在本教程的过程中,我们将逐步用我们自己的代码替换这些代码。
这是一个进行git提交的好时机,现在我们已经为Flutter开发搭建了基础。我们现在处于一个可以开始摆弄Flutter代码并同时在iOS和Android上看到结果的阶段。
Flutter使用Dart作为Android和iOS之间的中间语言,你将交互的所有代码都位于lib/
文件夹内。应该有一个main.dart
文件,这是我们开始动手的地方。
使用Amplify配置和部署新应用
既然我们的移动应用工具已经准备好工作,我们需要一些后端基础设施来支持应用的功能。
我们将使用AWS及其众多服务来支持我们的应用,但这一切都将通过AWS Amplify服务进行管理。其中大部分将对我们透明处理,我们不必担心要使用哪些服务,而是专注于我们想要部署哪些功能。
首先,在你的代码文件夹中运行以下命令:
amplify init
此命令在你的项目中初始化AWS Amplify。如果是初次使用,它会向你提出一系列问题。对于后续参与项目的合作者,运行此命令将使他们的本地环境预先配置好Amplify的设置。
这将创建一些初始的AWS资源来存储Amplify应用的配置和状态,主要是一个S3存储桶。
上述部署进度条和状态对某些人来说可能很熟悉——这是CloudFormation,与AWS CDK一样,Amplify在幕后使用CFN来配置所有必需的资源。你可以打开CloudFormation堆栈控制台来查看其运行情况:
最后,当CLI完成时,你应该会看到类似下面的确认信息,并且能够在Amplify控制台中看到你新部署的应用:
环境管理
AWS Amplify具有“环境”的概念,这是你的应用程序和资源的隔离部署。传统上,环境的概念必须在你的生态系统内创建(例如,CloudFormation,CDK),使用命名约定和参数等。在Amplify中,它是首要元素——你可以拥有多个环境,允许模式,如提供共享环境,变更通过推广(例如,Dev > QA > PreProd > Prod),以及为每个开发人员或特性分支提供环境。
Amplify还能为您配置和提供CI/CD服务,通过Amplify托管添加功能,并将其整合到您的应用中,构建一个端到端的开发生态系统。这包括设置CodeCommit、CodeBuild和CodeDeploy来处理源代码管理、构建和部署应用。本教程未涉及此部分,但它能用于自动化构建、测试及向应用商店发布更新。
第二部分:添加认证
通常,您需要了解AWS的认证服务Cognito以及如IAM等支持服务,并使用CloudFormation、Terraform或CDK等工具将其整合。而在Amplify中,操作简便至极:
amplify add auth
Amplify add
让您能为项目添加多种“特性”。幕后,Amplify通过CloudFormation部署并配置所有必需服务,使您能专注于应用功能而非底层架构。
虽说只需键入上述三个神奇词汇……但实际操作并非如此直白。Amplify会询问一系列问题,以了解您希望如何进行用户认证及设置哪些控制措施。选择“默认配置”,Amplify将以合理默认设置快速启动认证功能。为展示Amplify的可配置性,我将选择“手动配置”。
上述配置使您能够仅凭手机号码创建账户(无需电子邮件地址),并通过MFA进行验证,确保您是该号码的实际所有者,并用于后续登录尝试。我强烈建议使用OAuth作为标准化的认证机制,但为了简化,此处并未采用。
现在,当您添加功能时,它们不会立即生效。这就是为什么命令执行得异常迅速。所有这些命令都是为了准备您的Amplify应用配置(及本地环境)以便部署这些功能。
要部署功能(或任何配置更改),您需要执行一个推送操作:
amplify push
注意:这与amplify publish
命令不同,后者会构建并部署后端和前端服务。推送仅配置后端资源(在本教程中,我们只需构建移动应用,因此足够了)。
当您添加认证(或任何Amplify功能)时,Amplify会添加一个名为lib/amplifyconfiguration.dart
的dart文件。此文件被git忽略,因为它包含与您部署资源相关的敏感凭据,并会自动与您正在工作的Amplify环境同步。更多关于此信息,请参阅此处。
至此,我们已经完成了Amplify的设置,创建了应用程序和开发环境,并配置了Cognito进行身份验证。如果你正在跟随操作,现在是进行git提交的好时机,以便在需要时可以回退到这个点。Amplify应该已经为你创建了一个.gitignore
文件,排除了所有不必要的文件。
既然我们已经搭建好了后端认证基础设施,接下来就可以开始用Flutter构建我们的移动应用了。
在应用中验证用户身份
I’m following the steps outlined in the authentication for the AWS Amplify tutorial here.
这是利用amplify_flutter
内置的身份验证UI界面和工作流程。在pubspec.yaml
文件中的“dependencies”部分添加以下Amplify Flutter依赖项:
amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0
如果你没有使用VSCode中的Flutter和Dart扩展(或者没有使用VSCode),你需要在之后执行flutter pub get
命令。如果你正在使用,那么当你保存pubspec.yaml
文件时,VSCode会自动运行此命令。
有一种快速入门的方法来集成身份验证,它使用了一个预制的Authenticator UI库,非常适合快速启动一个可以稍后定制的登录流程。在本教程中,我们将使用这种方法来展示Amplify提供的丰富库集合,以及你如何迅速地将它们集成到你的应用中。
集成OOTB身份验证库的步骤位于此处。
我们可以将示例代码中配置的身份验证器装饰部件移植到Flutter快速启动示例提供的代码中,如下所示:
class _MyAppState extends State {
@override
Widget build(BuildContext context) {
return Authenticator(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// 这是你的应用程序主题。
//
// 尝试使用 "flutter run" 运行你的应用程序。你会看到
// 应用程序有一个蓝色的工具栏。然后,在不退出应用的情况下,尝试
// 将primarySwatch下改为Colors.green,然后调用
// "热重载"(在运行 "flutter run" 的控制台中按 "r",
// 或者只需保存更改以在Flutter IDE中进行 "热重载")。
// 注意计数器没有重置回零;应用程序
// 没有重新启动。
primarySwatch: Colors.blue,
useMaterial3: true),
home: const MyHomePage(title: 'Flutter Amplify Quickstart'),
builder: Authenticator.builder(),
));
}
@override
void initState() {
super.initState();
_configureAmplify();
}
void _configureAmplify() async {
try {
await Amplify.addPlugin(AmplifyAuthCognito());
await Amplify.configure(amplifyconfig);
} on Exception catch (e) {
print('Error configuring Amplify: $e');
}
}
}
什么是Widget?
它是Flutter中用于构建UI布局和组件的基本构建块。几乎所有Flutter布局中的内容都是Widget——列、脚手架、填充和样式、复杂组件等。Flutter入门文档中的示例使用了一个“Center”Widget后跟一个“Text”Widget来显示居中的文本,内容为“Hello World”。
上述代码通过一个身份验证器Widget装饰了MyHomePage
Widget,添加了AmplifyAuthCognito
插件,并采用了之前amplify add auth
命令在lib/amplifyconfiguration.dart
中生成的配置,以自动连接到您的AWS Cognito用户池。
在运行Flutter并演示身份验证集成时,我发现“运行pod安装”这一步耗时较长,需要耐心等待(将近5分钟)。
一旦完成身份验证的修改并且应用启动,你会看到一个基本但功能完备的登录界面。
通过使用“创建账户”流程,你可以输入手机号码和密码,随后会遇到一个多因素认证挑战以完成注册。此时,你可以在Cognito用户池中看到新创建的用户:
你也可以在虚拟Android设备上轻松测试这一点。如果你已安装Flutter和Dart插件,甚至无需离开VSCode,也不必打开Android Studio。只需在VSCode右下角选择当前活跃设备(iPhone)的名称,切换到已创建的虚拟Android设备,然后按下“F5”开始调试。体验与iOS非常相似:
在首次部署集成身份验证库后的应用时,我在尝试构建应用时遇到了以下异常:
uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared in library [:amplify_auth_cognito_android]
Flutter在这种情况下非常有帮助,因为在抛出堆栈跟踪后,它立即给出了建议:
Flutter SDK似乎已经在我们的build.gradle
文件中对此进行了覆盖:
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
...
defaultConfig {
// TODO: 指定您自己的唯一应用程序ID(参考:https://developer.android.com/studio/build/application-id.html)。
applicationId "com.example.flutterapp"
// 您可以根据应用程序需求更新以下值。
// 更多信息,请参阅:https://docs.flutter.dev/deployment/android#查看构建配置。
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
虽然Flutter作为最低要求,需要API 16才能使用(在flutter.gradle
中声明),但Amplify Auth库至少需要API 21。为解决此问题,只需将minSdkVersion
从“flutter.minSdkVersion”更改为“21”。
一旦认证完成,您将看到之前展示的示例“按钮点击器”应用。现在,是时候开始根据我们的需求进行定制了。
第三部分:上传个人头像
在此示例中,我们将利用此功能允许用户上传自己的照片,作为应用内的头像。
想要为您的应用添加存储功能?没问题,只需执行:
amplify add storage
Amplify将为您应用所需的云存储配置后端服务。Amplify轻松将Flutter与S3集成,允许您的应用用户存储对象。S3的灵活性允许您存储各种类型的资产,结合Cognito和Amplify,您可以轻松为用户的照片、视频、文件等提供私有存储区域。
文件可以设置为公开、受保护或私有访问:
Public | Read/Write/Delete by all users |
Protected | Creating Identify can Write and Delete, everyone else can Read |
Private | Read/Write/Delete only by Creating Identity |
对于我们的个人头像,我们将创建具有受保护访问权限的文件,以便只有用户可以更新和删除其头像,而应用内的其他用户则可以查看它。
我们将在此处开始为应用设计样式并构建结构。Flutter与Material Design系统紧密集成,该系统在移动应用开发中广泛使用,以提供一致的外观和感觉。它提供了一套跨平台兼容的组件,其样式可以被覆盖,以构建符合您品牌特色的体验。
Flutter的入门模板已经使用MaterialApp小部件搭建了一些小部件。我们之前用一个验证器小部件装饰了它。现在,我们将扩展MaterialApp的子部件MyHomePage,以提供一个头像图片。
您将小部件组合在一起形成一个树状结构,称为“小部件层次结构”。您总是从一个顶级小部件开始。在我们的应用中,是处理初始登录的验证器包装小部件。Scaffold
是一个很好的布局基础小部件:它常作为材料应用的顶级小部件使用;并且它有许多占位符,如浮动操作按钮、底部工作表(用于向上滑动以显示更多细节)、应用栏等。
首先,让我们添加一个指向网络URL的图像小部件。我们稍后将替换为我们拍摄并上传到S3的图像。我使用了以下资源来添加带有圆形占位符的图像:
- Flutter API
- Google Flutter
在嵌套列小部件的子项数组中,添加以下容器小部件:
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
Container(
width: 200,
height: 200,
decoration: const BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
fit: BoxFit.fill),
),
)
],
我们现在可以从网络显示图像:
接下来,我们将允许用户从其设备上的图像中选择一个头像。一番谷歌搜索后,我发现了一个抽象了选择图像细节的库:
- 谷歌搜索:“Pub Dev Packages Image Picker”
只需两行代码即可提示用户选择图片:
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
对于iOS,你需要在Xcode配置文件<项目根目录>/ios/Runner/Info.plist
中添加NSPhotoLibraryUsageDescription
键,以请求访问用户照片的权限;否则,应用将会崩溃。
我们将这一功能集成到一个GestureDetector
小部件中,当检测到点击时,会提示用户为其头像选择一张图片:
ImageProvider? _image;
...
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
GestureDetector(
onTap: _selectNewProfilePicture,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: _image ?? _placeholderProfilePicture(), fit: BoxFit.fill),
),
),
)
...
]
void _selectNewProfilePicture() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
var imageBytes = await image.readAsBytes();
setState(() {
_image = MemoryImage(imageBytes);
});
}
}
_placeholderProfilePicture() {
return const AssetImage("assets/profile-placeholder.png");
}
调用setState()
,更新传递给Flutter的Lambda中的小部件字段,以便它知道调用build()
函数,在此函数中可以使用更新后的状态重新绘制小部件。在我们的例子中,头像图片将被填充,因此我们将创建一个显示该图片的容器小部件。空值感知操作符??
在用户尚未选择图片时提供默认头像占位符。
你还需要将一个头像占位图添加到你的仓库中,并在pubspec.yml
文件中引用它,以便在构建过程中被识别。你可以使用我仓库中的图片,同时将其添加到你的pubspec.yml
文件中:
# 以下部分专为Flutter包而设。
flutter:
...
# 要向应用程序添加资源,请添加一个assets部分,如下所示:
assets:
- assets/profile-placeholder.png
此时,我们能够从设备的照片库中选择一个头像,并在应用中以圆形图片的形式展示。然而,这张图片并未在任何地方保存——一旦应用关闭,图片就会丢失(其他用户也无法看到您的头像)。
接下来,我们将把这一功能与云存储——AWS S3连接起来。当用户从其设备相册中选择照片时,我们将上传至S3中的私人区域,然后让图像控件从S3拉取图片(而非直接从设备获取)本身:
void _selectNewProfilePicture() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
var imageBytes = await image.readAsBytes();
final UploadFileResult result = await Amplify.Storage.uploadFile(
local: File.fromUri(Uri.file(image.path)),
key: profilePictureKey,
onProgress: (progress) {
safePrint('Fraction completed: ${progress.getFractionCompleted()}');
},
options:
UploadFileOptions(accessLevel: StorageAccessLevel.protected));
setState(() {
_image = MemoryImage(imageBytes);
});
}
}
现在,当用户从设备中选择图片时,我们的应用会上传至S3,并在屏幕上显示。
随后,我们将在应用启动时,使其从S3下载用户的头像:
@override
void initState() {
super.initState();
_retrieveProfilePicture();
}
void _retrieveProfilePicture() async {
final userFiles = await Amplify.Storage.list(
options: ListOptions(accessLevel: StorageAccessLevel.protected));
if (userFiles.items.any((element) => element.key == profilePictureKey)) {
final documentsDir = await getApplicationDocumentsDirectory();
final filepath = "${documentsDir.path}/ProfilePicture.jpg";
final file = File(filepath);
await Amplify.Storage.downloadFile(
key: profilePictureKey,
local: file,
options:
DownloadFileOptions(accessLevel: StorageAccessLevel.protected));
setState(() {
_image = FileImage(file);
});
} else {
setState(() {
_image = const AssetImage("assets/profile-placeholder.png");
});
}
}
接下来,我们将把头像逻辑重构为一个可重用的独立组件。您可以在我的GitHub仓库中查看完成后的组件,其中包含了上述所有逻辑。然后,您可以整理_MyHomePageStage
组件,并将新的小部件嵌入到层次结构中,如下所示:
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
'User Profile',
style: Theme.of(context).textTheme.titleLarge,
)),
const ProfilePicture(),
TextField(...
最后,为了完善头像功能,我们将添加一个加载指示器,向用户反馈正在进行的操作。我们将使用_isLoading
布尔字段来追踪图片加载状态,以此来切换显示的是指示器还是图片:
class _ProfilePictureState extends State<ProfilePicture> {
ImageProvider? _image;
bool _isLoading = true;
...
void _retrieveProfilePicture() async {
...
setState(() {
_image = FileImage(file);
_isLoading = false;
});
} else {
setState(() {
_image = const AssetImage("assets/profile-placeholder.png");
_isLoading = false;
});
}
}
void _selectNewProfilePicture() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_isLoading = true;
});
....
setState(() {
_image = MemoryImage(imageBytes);
_isLoading = false;
});
}
}
第四部分:存储用户详情(节选)
很好,我们现在已搭建起一个包含用户、认证和头像功能的移动应用框架。接下来,让我们尝试创建一个API,利用用户凭证来获取他们的更多信息。
通常我会说,“想要一个API?简单:”
amplify add api
但这里正是大部分努力和故障排查所在,因为根据你选择的配置,这在Amplify和Flutter生态系统中并未得到完全支持。使用现成的数据存储和模型也可能导致读取模式效率低下,这很快就会变得成本高昂且速度缓慢。
Amplify提供了一个与AppSync中数据交互的高级API,但本教程中我将使用低级别的GraphQL查询,因为它更为灵活,并允许在DynamoDB中使用全局二级索引来避免表扫描。如果你想了解我是如何做到的以及可能遇到的陷阱,请查阅“在Flutter中使用和调优AWS Amplify及Appsync的挑战”。
Amplify在创建API时试图默认回答问题,但你可以通过滚动到想要更改的选项来覆盖任何这些设置。在此场景中,我们希望有一个GraphQL端点(以便利用DataStore),并且API授权由Cognito用户池处理,因为我们希望实施细粒度访问控制,以便只有用户本人可以更新或删除自己的详细信息(但其他用户可以查看)。
当我们创建API时,Amplify会生成一个基本的ToDo GraphQL模式类型。我们将在推送API更改之前更新此模式并添加一些授权规则。
修改“待办事项”模板GraphQL架构,以适应我们的用户个人资料信息。
type UserProfile @model
@auth(rules: [
{ allow: private, operations: [read], provider: iam },
{ allow: owner, operations: [create, read, update, delete] }
])
{
userId: String! @index
name: String!
location: String
language: String
}
私有规则允许已登录用户查看其他任何人的资料。通过不使用公共规则,我们阻止了未登录用户查看个人资料。IAM提供商阻止用户直接访问GraphQL API,他们需要使用应用程序并通过我们的Cognito身份池中的“未认证”角色(即已登出状态)来查看用户详情。
“所有者”规则允许创建个人资料的用户创建、读取和更新自己的个人资料。在本例中,我们不允许他们删除自己的个人资料。
至此,我们可以配置支持API特性的云基础设施:
amplify push
当你将现有的GraphQL模型从待办事项更改为用户个人资料时,如果你之前已经执行过amplify push
并配置了基础设施,你可能会收到一个错误,指出请求的更改将需要销毁现有的DynamoDB表。Amplify防止你这样做,以防因删除现有的待办事项表而丢失数据。如果你遇到这个错误,你需要运行amplify push --allow-destructive-graphql-schema-updates
。
当你执行amplify push
时,Amplify和CloudFormation将建立一个AppSync GraphQL API、中间解析器和后端DynamoDB表,如下所示:

一旦我们定义了GraphQL模式,就可以使用Amplify生成代表模型和仓库层的Dart代码,这些代码能与API协同工作:
amplify codegen models
此时,我们可以在页面上添加一些输入字段,以填充用户的姓名、位置和最喜欢的编程语言。
这是在我们的_MyHomePageState
组件中,文本字段变化的样子:
class _MyHomePageState extends State<MyHomePage> {
final _nameController = TextEditingController();
final _locationController = TextEditingController();
final _languageController = TextEditingController();
@override
Widget build(BuildContext context) {
...
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
'User Profile',
style: Theme.of(context).textTheme.titleLarge,
)),
const ProfilePicture(),
TextField(
decoration: const InputDecoration(labelText: "Name"),
controller: _nameController,
),
TextField(
decoration:
const InputDecoration(labelText: "Location"),
controller: _locationController,
),
TextField(
decoration: const InputDecoration(
labelText: "Favourite Language"),
controller: _languageController,
)
]
接着,我们将TextField连接到AppSync GraphQL API,这样当用户点击一个“保存”浮动操作按钮时,更改会与DynamoDB同步:
floatingActionButton: FloatingActionButton(
onPressed: _updateUserDetails,
tooltip: 'Save Details',
child: const Icon(Icons.save),
),
)
],
);
}
Future<void> _updateUserDetails() async {
final currentUser = await Amplify.Auth.getCurrentUser();
final updatedUserProfile = _userProfile?.copyWith(
name: _nameController.text,
location: _locationController.text,
language: _languageController.text) ??
UserProfile(
name: _nameController.text,
location: _locationController.text,
language: _languageController.text);
final request = _userProfile == null
? ModelMutations.create(updatedUserProfile)
: ModelMutations.update(updatedUserProfile);
final response = await Amplify.API.mutate(request: request).response;
final createdProfile = response.data;
if (createdProfile == null) {
safePrint('errors: ${response.errors}');
}
}
最后,当用户打开应用时,我们希望从云端拉取最新的个人资料。为此,我们在_MyHomePageState
初始化时发起一次调用:
@override
void initState() {
super.initState();
_getUserProfile();
}
void _getUserProfile() async {
final currentUser = await Amplify.Auth.getCurrentUser();
GraphQLRequest<PaginatedResult<UserProfile>> request = GraphQLRequest(
document:
'''query MyQuery { userProfilesByUserId(userId: "${currentUser.userId}") {
items {
name
location
language
id
owner
createdAt
updatedAt
userId
}
}}''',
modelType: const PaginatedModelType(UserProfile.classType),
decodePath: "userProfilesByUserId");
final response = await Amplify.API.query(request: request).response;
if (response.data!.items.isNotEmpty) {
_userProfile = response.data?.items[0];
setState(() {
_nameController.text = _userProfile?.name ?? "";
_locationController.text = _userProfile?.location ?? "";
_languageController.text = _userProfile?.language ?? "";
});
}
}
现在,我们拥有了一个可以存储数据、通过Cognito进行安全保护并由DynamoDB支持的API。考虑到我无需编写任何基础设施代码,这相当不错。
至此,我们已经能够查询、展示和更新用户的个人资料信息。对我来说,这感觉像是另一个保存点。
第五部分:增添设计风采
最终,我们扩展的示例应用看起来有些平淡。是时候让它生动一些了。
虽然我不是UI专家,但我从dribbble.com汲取灵感,决定采用鲜艳的背景和对比鲜明的白色卡片区域来展示个人资料细节。
添加背景图片
首先,我想为应用添加一个背景图片,以增添色彩。
I had a go at wrapping the children of my Scaffold
widget in a Container
widget, which you can then apply a decoration property to. It works and it’s the more upvoted solution, but it doesn’t fill the app bar too, which would be nice.
I ended up using this approach, which utilizes a Stack
widget to lay a full-height background image under our Scaffold
: “Background Image for Scaffold” on Stack Overflow.
生成的代码如下所示:
@override
Widget build(BuildContext context) {
return Stack(
children: [
Image.asset(
"assets/background.jpg",
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
// 这里我们从由App.build方法创建的MyHomePage对象中获取值,并将其用于设置应用栏标题。
// 应用构建方法创建的MyHomePage对象中取值,用于设置应用栏标题。
title: Text(widget.title),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
),
body: Center(
...
嗯,看起来还算美观,但背景色与屏幕上的可编辑元素对比略显突兀:
因此,我像这样将文本字段和头像图片包裹在Card
中,并设置了一定的边距和内边距,使其看起来不那么拥挤:
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
),
body: Center(
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 30),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
...
这是一种实现方式,尽管我怀疑还有更符合材料设计系统的惯用方法。或许留待后续文章探讨。
更改应用图标和标题于菜单中
若要更改应用图标,你需要提供多个版本的徽标,针对iOS和Android分别提供不同分辨率的版本。两者各自有不同的要求(有些你可能忽略,以免应用审核不通过),这很快就会变成一项繁琐的工作。
幸运的是,有一个Dart包能完成所有繁重工作。只需提供应用图标的源图像,它就能为两个平台生成所需的所有变体。
对于这个演示应用,我直接从谷歌图片上随机选取了一个应用图标:
来源:谷歌图片
遵循README文件的指引,我定义了以下最小配置集,成功生成了图标。将其放置在你的pubspec.yaml
文件底部:
flutter_icons:
android: true
ios: true
remove_alpha_ios: true
image_path: "assets/app-icon.png"
配置好上述内容后,运行此命令以生成iOS和Android所需的图标变体:
flutter pub run flutter_launcher_icons
在android/app/src/main/res/mipmap-hdpi/
和ios/Runner/Assets.xcassets/AppIcon.appiconset/
中,分别对应Android和iOS平台,你应该能看到一系列生成的图标文件。
遗憾的是,根据我的了解,更改应用名称仍然是一个手动过程。参考Flutter Beads上的一篇文章“如何在2023年正确更改Flutter应用名称”,我在以下两个文件中分别更改了iOS和Android的应用名称:
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>Flutter Amplify</string> <--- App Name Here
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flutterapp">
<application
android:label="Flutter Amplify" <--- App Name Here
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
这样,你就拥有了一个带有漂亮图标和标题的应用。
总结
至此,我们完成了使用Flutter和AWS Amplify快速启动并运行的教程,并展示了如何快速部署支持资源和脚手架代码,以便快速原型化跨平台移动应用。
I’m keen to get feedback on this tutorial, or any follow up tutorials people would like to see. All feedback is welcome and appreciated!
遇到的问题
缺少Android命令行工具
我的Android SDK管理器位置是:
/Users/benfoster/Library/Android/sdk/tools/bin/sdkmanager
执行以下步骤安装了Android命令行工具:Stack Overflow上的“安装Android SDK时失败:Java Lang Noclassdeffounderror JavaX XML Bind A.”
flutter doctor --android-licenses
应用在iOS上保持登录状态
在开发过程中,我希望能够重复登录应用的流程。但不幸的是,应用在关闭后仍保留用户信息——即使关闭再重新打开应用,它仍保持登录状态。
基于我以往的Android开发和Amplify经验,我本以为卸载应用并重新执行“flutter run”能清除用户状态,从而重新开始。然而,这一措施也未能达到预期效果,最终我不得不在每次需要从零开始时,都选择清除手机数据。
Source:
https://dzone.com/articles/cross-platform-mobile-app-prototyping-with-flutter-and-aws-amplify