Connecting to AWS AppSync using Amplify for Flutter for our Football Match Center

In the last weeks – or already months – I’ve been working together with Christian, also an AWS Community Builder, on our project named “Football Match Center”. Christian has already been writing a lot about our project on LinkedIn:

  1. Project announcement
  2. Polling 
  3. Choosing our API

Today, I want to put the attention on our chosen framework for the UI and the way that we are connecting from the UI to the backend. Our backend in this project is a GraphQL API endpoint hosted on AWS AppSync.

Building our UI in Flutter

Since last year Amplify Flutter includes support for Web and Desktop. As we are looking to reach users both on mobile as also on the desktop, choosing a cross-platform development tool like Flutter seemed to be an obvious choice. Christian and I are a small team, and we want to focus on building a simple UI quickly without the need to implement for multiple platforms and Flutter allows exactly that.

Flutter provides easily extendable widgets that can be used on all major platforms.

Connecting to our GraphQL backend

Our project is not based on an Amplify backend, but on AWS infrastructure written in AWS CDK. This made it rather difficult to use the Amplify Flutter SDK as most of the documentations and blog posts expect you to connect the Amplify SDK with an Amplify backend (which can then include a GraphQL API).

But that’s not only what made it difficult – I also had very little experience with Amplify or the Amplify SDK when starting to work on the connection.

Using the Flutter SDK for Amplify we will be connecting to our Cognito instance for Authentication and to our existing GraphQL endpoint. In this post I am going to look at the GraphQL connection and not on the integration of Cognito as an authentication endpoint.

Setting up Amplify SDK for Flutter can be done through the amplify cli if you are starting a new project.

This will then also create the required amplifyconfiguration.dart and some example code through amplify init.

You can then set up the Amplify SDK for Flutter from within your main widget using this code:

import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_api/amplify_api.dart';
import 'amplifyconfiguration.dart';
import 'models/ModelProvider.dart';

….

 Future<void> _configureAmplify() async {
    final api = AmplifyAPI(modelProvider: ModelProvider.instance);
    await Amplify.addPlugin(api);
    await Amplify.configure(amplifyconfig);
    try {
      await Amplify.configure(amplifyconfig);
    } on AmplifyAlreadyConfiguredException {
      safePrint(
          'Tried to reconfigure Amplify; this can occur when your app restarts on Android.');
    }
  }

While this looks easy when reading the documentation (and a lot of very good blog posts), this was rather difficult for me as I was not able to use the amplify init command. Finding out the structure of the “amplifyconfiguration.dart” and the implementation for the “ModelProvider” were my main challenges.

Lately, the related documentation has been updated and it is now easier to work with existing resources.

The Amplify Configuration file

The Amplify Configuration (amplifyconfiguration.dart) configures all of the required Amplify Plugins. In our implementation we started with the GraphQL backend:

const amplifyconfig = """{
"UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "matchcenter": {
                    "endpointType": "GraphQL",
                    "endpoint": "https://xxxx.appsync-api.eu-central-1.amazonaws.com/graphql",
                    "region": "eu-central-1",
                    "authorizationType": "API_KEY",
                    "apiKey": "xx0-3425ddregsGDE42dfw"
                }
            }
        }
    }
}""";

This tells  the Amplify SDK to talk to a specific API endpoint when the “Amplify.API” is invoked. As far as I understand this Github issue, right now only one API can be queried from a specific Amplify instance. 

When using the apiKey to do the authentication with the API, we will need to regularly update the Flutter application as the default API expires after 7 days.

This documentation was not available when we started to work on the project and I have the suspicion that Salih made this happen 🙂 (if not, still THANKS for the help you gave me! 🙂)

The ModelProvider

The ModelProvider should be a generated file, which you can generate from an existing GraphQL API. If you are using a schema that is not managed by Amplify, you will need to use “amplify codegen” based on an existing schema file. 

The command expects a schema.graphql to be available in the “root” folder of the Amplify Flutter project. If you execute “amplify codegen models”, required Dart files will be generated in the “lib/models” directory.

The result should be a file similar to this one:

import 'package:amplify_core/amplify_core.dart';
import 'Match.dart';
import 'PaginatedMatches.dart';
import 'PaginatedTeams.dart';
import 'Team.dart';
export 'Match.dart';
export 'PaginatedMatches.dart';
export 'PaginatedTeams.dart';
export 'Team.dart';

