Working with the Google API in Serverpod: Authentication — Part 2.5

An example integration with the YouTube API

Isak
Serverpod
10 min readJun 1, 2023

--

Authentication Series

Part 1 — Email and Password Authentication
Part 2 — Google Authentication
Part 2.5 — Google API
Part 3 — Apple Authentication

Working with the Google API

The default setup for Google Sign-In integration in your Flutter app allows you access to basic user information such as email, profile image, and name. However, you might require additional functionality, like accessing a user’s calendar, contacts, or files. We will showcase how to work with the google API by implementing an integration with YouTube to fetch the users liked videos. This feature goes beyond simple authentication, if you only want to let users sign in you can skip this tutorial.

Render a list of liked YouTube videos, fetched with the users auth token from google.

Prerequisite

  • Serverpod Project: An existing Serverpod project with the serverpod_auth module installed is necessary for this tutorial. If you haven’t done this already, our Part 1: Email and Password Authentication provides a step-by-step guide.
  • Google Sign in integration: You need to integrate Google Sign in to be able to complete this tutorial. Take a look at Part 2: Google Authentication if you have not completed this step already.

Requesting Additional Access Scopes

  1. Update the OAuth Consent Screen: To gain access to more than just the basic user information, you need to add the required scopes to the OAuth consent screen.
  2. Request Access at Sign-In: After updating the consent screen, request access to the additional scopes at the time of signing in. You can do this by setting the additionalScopes parameter of the signInWithGoogle method or the SignInWithGoogleButton widget.
SignInWithGoogleButton(
...
additionalScopes: const ['https://www.googleapis.com/auth/youtube'],
)

A full list of available scopes can be found here.

Accessing Google APIs from the Server

With additional access scopes, you can access a range of Google APIs on the server side. Here is how you do it:

  1. Install googleapis: Use the googleapis package to access the different google apis. Run dart pub add googleapis in your server project.
  2. Enable the API: Go to the google cloud console and enable the specific api you want to use. In this example we are using the YouTube API, you can enable it here.
  3. Get an Auth Client: Use the authClientForUser method from the serverpod_auth_server package to request an AutoRefreshingAuthClient. This client can be used to access Google's APIs on behalf of the user.
import 'package:serverpod_auth_server/module.dart';

// Retrieve the google client
final googleClient = await GoogleAuth.authClientForUser(session, userId!);

Use the google client to retrieve data from some api.

var youTubeApi = YouTubeApi(googleClient);
var favorites = await youTubeApi.playlistItems.list(
['snippet'],
playlistId: 'LL', // Liked List
);

In essence this is all you need to do to work with the google API. But let’s see how we can combine this info a fully functional Serverpod endpoint complete with error handling.

Setting Up Protocol Classes and Generating Dart Models

First, we will define two protocol classes in their respective YAML files. The first, NotSignedInWithGoogle, is an exception class used when the user has not signed in with Google. The second, VideoPreview, is a data class used to hold video information retrieved from the YouTube API.

Create the protocol/not_signed_in_with_google.yaml file with the following content:

exception: NotSignedInWithGoogle
fields:
message: String

Then, create the protocol/video_preview.yaml file with this content:

class: VideoPreview
fields:
videoId: String
title: String
thumbnailUrl: String
publishedAt: DateTime
videoUrl: String

After creating these files, we need to generate the corresponding Dart models. Serverpod comes with a handy tool for this, which we’ll use by running the following command in the terminal:

serverpod generate

This command should generate the not_signed_in_with_google.dart and video_preview.dart files in the generated directory of your server project, and these classes are ready to use in your server code.

Creating the YouTube Endpoint

Now, let’s build an endpoint to interact with the YouTube API. To do this, create a new file named youtube_endpoint.dart inside the endpoints directory and start by importing the necessary modules.

import 'package:googleapis/youtube/v3.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_server/module.dart';

import '../generated/not_signed_in_with_google.dart';
import '../generated/video_preview.dart';

Next, define a new class YoutubeEndpoint that extends Endpoint and requires user login.

