Développer une application de bureau avec Flutter

Flutter est un kit d'interface utilisateur Google qui permet de développer des applications esthétiques compilées de manière native pour les mobiles, le Web et les ordinateurs de bureau, à partir d'un seul codebase. Dans cet atelier de programmation, vous allez développer une application de bureau Flutter qui accède à des API sur GitHub pour récupérer vos dépôts, vos demandes d'extraction et les problèmes qui vous sont attribués. Lors de cette tâche, vous allez créer et utiliser des plug-ins pour interagir avec des API et des applications de bureau natives, puis employer la génération de code pour créer des bibliothèques clientes avec sûreté du typage pour des API de GitHub.

Ce que vous allez apprendre

  • Développer une application de bureau avec Flutter
  • S'authentifier avec OAuth2 sur ordinateur de bureau
  • Utiliser GraphQL dans Flutter avec la génération de code
  • Créer un plug-in Flutter à intégrer à des API natives

Ce que vous allez faire

Dans cet atelier de programmation, vous allez développer une application de bureau avec l'intégration d'une API GraphQL de GitHub à l'aide du SDK Flutter. Votre application effectuera les opérations suivantes :

  • S'authentifier auprès de GitHub
  • Récupérer des données à partir de l'API v4 de GitHub
  • Créer un plug-in Flutter pour Windows, macOS et/ou Linux
  • Développer une actualisation à chaud de l'interface utilisateur Flutter dans une application de bureau native

