1. Introdução
O Flutter é um kit de ferramentas de IU do Google para criar apps incríveis e nativos para dispositivos móveis, Web e computadores com uma única base de código. Neste codelab, você criará um app do Flutter para computador que acessa as APIs do GitHub para recuperar repositórios, problemas atribuídos e solicitações de envio. Ao realizar essa tarefa, você criará e usará plug-ins para interagir com APIs nativas e aplicativos para computador, além de usar a geração de código para criar bibliotecas de cliente com segurança de tipo para as APIs do GitHub.
O que você aprenderá
- Como criar um aplicativo do Flutter para computador
- Como autenticar usando o OAuth2 no computador
- Como usar o pacote do GitHub do Dart
- Como criar um plug-in do Flutter para integração com APIs nativas
O que você vai criar
Neste codelab, você vai criar um aplicativo para computador com integração do GitHub usando o SDK do Flutter. Seu app vai realizar as seguintes ações:
- Autenticar no GitHub
- Recuperar dados do GitHub
- Criar de um plug-in do Flutter para Windows, macOS e/ou Linux
- Desenvolver uma atualização dinâmica da IU do Flutter em um aplicativo nativo para computadores
Esta é uma captura de tela do aplicativo para computador que você vai criar em execução no Windows.
O foco deste codelab é adicionar os recursos de acesso OAuth2 e GitHub a um app do Flutter para computador. Conceitos e blocos de código não relevantes são apenas citados e fornecidos para você copiar e colar.
O que você quer aprender com este codelab?
2. Configurar o ambiente de desenvolvimento do Flutter
Você precisa de três softwares para concluir o laboratório, o SDK do Flutter, um editor e a cadeia de builds adequada para o sistema operacional do seu computador.
É possível executar o codelab como aplicativo para Windows, Linux ou macOS. No caso do Flutter para computador, desenvolva o aplicativo na plataforma onde ele será implementado. Portanto, se quiser desenvolver um app para Windows, você terá que desenvolver nesse sistema operacional para acessar a cadeia de builds adequada. Confira mais detalhes sobre os requisitos específicos dos sistemas operacionais em docs.flutter.dev/desktop.
3. Começar
Primeiras etapas para desenvolver aplicativos de computador com o Flutter
Para confirmar se o Flutter para computador está ativado, execute o seguinte comando:
$ flutter devices 1 connected device: Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19041.508] macOS (desktop) • macos • darwin-x64 • macOS 11.2.3 20D91 darwin-x64 Linux (desktop) • linux • linux-x64 • Linux
Se a linha relacionada a computadores não aparecer na saída anterior, considere o seguinte:
- Você está usando a plataforma para a qual está desenvolvendo?
- Você está usando o Flutter 3? Antes dessa versão, era necessário ativar o suporte do computador ao macOS e Linux.
Uma maneira fácil de começar a criar apps no Flutter é usar a ferramenta de linha de comando para iniciar um projeto. Como alternativa, o ambiente de desenvolvimento integrado pode fornecer um fluxo de trabalho para criar um projeto do Flutter pela própria IU.
$ flutter create github_client Creating project github_client... Running "flutter pub get" in github_client... 1,103ms Wrote 128 files. All done! In order to run your application, type: $ cd github_client $ flutter run Your application code is in github_client\lib\main.dart.
Para simplificar este codelab, exclua os arquivos de compatibilidade com Android, iOS e Web. Esses arquivos não são necessários na criação de aplicativos para computador no Flutter. A exclusão dos arquivos ajuda a eliminar a possibilidade de uma execução acidental da variante errada durante este codelab.
No macOS e Linux:
$ rm -r android ios web
No Windows:
PS C:\src\github_client> rmdir android PS C:\src\github_client> rmdir ios PS C:\src\github_client> rmdir web
Para garantir que tudo esteja funcionando, execute o aplicativo boilerplate do Flutter como um aplicativo para computador, conforme mostrado abaixo. Como alternativa, abra esse projeto no seu ambiente de desenvolvimento integrado e use as ferramentas dele para executar o aplicativo. Graças à etapa anterior, a execução como um aplicativo para computador é a única opção disponível.
$ flutter run
Launching lib\main.dart on Windows in debug mode...
Building Windows application...
Syncing files to device Windows... 56ms
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
💪 Running with sound null safety 💪
An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:61920/OHTnly7_TMk=/
The Flutter DevTools debugger and profiler on Windows is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:61920/OHTnly7_TMk=/
Agora você verá a seguinte janela do aplicativo na tela. Clique no botão de ação flutuante para conferir se o incrementador funciona normalmente. Você também pode testar a atualização dinâmica mudando a cor do tema ou o comportamento do método _incrementCounter
em lib/main.dart
.
Confira este exemplo da execução do aplicativo no Windows.
Na próxima seção, você vai fazer a autenticação no GitHub usando o OAuth2.
4. Adicionar autenticação
Autenticar no computador
Se você usa o Flutter no Android, no iOS ou na Web, tem várias opções em relação a pacotes de autenticação. No entanto, no desenvolvimento para computador a situação é outra. Atualmente, você precisa criar a integração de autenticação do zero, mas isso mudará quando os autores de pacote implementarem o Flutter para compatibilidade com computadores.
Registrar um aplicativo OAuth do GitHub
Para criar um aplicativo para computador que use as APIs do GitHub, primeiro é preciso autenticar. Há várias opções disponíveis, mas a melhor experiência para o usuário é direcioná-lo pelo fluxo de login do OAuth2 do GitHub no navegador. Isso possibilita a autenticação de dois fatores e a integração simples dos gerenciadores de senhas.
Para registrar um aplicativo para o fluxo OAuth2 do GitHub, acesse github.com (link em inglês) e siga as instruções apenas do primeiro passo em Criar aplicativos OAuth. As etapas a seguir são importantes quando você tem um aplicativo para iniciar, mas não durante um codelab.
Ao concluir Criar um aplicativo OAuth, a etapa 8 pede que você forneça o URL do callback de autorização. Para um app para computador, insira http://localhost/
como o URL do callback. O fluxo OAuth2 do GitHub foi configurado de modo que a definição de um URL do callback do host local autorize qualquer porta, possibilitando que você estabeleça um servidor da Web em uma porta local temporária. Isso evita que o usuário copie o token do código OAuth no aplicativo como parte do processo OAuth.
Confira esta captura de tela que mostra como preencher o formulário para criar um aplicativo OAuth do GitHub:
Depois de registrar um app OAuth na interface de administração do GitHub, você receberá um ID do cliente e uma chave secreta do cliente. Se esses valores forem necessários mais tarde, será possível recuperá-los nas configurações de desenvolvedor do GitHub (link em inglês). Essas credenciais são necessárias no seu aplicativo para construir um URL de autorização OAuth2 válido. Você usará o pacote Dart oauth2
para processar o fluxo OAuth2 e o plug-in do Flutter url_launcher
(links em inglês) para ativar a inicialização do navegador da Web do usuário.
Adicionar oauth2 e url_launcher ao pubspec.yaml
Para adicionar dependências de pacote ao seu aplicativo, execute flutter pub add
da seguinte maneira:
$ flutter pub add http Resolving dependencies... + http 0.13.4 + http_parser 4.0.0 path 1.8.0 (1.8.1 available) source_span 1.8.1 (1.8.2 available) test_api 0.4.8 (0.4.9 available) Changed 2 dependencies!
Esse primeiro comando adiciona o pacote http
para fazer chamadas HTTP de maneira consistente em várias plataformas. Em seguida, adicione o pacote oauth2
desta forma:
$ flutter pub add oauth2 Resolving dependencies... + crypto 3.0.1 + oauth2 2.0.0 path 1.8.0 (1.8.1 available) source_span 1.8.1 (1.8.2 available) test_api 0.4.8 (0.4.9 available) Changed 2 dependencies!
Por último, adicione o pacote url_launcher
.
$ flutter pub add url_launcher Resolving dependencies... + flutter_web_plugins 0.0.0 from sdk flutter + js 0.6.3 (0.6.4 available) path 1.8.0 (1.8.1 available) + plugin_platform_interface 2.1.2 source_span 1.8.1 (1.8.2 available) test_api 0.4.8 (0.4.9 available) + url_launcher 6.0.18 + url_launcher_android 6.0.14 + url_launcher_ios 6.0.14 + url_launcher_linux 2.0.3 + url_launcher_macos 2.0.2 + url_launcher_platform_interface 2.0.5 + url_launcher_web 2.0.6 + url_launcher_windows 2.0.2 Downloading url_launcher 6.0.18... Downloading url_launcher_ios 6.0.14... Downloading url_launcher_android 6.0.14... Downloading url_launcher_platform_interface 2.0.5... Downloading plugin_platform_interface 2.1.2... Downloading url_launcher_linux 2.0.3... Downloading url_launcher_web 2.0.6... Changed 11 dependencies!
Incluir credenciais de clientes
Adicione as credenciais de cliente a um novo arquivo, lib/github_oauth_credentials.dart
, da seguinte maneira:
lib/github_oauth_credentials.dart (link em inglês)
// TODO(CodelabUser): Create an OAuth App
const githubClientId = 'YOUR_GITHUB_CLIENT_ID_HERE';
const githubClientSecret = 'YOUR_GITHUB_CLIENT_SECRET_HERE';
// OAuth scopes for repository and user information
const githubScopes = ['repo', 'read:org'];
Copie e cole suas credenciais de cliente da etapa anterior nesse arquivo.
Criar o fluxo OAuth2 para computador
Crie um widget para conter o fluxo OAuth2 para computador. Essa é uma parte razoavelmente complexa da lógica, porque você precisa executar um servidor da Web temporário, redirecionar o usuário para um endpoint no GitHub no navegador da Web, aguardar o usuário concluir o fluxo de autorização no navegador e processar uma chamada de redirecionamento do GitHub contendo código, que precisa ser convertido em um token OAuth2 com uma chamada separada para os servidores da API do GitHub.
lib/src/github_login.dart (link em inglês)
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:url_launcher/url_launcher.dart';
final _authorizationEndpoint =
Uri.parse('https://github.com/login/oauth/authorize');
final _tokenEndpoint = Uri.parse('https://github.com/login/oauth/access_token');
class GithubLoginWidget extends StatefulWidget {
const GithubLoginWidget({
required this.builder,
required this.githubClientId,
required this.githubClientSecret,
required this.githubScopes,
super.key,
});
final AuthenticatedBuilder builder;
final String githubClientId;
final String githubClientSecret;
final List<String> githubScopes;
@override
State<GithubLoginWidget> createState() => _GithubLoginState();
}
typedef AuthenticatedBuilder = Widget Function(
BuildContext context, oauth2.Client client);
class _GithubLoginState extends State<GithubLoginWidget> {
HttpServer? _redirectServer;
oauth2.Client? _client;
@override
Widget build(BuildContext context) {
final client = _client;
if (client != null) {
return widget.builder(context, client);
}
return Scaffold(
appBar: AppBar(
title: const Text('Github Login'),
),
body: Center(
child: ElevatedButton(
onPressed: () async {
await _redirectServer?.close();
// Bind to an ephemeral port on localhost
_redirectServer = await HttpServer.bind('localhost', 0);
var authenticatedHttpClient = await _getOAuth2Client(
Uri.parse('http://localhost:${_redirectServer!.port}/auth'));
setState(() {
_client = authenticatedHttpClient;
});
},
child: const Text('Login to Github'),
),
),
);
}
Future<oauth2.Client> _getOAuth2Client(Uri redirectUrl) async {
if (widget.githubClientId.isEmpty || widget.githubClientSecret.isEmpty) {
throw const GithubLoginException(
'githubClientId and githubClientSecret must be not empty. '
'See `lib/github_oauth_credentials.dart` for more detail.');
}
var grant = oauth2.AuthorizationCodeGrant(
widget.githubClientId,
_authorizationEndpoint,
_tokenEndpoint,
secret: widget.githubClientSecret,
httpClient: _JsonAcceptingHttpClient(),
);
var authorizationUrl =
grant.getAuthorizationUrl(redirectUrl, scopes: widget.githubScopes);
await _redirect(authorizationUrl);
var responseQueryParameters = await _listen();
var client =
await grant.handleAuthorizationResponse(responseQueryParameters);
return client;
}
Future<void> _redirect(Uri authorizationUrl) async {
if (await canLaunchUrl(authorizationUrl)) {
await launchUrl(authorizationUrl);
} else {
throw GithubLoginException('Could not launch $authorizationUrl');
}
}
Future<Map<String, String>> _listen() async {
var request = await _redirectServer!.first;
var params = request.uri.queryParameters;
request.response.statusCode = 200;
request.response.headers.set('content-type', 'text/plain');
request.response.writeln('Authenticated! You can close this tab.');
await request.response.close();
await _redirectServer!.close();
_redirectServer = null;
return params;
}
}
class _JsonAcceptingHttpClient extends http.BaseClient {
final _httpClient = http.Client();
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['Accept'] = 'application/json';
return _httpClient.send(request);
}
}
class GithubLoginException implements Exception {
const GithubLoginException(this.message);
final String message;
@override
String toString() => message;
}
Vale a pena passar algum tempo trabalhando nesse código, porque ele demonstra alguns recursos do Flutter e do Dart no computador. Sim, o código é complicado, mas muitas funcionalidades são encapsuladas em um widget relativamente fácil de usar.
Esse widget expõe um servidor da Web temporário e faz solicitações HTTP seguras. No macOS, os dois recursos precisam ser solicitados por arquivos de direitos.
Mudar direitos do cliente e do servidor (apenas macOS)
Fazer solicitações da Web e executar um servidor da Web como um aplicativo para computador macOS exige mudanças nos direitos do aplicativo. Para mais informações, consulte Direitos e aplicativos sandbox (link em inglês).
macos/Runner/DebugProfile.entitlements (link em inglês)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add this entry -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Você também precisa modificar os direitos de lançamento para builds de produção.
macos/Runner/Release.entitlements (link em inglês)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add the following two entries -->
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Reunir todos os elementos
Você configurou um novo aplicativo OAuth, o projeto está configurado com os pacotes e plug-ins necessários, você criou um widget para encapsular o fluxo de autenticação OAuth e permitiu ao aplicativo atuar como um cliente de rede e um servidor em macOS pelos direitos. Com todos esses elementos essenciais no devido lugar, é possível juntar tudo isso no arquivo lib/main.dart
.
lib/main.dart (link em inglês)
import 'package:flutter/material.dart';
import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GitHub Client',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const MyHomePage(title: 'GitHub Client'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return GithubLoginWidget(
builder: (context, httpClient) {
return Scaffold(
appBar: AppBar(
title: Text(title),
elevation: 2,
),
body: const Center(
child: Text(
'You are logged in to GitHub!',
),
),
);
},
githubClientId: githubClientId,
githubClientSecret: githubClientSecret,
githubScopes: githubScopes,
);
}
}
Ao executar esse aplicativo do Flutter, primeiramente você vê um botão para iniciar o fluxo de login OAuth do GitHub. Depois de clicar no botão, conclua o fluxo de login no navegador da Web para ver se o app está conectado.
Agora que você conseguiu a autenticação OAuth, comece a usar o pacote do GitHub.
5. Acessar o GitHub
Como se conectar ao GitHub
Com o fluxo de autenticação OAuth, você recebe o token necessário para acessar seus dados no GitHub. Use o pacote github
, disponível em pub.dev, se quiser fazer isso com mais facilidade.
Adicionar mais dependências
Execute este comando:
$ flutter pub add github Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + github 9.4.0 + json_annotation 4.6.0 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Changed 2 dependencies!
Como usar as credenciais do OAuth com o pacote do GitHub
O GithubLoginWidget
que você criou na etapa anterior fornece HttpClient
, que pode interagir com a API do GitHub. Nesta etapa, você vai usar as credenciais de HttpClient
para acessar a API do GitHub com o pacote do GitHub, conforme demonstrado abaixo:
final accessToken = httpClient.credentials.accessToken;
final gitHub = GitHub(auth: Authentication.withToken(accessToken));
Reunir todos os elementos, novamente
É hora de integrar o cliente do GitHub ao arquivo lib/main.dart
.
lib/main.dart
import 'package:flutter/material.dart';
import 'package:github/github.dart';
import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GitHub Client',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const MyHomePage(title: 'GitHub Client'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return GithubLoginWidget(
builder: (context, httpClient) {
return FutureBuilder<CurrentUser>(
future: viewerDetail(httpClient.credentials.accessToken),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
title: Text(title),
elevation: 2,
),
body: Center(
child: Text(
snapshot.hasData
? 'Hello ${snapshot.data!.login}!'
: 'Retrieving viewer login details...',
),
),
);
},
);
},
githubClientId: githubClientId,
githubClientSecret: githubClientSecret,
githubScopes: githubScopes,
);
}
}
Future<CurrentUser> viewerDetail(String accessToken) async {
final gitHub = GitHub(auth: Authentication.withToken(accessToken));
return gitHub.users.getCurrentUser();
}
Após executar esse aplicativo do Flutter, será exibido um botão que inicia o fluxo de login OAuth do GitHub. Depois de clicar no botão, conclua o fluxo de login no navegador da Web. Agora você fez login no app.
Na próxima etapa, você eliminará um problema na base de código atual. Depois que você autenticar o aplicativo no navegador da Web, ele vai voltar ao primeiro plano.
6. Criar um plug-in do Flutter para Windows, macOS e Linux
Resolver problemas
No momento, o código tem um aspecto desagradável. Após o fluxo de autenticação, quando o GitHub autentica seu aplicativo, você permanece em uma página do navegador da Web. O ideal é retornar automaticamente ao aplicativo. Para corrigir isso, é necessário criar um plug-in do Flutter para as plataformas de computador.
Criar um plug-in do Flutter para Windows, macOS e Linux
É necessário código nativo para que o aplicativo vá automaticamente para a frente da pilha de janelas após a conclusão do fluxo OAuth. Para macOS, a API de que você precisa é o método da instância activate(ignoringOtherApps:)
(link em inglês) de NSApplication
. Para Linux, usaremos gtk_window_present
(link em inglês) e, para Windows, recorremos ao Stack Overflow. Para poder chamar essas APIs, é preciso criar um plug-in do Flutter.
Use flutter
para criar um novo projeto de plug-in.
$ cd .. # step outside of the github_client project $ flutter create -t plugin --platforms=linux,macos,windows window_to_front $ cd window_to_front
Exclua o código de exemplo. Ele não é necessário. No macOS e Linux:
$ rm -r example
No Windows:
PS C:\src\github_client> rmdir example
Confirme se o arquivo pubspec.yaml gerado está como no exemplo a seguir.
../window_to_front/pubspec.yaml
name: window_to_front
description: A new Flutter plugin project.
version: 0.0.1
homepage:
environment:
sdk: ">=2.17.5 <3.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
plugin:
platforms:
linux:
pluginClass: WindowToFrontPlugin
macos:
pluginClass: WindowToFrontPlugin
windows:
pluginClass: WindowToFrontPluginCApi
Esse plug-in está configurado para macOS, Linux e Windows. Agora, você pode adicionar o Código SWIFT que faz o pop-up a janela do Flutter aparecer. Edite macos/Classes/WindowToFrontPlugin.swift
da seguinte maneira:
../window_to_front/macos/Classes/WindowToFrontPlugin.swift (link em inglês)
import Cocoa
import FlutterMacOS
public class WindowToFrontPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "window_to_front", binaryMessenger: registrar.messenger)
let instance = WindowToFrontPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
// Add from here
case "activate":
NSApplication.shared.activate(ignoringOtherApps: true)
result(nil)
// to here.
// Delete the getPlatformVersion case,
// as you won't be using it.
default:
result(FlutterMethodNotImplemented)
}
}
}
Para fazer o mesmo no plug-in do Linux, substitua o conteúdo de linux/window_to_front_plugin.cc
pelo seguinte:
../window_to_front/linux/window_to_front_plugin.cc (link em inglês)
#include "include/window_to_front/window_to_front_plugin.h"
#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <sys/utsname.h>
#define WINDOW_TO_FRONT_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), window_to_front_plugin_get_type(), \
WindowToFrontPlugin))
struct _WindowToFrontPlugin {
GObject parent_instance;
FlPluginRegistrar* registrar;
};
G_DEFINE_TYPE(WindowToFrontPlugin, window_to_front_plugin, g_object_get_type())
// Called when a method call is received from Flutter.
static void window_to_front_plugin_handle_method_call(
WindowToFrontPlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = nullptr;
const gchar* method = fl_method_call_get_name(method_call);
if (strcmp(method, "activate") == 0) {
FlView* view = fl_plugin_registrar_get_view(self->registrar);
if (view != nullptr) {
GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view)));
gtk_window_present(window);
}
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
static void window_to_front_plugin_dispose(GObject* object) {
G_OBJECT_CLASS(window_to_front_plugin_parent_class)->dispose(object);
}
static void window_to_front_plugin_class_init(WindowToFrontPluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = window_to_front_plugin_dispose;
}
static void window_to_front_plugin_init(WindowToFrontPlugin* self) {}
static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
gpointer user_data) {
WindowToFrontPlugin* plugin = WINDOW_TO_FRONT_PLUGIN(user_data);
window_to_front_plugin_handle_method_call(plugin, method_call);
}
void window_to_front_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
WindowToFrontPlugin* plugin = WINDOW_TO_FRONT_PLUGIN(
g_object_new(window_to_front_plugin_get_type(), nullptr));
plugin->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
g_autoptr(FlMethodChannel) channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
"window_to_front",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(channel, method_call_cb,
g_object_ref(plugin),
g_object_unref);
g_object_unref(plugin);
}
Para fazer o mesmo no plug-in do Windows, substitua o conteúdo de windows\window_to_front_plugin.cc
pelo seguinte:
..\window_to_front\windows\window_to_front_plugin.cpp
#include "window_to_front_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
namespace window_to_front {
// static
void WindowToFrontPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows *registrar) {
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "window_to_front",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<WindowToFrontPlugin>(registrar);
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
WindowToFrontPlugin::WindowToFrontPlugin(flutter::PluginRegistrarWindows *registrar)
: registrar_(registrar) {}
WindowToFrontPlugin::~WindowToFrontPlugin() {}
void WindowToFrontPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("activate") == 0) {
// See https://stackoverflow.com/a/34414846/2142626 for an explanation of how
// this raises a window to the foreground.
HWND m_hWnd = registrar_->GetView()->GetNativeWindow();
HWND hCurWnd = ::GetForegroundWindow();
DWORD dwMyID = ::GetCurrentThreadId();
DWORD dwCurID = ::GetWindowThreadProcessId(hCurWnd, NULL);
::AttachThreadInput(dwCurID, dwMyID, TRUE);
::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
::SetWindowPos(m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
::SetForegroundWindow(m_hWnd);
::SetFocus(m_hWnd);
::SetActiveWindow(m_hWnd);
::AttachThreadInput(dwCurID, dwMyID, FALSE);
result->Success();
} else {
result->NotImplemented();
}
}
} // namespace window_to_front
Além disso, o conteúdo de windows\window_to_front_plugin.h
deve ser substituído por:
..\window_to_front\windows\window_to_front_plugin.h
#ifndef FLUTTER_PLUGIN_WINDOW_TO_FRONT_PLUGIN_H_
#define FLUTTER_PLUGIN_WINDOW_TO_FRONT_PLUGIN_H_
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <memory>
namespace window_to_front {
class WindowToFrontPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
WindowToFrontPlugin(flutter::PluginRegistrarWindows *registrar);
virtual ~WindowToFrontPlugin();
// Disallow copy and assign.
WindowToFrontPlugin(const WindowToFrontPlugin&) = delete;
WindowToFrontPlugin& operator=(const WindowToFrontPlugin&) = delete;
private:
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
// The registrar for this plugin, for accessing the window.
flutter::PluginRegistrarWindows *registrar_;
};
} // namespace window_to_front
#endif // FLUTTER_PLUGIN_WINDOW_TO_FRONT_PLUGIN_H_
Adicione o código para disponibilizar a funcionalidade nativa que criamos acima no ambiente do Flutter. Primeiro, edite a janela de "front platform interface" e adicione um método activate()
, com uma implementação de substituição.
../window_to_front/lib/window_to_front_platform_interface.dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'window_to_front_method_channel.dart';
abstract class WindowToFrontPlatform extends PlatformInterface {
/// Constructs a WindowToFrontPlatform.
WindowToFrontPlatform() : super(token: _token);
static final Object _token = Object();
static WindowToFrontPlatform _instance = MethodChannelWindowToFront();
/// The default instance of [WindowToFrontPlatform] to use.
///
/// Defaults to [MethodChannelWindowToFront].
static WindowToFrontPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [WindowToFrontPlatform] when
/// they register themselves.
static set instance(WindowToFrontPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
// Replace getPlatformVersion() with the following activate() method
Future<void> activate() {
throw UnimplementedError('activate() has not been implemented.');
}
}
Em seguida, forneça uma implementação de canal de métodos da janela a "front platform interface".
../window_to_front/lib/window_to_front_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'window_to_front_platform_interface.dart';
/// An implementation of [WindowToFrontPlatform] that uses method channels.
class MethodChannelWindowToFront extends WindowToFrontPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('window_to_front');
// Replace the getPlatformVersion() with the following implementation
@override
Future<void> activate() async {
return methodChannel.invokeMethod('activate');
}
}
Por fim, exponha a janela à funcionalidade "front" para que fique disponível aos usuários.
../window_to_front/lib/window_to_front.dart
import 'window_to_front_platform_interface.dart';
class WindowToFront {
// Remove the getPlatformVersion() implementation and add the following
static Future<void> activate() {
return WindowToFrontPlatform.instance.activate();
}
}
Esse plug-in do Flutter foi concluído. Você pode voltar à edição do projeto github_graphql_client
.
$ cd ../github_client
Adicionar dependências
O plug-in do Flutter que você acabou de criar é ótimo, mas ele não é muito útil sozinho. É necessário adicioná-lo como uma dependência no aplicativo do Flutter para usá-lo.
$ flutter pub add --path ../window_to_front window_to_front Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) url_launcher 6.1.4 (6.1.5 available) + window_to_front 0.0.1 from path ../window_to_front Changed 1 dependency!
Observe o caminho especificado para a dependência window_to_front
: como esse é um pacote local em vez de ser publicado no pub.dev, especifique um caminho em vez de um número de versão.
Reunir todos os elementos, mais uma vez
É hora de integrar window_to_front
ao arquivo lib/main.dart
. Só precisamos adicionar uma importação e chamar o código nativo no momento certo.
lib/main.dart
import 'package:flutter/material.dart';
import 'package:github/github.dart';
import 'package:window_to_front/window_to_front.dart'; // Add this
import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GitHub Client',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const MyHomePage(title: 'GitHub Client'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return GithubLoginWidget(
builder: (context, httpClient) {
WindowToFront.activate(); // and this.
return FutureBuilder<CurrentUser>(
future: viewerDetail(httpClient.credentials.accessToken),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
title: Text(title),
elevation: 2,
),
body: Center(
child: Text(
snapshot.hasData
? 'Hello ${snapshot.data!.login}!'
: 'Retrieving viewer login details...',
),
),
);
},
);
},
githubClientId: githubClientId,
githubClientSecret: githubClientSecret,
githubScopes: githubScopes,
);
}
}
Future<CurrentUser> viewerDetail(String accessToken) async {
final gitHub = GitHub(auth: Authentication.withToken(accessToken));
return gitHub.users.getCurrentUser();
}
Após executar esse aplicativo do Flutter, você verá um aplicativo de aparência idêntica, mas ao clicar no botão notará uma diferença de comportamento. Se você colocar o aplicativo no navegador da Web que está usando para autenticação, ao clicar no botão de login, seu aplicativo será enviado para atrás do navegador. Porém, após você concluir o fluxo de autenticação no navegador, o aplicativo voltará para frente. Muito mais sofisticado.
Na próxima seção, você vai usar a base que já tem para criar um cliente GitHub de computador que fornece informações sobre o que você tem no GitHub. Você vai inspecionar a lista de repositórios da conta, as solicitações de envio do projeto do Flutter e os problemas atribuídos.
7. Ver repositórios, solicitações de envio e problemas atribuídos
Você já foi longe na criação do aplicativo, mas tudo que ele faz é dizer qual é o login. Talvez você queira um pouco mais de um cliente GitHub para computador. A seguir, você vai adicionar a capacidade de listar repositórios, solicitações de envio e problemas atribuídos.
Adicionar uma última dependência
Ao renderizar os dados retornados das consultas acima, você vai usar mais um pacote, fluttericon
, para exibir facilmente os Octicons do GitHub (links em inglês).
$ flutter pub add fluttericon Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + fluttericon 2.0.0 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) url_launcher 6.1.4 (6.1.5 available) Changed 1 dependency!
Widgets para renderizar os resultados na tela
Você vai usar o pacote do GitHub que adicionou antes para preencher um widget NavigationRail
(em inglês) com visualizações dos repositórios, problemas atribuídos e solicitações de envio do projeto do Flutter. A documentação do sistema de design Material.io explica como as faixas de navegação (links em inglês) fornecem movimentação ergonômica entre os destinos principais em aplicativos.
Crie um novo arquivo e preencha-o com o conteúdo a seguir.
lib/src/github_summary.dart (link em inglês)
import 'package:flutter/material.dart';
import 'package:fluttericon/octicons_icons.dart';
import 'package:github/github.dart';
import 'package:url_launcher/url_launcher_string.dart';
class GitHubSummary extends StatefulWidget {
const GitHubSummary({required this.gitHub, super.key});
final GitHub gitHub;
@override
State<GitHubSummary> createState() => _GitHubSummaryState();
}
class _GitHubSummaryState extends State<GitHubSummary> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.selected,
destinations: const [
NavigationRailDestination(
icon: Icon(Octicons.repo),
label: Text('Repositories'),
),
NavigationRailDestination(
icon: Icon(Octicons.issue_opened),
label: Text('Assigned Issues'),
),
NavigationRailDestination(
icon: Icon(Octicons.git_pull_request),
label: Text('Pull Requests'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: IndexedStack(
index: _selectedIndex,
children: [
RepositoriesList(gitHub: widget.gitHub),
AssignedIssuesList(gitHub: widget.gitHub),
PullRequestsList(gitHub: widget.gitHub),
],
),
),
],
);
}
}
class RepositoriesList extends StatefulWidget {
const RepositoriesList({required this.gitHub, super.key});
final GitHub gitHub;
@override
State<RepositoriesList> createState() => _RepositoriesListState();
}
class _RepositoriesListState extends State<RepositoriesList> {
@override
initState() {
super.initState();
_repositories = widget.gitHub.repositories.listRepositories().toList();
}
late Future<List<Repository>> _repositories;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Repository>>(
future: _repositories,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
var repositories = snapshot.data;
return ListView.builder(
primary: false,
itemBuilder: (context, index) {
var repository = repositories![index];
return ListTile(
title:
Text('${repository.owner?.login ?? ''}/${repository.name}'),
subtitle: Text(repository.description),
onTap: () => _launchUrl(context, repository.htmlUrl),
);
},
itemCount: repositories!.length,
);
},
);
}
}
class AssignedIssuesList extends StatefulWidget {
const AssignedIssuesList({required this.gitHub, super.key});
final GitHub gitHub;
@override
State<AssignedIssuesList> createState() => _AssignedIssuesListState();
}
class _AssignedIssuesListState extends State<AssignedIssuesList> {
@override
initState() {
super.initState();
_assignedIssues = widget.gitHub.issues.listByUser().toList();
}
late Future<List<Issue>> _assignedIssues;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Issue>>(
future: _assignedIssues,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
var assignedIssues = snapshot.data;
return ListView.builder(
primary: false,
itemBuilder: (context, index) {
var assignedIssue = assignedIssues![index];
return ListTile(
title: Text(assignedIssue.title),
subtitle: Text('${_nameWithOwner(assignedIssue)} '
'Issue #${assignedIssue.number} '
'opened by ${assignedIssue.user?.login ?? ''}'),
onTap: () => _launchUrl(context, assignedIssue.htmlUrl),
);
},
itemCount: assignedIssues!.length,
);
},
);
}
String _nameWithOwner(Issue assignedIssue) {
final endIndex = assignedIssue.url.lastIndexOf('/issues/');
return assignedIssue.url.substring(29, endIndex);
}
}
class PullRequestsList extends StatefulWidget {
const PullRequestsList({required this.gitHub, super.key});
final GitHub gitHub;
@override
State<PullRequestsList> createState() => _PullRequestsListState();
}
class _PullRequestsListState extends State<PullRequestsList> {
@override
initState() {
super.initState();
_pullRequests = widget.gitHub.pullRequests
.list(RepositorySlug('flutter', 'flutter'))
.toList();
}
late Future<List<PullRequest>> _pullRequests;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<PullRequest>>(
future: _pullRequests,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
var pullRequests = snapshot.data;
return ListView.builder(
primary: false,
itemBuilder: (context, index) {
var pullRequest = pullRequests![index];
return ListTile(
title: Text(pullRequest.title ?? ''),
subtitle: Text('flutter/flutter '
'PR #${pullRequest.number} '
'opened by ${pullRequest.user?.login ?? ''} '
'(${pullRequest.state?.toLowerCase() ?? ''})'),
onTap: () => _launchUrl(context, pullRequest.htmlUrl ?? ''),
);
},
itemCount: pullRequests!.length,
);
},
);
}
}
Future<void> _launchUrl(BuildContext context, String url) async {
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Navigation error'),
content: Text('Could not launch $url'),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Close'),
),
],
),
);
}
}
Você adicionou vários códigos novos aqui. A vantagem é que eles são códigos normais do Flutter, com widgets usados para separar a responsabilidade em diferentes preocupações. Revise o código antes de prosseguir para a próxima etapa.
Reunir todos os elementos, uma última vez
É hora de integrar o GitHubSummary
ao arquivo lib/main.dart
. As mudanças são bem significativas desta vez, mas consistem principalmente em exclusões. Substitua o conteúdo do arquivo lib/main.dart
pelo indicado a seguir.
lib/main.dart
import 'package:flutter/material.dart';
import 'package:github/github.dart';
import 'package:window_to_front/window_to_front.dart';
import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
import 'src/github_summary.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GitHub Client',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const MyHomePage(title: 'GitHub Client'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return GithubLoginWidget(
builder: (context, httpClient) {
WindowToFront.activate();
return Scaffold(
appBar: AppBar(
title: Text(title),
elevation: 2,
),
body: GitHubSummary(
gitHub: _getGitHub(httpClient.credentials.accessToken),
),
);
},
githubClientId: githubClientId,
githubClientSecret: githubClientSecret,
githubScopes: githubScopes,
);
}
}
GitHub _getGitHub(String accessToken) {
return GitHub(auth: Authentication.withToken(accessToken));
}
Quando você executar o aplicativo, uma mensagem parecida com esta deve aparecer:
8. Próximas etapas
Parabéns!
Você concluiu o codelab e criou um aplicativo do Flutter para computador que acessa a API do GitHub. Você usou uma API autenticada com OAuth, bem como as APIs nativas com um plug-in que você criou.
Para saber mais sobre o Flutter para computador, acesse flutter.dev/desktop (em inglês). Por fim, para ver uma abordagem totalmente diferente sobre o Flutter e o GitHub, consulte o GitHub-Activity-Feed do GroovinChip (em inglês).