class YoutubeEndpoint extends Endpoint {
@override
bool get requireLogin => true;
// ...
}

Inside this class, let’s implement a readFavoriteVideos method. This method retrieves a user's favorite videos from YouTube. It returns a Future that completes with a List of VideoPreview objects.

Future<List<VideoPreview>> readFavoriteVideos(Session session) async {
// ...
}

Fetching the User’s ID and Google Client
Firstly, retrieve the ID of the currently authenticated user and the Google client associated with this user.

final userId = await session.auth.authenticatedUserId;
final googleClient = await GoogleAuth.authClientForUser(session, userId!);

Checking if User is Authenticated with Google
If the googleClient is null, it implies that the user isn't authenticated with Google. In such a case, throw a NotSignedInWithGoogle exception. This is the exception we defined in the protocol file earlier!

if (googleClient == null) {
// User is not authenticated with Google
throw NotSignedInWithGoogle(message: 'Sign in with Google to access this feature.');
}

Retrieving Liked Videos from YouTube
Establish a connection to the YouTube API and fetch the user’s favorite videos. The googleClient is an object containing the user credentials to access the different google resources.

final youTubeApi = YouTubeApi(googleClient);
final favorites = await youTubeApi.playlistItems.list(
['snippet'],
playlistId: 'LL', // Liked List
);

Transforming the Response
Finally, transform the API response into a list of VideoPreview objects.

final videoPreviews = favorites.items
?.where(_isValidPlaylistItem)
.map(_converToVideoPreview)
.toList();
return videoPreviews ?? [];

What happens here? Let’s break it down!

  1. favorites.items: This is the list of playlist items we received from the YouTube API. Each item in this list corresponds to a video in the user's favorites.
  2. ?.where(_isValidPlaylistItem): We're using the where method to filter the list of items. The where method loops through each item in the list and keeps only those items that satisfy the condition specified by _isValidPlaylistItem. The condition in _isValidPlaylistItem checks that all required fields (title, publishedAt, thumbnail url, and videoId) are present in the item. The ? before .where is a null-aware operator, which means if favorites.items is null, the code after it won't be executed and null will be returned.
  3. .map(_converToVideoPreview): After filtering the list, we use the map method to transform each item in the list from a PlaylistItem object to a VideoPreview object. The transformation is performed by the _converToVideoPreview function.
  4. .toList(): The map method returns an iterable, so we convert it back into a list.
  5. Lastly we return the videoPreviews list. However, if videoPreviews is null (which can happen if favorites.items was null), the null-coalescing operator ?? ensures that an empty list is returned instead. This is a best practice to avoid null pointer exceptions elsewhere in your code.

Defining Helper Methods

You might have noticed the two helper methods: _isValidPlaylistItem and _converToVideoPreview is missing an implementation. Let's define them now.

  1. _isValidPlaylistItem: This method takes a PlaylistItem as input and checks if it's valid i.e., it has all the required fields: title, publishedAt, url, and videoId.
  2. _converToVideoPreview: This method takes a PlaylistItem and transforms it into a VideoPreview object.
bool _isValidPlaylistItem(PlaylistItem item) {
final snippet = item.snippet;

if (snippet?.title == null) return false;
if (snippet?.publishedAt == null) return false;
if (snippet?.thumbnails?.default_?.url == null) return false;
if (snippet?.resourceId?.videoId == null) return false;

return true;
}

VideoPreview _converToVideoPreview(PlaylistItem item) {
return VideoPreview(
videoId: item.snippet!.resourceId!.videoId!,
title: item.snippet!.title!,
thumbnailUrl: item.snippet!.thumbnails!.default_!.url!,
publishedAt: item.snippet!.publishedAt!,
videoUrl:
'https://www.youtube.com/watch?v=${item.snippet!.resourceId!.videoId!}',
);
}

Putting it all together you should end up with a file inside endpoints/youtube_endpoint.dart that looks like this:

import 'package:googleapis/youtube/v3.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_server/module.dart';