Voici une capture d'écran de l'application de bureau que vous allez développer (qui s'exécutera sous Windows).

775e773e58e53e85.png

Cet atelier de programmation porte sur l'ajout de fonctionnalités GraphQL à une application de bureau Flutter. Les concepts et blocs de codes non pertinents ne sont pas abordés, mais sont fournis afin que vous puissiez simplement les copier et les coller.

Qu'attendez-vous de cet atelier de programmation ?

Je suis novice en la matière et je voudrais avoir un bon aperçu. Je connais un peu le sujet, mais j'aimerais revoir certains points. Je recherche un exemple de code à utiliser dans mon projet. Je cherche des explications sur un point spécifique.

Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez la développer sous Windows pour accéder à la chaîne de compilation appropriée.

Si vous la développez pour tous les systèmes d'exploitation, vous avez alors besoin de deux logiciels pour réaliser cet atelier : le SDK Flutter et un éditeur.

Prenez également connaissance des exigences spécifiques aux systèmes d'exploitation, détaillées sur flutter.dev/desktop.

Premiers pas pour développer des applications de bureau avec Flutter

Vous devez apporter une modification ponctuelle à la configuration de sorte que les ordinateurs de bureau soient pris en charge.

$ 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

Afin de vérifier que Flutter pour le bureau est activé, exécutez la commande ci-dessous.

$ 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

Si la ligne "desktop" appropriée n'est pas affichée ci-dessus, posez-vous les questions suivantes :

  • Est-ce que vous développez l'application sur la bonne plate-forme ?
  • Est-ce que l'exécution de flutter config indique que macOS est activé (enable-[os]-desktop: true) ?
  • Est-ce que l'exécution de flutter channel indique dev ou master comme canal actuel ? Cela est indispensable, car le code ne s'exécute pas sur les canaux stable ou beta.

Pour bien commencer, utilisez l'outil de ligne de commande Flutter afin de créer un projet Flutter. Votre IDE peut aussi fournir, via son interface utilisateur, le workflow nécessaire pour créer un projet Flutter.

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

Activez la sécurité nulle en migrant le projet comme suit sous macOS et Linux :

$ cd github_graphql_client
$ dart migrate --apply-changes

De même, sous Windows :

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

Pour simplifier cet atelier de programmation, supprimez les fichiers d'aide pour Android, iOS et le Web. Vous n'en aurez pas besoin pour développer votre application de bureau avec Flutter. Cela vous évitera aussi d'exécuter une variante par erreur lors de cet atelier de programmation.

Pour macOS et Linux :

$ rm -r android ios web

Pour 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

Pour être sûr que tout fonctionne, exécutez l'application Flutter standard en tant qu'application de bureau, comme indiqué ci-dessous. Vous pouvez aussi ouvrir ce projet dans votre IDE et utiliser les outils qui s'y trouvent pour exécuter l'application. Grâce à l'étape précédente, l'exécution en tant qu'application de bureau devrait être la seule option disponible.

$ 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 💪

La fenêtre ci-dessous devrait maintenant être affichée. Cliquez sur le bouton d'action flottant pour vérifier que l'incrémenteur fonctionne comme prévu. Vous pouvez également effectuer une actualisation à chaud en modifiant la couleur du thème ou le comportement de la méthode _incrementCounter dans lib/main.dart.

Voici l'application exécutée sous Windows.

ea232028115f24c.png

Dans la section suivante, vous allez vous authentifier sur GitHub avec OAuth2.

S'authentifier sur ordinateur de bureau

Si vous utilisez Flutter sous Android, sous iOS ou sur le Web, vous disposez d'une multitude d'options concernant les packages d'authentification. En revanche, l'équation n'est pas la même pour développer une application de bureau. Actuellement, vous devez créer entièrement l'intégration d'authentification. Toutefois, cela va changer à mesure que les auteurs de packages implémenteront Flutter de sorte que les ordinateurs de bureau soient pris en charge.

Enregistrer une application OAuth sur GitHub

Pour développer une application de bureau qui utilise des API sur GitHub, vous devez d'abord vous authentifier. Vous avez plusieurs options, mais la meilleure consiste à rediriger l'utilisateur vers la page d'authentification OAuth2 de GitHub depuis son navigateur. Cela permet de gérer l'authentification à deux facteurs et d'intégrer sans effort les gestionnaires de mots de passe.

Pour enregistrer une application via le flux OAuth2 de GitHub, rendez-vous sur github.com et suivez uniquement les instructions de la première étape de création d'applications OAuth sur GitHub. Les autres étapes sont importantes quand vous avez une application à lancer, mais pas lorsque vous suivez un atelier de programmation.

Lors de la création d'une application OAuth, il vous est demandé à l'étape 8 de fournir l'URL de rappel d'autorisation. Pour une application de bureau, indiquez http://localhost/ comme URL de rappel. Le flux OAuth2 de GitHub a été configuré de sorte que la définition d'une URL de rappel localhost autorise tous les ports, ce qui vous permet de mettre en place un serveur Web sur un port haut local éphémère. Cela évite de demander à l'utilisateur de copier le jeton OAuth dans l'application lors du processus OAuth.

Voici une capture d'écran montrant comment remplir le formulaire de création d'une application OAuth sur GitHub :

be454222e07f01d9.png

Une fois que vous avez enregistré une application OAuth dans l'interface d'administration GitHub, vous recevez un ID client et un code secret du client. Si vous avez besoin de ces valeurs par la suite, vous pourrez les récupérer dans les paramètres développeur sur GitHub. Ces identifiants vous sont utiles dans votre application pour créer une URL d'autorisation OAuth2 valide. Vous allez utiliser le package Dart oauth2 pour gérer le flux OAuth2, et le plug-in Flutter url_launcher pour permettre le lancement du navigateur Web de l'utilisateur.

Ajouter oauth2 et url_launcher à pubspec.yaml

Pour ajouter des dépendances de packages pour votre application, vous devez ajouter des entrées au fichier pubspec.yaml, comme suit :

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

Inclure les identifiants client

Ajoutez les identifiants client à un nouveau fichier (lib/github_oauth_credentials.dart) comme suit :

lib/github_oauth_credentials.dart

// 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'];

Copiez vos identifiants client de l'étape précédente et collez-les dans ce fichier.

Créer le flux OAuth2 pour ordinateur de bureau

Créez un widget qui contient le flux OAuth2 pour ordinateur de bureau. Il s'agit d'un fragment de logique relativement complexe, car vous devez exécuter un serveur Web temporaire, rediriger l'utilisateur vers un point de terminaison sur GitHub dans son navigateur Web, attendre que l'utilisateur termine le flux d'autorisation dans son navigateur et gérer un appel de redirection de GitHub qui contient du code (lequel doit ensuite être converti en jeton OAuth2 avec un appel séparé aux serveurs d'API de GitHub).

