Criar um aplicativo do Flutter para computador

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 GraphQL do Flutter com geração de código
  • Como criar um plug-in do Flutter para integração com APIs nativas

O que você criará

Neste codelab, você criará um aplicativo para computador com integração da API GraphQL do GitHub usando o SDK do Flutter. Seu app realizará as seguintes ações:

  • Autenticar no GitHub
  • Recuperar dados da API GitHub v4
  • 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ê criará, em execução no Windows.

775e773e58e53e85.png

Este codelab se concentra na adição de recursos do GraphQL 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?

Sou iniciante no assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero uma recapitulação. Estou procurando um exemplo de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

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

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_graphql_client
Creating project github_graphql_client...
[Eliding listing of created files]
Wrote 127 files.

All done!
In order to run your application, type:

  $ cd github_graphql_client
  $ flutter run

To enable null safety, type:

  $ cd github_graphql_client
  $ dart migrate --apply-changes

Your application code is in github_graphql_client/lib/main.dart.

Ative a segurança contra nulidade migrando o projeto da seguinte maneira no macOS e no Linux:

$ cd github_graphql_client
$ dart migrate --apply-changes

Da mesma forma, no Windows:

PS C:\src\> cd github_graphql_client
PS C:\src\github_graphql_client> dart migrate --apply-changes

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_graphql_client> rmdir android
PS C:\src\github_graphql_client> rmdir ios
PS C:\src\github_graphql_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 macOS in debug mode...
Building macOS application...
Activating Dart DevTools...                                         4.2s
Syncing files to device macOS...                                    55ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:56370/WURs0AbCsEY=/

Flutter DevTools, a Flutter debugger and profiler, on macOS is available at:
http://127.0.0.1:56397?uri=http%3A%2F%2F127.0.0.1%3A56370%2FWURs0AbCsEY%3D%2F

💪 Running with sound null safety 💪

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.

ea232028115f24c.png

Na próxima seção, você autenticará no GitHub usando o OAuth2.

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

Adicione dependências de pacote ao aplicativo inserindo entradas no arquivo pubspec.yaml da seguinte maneira:

pubspec.yaml (link em inglês)

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.1        # Add this line,
  oauth2: ^2.0.0       # and this line,
  url_launcher: ^6.0.2 # and this one too.

dev_dependencies:
  flutter_test:
    sdk: flutter
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

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,
  });
  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(MyApp());
}

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

