Flutter + AWS Realtime database (No Amplify): Part 1

Taehoon Kim
8 min readOct 30, 2020

--

Here’s today's topic.

How do I use AWS real-time database in Flutter?

In fact, there are many other ways of implementing AWS real-time database if you are using javascript based frameworks or libraries such as React.
For example, you can use Amplify or AppSync. Since they provide a javascript version of AWS SDK, there are not many issues with the implementation.

How about Flutter? Amplify team just started supporting Flutter. Therefore there are not many services provided at this moment in 2020 October.
Here we can check the latest update of amplify-flutter.

Then what is this tutorial about?
Of course, this is not a solution (or replacement) of amplify. Of course not.
This is just an experiment to create a “real-time database like” environment.

I’ll split this tutorial into two parts.

  1. Setup AWS and basic web socket
  2. AWS DynamoDB setup and more about web socket

In this tutorial, let’s cover Setup AWS and basic web socket.

Here’s what we need:

For Flutter, we need the web_socket_channel package and Stream.
Web socket allows us to connect to the server and Stream receives the message from the server.

For AWS, we’ll be using API Gateway and Lambda.
For our convenience, we’ll use the Serverless framework in this tutorial.

AWS Setup

First, log in to the AWS console, go to IAM then create a user.
No need to care much about permissions and access. We can deal with them later. For our convenience, we’ll give AdministratorAccess.

AWS add a user with programmatic access
AWS add a user set the permission as AdministratorAccess
AWS adding a user completed

After all “adding a user” process, we’ll have the Access key ID and Secret access key. Save them in a safe place and let’s move on.

Create AWS Project with Serverless

Let’s create an empty folder and install the serverless framework if you haven’t.

npm install -g serverless

Then let’s set up an AWS user profile. If you have more than one account, it’s better to create a profile. I’m naming the profile as “taehoon-flutter-study”.

aws configure --profile taehoon-flutter-study

It’ll ask for the Access key ID as well as the Secret access key. Put them in and set everything else as default then finish setting up.

creating AWS profile

Once it creates a profile successfully, let’s install the Serverless framework template into the AWS project folder.

sls create --template aws-nodejs

sls is the short form of serverless. Since we are writing lambda codes in nodejs, we’ll set aws-nodejs as the template.
Once it’s done, we’ll have handler.js and serverless.yml files in the project folder.

In serverless.yml, let’s apply the profile we just created.

provider:
name: aws
runtime: nodejs12.x
region: us-east-1
stage: dev
profile: taehoon-flutter-study
logs:
websocket: true

logs allows you to log lambda functions in Cloud Watch.
stage can be any name but I’ll just set to dev here. Usually, it goes something like, dev or prod.

It’s time to set up functions.

functions:
connectionHandler:
handler: handler.connectionHandler
events:
- websocket:
route: $connect
- websocket:
route: $disconnect
defaultHandler:
handler: handler.defaultHandler
events:
- websocket: $default
databaseStreamHandler:
handler: handler.databaseStreamHandler
events:
- websocket:
route: databaseStream

connectionHandler takes an effect when there’s a connect or disconnect event occurs. Nothing fancy.

defaultHandler is for when you call a function that doesn’t exist. It like the default case in the switch state.

databaseStreamHandler is the function we need to focus on.

We’ll code each function as below,

'use strict';
const AWS = require("aws-sdk");
require("aws-sdk/clients/apigatewaymanagementapi");
const success = {
statusCode: 200
};
module.exports.connectionHandler = async (event, context) => {

if (event.requestContext.eventType === 'CONNECT') {
console.log(`[ConnectionHandler] Connected: ${JSON.stringify(event, null, 2)}`);
return success;
}
else if (event.requestContext.eventType === 'DISCONNECT') {
console.log(`[ConnectionHandler] Disconnected: ${JSON.stringify(event, null, 2)}`);
return success;
}
};module.exports.defaultHandler = async (event, context) => {
let connectionId = event.requestContext.connectionId;
const endpoint = event.requestContext.domainName + "/" + event.requestContext.stage;
console.log('[defaultHandler] endpoint is: ' + endpoint);
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: "2018-11-29",
endpoint: endpoint
});
const params = {
ConnectionId: connectionId,
Data: 'Seems like wrong endpoint'
};
return apigwManagementApi.postToConnection(params).promise();
};
module.exports.databaseStreamHandler = async (event, context) => {
console.log(JSON.stringify(event, null, 2));

const body = JSON.parse(event.body);
const randNum = body.randNum;
const msg = body.msg;

let connectionId = event.requestContext.connectionId;
const endpoint = event.requestContext.domainName + "/" + event.requestContext.stage;
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: "2018-11-29",
endpoint: endpoint
});
const params = {
ConnectionId: connectionId,
Data: JSON.stringify({connectionID: connectionId, randNum: randNum, msg: msg}),
};
return apigwManagementApi.postToConnection(params).promise();
};

Once it’s done, let’ deploy.

sls deploy -s dev

-s is the short form of a stage. We are deploying to the dev stage.
When it’s completed, you’ll see messages as follows,

serverless deployment succeeded

An endpoint starts with wss or ws means a web socket had been successfully applied to an API gateway.
Let’s double-check if the deployment was successful by visiting Cloud Formation.

