Criar um aplicativo do Flutter para computador

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.

a456fca6e2997992.png

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?

Ainda não conheço bem o assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero me atualizar. Estou procurando exemplos de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

2. Configurar seu ambiente do Flutter

Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de compilação adequada.

O desenvolvimento para todos os sistemas operacionais exige dois softwares para concluir este laboratório, o SDK do Flutter e um editor.

Além disso, há requisitos específicos de cada sistema operacional que são abordados em detalhes em flutter.dev/desktop (link em inglês).

3. Começar

Primeiros passos para desenvolver aplicativos para computador com o Flutter

É preciso configurar a compatibilidade com computador usando uma mudança de configuração única.

$ flutter config --enable-windows-desktop # for the Windows runner
$ flutter config --enable-macos-desktop   # for the macOS runner
$ flutter config --enable-linux-desktop   # for the Linux runner

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?
  • Executar flutter config lista o macOS como ativado com enable-[os]-desktop: true?
  • Executar flutter channel lista dev ou master como o canal atual? Isso é necessário porque o código não será executado nos canais stable ou beta.

Uma maneira fácil de começar a criar apps para computador no Flutter é usar a ferramenta de linha de comando do Flutter para criar 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.

Veja o aplicativo em execução no Windows.

bee40fe7a8e69791.png

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.

Veja um exemplo de captura de tela de como preencher o formulário para criar um aplicativo OAuth do GitHub:

be454222e07f01d9.png

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 cliente

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,
    Key? key,
  }) : super(key: key);
  final AuthenticatedBuilder builder;
  final String githubClientId;
  final String githubClientSecret;
  final List<String> githubScopes;

  @override
  _GithubLoginState 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 {
    var url = authorizationUrl.toString();
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw GithubLoginException('Could not launch $url');
    }
  }

  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({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitHub Client',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'GitHub Client'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          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. Para facilitar essa tarefa, use o pacote github, disponível em pub.dev.

Adicionar mais dependências

Execute este comando:

$ flutter pub add github

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({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitHub Client',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'GitHub Client'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  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),
              ),
              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

Confirme se o arquivo pubspec.yaml gerado está como o exemplo a seguir.

../window_to_front/pubspec.yaml

name: window_to_front
description: A new flutter plugin project.
version: 0.0.1

environment:
  sdk: ">=2.12.0 <3.0.0"
  flutter: ">=1.20.0"

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^1.0.0

flutter:
  plugin:
    platforms:
      linux:
        pluginClass: WindowToFrontPlugin
      macos:
        pluginClass: WindowToFrontPlugin
      windows:
        pluginClass: WindowToFrontPlugin

Esse plug-in está configurado para macOS, Linux e Windows. Agora, você pode adicionar o código Swift que faz a janela abrir à frente. 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 we 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 (link em inglês)

#include "include/window_to_front/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 <map>
#include <memory>

namespace {

class WindowToFrontPlugin : public flutter::Plugin {
 public:
  static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);

  WindowToFrontPlugin(flutter::PluginRegistrarWindows *registrar);

  virtual ~WindowToFrontPlugin();

 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<>> result);

  // The registrar for this plugin, for accessing the window.
  flutter::PluginRegistrarWindows *registrar_;
};

// static
void WindowToFrontPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrarWindows *registrar) {
  auto channel =
      std::make_unique<flutter::MethodChannel<>>(
          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<> &method_call,
    std::unique_ptr<flutter::MethodResult<>> 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

void WindowToFrontPluginRegisterWithRegistrar(
    FlutterDesktopPluginRegistrarRef registrar) {
  WindowToFrontPlugin::RegisterWithRegistrar(
      flutter::PluginRegistrarManager::GetInstance()
          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

Adicione o código para disponibilizar a funcionalidade nativa que criamos acima no ambiente do Flutter.

../window_to_front/lib/window_to_front.dart (link em inglês)

import 'dart:async';

import 'package:flutter/services.dart';

class WindowToFront {
  static const MethodChannel _channel = MethodChannel('window_to_front');
  // Add from here
  static Future<void> activate(){
    return _channel.invokeMethod('activate');
  }
  // to here.

  // Delete the getPlatformVersion getter method.
}

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...
  js 0.6.3 (0.6.4 available)
  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)
+ 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({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitHub Client',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'GitHub Client'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  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),
              ),
              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...
+ fluttericon 2.0.0
  js 0.6.3 (0.6.4 available)
  material_color_utilities 0.1.3 (0.1.4 available)
  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)
  url_launcher_macos 2.0.2 (2.0.3 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.dart';

class GitHubSummary extends StatefulWidget {
  const GitHubSummary({required this.gitHub, Key? key}) : super(key: key);
  final GitHub gitHub;

  @override
  _GitHubSummaryState 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, Key? key}) : super(key: key);
  final GitHub gitHub;

  @override
  _RepositoriesListState 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(
          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, Key? key}) : super(key: key);
  final GitHub gitHub;

  @override
  _AssignedIssuesListState 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(
          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, Key? key}) : super(key: key);
  final GitHub gitHub;

  @override
  _PullRequestsListState 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(
          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 canLaunch(url)) {
    await launch(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({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitHub Client',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'GitHub Client'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        WindowToFront.activate(); // and this.
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: GitHubSummary(
            gitHub: _getGitHub(httpClient.credentials.accessToken),
          ),
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

GitHub _getGitHub(String accessToken) {
  return GitHub(auth: Authentication.withToken(accessToken));
}

Execute o aplicativo para que seja exibida uma mensagem parecida com esta:

d5c9bebf448a2519.png

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).