[MUSIC PLAYING] JUSTIN MCCANDLESS:
Hi, I'm Justin, and I work on building the
Flutter framework at Google. In this workshop, we're going
to be creating a desktop app from scratch using Flutter. The ability to
build desktop apps is a feature that I'm
personally really excited about. Even when I'm working
on a mobile feature, I can't help myself but just run
the app on the desktop platform that I'm developing for just
for fun because it's so easy. I'm always amazed seeing a
full-blown desktop app pop up in seconds. And I'm really excited
to get to teach everyone more about Flutter's
desktop support today. In this workshop,
we're going to take it a little bit further
than that, and we're going to build a desktop
application integrated with the public-facing
GitHub API. We're going to add
authentication. And we're going to show
some useful activity for the logged in user. So let's go ahead
and get started. The first thing we should do is
to get the Flutter boilerplate app up and running on desktop. Be sure that your
local copy of Flutter is configured to build
desktop apps on your platform. You can do that by
running flutter config --enable-macos-desktop. If you're on Linux
or Windows, replace Mac OS with your platform. Now I can create the
app with flutter create, and let's call it github_client. And I'll change directory
into github_client. I'm going to go ahead and delete
the folders for other platforms since we're just focusing
on desktop in our case. So rm -r Android iOS and web. So what this will do,
when I flutter run, is it'll allow Flutter
to just focus on desktop and not worry about asking
us if we want to run this on any other platform. And there it is, our very
own Flutter desktop app. And I can see it's in
a native Mac OS window just like I'd expect. Let's go ahead and add version
control to our project. Let's run get init to create
a Git repository here, get add to add everything in this
directory we just created, and make my first commit. Since we're going to be
interacting with the GitHub API, it'll be nice to have
not just a Git repository, but a GitHub project that we
can refer to on the web as well. So let's create one and
push our code to it. In our browser here, let's
navigate to GitHub.com. And we'll press the plus
button here in the top right and create a new repository. We can give this the name
flutter_github_client, or whatever we want, and a
description if we want to. All the other settings are fine,
so let's create the repository. There's our new project. So let's copy that git remote
URL so we can push up our code. All right. So I'm going to add a new
git remote just like we would with any normal GitHub project,
pasting that URL that I copied. And then I'm going to
push my code to it. And going back to GitHub,
if I refresh, there we go. I can see my code right here. And I'm going to go ahead and
copy this URL for use later when we set up our OAuth app. Now that we've got a
dummy application running, let's start by adding the
ability to authenticate with GitHub's API. In order to talk
to the GitHub API, we're going to need to
register our app with GitHub and obtain some credentials. Navigate to your
OAuth apps on GitHub, which we can do here in
Settings and Developer Settings and OAuth Apps. I can click New OAuth
App to create a new app, give it a name, call it
Flutter GitHub Client. The homepage URL-- since we
do have a GitHub repository on the web for it,
let's paste that URL that we copied just before. If we want, we could
give it a description, but it's not required. And the authorization
callback URL-- since we're just
working locally, we'll make that localhost. And click Register application. Now that I see my OAuth app,
it's given me a client ID, and it gives me the option
to generate a client secret. We'll need both
of those in order to authenticate with our app. So go ahead and generate
a client secret now. I'm going to do this off
screen because the client secret is meant to be secret. So after you've generated
your client secret, make sure you copy it before you
refresh or close your window, otherwise it'll disappear. You can always generate
a new client secret, but it's nice to get it
right on the first time. Let's create a file to
store these credentials. Use your editor to create
a file at lib/github oauth credentials.dart. So this file is going to contain
three global variables that are just going to be strings
that are accessible to anyone who imports the file. And they're going to
hold our credentials. So first we need the
GitHub client ID. We also need the
GitHub client secret, and lastly, the GitHub scopes. And since we're just dealing
with some simple read information, we can just
set that in our scopes here. And that will tell GitHub that
that's the only permissions that our app needs. So paste in your client
ID and your client secret from the OAuth page on
GitHub that we created just before this. When using version
control for a project, it's a good idea to make sure
that the credentials, like what we've created, do not get
checked into the repository. This prevents anyone with
access to the repo now and in the future from obtaining
GitHub API access as your app. For Git, adding an entry to dot
gitignore for the credentials file will do the trick. So if I run git
status now, I can see that Git could track
the credentials file that we've created. But if I edit
gitignore and I add an entry just for
the credentials file that we've
created and save that, if I run git status
again, you can see it now does not show
the credentials file. It only shows the changes to
gitignore that we've made. So I can comfortably run
git add dot for everything. I see only gitignore
is still changed. And I can commit
that with no fear of checking in the credentials. Now let's start actually
working on our app. The first thing I
want to do is build a page which simply
shows a login button when we're not logged
in and a message when we are. Let's edit main.dart. So in here, we see just the
default boilerplate Flutter code. So we're going to replace
this with the code from the workshop. And taking a look at this,
it's just a simple app. The only thing of note is
the GithubLoginWidget here. And what this does is
to show a login button. And when clicked, it logs in. And after logging
in, it builds what's here in the builder function. So this will display
after logging in. Let's go ahead and
implement GithubLoginWidget with some naive
authentication that just sets a Boolean to true. So here in a new file, I'm
going to import material.dart, and I'm going to create a
new stateful widget called GithubLoginWidget. We're going to take a
builder method, which is what's going to be
built once we're logged in. That looks good. And we're going to need a
small piece of state just to say naively if
we're logged in or not, without actually doing
any authentication with GitHub yet. So if we are logged
in, then we just want to build the
builder method. And otherwise, we're going
to build our login button. So let's create just
an ElevatedButton here. And it's going to have an
onPressed handler and a child that just displays some text. And when this is
pressed, we just want to set our login
variable to true, naively. All right. This looks good. So let's run this
and see how it looks. And here is our app
with the login button. If I click it, it'll set the
Boolean immediately to true and tell us that we are, in
a fake way, logged in GitHub. We're going to need to add three
pub packages to our project if we want to actually
authenticate with GitHub. Those are HTTP, OAuth
2 and URL Launcher. So HTTP lets us set
up a redirect server. OAuth 2 deals with handling
the actual OAuth protocol. And URL Launcher lets us
open the user's browser so they can go authenticate. Let's install those
three packages now. So I can just run flutter
pub add just like any package and give the three names of the
packages that I want to add-- http, oauth2, and url_launcher. Great. Now that that succeeded,
let's take a quick look in pubspec.yaml and make sure
that all three packages are there. And scrolling down, yes, they
are, along with the stuff included in the boilerplate app. We got all three packages. Great. Let's walk through what the
real login flow looks like. The meat of the
authentication logic happens in the
onPressed handler just like it did when we set our
fake logged-in Boolean before. Now, however, it's an
asynchronous function that makes a few
different real calls. It sets up the redirect
server, and it actually allows the user to log
in and give us back an authenticated HTTP client. At that point, we can
set that as our state, and we can use that to decide
that we're now logged in. And that also is what we'll
use to access the GitHub API. Let's paste the full
authentication code from the workshop into our app. So back in our GitHub
login.dart file, instead of doing
our naive approach, let's paste in the code. And so this will handle all
of the more complicated OAuth login for us. Back in main.dart--
let's open that up-- let's also pass
in our credentials to GithubLoginWidget now that
we're going to be using them. And we'll also receive the
HTTP client after logging in. So here in the
builder, we're going to be receiving the HTTP client. And then as a parameter
to GithubLoginWidget, we can pass the githubClientID,
githubClientSecret, all the things from
our credentials file, githubScopes being the last one. Now if you're on a Mac, be
sure to also update your Mac OS runner
DebugProfile.entitlements, and also the
Release.entitlements. This allows network
permissions, which are required to be set for Mac desktop apps. So I'm opening my
DebugProfile.entitlements file. And I'm going to copy
in a little bit of code from the workshop,
just one key for debug. And now for the
Release.entitlements file, I'll copy in two keys. All right. Now let's run the app again. All right, now we see an
empty app with a login button, just as before. Let's click on that and
go through the login flow. On clicking, my browser
window opens up, and it asks me to log
in to my GitHub account. I can see it's giving
us the name that we provided when we created
our OAuth app on GitHub. And the permissions
that it gives are also the same as from
our GitHub credentials file. So if I authorize that and close
the tab, go back to our app here, I see now it says, "You
are logged in to GitHub." So this is how the
OAuth flow works. It allows the user
to authorize an app to access their own data on
a third-party service, which is really useful when
you want to create an app that works with
the user's own data like we are today. The point of
building this app is to actually use the GitHub
API, not just to log in. So let's start with
something simple and update our logged
in message just to display the user's
GitHub user ID. Fortunately, there's
already a package on pub that gives us an interface
into the GitHub API, and it's called GitHub. Let's add that to the project. So if I run flutter pub add
github just like normal, I will install that package. In main.dart, let's
write a simple function to fetch the current
user's user ID. Let's go ahead and import this
package that we just installed and write that function
to call the GitHub API and receive the user's user ID. So we're going to
return a future that resolves with the current user. And this current user class is
coming from the GitHub package that we installed. And we'll call it
viewerDetail, and it receives that accessToken. And it's going to create an
instance of the GitHub object, and we're going to
pass it that token. That way, it can go
ahead and log in for us. And here we'll return a
usage of the GitHub API. We're going to get users
and getCurrentUser, which is a method that already exists
for us thanks to that package. And there we go. This should be users. And great, looks like we have
no compiler errors there. Now we just need to
call this function and use the result
in our logged in UI so we can see the
logged in user ID. An easy way to do this is to
use a FutureBuilder, which allows you to build
some widgets based on the status of a future. So using this, we can
display a loading state while the results of the
API call are still pending, and we can show the
user ID once it's ready. So I'm going to add a
FutureBuilder right here. And the type is going
to be CurrentUser, which we got from the GitHub package. The future that
we want to pass is the result of the viewerDetail
method that we created. And we can get that access
token just from the HTTP client that we got from logging in. Now we just need
our builder method with a context and
a snapshot, which will contain the data that
was retrieved from the API. All right. So now we want to use
that snapshot data. So here, where we previously
just had a logged in message to GitHub, we can
now display something based on the snapshot. So if it has data, then let's
display the user's user ID. And if not, we can add
a quick loading message. And this is no longer
const, so we'll delete that. And that looks good. So let's run the app There's our app. So now let's go through
the login flow again and see what's changed. We automatically authenticate,
because we've already done it before. And this time, after it
finishes, we now see, "Hello justinmc." That's my GitHub user ID,
so everything looks great. One slight problem with the
login flow that we have so far is that after authenticating
in the browser, it's up to the user to
manually navigate back to the app window. It would be much better
if we could automatically bring the app window into
focus after authenticating. Doing something like
managing window positioning brings us out of the world
of Flutter's rendering and into the realm of
the native platform. If we want to run some
native code like this, we're going to need
to write a plugin. Flutter's plugins allow
you to run native platform code for your Flutter
app by providing places to write native code for
each platform and a channel over which to communicate
with the Flutter app. Let's create the plugin in a
new directory outside of our app and specify the
desktop platforms that we want to support. I'll change directory
back out of the app. And I'll run
flutter create, just like when we're creating an
app, but I'll use -t plugin. And I'll specify the platforms
that we want to support. I'm on a Mac right
now, but let's go ahead and add all desktop platforms
that Flutter supports. Lastly, we'll call
it window_to_front. There we are. So let's change directory into
our window_to_front. plugin. In the root of the
newly created plugin, there are folders for
each of the platforms that we're going to use. And I'm on Mac OS. So let's go ahead
and edit the Mac OS classes and the name
of the plugin file, WindowToFrontPlugin.swift. So this is our native
Swift code here that can run on a native
Mac desktop application. The handle method here
receives messages from Flutter. Looking at the code above, it
listens on a method channel called window_to_front,
the name of our plugin. Let's create a case
for a new method on this channel
called activate, which we'll call when we want to
bring the app window into focus. So instead of this
boilerplate method, we can call this activate. And here we'll call
the native Swift code to activate
the window, which is NSApplication.shared.activate. And we'll pass
ignoringOtherApps true. Now we have our message
receiver on the native side, but this plugin that
we've created also includes the Dart interface
that our app will use. That's located in
lib/window_to_front.dart. The WindowToFront
class in this file is exactly what will be exposed
to any consumer of this plugin, like our app. So let's add an activate
method to the class and have it call the
native activate handler that we wrote in Swift code. First, let's create the
window_to_front method channel, which is what our Swift
code is listening to. And then we'll write
an activate method that calls activate on
that method channel. So I can create a
static variable here that's just private and local. And I will define it with the
window_to_front string, which is what our native
code is listening to. Then we no longer need
this boilerplate method, so I'll replace that with a
new method called activate so that from our app, we can
just call this activate method. And what this will do is to
invoke a method on the method channel that we
created, and it'll be called activate, which
is what's being listened to. So let's get our
imports correct here. We're importing dart
async for the future, and we're importing
flutter/services for the method channel. And that looks like we've
got all the errors away. So at this point, our
plugin is complete. Let's change back
into our app directory and start using this plugin. And I'm back into the
GitHub client directory. We can add this local
plugin to our app just like we would a
public plugin from pub. So I can do flutter
pub add, just like we would with any other plugin. But this one is going
to be added with a path, and I'll provide the
directory that we just created our plugin in and
the name window_to_front. Great, that succeeded. Back in our app
code, in main.dart, let's go ahead and
import our plugin just like any other
plugin would be imported. Now we can go ahead and
call WindowToFront.activate, the method that we set up. And that will bring
the window into view. Let's put that line in
GithubLoginWidget's builder so that it's called when
the user has successfully authenticated. So here we just call
WindowToFront.activate. Now let's run the app
again and see what happens. Here's our app. So let's log in
again with GitHub. After we authenticate,
the window immediately jumps back to the front. That's a lot easier for the
user to get back to the app rather than being lost
in their browser window. The ability to send
and receive messages between Flutter and the native
platform is really powerful. Just because you're using
a cross-platform framework like Flutter doesn't
mean you have to give up on all of
the powerful features that native code
gives you access to. If you don't find exactly
what you want already built on pub.dev,
you see how easy it is to roll your own
like we just did here. Now that we've got
everything set up, let's actually use the GitHub
API to fetch and display some useful information. Using the GitHub object that
we created in main.dart, we have easy access
to a huge amount of data that can be retrieved
for us from the GitHub API. Simply by doing GitHub.pull
requests or dot repositories or dot issues and specifying
any search parameters, we'll get back a future that
resolves with the data we want. Let's go ahead and
use this to show the user's pull request for the
Flutter framework repository. So back in main.dart, instead
of using the GitHub API to fetch the current
user ID like we are now, let's write a function to
fetch the pull requests. So we can return a
future just like we are with the other
method, but instead of resolving to a
current user, we'll resolve to a list
of pull requests. And this PullRequest class
comes from the GitHub package, so it's already set up for us. Call is getPullRequests,
and it's going to take an accessToken
just like our other method. We are going to get our
gitHub object, as always. But instead of
calling dot users, we will call dot pullRequests. And we will specify that
we want only the pull requests from the Flutter
Flutter repository. And make sure that
we get a list. That looks good there. Now let's modify our page to
display the pull requests. We won't have our
loading text anymore, so let's create a nicer loading
spinner and even an error message if we enter that state. So getting rid of our
viewerDetail method, we're going to replace
that with getPullRequests, passing the same
access token as before. Now instead of using
the current user type, we will use the list
of pull requests. All right. Now, if this snapshot
has an error, we can return just a
simple error message in the center of the screen. And if the snapshot
has no data yet, then we can return
a loading spinner. And otherwise, if both of
these two cases are not met, then we're going to be
loading our page with our data available. Finally, let's write a list
view to display the actual pull request data. We're not doing anything
fancy like loading only the visible pull
requests from the server. But we'll use a ListView.builder
to at least dispose the off-screen items. So instead of our
text, we're going to create a ListView.builder. And the item count here will
be the number of pull requests. So let's get that pull request
data out of our snapshot. And then we can do
pullRequests.length here for the item count. Now our item builder takes
a context and an index. And here we can get the
relevant pull requests. And then let's build a
ListTile for this pull request. And we'll just do
something simple with the title and nothing else. If there's no title, then
we'll display an empty string. That looks good. Now let's run the app again. And here's my app. Let's log in. So after I log in, I see
my loading spinner here. And in my case,
working on the Flutter repository is my day job,
so there's a lot of data. And here are all
the pull requests that I'm involved in
on the Flutter repo. All right, if you see any in
here that haven't landed yet and you're looking forward
to, I promise to get back to work right after this talk. You can probably already
imagine many features that we could add with
this kind of GitHub data. We could display a bunch
more information for each PR, link out to their GitHub
pages, and do the same thing for things like issues
and repositories. But fortunately,
someone has already gone through the
trouble of adding all of these features
in the workshop. And if we quickly copy in
those changes to main.dart and gitHub_summary.dart. Add the fluttericon
package as well, then we can take a look at what
the fully featured app looks like. So I will add fluttericon here. And then I'm just going to copy
in the code from the workshop. Now let's run the app again. All right, let's log in
to GitHub one more time. And here we have three
panels, repositories, issues, and pull requests, with
organized lists of each one. And you can see
there's much more data. We have the repository,
PR number, author. And clicking on any of these
will open them in the browser. All of this was easily
doable with the data that we get from the GitHub
API and a few simple Flutter widgets. Let's take a quick
look back at what we've achieved in this workshop. We started by immediately
getting a boilerplate desktop app up and running, which
was as simple as flutter run. From there, we added our
working GitHub login button with authentication. Most of the complexity of
the OAuth login process was handled for us by the three
pub packages that we installed. After that, pubs saved
the day again when we wanted to fetch data
via the GitHub API, and the unofficial GitHub
package did the hard work and gave us the relevant
classes to work with. We got to take a look at
building our own plugin when we wanted to
rearrange the user's windows on their desktop. And we saw how easy
it is to run custom native code from Flutter. Finally, we really took
advantage of the GitHub API by fetching and displaying
real activity data that was relevant to the logged-in user. I hope I was able to show how
fun it is to throw together a real, great-looking,
functional app on desktop using Flutter. It still blows me
away every time I enter flutter run
and my app pops up in a real native desktop
window, though, as always, my favorite part of
working on Flutter isn't what I'm building. It's the Flutter community. And I can't wait to see
what everyone creates on desktop with Flutter. Thank you so much
for joining me. [MUSIC PLAYING]