lib/src/github_login.dart

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;
}

Il convient de consacrer du temps à ce code pour observer certaines possibilités qu'offrent Flutter et Dart sur l'ordinateur de bureau. Oui, le code est complexe, mais un grand nombre de fonctionnalités sont encapsulées dans un widget relativement simple à utiliser.

Ce widget présente un serveur Web temporaire et exécute des requêtes HTTP sécurisées. Sous macOS, ces deux fonctionnalités doivent être demandées via des fichiers de droits d'accès.

Modifier les droits d'accès côté client et serveur (macOS uniquement)

Les requêtes Web et l'exécution d'un serveur Web en tant qu'application de bureau macOS exigent de modifier les droits d'accès de l'application. Pour en savoir plus, consultez la section sur les droits d'accès et le bac à sable de l'application.

macos/Runner/DebugProfile.entitlements

<?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>

Vous devez également modifier les droits d'accès de release des builds de production.

macos/Runner/Release.entitlements

<?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>

Regrouper tous les éléments

Vous avez configuré une nouvelle application OAuth, le projet est configuré avec les packages et plug-ins requis, vous avez créé un widget pour encapsuler le flux d'authentification OAuth, et vous avez permis à l'application d'agir à la fois comme client réseau et serveur sous macOS via des droits d'accès. Avec tous ces éléments essentiels en place, vous pouvez les regrouper dans le fichier lib/main.dart.

lib/main.dart

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

Lorsque vous exécutez cette application Flutter, un bouton vous est proposé pour lancer le flux de connexion OAuth sur GitHub. Une fois que vous avez cliqué sur ce bouton, terminez le flux de connexion dans votre navigateur Web pour voir si l'application est maintenant connectée.

Maintenant que vous avez passé avec succès l'étape d'authentification OAuth, vous pouvez commencer à utiliser l'API GraphQL de GitHub.

Présentation de GraphQL

Sélectionnée sur graphql.org, GraphQL fournit une description complète et compréhensible des données de votre API. Elle offre aux clients la possibilité de demander exactement ce dont ils ont besoin et rien de plus. Le fait de pouvoir poser des questions ciblées à l'API pour renseigner des parties spécifiques d'une interface utilisateur constitue un réel avantage pour les développeurs.

L'API v4 de GitHub est définie en termes de GraphQL, ce qui offre un excellent terrain de jeu pour explorer GraphQL avec des données réelles. GitHub propose l'explorateur GraphQL GitHub, optimisé par GraphiQL, qui vous permet de créer des requêtes GraphQL sur l'API GraphQL de GitHub. Pour en savoir plus sur l'utilisation de cet explorateur, cliquez ici.

Dans cet atelier de programmation, vous allez utiliser le package gql pour générer du code de marshalling avec sûreté du typage pour les requêtes que vous créez dans l'explorateur.

Ajouter d'autres dépendances

Pour créer une bibliothèque cliente GraphQL à l'aide de la génération de code, vous avez besoin de build_runner et de nombreux packages gql. Commencez par ajouter les dépendances suivantes à votre fichier 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

Récupérer le schéma GraphQL sur GitHub

GitHub publie un schéma qui décrit son API. Vous mettez en cache ce schéma de sorte que vos outils de génération de code puissent l'utiliser pour créer des bibliothèques clientes avec sûreté du typage pour vos requêtes.

Pour macOS et 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

Pour 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

Les deux commandes ci-dessus créent un objet schema.docs.graphql que le pipeline de génération de code utilise pour vérifier le type de vos requêtes et générer des bibliothèques clientes avec sûreté du typage. Vous avez également besoin d'une requête. Commencez par la requête par défaut avec laquelle débute l'explorateur GraphQL GitHub, avec une légère modification. Vous devez nommer la requête pour que le générateur de code puisse générer votre bibliothèque cliente avec sûreté du typage.

