Gonçalo Palma
November 14, 2021

Creating Cloud Functions in Dart

Photo by Pero Kalimero on Unsplash.

Cloud Functions are serverless functions that are executed each time we access them via an HTTP request. They can be used to react to specific triggers - such as additions in Firebase Storage that trigger a Push Notification, or directly called from our app to access a specific resource using a Service Account.

In this article, we’ll explore a specific usage of Cloud Functions - abstracting Secret API keys from the AccuWeather API and accessing and using it to query the API.

Creating our first Cloud Function

When building our first Dart Cloud Function, we have the bulk of the work is already done for us by the dartfn package. This package will create:

To use dartfn, we must first activate it globally:

$ dart pub global activate dartfn

Next, we create a new directory for our cloud function:

$ mkdir example_function && cd example_function

And finally, we can use dartfn to populate this folder with a template. Since we want to create a simple Cloud Function, we’ll use the helloworld template:

$ dartfn generate helloworld

This results in the following files being created:

.
├── Dockerfile
├── Makefile
├── README.md
├── analysis_options.yaml
├── bin
│   └── server.dart
├── lib
│   └── functions.dart
├── pubspec.yaml
└── test
    └── function_test.dart

What we want to be looking at is the lib/functions.dart file, which is where our cloud functions will be:

import 'package:functions_framework/functions_framework.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Response function(Request request) => Response.ok('Hello, World!');

Basically, our cloud function will accept any request and reply with a String message saying Hello World.

To test it locally, we first need to run our server:

$ dart bin/server.dart

This server will be hosted at localhost:8080, which means we will be able to access our cloud function by creating a HTTP POST request via curl:

$ curl -i localhost:8080
HTTP/1.1 200 OK
date: Sun, 14 Nov 2021 10:47:30 GMT
content-length: 13
x-frame-options: SAMEORIGIN
content-type: text/plain; charset=utf-8
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
server: dart:io with Shelf

Hello, World!%

And, as expected, it replies back with “Hello, World!”.

Using a Cloud Function to hide Secret Keys

Let us imagine we are developing a weather application that needs to retrieve data from a private 3rd party service. Most 3rd party services will provide us with some sort of API key that will be linked to our billing account - the more calls we make to their services, the more we will have to pay at the end of the month.

If we use these keys directly in our Flutter applications, there’s always a chance that someone will try to either reverse engineer the mobile and desktop apps or see the main.dart.js file on Flutter Web to look for these keys and use them for their purposes and needs.

A common way to go around this is to hide these secret keys behind a backend server - we expose an endpoint in our server that uses this API key, processes the data, and then gives it back to us. However, sometimes it is not feasible to create a full-blown backend server just for this. Instead, we can create a Cloud Function whose purpose is to access a specific API endpoint and return the response to the application.

As an example, we will want to use AccuWeather to get the daily forecast for a specific city.

Looking at the documentation, we will need to use two endpoints:

  1. City Search endpoints will get us a list of cities with their keys
  2. We use the keys from the previous step and call the 1 Day of Daily Forecasts

Accepting body data from the request

As seen in the helloworld example, the Cloud Function will accept a Request object, which allows us to read the body of the HTTP POST request using the readAsString:

@CloudFunction()
Future<Response> function(Request request) async {
  final queryBody = await request.readAsString();
}

Since we want to query this cloud function with a city name, we can stipulate that we will accept a JSON body with the following structure:

{
    "query": "<city_name>"
}

As such, we can get now the query parameter from the request body:

@CloudFunction()
Future<Response> function(Request request) async {
  final queryBody = await request.readAsString();
  final query = jsonDecode(queryBody)['query'];
}

The query variable is what we are going to use when we use the Cities Search AccuWeather endpoint.

Searching for Cities in AccuWeather

Now that we have our query from the request, we can use it to make a call to AccuWeather’s Cities API. For convenience, the Location class was created to deserialize the response.