serverless deployment can be checked in Cloud Formation

In the same manner, you can visit API Gateway as well as Lambda to check whether the deployment was successful.

Finally, it’s time for Flutter UI now.

UI with Flutter

Create a flutter project and install the web_socket_channel package. I assume we all know how to do so.

There are plenty ways of implementing the web socket but in here we’ll try the following process.

  1. Connection begins when it’s routing to the page.
  2. Starts listening when initState
  3. For every new data listened, update with setState
  4. For sending data, use randomly generated numbers.

For starting connections in routing, we’ll do the following,

AWSRealtimeSocketTutorialPage(
socketChannel: HtmlWebSocketChannel.connect(
"wss://<GIVEN_ID>.execute-api.us-east-1.amazonaws.com/dev"),
)

As expected, the page name is AWSRealtimeSocketTutorialPage and the URL is the endpoint we’ve seen when we deploy the serverless.
If your flutter project is a web application, you should use HtmlWebSocketChannel . Otherwise, use other methods. For more info, please refer to the documents of web_socket_channel.

In initState, we start listening:

@override
void initState() {
// Start listening socket stream
widget.socketChannel.stream.listen((message) {
print('Message from stream listen: $message');
setState(() => socketData = message);
});
super.initState();
}
@override
void dispose() {
// Make sure to close the stream when its not in use
widget.socketChannel.sink.close();
super.dispose();
}

So far, when the page is loaded, a connection is already made and starts listening to the socket.

Sending data through the socket can be done as the following,

void _onWriteThroughSocket() async {
// Generate a random number
var rng = new Random();
int generatedInt = rng.nextInt(10000);
// Write through lambda function & API Gateway
widget.socketChannel.sink.add(jsonEncode({
"action": "databaseStream",
"randNum": generatedInt,
"msg": "tester from $generatedInt"
}));
}

sink.add allows you to send out the data but there’s one rule that you must be aware of. You must set the action key. For example, we put the action key can used databaseStream as the value of it.
Please notice that databaseStream is the name of the function we’ve created in serverless.
randNum and msg are just sample data.

Here’s the all code for the UI.

import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_study_web/widgets/default_page_frame.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class AWSRealtimeSocketTutorialPage extends StatefulWidget {
AWSRealtimeSocketTutorialPage({@required this.socketChannel});
final WebSocketChannel socketChannel;
@override
_AWSRealtimeSocketTutorialPageState createState() =>
_AWSRealtimeSocketTutorialPageState();
}
class _AWSRealtimeSocketTutorialPageState
extends State<AWSRealtimeSocketTutorialPage> {
String socketData;
@override
void initState() {
// Start listening socket stream
widget.socketChannel.stream.listen((message) {
print('Message from stream listen: $message');
setState(() => socketData = message);
});
super.initState();
}
@override
void dispose() {
// Make sure to close the stream when its not in use
widget.socketChannel.sink.close();
super.dispose();
}
void _onWriteThroughSocket() async {
// Generate a random number
var rng = new Random();
int generatedInt = rng.nextInt(10000);
// Write through lambda function & API Gateway
widget.socketChannel.sink.add(jsonEncode({
"action": "databaseStream",
"data": {
"body": {"randNum": generatedInt, "msg": "tester from $generatedInt"}
}
}));
}
@override
Widget build(BuildContext context) {
// var screenSize = MediaQuery.of(context).size;
ThemeData themeData = Theme.of(context);
return DefaultPageFrame(
body: Column(
children: [
Container(
child: FlatButton(
onPressed: _onWriteThroughSocket,
child: Text(
'Write to DynamoDB',
style: themeData.primaryTextTheme.button,
),
color: themeData.buttonColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
side: BorderSide(color: Theme.of(context).buttonColor),
),
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 10),
child: Text(socketData != null ? '$socketData' : 'Empty'),
),
)
],
),
);
}
}

Another rule to be noticed is that all data must be sent or received in String format.

Now let’s run this.

Then let’s press the button “Send data using socket”.

Seems working well. Let’s check what happened in Cloud Watch.

Go to AWS console -> Cloud Watch and click on “Log groups” on the left.
You’ll see the list of APIs we’ve just created. Let’s click on databaseStreamHandler and check the latest log there.

Cloud Watch logs

It worked all well including the body data. 😆
You also can check the log for connectionHandler function as well.

You might have a question like,

“Can we send data from the server-side if it has all the necessary info? (as long as the connection is there)”

In order to resolve this question, be sure to make a connection then run the following command in AWS project folder.

sls invoke -f databaseStreamHandler -d '{"requestContext": {"stage": "<STAGE_NAME>", "domainName": "<DOMAIN NAME>", "connectionId": "<CONNECTION ID>"}, "body": "{\"randNum\":123456789,\"msg\":\"test from local invoke\"}"}'

The invoke command allows you to test functions locally.
This command tests databaseStreamHandler function locally with the data followed by -d.

The data contains only the necessary values such as stage, domainNameand connectionId. The body only contains testing variables.

When we run the command,

We can see the value we’ve sent from the function.

This is great! 😄
We can now move on to the next process which is the implementation of DynamoDB and more Web socket settings.

Let’s continue in the very next tutorial.

--

--