lib/src/github_gql/github_queries.graphql

query ViewerDetail {
  viewer {
    login
  }
}

Configurer build_runner

Pour configurer build_runner, ajoutez des règles à build.yaml. Dans ce cas, vous allez configurer la manière dont le package gql génère du code à partir du schéma GraphQL GitHub et des requêtes que vous créez dans l'explorateur.

build.yaml

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

Le package build_runner est assez puissant (plus que nous ne pouvons l'expliquer ici). Pour en savoir plus, regardez la vidéo YouTube de Kevin Moore sur la génération de code avec le système de compilation Dart.

Maintenant que tous les éléments sont en place, vous pouvez exécuter build_runner pour générer votre bibliothèque cliente GraphQL.

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

Si vous parcourez lib/third_party/github_graphql_schema/ et lib/src/github_gql/,, vous constaterez que vous disposez maintenant de beaucoup de codes générés récemment.

Regrouper de nouveau tous les éléments

Il est temps d'intégrer tous les avantages de GraphQL dans votre fichier 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(',')}';
  }
}

Après avoir exécuté cette application, un bouton permettant de lancer le flux de connexion OAuth sur GitHub s'affiche. Une fois que vous avez cliqué sur ce bouton, terminez le flux de connexion dans votre navigateur Web. Vous êtes maintenant connecté à l'application.

À l'étape suivante, vous allez éliminer un aspect ennuyeux du code base actuel. Après avoir authentifié l'application dans le navigateur Web, vous la remettrez au premier plan.

Éliminer ce qui pose problème

Actuellement, le code présente un aspect ennuyeux. Après le flux d'authentification, lorsque GitHub a authentifié votre application, vous êtes redirigé vers une page de navigateur Web. Dans l'idéal, vous devriez revenir automatiquement à l'application. Pour corriger ce problème, vous devez créer un plug-in Flutter pour vos plates-formes de bureau.

Créer un plug-in Flutter pour Windows, macOS et Linux

Pour que l'application s'affiche automatiquement au début de la pile des fenêtres d'application une fois le flux OAuth terminé, un code natif est nécessaire. Pour macOS, l'API dont vous avez besoin est la méthode d'instance activate(ignoringOtherApps:) de NSApplication. Pour Linux, nous utiliserons gtk_window_present, tandis que sous Windows, nous aurons recours à Stack Overflow. Pour pouvoir appeler ces API, vous devez créer un plug-in Flutter.

Vous pouvez utiliser flutter pour créer un projet de plug-in.

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

Migrez maintenant votre plug-in vers une sécurité nulle, et supprimez l'exemple d'application. Pour macOS ou Linux :

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

De même, pour 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

Vérifiez que le fichier pubspec.yaml généré se présente comme suit :

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

Ce plug-in est configuré pour macOS, Linux et Windows. Vous pouvez maintenant ajouter le code Swift qui fait apparaître la fenêtre à l'avant. Modifiez macos/Classes/WindowToFrontPlugin.swift comme suit :

../window_to_front/macos/Classes/WindowToFrontPlugin.swift

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

Pour faire la même chose dans le plug-in Linux, remplacez le contenu de linux/window_to_front_plugin.cc par ce qui suit :

../window_to_front/linux/window_to_front_plugin.cc

#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);
}

Pour faire la même chose dans le plug-in Windows, remplacez le contenu de windows/window_to_front_plugin.cc par ce qui suit :

..\window_to_front\windows\window_to_front_plugin.cpp

#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));
}

Ajoutez le code pour que la fonctionnalité native que nous avons créée ci-dessus soit accessible aux utilisateurs de Flutter.

../window_to_front/lib/window_to_front.dart

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

Ce plug-in Flutter est terminé. Vous pouvez revenir à la modification du projet github_graphql_client.

$ cd ../github_graphql_client

Ajouter des dépendances

Le plug-in Flutter que vous venez de créer est un très bon outil qu'il convient maintenant de mettre à profit. Vous devez l'ajouter en tant que dépendance dans votre application Flutter afin de pouvoir l'utiliser.

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