@CloudFunction()
Future<Response> function(Request request) async {
  final queryBody = await request.readAsString();
  final query = jsonDecode(queryBody)['query'];
  final searchUri = Uri.parse(
      "http://dataservice.accuweather.com/locations/v1/cities/search?apikey=$weatherApiKey&q=$query");
  final cities = await http
      .get(searchUri)
      .then((response) =>
        jsonDecode(response.body) as Iterable
      )
      .then((locations) => locations.map((value) => Location.fromMap(value)))
      .then((list) => list.take(3));
}

We are using take(3) to only get 3 results from our search. This means that we will only search for the daily forecast of the first 3 cities that matches our search criteria.

However, what happens if there are no cities available?

In that case, we should return a response from the cloud function with an empty JSON:

@CloudFunction()
Future<Response> function(Request request) async {
  ///...

  if (cities.isEmpty) {
    return Response.ok("{}");
  }
}

Obtaining the Weather Forecast

We can to query the list of cities from the AccuWeather API and get the forecast for each:

@CloudFunction()
Future<Response> function(Request request) async {
  // ... 
  for (var city in cities) {
    final weatherSearch = Uri.parse(
        'http://dataservice.accuweather.com//forecasts/v1/daily/1day/${city.key}?apikey=$weatherApiKey&metric=true');
    final weather = await http
        .get(weatherSearch)
        .then((response) => jsonDecode(response.body))
        .then((weather) => Weather.fromMap(weather));
  }
}

Adding a Response to our Cloud Function request

The first thing we should determine is: what data does the application need to display?

For now, we can say that it needs:

  1. The city name
  2. The country name
  3. The maximum temperature
  4. The minimum temperature
  5. The date for the forecast

With this information, we can create a new class that encapsulates all this data, with a toJson method that allows serializing the content.

import 'dart:convert';

class WeatherLocation {
  final String location;
  final String country;
  final int epochTime;
  final double maximum;
  final double minimum;

  WeatherLocation({
    required this.location,
    required this.country,
    required this.maximum,
    required this.minimum,
    required this.epochTime,
  });

  Map<String, dynamic> toMap() {
    return {
      'location': location,
      'country': country,
      'epochTime': epochTime,
      'maximum': maximum,
      'minimum': minimum,
    };
  }

  String toJson() => json.encode(toMap());

  factory WeatherLocation.fromMap(Map<String, dynamic> map) {
    return WeatherLocation(
      location: map['location'],
      country: map['country'],
      epochTime: map['epochTime'],
      maximum: map['maximum'],
      minimum: map['minimum'],
    );
  }

  factory WeatherLocation.fromJson(String source) => WeatherLocation.fromMap(json.decode(source));
}

Next, we can change our forecast query loop to create a new WeatherLocation object each time we get a response from the server:

@CloudFunction()
Future<Response> function(Request request) async {
  // ... 
  final List<WeatherLocation> result = <WeatherLocation>[];

  for (var city in cities) {
    final weatherSearch = Uri.parse(
        'http://dataservice.accuweather.com//forecasts/v1/daily/1day/${city.key}?apikey=$weatherApiKey&metric=true');
    final weather = await http
        .get(weatherSearch)
        .then((response) => jsonDecode(response.body))
        .then((weather) => Weather.fromMap(weather));

    result.add(
      WeatherLocation(
        epochTime: weather.dailyForecasts.first.epochDate,
        location: city.localizedName,
        country: city.region.englishName,
        maximum: weather.dailyForecasts.first.temperature.maximum.value,
        minimum: weather.dailyForecasts.first.temperature.minimum.value,
      ),
    );
  }
}

Finally, we create a Map from the WeatherLocation list, and serialize it to JSON using the jsonEncode function so that it outputs a String. This String is going to be what we send as a response with the Response.ok function:

@CloudFunction()
Future<Response> function(Request request) async {
  //...
  final resultMap = result.map((location) => location.toMap()).toList();

  return Response.ok(jsonEncode(resultMap));
}