import '../generated/not_signed_in_with_google.dart';
import '../generated/video_preview.dart';

class YoutubeEndpoint extends Endpoint {

@override
bool get requireLogin => true;

Future<List<VideoPreview>> readFavoriteVideos(Session session) async {
final userId = await session.auth.authenticatedUserId;
final googleClient = await GoogleAuth.authClientForUser(session, userId!);

if (googleClient == null) {
// User is not authenticated with Google
throw NotSignedInWithGoogle(message: 'Sign in with Google to access this feature.');
}

try {
final youTubeApi = YouTubeApi(googleClient);
final favorites = await youTubeApi.playlistItems.list(
['snippet'],
playlistId: 'LL', // Liked List
);

final videoPreviews = favorites.items
?.where(_isValidPlaylistItem)
.map(_converToVideoPreview)
.toList();

return videoPreviews ?? [];
} catch (error) {
print('Failed to read favorites videos from the google api.');
// There are a number of things that can go wrong here, for example if
// we didn't enable the youtube api in the google cloud console.
rethrow;
}
}

bool _isValidPlaylistItem(PlaylistItem item) {
final snippet = item.snippet;

if (snippet?.title == null) return false;
if (snippet?.publishedAt == null) return false;
if (snippet?.thumbnails?.default_?.url == null) return false;
if (snippet?.resourceId?.videoId == null) return false;

return true;
}

VideoPreview _converToVideoPreview(PlaylistItem item) {
return VideoPreview(
videoId: item.snippet!.resourceId!.videoId!,
title: item.snippet!.title!,
thumbnailUrl: item.snippet!.thumbnails!.default_!.url!,
publishedAt: item.snippet!.publishedAt!,
videoUrl:
'https://www.youtube.com/watch?v=${item.snippet!.resourceId!.videoId!}',
);
}
}

With this, you have created an endpoint that allows you to fetch a user’s favorite videos from YouTube. Great job! Run serverpod generate to generate all the client code we need to integrate this in our flutter app.

Creating the Flutter widget

Let’s connect our server code to our Flutter app and render a list of our favorite YouTube videos. In this part we will make use of the url_launcher package to open the YouTube videos to play them. You can add the dependency by running:

flutter pub add url_launcher

In our Flutter project create a new file inside src/widgets/youtube_favorits.dart. This is where we'll create a widget to display a user's favorite YouTube videos.

Setting up the Widget
Begin by importing the necessary packages at the start of the file and declare the YoutubeFavorits StatefulWidget.

import 'package:flutter/material.dart';
import 'package:your_app_name/src/client/protocol.dart'; // Import your generated protocol file

class YoutubeFavorits extends StatefulWidget {
const YoutubeFavorits({Key? key}) : super(key: key);

@override
_YoutubeFavoritsState createState() => _YoutubeFavoritsState();
}

We’re creating a StatefulWidget because the list of favorite videos can change, and we want to be able to rebuild our widget when that happens.

Defining the State
Next, define the _YoutubeFavoritsState class. This class will hold the mutable state for our YoutubeFavorits widget.

class _YoutubeFavoritsState extends State<YoutubeFavorits> {
List<VideoPreview> _favorites = [];
String? _errorMessage;
}

Here we’ve declared a list _favorites to hold the videos, and a String _errorMessage to hold any error messages.

Fetching YouTube Favorites
Now we’ll define a method to fetch the user’s favorite videos from the server.

void readYouTubeFavorites() async {
try {
final favorites = await client.youtube.readFavoriteVideos();
setState(() {
_favorites = favorites;
_errorMessage = null;
});
} catch (e) {
var message = 'Something went wrong';
if (e is NotSignedInWithGoogle) {
message = e.message;
}
setState(() {
_errorMessage = message;
});
}
}

The readYouTubeFavorites method makes a call to the server using the readFavoriteVideos method we defined in the server code. If successful, it updates the state with the new list of favorite videos. If an error occurs, it updates the state with an appropriate error message.

Building the UI
Now we’ll build the UI for our widget in the build method.