Notez le chemin d'accès spécifié pour la dépendance window_to_front : étant donné qu'il s'agit d'un package local et non un package publié dans pub.dev, vous devez spécifier un chemin d'accès au lieu d'un numéro de version.

Regrouper les éléments encore et encore

Il est temps d'intégrer window_to_front à votre fichier lib/main.dart. Il nous suffit d'ajouter une importation et d'appeler le code natif au bon moment.

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

Après avoir exécuté cette application Flutter, vous aurez alors une application d'apparence identique. Pour observer la différence de comportement, cliquez sur le bouton. Si vous placez l'application par-dessus le navigateur Web avec lequel vous vous authentifiez, lorsque vous cliquerez sur le bouton de connexion, votre application sera poussée derrière le navigateur Web. Toutefois, une fois le flux d'authentification terminé dans le navigateur, votre application reviendra au premier plan en étant bien plus esthétique.

Dans la section suivante, vous allez utiliser la base dont vous disposez pour créer un client de bureau GitHub qui vous donne un aperçu de ce que vous avez sur GitHub. Vous allez inspecter la liste des dépôts associés au compte, les demandes d'extraction créées et les problèmes attribués.

Vous avez presque terminé de développer cette application et, pourtant, elle ne fait que vous indiquer votre nom de connexion. Vous attendez probablement davantage d'un client de bureau GitHub. Vous allez maintenant ajouter la fonctionnalité permettant de répertorier les dépôts, les demandes d'extraction et les problèmes attribués.

Interroger les dépôts, demandes d'extraction et problèmes à l'aide de GraphQL

Pour pouvoir afficher des informations de GitHub, vous devez les récupérer. Pour cela, ajoutez les requêtes GraphQL comme suit :

lib/src/github_gql/github_queries.graphql

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
        }
      }
    }
  }
}

Pour générer de nouveau votre bibliothèque cliente GraphQL, exécutez la commande suivante :

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

Ajouter une dernière dépendance

Pour rendre les données renvoyées par les requêtes ci-dessus, vous allez utiliser un package supplémentaire (fluttericon) pour afficher facilement les icônes de GitHub (appelées "octicons").

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 pour afficher les résultats à l'écran

Vous allez utiliser les requêtes GraphQL que vous avez créées ci-dessus pour renseigner un widget NavigationRail avec des vues de vos dépôts, de vos demandes d'extraction et des problèmes qui vous sont attribués. La documentation du système de conception Material.io explique comment les rails de navigation offrent un mouvement ergonomique entre les destinations principales des applications.

Créez un fichier et renseignez-le avec le contenu suivant.

lib/src/github_summary.dart

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'),
          ),
        ],
      ),
    );
  }
}

Vous venez ici d'ajouter beaucoup de nouveaux codes. L'avantage est qu'il s'agit d'un code assez normal, avec des widgets utilisés pour séparer les responsabilités concernant différents problèmes. Avant de l'exécuter à l'étape suivante, prenez le temps de l'examiner.

Regrouper tout une dernière fois

Il est temps d'intégrer GitHubSummary à votre fichier lib/main.dart. Les changements sont assez importants cette fois-ci, avec principalement des suppressions. Remplacez le contenu du fichier lib/main.dart par le code ci-dessous.

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

Exécutez l'application. Vous devriez obtenir ce qui suit :

775e773e58e53e85.png

Félicitations !

Vous avez terminé l'atelier de programmation et développé une application de bureau Flutter qui accède à l'API GraphQL de GitHub. Vous avez utilisé une API authentifiée avec OAuth, généré une bibliothèque cliente avec sûreté du typage, et utilisé des API natives via un plug-in que vous avez aussi créé.

Pour en savoir plus sur Flutter pour ordinateur de bureau, rendez-vous sur flutter.dev/desktop. Pour en savoir plus sur GraphQL, consultez la page graphql.org/learn. Enfin, pour voir une approche totalement différente sur Flutter et GitHub, consultez le flux d'activité GitHub de GroovvinChip.