class MyHomePage extends StatelessWidget {
  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: 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 aplicativo está conectado.

Agora que você conseguiu a autenticação OAuth, comece a usar a API GraphQL do GitHub.

Introdução ao GraphQL

Encontrado em graphql.org, o GraphQL oferece uma descrição completa e compreensível dos dados da API e fornece aos clientes o poder de solicitar exatamente o que precisam e nada mais. Poder fazer perguntas focadas na API que preenchem partes específicas de uma IU é muito vantajoso para os desenvolvedores.

A API v4 do GitHub é definida em relação ao GraphQL e disponibiliza um excelente playground para explorar o GraphQL usando dados reais. O GitHub fornece o Explorador do GraphQL, em execução no GraphiQL, que oferece uma forma de criar consultas do GraphQL com a API GraphQL do GitHub. Para mais informações sobre como usar o Explorador do GraphQL do GitHub, consulte Usar o Explorador no GitHub.

Neste codelab, você usará o pacote gql (link em inglês) para gerar código de marshalling com segurança de tipo para as consultas que você criar no Explorador.

Adicionar mais dependências

Para usar a geração de código para criar a biblioteca de cliente do GraphQL, você precisa de build_runner (link em inglês) e de muitos pacotes gql. Comece adicionando essas dependências ao arquivo pubspec.yaml:

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  gql: ^0.13.0-0          # Add from here
  gql_exec: ^0.3.0-0      #
  gql_link: ^0.4.0-0      #
  gql_http_link: ^0.4.0-0 # to here.
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0   # Add this line,
  gql_build: ^0.2.0-0     # and this one.
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

Recuperar o esquema GraphQL do GitHub

O GitHub publica um esquema que descreve a própria API. Armazene esse esquema em cache de modo que suas ferramentas de geração de código possam usá-lo para criar bibliotecas de cliente com segurança de tipo para consultas.

No macOS e Linux:

$ mkdir -p lib/third_party/github_graphql_schema
$ curl -o ./lib/third_party/github_graphql_schema/schema.docs.graphql \
  https://docs.github.com/public/schema.docs.graphql

No Windows:

PS C:\src\github_graphql_client> mkdir .\lib\third_party\github_graphql_schema\
PS C:\src\github_graphql_client> curl -o .\lib\third_party\github_graphql_schema\schema.docs.graphql https://docs.github.com/public/schema.docs.graphql

Os dois comandos anteriores criam um schema.docs.graphql que o pipeline de geração de código usa para conferir o tipo de suas consultas e gerar bibliotecas de cliente com segurança de tipo. Você também precisa de uma consulta. Comece com a consulta padrão iniciada pelo Explorador do GraphQL do GitHub, com uma pequena mudança. É necessário dar um nome à consulta para que o gerador de código gere sua biblioteca de cliente com segurança de tipo.

lib/src/github_gql/github_queries.graphql (link em inglês)

query ViewerDetail {
  viewer {
    login
  }
}

Configurar o build_runner

Para configurar o build_runner, adicione regras a build.yaml. Neste caso, você configura como o pacote gql gera código do esquema do GraphQL do GitHub e as consultas criadas no Explorador.

build.yaml (link em inglês)

targets:
  $default:
    builders:
      gql_build|ast_builder:
        enabled: true
      gql_build|req_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|serializer_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|schema_builder:
        enabled: true
      gql_build|data_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|var_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql

O pacote build_runner é bem poderoso, além do que é possível abordar aqui. Se quiser saber mais, assista ao vídeo Geração de código com o sistema de compilação do Dart (em inglês) de Kevin Moore no YouTube.

Agora que todas as partes estão no lugar, você pode executar build_runner para gerar a biblioteca de cliente do GraphQL.

$ flutter pub run build_runner build --delete-conflicting-outputs

Se você conferir lib/third_party/github_graphql_schema/ e lib/src/github_gql/,, verá que agora tem bastante código recém-gerado.

Reunir todos os elementos, novamente

É hora de integrar toda a qualidade do GraphQL ao seu arquivo lib/main.dart.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'github_oauth_credentials.dart';
import 'src/github_gql/github_queries.data.gql.dart';
import 'src/github_gql/github_queries.req.gql.dart';
import 'src/github_login.dart';

void main() {
  runApp(MyApp());
}

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        final link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: httpClient,
        );
        return FutureBuilder<GViewerDetailData_viewer>(
          future: viewerDetail(link),
          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<GViewerDetailData_viewer> viewerDetail(Link link) async {
  final req = GViewerDetail((b) => b);
  final result = await link
      .request(Request(
        operation: req.operation,
        variables: req.vars.toJson(),
      ))
      .first;
  final errors = result.errors;
  if (errors != null && errors.isNotEmpty) {
    throw QueryException(errors);
  }
  return GViewerDetailData.fromJson(result.data!)!.viewer;
}

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

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 voltará ao primeiro plano.

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.

Você pode usar flutter para criar um novo projeto de plug-in.

$ cd .. # step outside of the github_graphql_client project
$ flutter create -t plugin --platforms=linux,macos,windows window_to_front

Agora, migre seu plug-in para segurança contra nulidade e elimine o app de exemplo. Para macOS ou Linux:

$ cd window_to_front
$ dart migrate --apply-changes
$ rm -r example

Da mesma forma, para Windows:

PS C:\src> cd window_to_front
PS C:\src\window_to_front> dart migrate --apply-changes
PS C:\src\window_to_front> rmdir example

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:
  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 = const MethodChannel('window_to_front');
  // Add from here
  static Future<void> activate() async {
    await _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_graphql_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.

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  gql: ^0.13.0-0
  gql_exec: ^0.3.0-0
  gql_link: ^0.4.0-0
  gql_http_link: ^0.4.0-0
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2
  window_to_front:             # Add this dependency, from here
    path: '../window_to_front' # to here.

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0
  gql_build: ^0.2.0-0
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

Observe o caminho especificado para a dependência window_to_front: como esse é um pacote local em vez de ser publicado no pub.dev, você especifica 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:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:window_to_front/window_to_front.dart'; // Add this,
import 'github_oauth_credentials.dart';
import 'src/github_gql/github_queries.data.gql.dart';
import 'src/github_gql/github_queries.req.gql.dart';
import 'src/github_login.dart';

void main() {
  runApp(MyApp());
}

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

class MyHomePage extends StatelessWidget {
  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.
        final link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: httpClient,
        );
        return FutureBuilder<GViewerDetailData_viewer>(
          future: viewerDetail(link),
          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<GViewerDetailData_viewer> viewerDetail(Link link) async {
  final req = GViewerDetail((b) => b);
  final result = await link
      .request(Request(
        operation: req.operation,
        variables: req.vars.toJson(),
      ))
      .first;
  final errors = result.errors;
  if (errors != null && errors.isNotEmpty) {
    throw QueryException(errors);
  }
  return GViewerDetailData.fromJson(result.data!)!.viewer;
}

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

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ê usará a base que já tem para criar um cliente GitHub de computador que fornece informações sobre o que você tem no GitHub. Será possível inspecionar a lista de repositórios na conta, as solicitações de envio criadas e os 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ê adicionará a capacidade de listar repositórios, solicitações de envio e problemas atribuídos.

Consultar repositórios, solicitações de envio e problemas usando o GraphQL

Para exibir informações do GitHub, é necessário recuperá-las. Portanto, adicione as seguintes consultas GraphQL ao conjunto:

lib/src/github_gql/github_queries.graphql (link em inglês)

query ViewerDetail {
  viewer {
    login
  }
}

// Add everything below here.

query PullRequests($count: Int!) {
  viewer {
    pullRequests(
      first: $count
      orderBy: { field: CREATED_AT, direction: DESC }
    ) {
      edges {
        node {
          repository {
            nameWithOwner
            url
          }
          author {
            login
            url
          }
          number
          url
          title
          updatedAt
          url
          state
          isDraft
          comments {
            totalCount
          }
          files {
            totalCount
          }
        }
      }
    }
  }
}

query AssignedIssues($query: String!, $count: Int!) {
  search(query: $query, type: ISSUE, first: $count) {
    edges {
      node {
        ... on Issue {
          __typename
          repository {
            nameWithOwner
            url
          }
          number
          url
          title
          author {
            login
            url
          }
          labels(last: 10) {
            nodes {
              name
              color
            }
          }
          comments {
            totalCount
          }
        }
      }
    }
  }
}

query Repositories($count: Int!) {
  viewer {
    repositories(
      first: $count
      orderBy: { field: UPDATED_AT, direction: DESC }
    ) {
      nodes {
        name
        description
        isFork
        isPrivate
        stargazers {
          totalCount
        }
        url
        issues {
          totalCount
        }
        owner {
          login
          avatarUrl
        }
      }
    }
  }
}

Para gerar a biblioteca de cliente do GraphQL novamente, execute o seguinte comando:

$ flutter pub run build_runner build --delete-conflicting-outputs

Adicionar uma última dependência

Ao renderizar os dados retornados das consultas acima, você usará mais um pacote, fluttericon, para exibir facilmente os Octicons do GitHub (links em inglês).

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  fluttericon: ^2.0.0    # Add this dependency
  gql: ^0.13.0-0
  gql_exec: ^0.3.0-0
  gql_link: ^0.4.0-0
  gql_http_link: ^0.4.0-0
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2
  window_to_front:
    path: '../window_to_front'

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0
  gql_build: ^0.2.0-0
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

Widgets para renderizar os resultados na tela

Você usará as consultas do GraphQL criadas acima para preencher um widget NavigationRail (link em inglês) com visualizações dos repositórios, problemas atribuídos e solicitações de envio. 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:gql_exec/gql_exec.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:gql_link/gql_link.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'github_gql/github_queries.data.gql.dart';
import 'github_gql/github_queries.req.gql.dart';

class GitHubSummary extends StatefulWidget {
  GitHubSummary({required http.Client client})
      : _link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: client,
        );
  final HttpLink _link;
  @override
  _GitHubSummaryState createState() => _GitHubSummaryState();
}

class _GitHubSummaryState extends State<GitHubSummary> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        NavigationRail(
          selectedIndex: _selectedIndex,
          onDestinationSelected: (int index) {
            setState(() {
              _selectedIndex = index;
            });
          },
          labelType: NavigationRailLabelType.selected,
          destinations: [
            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'),
            ),
          ],
        ),
        VerticalDivider(thickness: 1, width: 1),
        // This is the main content.
        Expanded(
          child: IndexedStack(
            index: _selectedIndex,
            children: [
              RepositoriesList(link: widget._link),
              AssignedIssuesList(link: widget._link),
              PullRequestsList(link: widget._link),
            ],
          ),
        ),
      ],
    );
  }
}