We can easily convert our VideoPreview list into renderable widgets using the ListTile widget.

_favorites.map((v) => ListTile(
onTap: () => launchUrl(Uri.parse(v.videoUrl)),
leading: Image.network(v.thumbnailUrl),
title: Text(v.title),
subtitle: Text(
v.publishedAt.toString(),
),
))
  1. onTap: () => launchUrl(Uri.parse(v.videoUrl)): This line of code specifies what happens when a user taps on the ListTile. The launchUrl function is invoked, which would open the provided URL in a web browser. The URL is the videoUrl of the current video in our _favorites list.
  2. leading: Image.network(v.thumbnailUrl): The leading attribute typically holds a widget that appears at the start of the ListTile. In this case, it's an Image widget that displays an image from the provided URL, which is the thumbnail of the current video in our _favorites list.
  3. title: Text(v.title): The title attribute is the primary content of the ListTile. In our case, it's a Text widget that displays the title of the current video.
  4. subtitle: Text(v.publishedAt.toString()): The subtitle attribute is secondary content that is displayed below the title. Here, it's a Text widget displaying the publication date of the current video.

Note: On Flutter web loading the image may fail with a CORS error. This is because we are using the URL directly from google and by default Flutter uses canvaskit which loads images with http requests. To get around this you can run Flutter with the html renderer. (Read more)

flutter run -d chrome --web-renderer html

Putting all this together and adding in a button to fetch the videos we end up with a widget that looks like this:

class YoutubeFavorits extends StatefulWidget {
const YoutubeFavorits({Key? key}) : super(key: key);

@override
_YoutubeFavoritsState createState() => _YoutubeFavoritsState();
}

class _YoutubeFavoritsState extends State<YoutubeFavorits> {
List<VideoPreview> _favorites = [];
String? _errorMessage;

void readYouTubeFavorites() async {
try {
final favorites = await client.youtube.readFavorites();
setState(() {
_favorites = favorites;
_errorMessage = null;
});
} catch (e) {
var message = 'Something went wrong';
if (e is NotSignedInWithGoogle) {
message = e.message;
}
setState(() {
_errorMessage = message;
});
}
}

@override
Widget build(BuildContext context) {
return Column(
children: [
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: () {
readYouTubeFavorites();
},
child: const Text('Refresh videos'),
),
Text("Videos: ${_favorites.length}"),
],
),
),
_errorMessage != null
? Text(_errorMessage!)
: Column(
children: _favorites
.map(
(v) => ListTile(
onTap: () => launchUrl(Uri.parse(v.videoUrl)),
leading: Image.network(v.thumbnailUrl),
title: Text(v.title),
subtitle: Text(
v.publishedAt.toString(),
),
),
)
.toList(),
),
],
);
}
}

Last step we have to do is including this new widget in the AccountPage we created in the previous tutorial. The build method would now look like this for src/widgets/account_page.dart

@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
contentPadding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
leading: CircularUserImage(
userInfo: sessionManager.signedInUser,
size: 42,
),
title: Text(sessionManager.signedInUser!.userName),
subtitle: Text(sessionManager.signedInUser!.email ?? ''),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
sessionManager.signOut();
},
child: const Text('Sign out'),
),
),
const YoutubeFavorits(),
],
);
}

Running our app

Now that we’ve completed both the server-side and client-side code, it’s time to run our application to see our favorite YouTube videos in action.

Start the Serverpod Server: Navigate to the directory where your server-side code resides and run the following command to start your Serverpod server:

dart bin/main.dart

Run the Flutter App: Open a new terminal, navigate to your Flutter project’s directory and run:

flutter run
Sign in with Google and fetch the videos!

Conclusion

Congratulations on building this integration with the YouTube API using Serverpod and Flutter! We’ve covered a lot in this tutorial, from defining server-side endpoints and exceptions, calling the YouTube API, to creating a Flutter UI to display our data. Hopefully, you’ve found it useful for understanding how to work with external APIs in Serverpod and Flutter.

--

--