class ModelProvider implements ModelProviderInterface {
  @override
  String version = "4ba35f5f4a47ee16223f0e1f4adace8d";
  @override
  List<ModelSchema> modelSchemas = [Match.schema, PaginatedMatches.schema, PaginatedTeams.schema, Team.schema];
  static final ModelProvider _instance = ModelProvider();
  @override
  List<ModelSchema> customTypeSchemas = [];
  static ModelProvider get instance => _instance;
  ModelType getModelTypeByModelName(String modelName) {
    switch(modelName) {
      case "Match":
        return Match.classType;
      case "PaginatedMatches":
        return PaginatedMatches.classType;
      case "PaginatedTeams":
        return PaginatedTeams.classType;
      case "Team":
        return Team.classType;
      default:
        throw Exception("Failed to find model in model provider for model name: " + modelName);
    }
  }
}

Querying our GraphQL API

Now that we have been able to connect to our GraphQL AWS AppSync endpoint, we can start querying data.

Luckily, the preparations we made and the Amplify for Flutter SDK provides convenience methods that returned typed data structures that we can directly interact or work with.

You only need to write the GraphQL query that you are interested in and you can directly read data from the endpoint. In my example below I’m creating a Flutter Widget out of the returned elements and then I am adding them to a list of Widgets that I can display in a Column Widget:

Future<List<TeamWidget>> _getMatchesByCountry(String country) async {
    List<TeamWidget> teamsWidgetList = [];
    try {
      String graphQLDocument = '''query ListTeams {
        getTeamsByCountry(country: "${country}") {
            nextToken
            teams {
              PK
              PrimaryColor
              SK
              SecondaryColor
              TeamName
            }
          }
        }''';
      var operation = Amplify.API
          .query(request: GraphQLRequest<String>(document: graphQLDocument));
      var response = await operation.response;
      var data = response.data;
      if (data != null) {
        Map<String, dynamic> userMap = jsonDecode(data);
        List<dynamic> matches = userMap["getTeamsByCountry"]["teams"];
        matches.forEach((element) {
          if (element != null) {
            if (element["id"] == null) {
              element["id"] = "rnd-id";
            }
            var match = Team.fromJson(element);
            teamsWidgetList.add(TeamWidget(match));
          }
        });
      }
    } on ApiException catch (e) {
      print('Query failed: $e');
    }
    return teamsWidgetList;
  }

It is of course also possible to create, update or delete data.

Just today, we have merged a feature that adds a “subscription” to our AppSync endpoint – as as next step we plan to integrate this within the Amplify Flutter Application which will then allow us to implement notifications to the end users. Unfortunately, the Amplify SDK for Flutter does not yet support in-app messaging as it does for Javascript.

What YOU learned – and what I learned

Through this blog post you have learned how to connect an Flutter application with Amplify using the Flutter SDK for Amplify. You have also got to know our project, the “Football Match Center” – and you’ve seen some code to make your start easier when talking to a GraphQL (AppSync) backend.

I have learned to work with the Amplify for Flutter SDK and also how code generators can help you to speed up your implementation.  I’ve also gained experiences in accessing data from AppSync and on working with the returned data in Flutter.

Unfortunately, I have also found out that using the Flutter SDK for Amplify I can right now not implement the planned in-app notifications that Christian and I wanted to build for our Football Match Center to notify users about upcoming or recently completed games. 

We will need to find a workaround to that and not rely on the Flutter SDK for amplify – rather implement notifications using the flutter_local_notifications plugin or by using the Firebase possibility for notification.

Looking forward to hear your feedback if you have any ideas on how to make this happen!

In the next post about this project I will look at how we have set up our CI/CD pipeline in Amazon CodeCatalyst for this project!

Visits: 460

How to save costs using AWS Lambda SnapStart for Java based functions

At re:Invent 2022 in Las Vegas AWS has announced a new feature for AWS Lambda that allows you to reduce your lambda startup times for Java based functions – SnapStart. With this post you are going to understand how the feature works, how it can be enabled, how you will benefit from it and how it will reduce your costs for AWS lambda. The functionality needs to be activated (“opt-in”) and has a few pre-requites for your functions that I will also share with you. It is currently only available if you use Java (Corretto 11) as a runtime. Other runtimes (e.g. Python, Typescript) will be available at a later date.

