How to write a Flutter desktop application

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[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]
Info
Channel: Flutter
Views: 94,398
Rating: undefined out of 5
Keywords: Flutter, widgets, webviews, JavaScript, web development, app development, UI, user interface, open source, GitHub, desktop applications, code generation, type safe client libraries, GitHub API, Google I/O, Google IO, I/O, IO, Google I/O 2022, IO 2022
Id: 3HREQwLmy88
Channel Id: undefined
Length: 34min 23sec (2063 seconds)
Published: Wed May 11 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.