Now, if we run the server via dart bin/server.dart we can use curl to get test it:

$ curl -i -X POST -H "Content-Type: application/json" -d '{"query":"fenix"}' http://localhost:8080/
HTTP/1.1 200 OK
date: Sat, 13 Nov 2021 19:36:45 GMT
content-length: 194
x-frame-options: SAMEORIGIN
content-type: text/plain; charset=utf-8
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
server: dart:io with Shelf

[{"location":"Fenix","maximum":21.3,"minimum":0.3,"country":"United States","epochTime":1636804800},{"location":"Fenix","maximum":32.3,"minimum":22.6,"country":"Ecuador","epochTime":1636804800}]%

Publishing the Cloud Function

Since our Cloud Function works locally, we can now deploy it using the following steps:

  1. Create a new Google Cloud project and retrieve its ID;
  2. Activate billing for the project;
  3. Install Google Cloud SDK;
  4. Use $ gcloud auth login to login to your Google account;
  5. Set the specific project you want to deploy your cloud function with $ gcloud config set project <PROJECT_ID> with the ID from step 1.

Now we can manage our Google Cloud project via the command line, including deploying a new Cloud Function.

To publish the cloud function we will need to use:

$ gcloud beta run deploy <name> \
  --source=. \
  --region=<server> \
  --platform managed \
  --allow-unauthenticated # If we want everyone to access this endpoint

For our specific example it would be:

$ gcloud beta run deploy weather-cloud-function \
  --source=. \
  --region=us-central1 \
  --platform managed \
  --allow-unauthenticated

At the end of the process, you will receive URL for us to access it such as:

https://weather-cloud-function-io370i7pja-uc.a.run.app

As with the previous example, to test our cloud function, we can always use curl.

Using Cloud Functions inside our Flutter apps

Using a Cloud Function is as simple as querying a REST API endpoint.

We going to use the http package, with a specific query parameter, and use a POST method to the Cloud Function URL:

Future<List<WeatherLocation>> searchWeather(String input) {
    return http
        .post(
            Uri.parse("https://weather-cloud-function-io370i7pja-uc.a.run.app"),
            body: jsonEncode({'query': input}))
        .then((response) => jsonDecode(response.body) as Iterable)
        .then((json) =>
            json.map((value) => WeatherLocation.fromMap(value)).toList());  
}

With this, we can now create a simple Weather application:

Example App

Conclusion

Apart from serving as simple endpoints to protect API keys, we can use Cloud Functions to “react” to specific events:

  1. They can be called in response to an event, such as merging into the main branch of a GitHub repository, and react to it by performing actions - sending a slack message with a summary and calling our CD to start the deployment with the newly merged code;
  2. When a value is updated in Firebase Firestore, we can use a Cloud Function to dispatch a notification to certain users;

Moreover, with dartfn we not only have a simple setup project but a Docker file and a Makefile which we can explore in future content. This makes it easier to create maintainable cloud functions that can be tested and integrated with a simple CI/CD environment.

Finally, the ability to use Dart as a language to develop the cloud functions and the ease of use of the gcloud SDK, makes Cloud Functions a technology to consider when we want to have serverless execution of functions.

The code for this project is hosted in Github at: https://github.com/Vanethos/dart_weather_cloud_function

Want to get the latest articles and news? Subscribe to the newsletter here 👇

Diving Into Flutter

In Diving into Flutter we will discover the Flutter Framework, bit by bit.

From the BuildContext, to pubspec files, going through exotic backend solutions, there won't be a stone left unturned.

Each week I'll also share one or two links for interesting resources I found around the Web

And for other articles, check the rest of the blog! Blog - Gonçalo Palma

Want to get the latest articles and news? Subscribe to Diving Into Flutter

Hey! I'm Gonçalo Palma! 👋

I often share Deep Diving articles on Flutter 💙