class RepositoriesList extends StatefulWidget {
  const RepositoriesList({required this.link});
  final Link link;
  @override
  _RepositoriesListState createState() => _RepositoriesListState(link: link);
}

class _RepositoriesListState extends State<RepositoriesList> {
  _RepositoriesListState({required Link link}) {
    _repositories = _retreiveRespositories(link);
  }
  late Future<List<GRepositoriesData_viewer_repositories_nodes>> _repositories;

  Future<List<GRepositoriesData_viewer_repositories_nodes>>
      _retreiveRespositories(Link link) async {
    final req = GRepositories((b) => b..vars.count = 100);
    final result = await link
        .request(Request(
          operation: req.operation,
          variables: req.vars.toJson(),
        ))
        .first;
    final errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GRepositoriesData.fromJson(result.data!)!
        .viewer
        .repositories
        .nodes!
        .asList();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<GRepositoriesData_viewer_repositories_nodes>>(
      future: _repositories,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return 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 ?? 'No description'),
              onTap: () => _launchUrl(context, repository.url.value),
            );
          },
          itemCount: repositories!.length,
        );
      },
    );
  }
}

class AssignedIssuesList extends StatefulWidget {
  const AssignedIssuesList({required this.link});
  final Link link;
  @override
  _AssignedIssuesListState createState() =>
      _AssignedIssuesListState(link: link);
}

