# Amplify Flutter Workshop
## Prerequisites
### CLI 설치와 업데이트
* Update the AWS CLI
```bash
pip install --user --upgrade awscli
```
* Install the AWS Amplify CLI
```bash
npm install -g @aws-amplify/cli
```
* Install and use Node.js v12.x (to match AWS Lambda)
```bash
nvm ls-remote | grep v12
nvm install v12.11.0
nvm alias default v12.11.0
```
* Flutter version 2.0.0 or higher (make sure you are using a stable version of flutter)
### 기본 region 설정하기
가장 좋은 방법은 인프라를 고객과 가까운 지역에 구성하는 것입니다. (Amplify는 서울 리전도 지원합니다.)
**AWS config file이 없다면 Workshop 진행의 편의를 위해서 생성**합니다. 원하는 리전을 선택해서 하나만 진행하시면 됩니다.
* us-east-1(버지니아)를 사용하는 경우
```bash
cat <<END > ~/.aws/config
[default]
region=us-east-1
END
```
* ap-northeast-2(한국)를 사용하는 경우
```bash
cat <<END > ~/.aws/config
[default]
region=ap-northeast-2
END
```
AWS Amplify CLI는 모바일과 웹 어플리케이션을 개발을 심플하게 해주는 강력한 기능들을 제공하는 툴체인 입니다. 위의 단계에서는 설치만 진행했기 때문에 설정 단계가 추가적으로 필요합니다. AWS Amplify CLI는 ~/.aws/config을 찾아 작업할 Region 정보를 판별합니다. Cloud9은 유효한 Administrator credentials이 ~/.aws/credentials 파일안에 있는지 확인만 할 뿐 ~/.aws/config을 생성하지 않습니다.
## Flutter 프로젝트
* Flutter CLI를 이용하여 새 프로젝트를 시작합니다.
```bash
flutter create amplified_todo
```
### 애플리케이션에 Amplify를 추가합니다.
Amplify for Flutter는 pub.dev를 통해 배포됩니다.
프로젝트 루트 디렉터리에서 **pubspec.yaml**을 찾아 수정하고 Amplify 플러그인을 프로젝트 종속성에 추가합니다.
```yaml
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
amplify_flutter: ^0.2.0
amplify_auth_cognito: ^0.2.0
amplify_analytics_pinpoint: ^0.2.0
amplify_storage_s3: ^0.2.0
file_picker: ^3.0.0-nullsafety.2
```
다음 명령을 실행하여 종속성을 설치합니다. 개발 환경에 따라 IDE를 통해 이 단계를 수행할 수 있습니다.(또는 자동으로 수행될 수도 있음)
```bash
flutter pub get
```
### iOS 플랫폼 업데이트
프로젝트 루트에서 **ios/** 디렉터리로 이동하고 선택한 텍스트 편집기를 사용하여 **Podfile**을 수정하고 대상 iOS 플랫폼을 13.0 이상으로 업데이트합니다.
```yaml
platform :ios, '13.0'
```
### Android SDK 버전 업데이트
프로젝트 루트에서 **android/app/** 디렉터리로 이동하고 원하는 텍스트 편집기를 사용하여 **build.gradle**을 수정하고 대상 Android SDK 버전을 21 이상으로 업데이트합니다.
```
minSdkVersion 21
```
축하합니다🎉 Amplify로 구축을 시작할 준비가 되었습니다.
---
## Amplify CLI를 이용하여 백앤드 프로비저닝
백엔드에서 리소스 프로비저닝을 시작하려면 디렉터리를 프로젝트 디렉터리로 변경하고 amplify init를 실행합니다.
```bash
# in future, make sure you have Amplify CLI v4.28 and above for Flutter support
# npm install -g @aws-amplify/cli
amplify init
```
메시지가 표시되면 다음을 입력합니다.
```
? Enter a name for the environment
`dev`
? Choose your default editor:
`IntelliJ IDEA`
? Choose the type of app that you're building:
'flutter'
? Where do you want to store your configuration file?
./lib/
? Do you want to use an AWS profile?
`Yes`
? Please choose the profile you want to use
`default`
```
amplifyconfiguration.dart.
amplify init를 성공적으로 실행하면 ./lib/에 amplifyconfiguration.dart라는 구성 파일이 생성된 것을 볼 수 있습니다.
이 파일은 Amplify 라이브러리가 프로비저닝된 백엔드 리소스에 도달하는 방법을 알 수 있도록 애플리케이션에 번들로 제공됩니다.
:::warning
:information_source: 팁! iOS 오류 해결
* iOS에서 테스트를 하는 동안 아래와 같은 오류가 발생하였다면,
```
Error running pod install
Error launching application on iPhone 11.
```
* 다음과 같은 명령을 통해서 해결이 가능합니다.
```bash
cd ios && pod update
```
:::
---
## Amplify
* 이 프로젝트에서 사용할 Amplify 기능들을 추가합니다.
* Amplify 카테고리 추가(이 예에서는 기본값 선택)
```bash
amplify add auth
amplify add analytics
amplify add storage
```
* 클라우드에 변경 사항을 푸시하여 백엔드 리소스를 프로비저닝합니다.
```bash
amplify push
```
---
## lib/main.dart
* Amplify 패키지 추가합니다.
```java=
import 'package:amplify_flutter/amplify.dart';
import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';
import 'package:flutter/material.dart';
import 'amplifyconfiguration.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isAmplifyConfigured = false;
@override
initState() {
super.initState();
_initAmplifyFlutter();
}
void _initAmplifyFlutter() async {
Amplify.addPlugins(
[AmplifyAuthCognito(), AmplifyStorageS3(), AmplifyAnalyticsPinpoint()]);
// Initialize AmplifyFlutter
try {
await Amplify.configure(amplifyconfig);
print('Successfully configured Amplify 🎉');
} on AmplifyAlreadyConfiguredException {
print(
"Amplify was already configured. Looks like app restarted on android.");
}
setState(() {
_isAmplifyConfigured = true;
});
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Amplify App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text("Hello Flutter App"),
),
body: new Center(
child: new Text("Hello Flutter"),
),
),
);
}
}
```
### landing / loading page
* /lib/Pages 폴더를 생성합니다.
* Amplify SDK 가 로딩되지 않았을때, 표시해줄 페이지를 만듭니다.
* /lib/Pages/LoadingPage.dart
```java=
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class LoadingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Landing Page")),
body:
Center(child: Text("Please Wait. Configuring Amplify Flutter SDK")),
);
}
}
```
* 첫 페이지를 작성합니다.
* /lib/Pages/LandingPage.dart
```java=
import 'package:flutter/material.dart';
class LandingPage extends StatefulWidget {
LandingPage({Key? key}) : super(key: key);
@override
_LandingPageState createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
Future<Null> _showDialogForResult(
String text, Function onSuccess, Widget dialogWidget) async {
bool result = await showDialog(
context: context,
builder: (BuildContext context) {
return new SimpleDialog(title: Text(text), children: [
dialogWidget,
ElevatedButton(
child: const Text("Cancel"),
onPressed: () {
Navigator.pop(context, false);
},
),
]);
});
if (result) onSuccess();
}
// dialogWidget must return true or false
Widget openDialogButton(
String text, Function onSuccess, Widget dialogWidget) {
return ElevatedButton(
child: Text(text),
onPressed: () {
_showDialogForResult(text, onSuccess, dialogWidget);
});
}
void onSignInSuccess() {
print('Sign in Success');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Landing Page"),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(child: Text("This is a Landing Page!")),
],
)));
}
}
```
* main.dart파일에서 import를 추가 합니다.
```java=
import 'Pages/LoadingPage.dart';
import 'Pages/LandingPage.dart';
```
* _display 함수를 추가합니다.
```java=
Widget _display() {
if (_isAmplifyConfigured) {
return LandingPage();
} else {
return LoadingPage();
}
}
```
* build 함수를 업데이트 합니다.
```java=
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Amplify App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: _display());
}
```
---
## 뷰 작성
* 뷰를 위한 폴더를 생성합니다. : /lib/Views
### 오류 표기를 위한 페이지 작성
* 오류 페이지를 작성합니다. /lib/Views/ErrorView.dart
```java=
import 'package:flutter/material.dart';
class ErrorView extends StatelessWidget {
final String error;
ErrorView(this.error);
@override
Widget build(BuildContext context) {
if (error.isNotEmpty) {
return Column(children: <Widget>[
Text('Error: $error',
textAlign: TextAlign.center,
overflow: TextOverflow.visible,
style: TextStyle(fontWeight: FontWeight.bold)),
]);
} else {
return Container();
}
}
}
```
### 회원 가입과 로그인 뷰 만들기
* 회원 가입 뷰를 생성합니다.
* /lib/Views/SignUpView.dart
```java=
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify.dart';
import 'package:flutter/material.dart';
import 'ErrorView.dart';
class SignUpView extends StatefulWidget {
@override
_SignUpViewState createState() => _SignUpViewState();
}
class _SignUpViewState extends State<SignUpView> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final emailController = TextEditingController();
final confirmationCodeController = TextEditingController();
String _signUpError = "";
bool _isSignedUp = false;
@override
void initState() {
super.initState();
}
void _signUp() async {
setState(() {
_signUpError = "";
});
Map<String, String> userAttributes = {
"email": emailController.text,
};
try {
SignUpResult res = await Amplify.Auth.signUp(
username: usernameController.text.trim(),
password: passwordController.text.trim(),
options: CognitoSignUpOptions(userAttributes: userAttributes));
setState(() {
_isSignedUp = true;
});
} on AuthException catch (error) {
_setError(error);
}
}
void _confirmSignUp() async {
setState(() {
_signUpError = "";
});
try {
SignUpResult res = await Amplify.Auth.confirmSignUp(
username: usernameController.text.trim(),
confirmationCode: confirmationCodeController.text.trim());
Navigator.pop(context, true);
} on AuthException catch (error) {
_setError(error);
}
}
void _setError(AuthException error) {
setState(() {
_signUpError = error.message;
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
// wrap your Column in Expanded
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
Visibility(
visible: !_isSignedUp,
child: Column(children: [
TextFormField(
controller: usernameController,
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'Username',
labelText: 'Username *',
),
),
TextFormField(
obscureText: true,
controller: passwordController,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
hintText: 'Password',
labelText: 'Password *',
),
),
TextFormField(
controller: emailController,
decoration: const InputDecoration(
icon: Icon(Icons.email),
hintText: 'Email',
labelText: 'Email *',
),
),
ElevatedButton(
onPressed: _signUp,
child: const Text('Sign Up'),
),
]),
),
Visibility(
visible: _isSignedUp,
child: Column(children: [
TextFormField(
controller: confirmationCodeController,
decoration: const InputDecoration(
icon: Icon(Icons.confirmation_number),
hintText: 'The code we sent you',
labelText: 'Confirmation Code *',
)),
ElevatedButton(
onPressed: _confirmSignUp,
child: const Text('Confirm Sign Up'),
),
])),
const Padding(padding: EdgeInsets.all(10.0)),
ErrorView(_signUpError)
],
),
),
),
],
);
}
}
```
* 로그인 뷰를 생성합니다.
* /lib/Views/SignInView.dart
```java
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify.dart';
import 'package:flutter/material.dart';
import 'ErrorView.dart';
class SignInView extends StatefulWidget {
@override
_SignInViewState createState() => _SignInViewState();
}
class _SignInViewState extends State<SignInView> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
String _signUpError = "";
@override
void initState() {
super.initState();
}
void _signIn() async {
// Sign out before in case a user is already signed in
// If a user is already signed in - Amplify.Auth.signIn will throw an exception
try {
await Amplify.Auth.signOut();
} on AuthException catch (e) {
print(e);
}
try {
SignInResult res = await Amplify.Auth.signIn(
username: usernameController.text.trim(),
password: passwordController.text.trim());
Navigator.pop(context, true);
} on AuthException catch (e) {
setState(() {
_signUpError = e.message;
});
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
// wrap your Column in Expanded
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
TextFormField(
controller: usernameController,
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'Enter your username',
labelText: 'Username *',
),
),
TextFormField(
obscureText: true,
controller: passwordController,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
hintText: 'Enter your password',
labelText: 'Password *',
),
),
const Padding(padding: EdgeInsets.all(10.0)),
ElevatedButton(
onPressed: _signIn,
child: const Text('Sign In'),
),
ErrorView(_signUpError)
],
),
),
),
],
);
}
}
```
* 로그인과 회원가입 기능(버튼)을 추가합니다.
* /lib/Pages/LandingPage.dart
```java=
import '../Views/SignInView.dart';
import '../Views/SignUpView.dart';
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
openDialogButton("Sign In", onSignInSuccess, SignInView()),
openDialogButton(
"Sign Up", () => {print("sign up success")}, SignUpView())
],
)));
```
---
### MainPage
* /lib/Pages/MainPage.dart 파일을 생성합니다.
```java=
import 'package:amplify_flutter/amplify.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';
import 'package:flutter/material.dart';
class MainPage extends StatefulWidget {
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<String> itemKeys = [];
@override
void initState() {
super.initState();
_loadImages();
}
void _loadImages() async {
try {
print('In list');
S3ListOptions options =
S3ListOptions(accessLevel: StorageAccessLevel.guest);
ListResult result = await Amplify.Storage.list(options: options);
var newList = itemKeys.toList();
for (StorageItem item in result.items) {
newList.add(item.key);
}
setState(() {
itemKeys = newList;
});
} catch (e) {
print('List Err: ' + e.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [Text("Main Page")])),
body: new Center(
child: new Text("Hello Flutter. This is main page."),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
```
* 로그인후 페이지 이동
* /lib/Pages/LandingPage.dart
```java=
import 'MainPage.dart';
void onSignInSuccess() {
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (context) => MainPage()), (route) => false);
}
...
```
* 사용자 로그아웃을 위한 뷰를 생성합니다.
* /lib/Views/UserView.dart
```java=
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify.dart';
import 'package:flutter/material.dart';
import '../Pages/LandingPage.dart';
class UserView extends StatefulWidget {
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserView> {
@override
void initState() {
super.initState();
}
void _signOut() async {
try {
SignOutResult res = await Amplify.Auth.signOut();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LandingPage()),
(route) => false);
} on AuthException catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(onPressed: _signOut, child: const Text("Log Out")),
],
);
}
}
```
* /lib/Pages/MainPage.dart 파일을 생성합니다.
```java=
import '../Views/UserView.dart';
...
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [Text("Main Page"), UserView()])),
```
---
## 업로드된 이미지 보기 기능 추가
### 이미지 보기 뷰 추가
* /lib/Views/ImageLineItem.dart
```java=
import 'package:flutter/material.dart';
class ImageLineItem extends StatelessWidget {
final String storageKey;
const ImageLineItem({
Key? key,
required this.storageKey,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(5.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Text(storageKey),
Spacer(),
]));
}
}
```
### 메인 페이지에 기능 추가
* /lib/Pages/MainPage.dart 파일을 수정합니다.
* 업로드 이미지 보기
```java=
import '../Views/ImageLineItem.dart';
body: ListView.builder(
itemCount: itemKeys.length,
itemBuilder: (context, index) {
return ImageLineItem(storageKey: itemKeys[index]);
}),
```
---
## 이미지 업로드 기능 추가
### 이미지 업로드 뷰 추가
* /lib/Views/ImageUploader.dart
```java=
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:amplify_flutter/amplify.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';
class ImageUploader extends StatelessWidget {
void _upload(BuildContext context) async {
List<PlatformFile>? _paths;
try {
print('In upload');
// Uploading the file with options
_paths =
(await FilePicker.platform.pickFiles(type: FileType.image))?.files;
File local = File(_paths!.single.path!);
local.existsSync();
final key = new DateTime.now().toString();
Map<String, String> metadata = <String, String>{};
metadata['name'] = 'filename';
metadata['desc'] = 'A test file';
S3UploadFileOptions options = S3UploadFileOptions(
accessLevel: StorageAccessLevel.guest, metadata: metadata);
UploadFileResult result = await Amplify.Storage.uploadFile(
key: key, local: local, options: options);
print('File uploaded. Key: ' + result.key);
Navigator.pop(context, result.key);
} catch (e) {
print('UploadFile Err: ' + e.toString());
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
ElevatedButton(
child: const Text("Upload Image"),
onPressed: () {
_upload(context);
},
)
]);
}
}
```
### 메인 페이지에 기능 추가
* /lib/Pages/MainPage.dart 파일을 수정합니다.
* 이미지 업로드 기능 추가
```java=
import '../Views/ImageUploader.dart';
...
void _showImageUploader() async {
try {
String key;
key = await showDialog(
context: context,
builder: (BuildContext context) {
return new SimpleDialog(
title: Text("Upload Image"), children: [ImageUploader()]);
});
if (key.isNotEmpty) {
var newList = itemKeys.toList();
newList.add(key);
setState(() {
itemKeys = newList;
});
}
} catch (e) {
print('Dialog Err: ' + e.toString());
}
}
...
floatingActionButton: FloatingActionButton(
onPressed: () {
_showImageUploader();
},
tooltip: 'Increment',
child: Icon(Icons.add),
```
---
## 이미지 보기 기능 추가
### 이미지 보기 뷰 추가
* /lib/Views/ImagePreview.dart
```java=
import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart';
import 'package:amplify_flutter/amplify.dart';
import 'package:amplify_storage_s3/amplify_storage_s3.dart';
import 'package:flutter/material.dart';
class ImagePreview extends StatefulWidget {
final String storageKey;
ImagePreview({Key? key, required this.storageKey}) : super(key: key);
@override
_ImagePreviewState createState() => _ImagePreviewState();
}
class _ImagePreviewState extends State<ImagePreview> {
String _imageURL = '';
@override
void initState() {
super.initState();
_getUrl(widget.storageKey);
}
void _getUrl(String storageKey) async {
try {
print('In getUrl');
String key = storageKey;
S3GetUrlOptions options = S3GetUrlOptions(
accessLevel: StorageAccessLevel.guest, expires: 10000);
GetUrlResult result =
await Amplify.Storage.getUrl(key: key, options: options);
setState(() {
_imageURL = result.url;
});
print('URL: ' + _imageURL);
AnalyticsEvent event = AnalyticsEvent("image_url_retrieved");
event.properties.addStringProperty("file_key", storageKey);
Amplify.Analytics.recordEvent(event: event);
} catch (e) {
print('GetUrl Err: ' + e.toString());
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
//Image(image: AssetImage('images/image.png')),
Center(child: Image.network(_imageURL))
]);
}
}
```
### 이미지 리스트 뷰에 기능 추가
* /lib/Views/ImageLineItem.dart
```java=
import 'ImagePreview.dart';
...
return Padding(
padding: EdgeInsets.all(5.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Text(storageKey),
Spacer(),
ElevatedButton(
child: const Text("open"),
onPressed: () => {
showDialog(
context: context,
builder: (BuildContext context) {
return new SimpleDialog(
title: Text(storageKey),
children: [ImagePreview(storageKey: storageKey)]);
})
})
]));
```
---
## References
* https://github.com/aws-amplify/amplify-flutter/tree/main/example
* https://docs.amplify.aws/start/q/integration/flutter
* https://docs.amplify.aws/lib/q/platform/flutter
* https://aws.amazon.com/ko/blogs/korea/amplify-flutter-is-now-generally-available-build-beautiful-cross-platform-apps/
* https://aws.amazon.com/ko/getting-started/hands-on/build-flutter-app-amplify/