Flutter + AWS Realtime database (No Amplify): Part 1
- Flutter + AWS Realtime database (No Amplify): Part 1
- Flutter + AWS Realtime database (No Amplify): Part 2
- Flutter + AWS Realtime database (No Amplify): Part 3
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.
- Setup AWS and basic web socket
- 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.
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.
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,
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.
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.
- Connection begins when it’s routing to the page.
- Starts listening when
initState
- For every new data listened, update with
setState
- 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.
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
, domainName
and 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.