class _AssignedIssuesListState extends State<AssignedIssuesList> {
  _AssignedIssuesListState({required Link link}) {
    _assignedIssues = _retrieveAssignedIssues(link);
  }

  late Future<List<GAssignedIssuesData_search_edges_node__asIssue>>
      _assignedIssues;

  Future<List<GAssignedIssuesData_search_edges_node__asIssue>>
      _retrieveAssignedIssues(Link link) async {
    final viewerReq = GViewerDetail((b) => b);
    var result = await link
        .request(Request(
          operation: viewerReq.operation,
          variables: viewerReq.vars.toJson(),
        ))
        .first;
    var errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    final _viewer = GViewerDetailData.fromJson(result.data!)!.viewer;

    final issuesReq = GAssignedIssues((b) => b
      ..vars.count = 100
      ..vars.query = 'is:open assignee:${_viewer.login} archived:false');

    result = await link
        .request(Request(
          operation: issuesReq.operation,
          variables: issuesReq.vars.toJson(),
        ))
        .first;
    errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GAssignedIssuesData.fromJson(result.data!)!
        .search
        .edges!
        .map((e) => e.node)
        .whereType<GAssignedIssuesData_search_edges_node__asIssue>()
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<GAssignedIssuesData_search_edges_node__asIssue>>(
      future: _assignedIssues,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return 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('${assignedIssue.repository.nameWithOwner} '
                  'Issue #${assignedIssue.number} '
                  'opened by ${assignedIssue.author!.login}'),
              onTap: () => _launchUrl(context, assignedIssue.url.value),
            );
          },
          itemCount: assignedIssues!.length,
        );
      },
    );
  }
}

class PullRequestsList extends StatefulWidget {
  const PullRequestsList({required this.link});
  final Link link;
  @override
  _PullRequestsListState createState() => _PullRequestsListState(link: link);
}

class _PullRequestsListState extends State<PullRequestsList> {
  _PullRequestsListState({required Link link}) {
    _pullRequests = _retrievePullRequests(link);
  }
  late Future<List<GPullRequestsData_viewer_pullRequests_edges_node>>
      _pullRequests;

  Future<List<GPullRequestsData_viewer_pullRequests_edges_node>>
      _retrievePullRequests(Link link) async {
    final req = GPullRequests((b) => b..vars.count = 100);
    final result = await link
        .request(Request(
          operation: req.operation,
          variables: req.vars.toJson(),
        ))
        .first;
    final errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GPullRequestsData.fromJson(result.data!)!
        .viewer
        .pullRequests
        .edges!
        .map((e) => e.node)
        .whereType<GPullRequestsData_viewer_pullRequests_edges_node>()
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<
        List<GPullRequestsData_viewer_pullRequests_edges_node>>(
      future: _pullRequests,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return 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('${pullRequest.repository.nameWithOwner} '
                  'PR #${pullRequest.number} '
                  'opened by ${pullRequest.author!.login} '
                  '(${pullRequest.state.name.toLowerCase()})'),
              onTap: () => _launchUrl(context, pullRequest.url.value),
            );
          },
          itemCount: pullRequests!.length,
        );
      },
    );
  }
}

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

Future<void> _launchUrl(BuildContext context, String url) async {
  if (await canLaunch(url)) {
    await launch(url);
  } else {
    return showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Navigation error'),
        content: Text('Could not launch $url'),
        actions: <Widget>[
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: 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: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(MyApp());
}

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, client) {
        WindowToFront.activate();
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: GitHubSummary(client: client),
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Execute o aplicativo e você verá uma mensagem parecida com esta:

775e773e58e53e85.png

Parabéns!

Você concluiu o codelab e criou um aplicativo do Flutter para computador que acessa a API GraphQL do GitHub. Você usou uma API autenticada com OAuth, gerou uma biblioteca de cliente com segurança de tipo e usou APIs nativas com um plug-in que também foi criado por você.

Para saber mais sobre o Flutter para computador, acesse flutter.dev/desktop. Para saber mais sobre o GraphQL, acesse graphql.org/learn. Por fim, para ver uma abordagem totalmente diferente sobre o Flutter e o GitHub, consulte o GitHub-Activity-Feed do GroovinChip (links em inglês).