What is AWS Lambda SnapStart – and reduced latency for cold starts?

SnapStart is a new feature that helps to improve the “cold starts” of your lambda function.

AWS Lambda is a serverless possibility to execute code or a part of your application on AWS. The service can be used with different languages – Typescript/Node.JS, Python, Golang, … and Java. Lambda can be seen as “function as a service” with very convenient integration possibilities. Under the hood of Lambda, AWS has a very quick and mature provisioning system that brings up a container that executes your code. AWS Lambda as a “serverless” service allows you to not pay anything for your deployed infrastructure if it is not being used – this is also known as “scale-to-zero”. AWS Lambda is billed by “usage time” of the capacity that your lambda functions use – a lambda function can run up to 15min, but most of the lambda functions execute within a few seconds. Because of the  “scale-to-zero” functionality, there is not always a “running” version of your code on AWS. This is where AWS distinguishes between “cold starts” and “warm starts” for your lambda function.

When SnapStart is enabled, function code is initialized once when a function version is published. Lambda then takes a snapshot of the memory and disk state of the initialized execution environment, persists the encrypted snapshot, and caches it for low-latency access. When the function is first invoked or subsequently scaled, Lambda resumes new execution environments from the persisted snapshot instead of initializing from scratch, avoiding several seconds of variable latency.

So – why is SnapStart an “opt-in” feature?

AWS has decided to not make SnapStart a default functionality of AWS Lambda, because there are certain pre-requisites for being able to use this new functionality.

The most important one is that the code executed as part of the lambda function cannot rely on the “randomization” features of Java, and this has implications for the code that you write. Please read the SnapChart documentation for further details – the FAQ is very detailed on  this. This does not only include the code that YOU write as part of your lambda function but also the code of every library that you use as part of your code.

Other things to consider are that you cannot rely on network connections that you created/set up during the initialization of your code to be still valid and available when your function is resumed from a snapshort. This means that you will need to verify any network connections (e.g. connections for a backend database) before you actually use it.

Any ephemeral data that you rely on can be invalid when your function is resumed from a snapshot – and this means that you will need to ensure that any ephemeral data is still valid if you want to access it. This could be temporary credentials, machine learning models or just temporary data that you use within your function.

In it’s documentation, AWS also provides a bunch of examples and best practices to make sure that your code is “SnapSafe” and can work with this new functionality without causing problems.

How do you activate AWS Lambda SnapStart?

You can activate AWS Lambda either in the AWS console or through the AWS cli. Surprisingly this feature also comes with direct CloudFormation, CDK and SDK support. Please remember that after activating it, you will need to create a new version of lambda function that you want to test.

Further details can be found in the AWS documentation.

Let’s go back to the “teaser” of this post – How can YOU safe costs with this functionality?

For Lambda functions you are based on the number of requests for your functions, the allocated memory and the time that it takes to execute your java code. The duration is calculated from the time your code begins executing until it returns or otherwise terminated, rounded up to the nearest 1ms.

The total duration is a combination of the initialization code (in the constructor of your function) and the code in the “handler”.
With SnapStart enabled, the “initialization code” / phase is moved to the provisioning of the function (when you set it up) – and when the function is executed the “initialization phase” is replaced by re-starting the function from the already created snapshot.

This means that for functions, that have an intense “initialization” phase the potential savings are bigger than for functions that run all of their code in the “handler”. More details around how to optimize your Java functions for cold starts can be found in this blog.

Let’s do a few calculations on how this helps us to safe money:

The table assumes that the initialization phase is reduced to 25 milliseconds (which is not technically correct according to the AWS documentation, but its as close as I can guess today).

What this table shows is that the SnapStart feature will help us to reduce our costs for AWS Lambda significantly, if we have Lambda functions that have a longer initialization time than “handler”  execution time.

Overall I calculated between 10% and 60% savings with this feature, depending on your use case. Please be aware: This is calculation/guessing only, I have not yet measured that during a test! For further analysis and details you will need to analyze your current performance metrics of your lambda functions and perform additional tests and calculations.
Before you can use the feature, as already mentioned, you will need to verify that your lambda code is “SnapSafe”.

If you try out this new feature and gain meaningful experiences out of it, please let me know and reach out to me in the comments section, through LinkedIn or E-Mail.

Visits: 300