Amplify SDK for Flutter - Developer experience and challenges in a hackathon
Johannes Koch
Posted on June 29, 2023
Background of this post
This blog post is part of a a series of posts that explains details and technical challenges that we (Danielle, Matt, Julian and myself) faced during an hackathon that was focused on the Transformers Huggingface tools - please read Matt's article for additional information and details.
In this post we are focusing on the experiences we made in the project using the Amplify SDK for Flutter.
Project Setup - architecture dependencies for project set up
Initially our project is targetting to be available to all AWS User Group Leaders on the web from any browser. As we mature the project, at least I am hoping to be able to also publish this application as an Android and iOS application using cross platform capabilities.
Here is a birds eyes view to our UI architecture:
Why did we choose Flutter as UI?
Flutter is a trending cross-platform development toolkit, altough it is mainly sponsored by Google there is a vibrant open source community. I personally have had some exposure to Flutter in the past and I liked the developer experience and the short iteration cycles. Also the AWS Amplify Team has been investing into making their SDK available to developers, so we thought this is a good possibility to try out our use case and implement the Web app in Flutter using the Amplify SDK for Flutter to connect to the backend resources which we were planning to implement using the AWS CDK. With this approach, we also want to draw some attention on the fact that the Amplify SDKs can be used without an Amplify owned backend. This is cool and opens up a lot of possibilities - but also challenges as we will see later. I convinced Danielle, Matt and Julian to also use Flutter for our frontend. They also saw this as a good opportunity to learn a bit of Flutter by themselves.
Amplify SDK for Flutter - Developer experience and challenges
The Amplify SDK for Flutter recently announced the "GA" for Web and Desktop which is a milestone for the team to reach. As we outlined in Matt's blog post, we are using Typescript in the backend. Amplify Flutter has native support for AppSync/GraphQL - we needed to connect the Flutter App to existing AppSync endpoints.
The AppSync schema however was written manually. No, we needed to use "amplify codegen" to generate the Dart models for the GraphQL types - but we also needed to write a type model in Typescript to be able to work with the same objects on the backend.
This turned out to be more difficult than expected: the [amplify codegen](https://docs.amplify.aws/cli/graphql/client-code-generation/#shared-schema-modified-elsewhere-eg-console-or-team-workflows)
functionality is available for Flutter, too, but it was difficult to get this to work.
We ended up creating a new Flutter application using amplify init
and then copy/pasted our schema(s) into the expected location. Then we needed to manually copy the generated models into our project.
Oh, I neqarly forgot: When using amplify codegen
you need to ensure that your schema is anotated correctly, types e.g. need to have an @model
annotation - but if you have this annotation in the schema when trying to deploy to AppSync, that deployment failed...so we needed to also manually adjust the schema before we were able to execute amplify codegen
.
Using the Amplify SDK for Flutter
In our use case, all of the backend infrastructure is created using the AWS CDK. We are not using Amplify to create backend resources - and this use case has - until recently - not really been promoted by the Amplify team. Thanks to one of my last blog posts around the same topic, this new documentation page has been added which simplifies the setup of your amplifyconfiguration.dart
- but we were missing the same documentation for the "Authentication" library. One again, we workarounded this problem by using a temporary Amplify Flutter project. That allowed us to copy/paste the configuration and adjust it to our needs.
Environment aware connections
In our current setup, we have at least three environments to test and promote our application. In order to be able to execute and test the Flutter application on all environments without code changes, we needed to make the maplifyconfiguration.dart
environment aware:
class EnvironmentConfig {
static const WEB_URL = String.fromEnvironment('WEB_URL');
static const API_URL = String.fromEnvironment('API_URL');
static const CLIENT_ID = String.fromEnvironment('CLIENT_ID');
static const POOL_ID = String.fromEnvironment('POOL_ID');
}
These environment variables are then used in our configuration object:
const amplifyconfig = """{
"UserAgent": "aws-amplify-cli/2.0",
"Version": "1.0",
"api": {
"plugins": {
"awsAPIPlugin": {
"frontend": {
"endpointType": "GraphQL",
"endpoint": "${EnvironmentConfig.API_URL}",
"region": "us-east-1",
"authorizationType": "AMAZON_COGNITO_USER_POOLS"
}
}
}
},
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"UserAgent": "aws-amplify-cli/0.1.0",
"Version": "0.1.0",
"IdentityManager": {
"Default": {}
},
"CognitoUserPool": {
"Default": {
"PoolId": "${EnvironmentConfig.POOL_ID}",
"AppClientId": "${EnvironmentConfig.CLIENT_ID}",
"Region": "us-east-1"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_PASSWORD_AUTH",
"socialProviders": [],
"usernameAttributes": [
"EMAIL"
],
"signupAttributes": [
"BIRTHDATE",
"EMAIL",
"FAMILY_NAME",
"NAME",
"NICKNAME",
"PREFERRED_USERNAME",
"WEBSITE",
"ZONEINFO"
],
"passwordProtectionSettings": {
"passwordPolicyMinLength": 8,
"passwordPolicyCharacters": []
},
"mfaConfiguration": "OFF",
"mfaTypes": [
"SMS"
],
"verificationMechanisms": [
"EMAIL"
]
}
}
}
}
}
}""";
Within our CI/CD workflow we then pass the correct values for the variables and during the flutter build web
they are backed into the application.
After solving these challenges, using the Amplify Flutter library worked without noteworthy problems or hickups.
Using the Amplify Flutter Library to authenticate a user with Cognito
The documentation for Amplify Flutter is really good and we decided to also use the Authenticator Widget - at the end, this project was born through a hackathon - and we did not have much time to implement the authentication flow ourselves.
In our main.dart
we needed to include the Amplify configuration:
Future<void> _configureAmplify() async {
final api = AmplifyAPI(modelProvider: ModelProvider.instance);
await Amplify.addPlugin(api);
// Add any Amplify plugins you want to use
final authPlugin = AmplifyAuthCognito();
await Amplify.addPlugin(authPlugin);
try {
await Amplify.configure(amplifyconfig);
} on AmplifyAlreadyConfiguredException {
safePrint(
'Tried to reconfigure Amplify; this can occur when your app restarts on Android.');
}
}
and then we only needed to adjust the build
method to distinguish betwen AuthenticatedView
s and "normal" views:
Widget build(BuildContext context) {
final GoRouter router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return HomeScreen();
},
),
GoRoute(
path: '/profile',
builder: (BuildContext context, GoRouterState state) {
return AuthenticatedView(
child: ProfileScreen(),
);
},
),
GoRoute(
path: '/imprint',
builder: (BuildContext context, GoRouterState state) {
return ImprintScreen();
},
),
GoRoute(
path: '/privacy',
builder: (BuildContext context, GoRouterState state) {
return PrivacyScreen();
},
),
GoRoute(
path: '/in',
builder: (BuildContext context, GoRouterState state) {
return AuthenticatedView(
child: LoggedInHomepage(title: 'AWS Speakers Directory'));
},
),
],
);
return Authenticator(
initialStep: AuthenticatorStep.signIn,
signUpForm: SignUpForm.custom(
fields: [
SignUpFormField.username(),
SignUpFormField.password(),
SignUpFormField.passwordConfirmation(),
SignUpFormField.email(),
SignUpFormField.custom(
title: "First name",
attributeKey: CognitoUserAttributeKey.givenName),
SignUpFormField.custom(
title: "Last name",
attributeKey: CognitoUserAttributeKey.familyName),
SignUpFormField.custom(
title: "City",
attributeKey: CognitoUserAttributeKey.custom("city")),
SignUpFormField.custom(
title: "Country",
attributeKey: CognitoUserAttributeKey.custom("country")),
SignUpFormField.birthdate(),
SignUpFormField.gender(),
],
),
child: MaterialApp.router(
title: 'AWS Speakers Directory',
routerConfig: router,
),
);
}
We would have loved to open source our code, but unfortunately that option is currently not available in Amazon CodeCatalst.
If you're an experienced Flutter developer and don't like the code you see above - that's fine, the four of us have been learning Flutter iwth this project - approach us and tell us how to do this better! ;)
Using the Amplify Flutter Library to execute AppSync / GraphQL queries
Accessing AppSync was also really easy by following the Amplify Flutter documentation. Here is our code for retrieving events from the backend:
Future<List<EventRow?>> _listEvents() async {
try {
String graphQLDocument = '''query GetEvents {
listEvents {
pk
sk
title
description
tags
length
}
}''';
var operation = Amplify.API
.query(request: GraphQLRequest<String>(document: graphQLDocument));
List<EventRow> events = [];
var response = await operation.response;
var data = response.data;
if (data != null) {
Map<String, dynamic> userMap = jsonDecode(data);
print('Query result: ' + data.toString());
List<dynamic> matches =
userMap["listEvents"] != null ? userMap["listEvents"] : [];
matches.forEach((element) {
if (element != null) {
if (element["id"] == null) {
element["id"] = "rnd-id";
}
var event = Event.fromJson(element);
events.add(EventRow(event, context));
}
});
return events;
}
} on ApiException catch (e) {
print('Query failed: $e');
}
return <EventRow?>[];
}
This uses the previously generated models. All of the queries are authenticated using the Cognito Authentication - and this is the identity that we can also use in the backend to authorize access.
The Amplify documentation is pretty good, hence I will not be adding more details into this post.
Please ask if you have any specific questions.
Wishes for the Amplify SDK for Flutter
We already mentioned most of them: better amplify codegen
support (including the option to generate both Flutter and Typescript models at the same time), better documentation or support for setting up the Amplify configuration completely without an Amplify backend.
Another thing we did not talk about here but Julian mentions in his blog post are the AppSync [merged APIs] which are currently not supported - thus we needed to execute amplify codegen
for each of our microservices schemas and then for the merged APi - and then manually needed to bring the models into a usable structure (e.g. copy/pasting from the different ModelProviders).
What's next for our project
So, what's next? After the hackathon we are thinking about making this a "kind of" OpenSource, collaborative project. Here we are looking for contributors - please contact us if you are interested.
Besides that, Matt covers a godd bunch of roadmap items already - and we definately need someone with more Flutter experience to review our UI code to e.g. introduce the BloC pattern for cleaner programming, adding some styling and to expand existing functionalities.
And we still need to play the "cross-platform" card - we need a build step to generate iOS and Android versions of our application and need to make sure that the apps land in the Appstore(s)!
Please reach out to us if you would like to get involved!
Posted on June 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.