Développer une application de bureau avec Flutter

1. Présentation

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 le package GitHub de Dart
  • 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 une intégration GitHub utilisant le SDK Flutter. Votre application effectuera les opérations suivantes :

  • S'authentifier auprès de GitHub
  • Récupérer des données à partir de GitHub
  • Créer un plug-in Flutter pour Windows, macOS et/ou Linux
  • Développer un hot reload 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).

a456fca6e2997992.png

Cet atelier de programmation porte sur l'ajout de fonctionnalités d'accès OAuth2 et GitHub à 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.

2. Configurer votre environnement Flutter

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.

3. Premiers pas

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_client
Creating project github_client...
Running "flutter pub get" in github_client...                    1,103ms
Wrote 128 files.

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

  $ cd github_client
  $ flutter run

Your application code is in github_client\lib\main.dart.

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_client> rmdir android
PS C:\src\github_client> rmdir ios
PS C:\src\github_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 Windows in debug mode...
Building Windows application...
Syncing files to device Windows...                                  56ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:61920/OHTnly7_TMk=/
The Flutter DevTools debugger and profiler on Windows is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:61920/OHTnly7_TMk=/

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.

bee40fe7a8e69791.png

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

4. Ajouter l'authentification

S'authentifier sur un 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, exécutez flutter pub add comme suit :

$ flutter pub add http
Resolving dependencies...
+ http 0.13.4
+ http_parser 4.0.0
  path 1.8.0 (1.8.1 available)
  source_span 1.8.1 (1.8.2 available)
  test_api 0.4.8 (0.4.9 available)
Changed 2 dependencies!

La première commande ajoute le package http pour effectuer des appels HTTP de manière cohérente sur plusieurs plates-formes. Ensuite, ajoutez le package oauth2 comme suit.

$ flutter pub add oauth2
Resolving dependencies...
+ crypto 3.0.1
+ oauth2 2.0.0
  path 1.8.0 (1.8.1 available)
  source_span 1.8.1 (1.8.2 available)
  test_api 0.4.8 (0.4.9 available)
Changed 2 dependencies!

Enfin, ajoutez le package url_launcher.

$ flutter pub add url_launcher
Resolving dependencies...
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3 (0.6.4 available)
  path 1.8.0 (1.8.1 available)
+ plugin_platform_interface 2.1.2
  source_span 1.8.1 (1.8.2 available)
  test_api 0.4.8 (0.4.9 available)
+ url_launcher 6.0.18
+ url_launcher_android 6.0.14
+ url_launcher_ios 6.0.14
+ url_launcher_linux 2.0.3
+ url_launcher_macos 2.0.2
+ url_launcher_platform_interface 2.0.5
+ url_launcher_web 2.0.6
+ url_launcher_windows 2.0.2
Downloading url_launcher 6.0.18...
Downloading url_launcher_ios 6.0.14...
Downloading url_launcher_android 6.0.14...
Downloading url_launcher_platform_interface 2.0.5...
Downloading plugin_platform_interface 2.1.2...
Downloading url_launcher_linux 2.0.3...
Downloading url_launcher_web 2.0.6...
Changed 11 dependencies!

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

  @override
  _GithubLoginState createState() => _GithubLoginState();
}

typedef AuthenticatedBuilder = Widget Function(
    BuildContext context, oauth2.Client client);

class _GithubLoginState extends State<GithubLoginWidget> {
  HttpServer? _redirectServer;
  oauth2.Client? _client;

  @override
  Widget build(BuildContext context) {
    final client = _client;
    if (client != null) {
      return widget.builder(context, client);
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Github Login'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await _redirectServer?.close();
            // Bind to an ephemeral port on localhost
            _redirectServer = await HttpServer.bind('localhost', 0);
            var authenticatedHttpClient = await _getOAuth2Client(
                Uri.parse('http://localhost:${_redirectServer!.port}/auth'));
            setState(() {
              _client = authenticatedHttpClient;
            });
          },
          child: const Text('Login to Github'),
        ),
      ),
    );
  }

  Future<oauth2.Client> _getOAuth2Client(Uri redirectUrl) async {
    if (widget.githubClientId.isEmpty || widget.githubClientSecret.isEmpty) {
      throw const GithubLoginException(
          'githubClientId and githubClientSecret must be not empty. '
          'See `lib/github_oauth_credentials.dart` for more detail.');
    }
    var grant = oauth2.AuthorizationCodeGrant(
      widget.githubClientId,
      _authorizationEndpoint,
      _tokenEndpoint,
      secret: widget.githubClientSecret,
      httpClient: _JsonAcceptingHttpClient(),
    );
    var authorizationUrl =
        grant.getAuthorizationUrl(redirectUrl, scopes: widget.githubScopes);

    await _redirect(authorizationUrl);
    var responseQueryParameters = await _listen();
    var client =
        await grant.handleAuthorizationResponse(responseQueryParameters);
    return client;
  }

  Future<void> _redirect(Uri authorizationUrl) async {
    var url = authorizationUrl.toString();
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw GithubLoginException('Could not launch $url');
    }
  }

  Future<Map<String, String>> _listen() async {
    var request = await _redirectServer!.first;
    var params = request.uri.queryParameters;
    request.response.statusCode = 200;
    request.response.headers.set('content-type', 'text/plain');
    request.response.writeln('Authenticated! You can close this tab.');
    await request.response.close();
    await _redirectServer!.close();
    _redirectServer = null;
    return params;
  }
}

class _JsonAcceptingHttpClient extends http.BaseClient {
  final _httpClient = http.Client();
  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    request.headers['Accept'] = 'application/json';
    return _httpClient.send(request);
  }
}

class GithubLoginException implements Exception {
  const GithubLoginException(this.message);
  final String message;
  @override
  String toString() => message;
}

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: const Center(
            child: Text(
              'You are logged in to GitHub!',
            ),
          ),
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

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 le package GitHub.

5. Accéder à GitHub

Se connecter à GitHub

Avec le flux d'authentification OAuth, vous avez obtenu le jeton nécessaire pour accéder à vos données sur GitHub. Pour faciliter cette tâche, vous allez utiliser le package github disponible sur pub.dev.

Ajouter d'autres dépendances

Exécutez la commande suivante :

$ flutter pub add github

Utiliser les identifiants OAuth avec le package GitHub

Le GithubLoginWidget que vous avez créé à l'étape précédente fournit un HttpClient pouvant interagir avec l'API GitHub. À cette étape, vous allez utiliser les identifiants contenus dans HttpClient pour accéder à l'API GitHub à l'aide du package GitHub, comme indiqué ci-dessous :

final accessToken = httpClient.credentials.accessToken;
final gitHub = GitHub(auth: Authentication.withToken(accessToken));

Regrouper de nouveau tous les éléments

Il est temps d'intégrer le client GitHub à votre fichier lib/main.dart.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:github/github.dart';

import 'github_oauth_credentials.dart';
import 'src/github_login.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        return FutureBuilder<CurrentUser>(
          future: viewerDetail(httpClient.credentials.accessToken),
          builder: (context, snapshot) {
            return Scaffold(
              appBar: AppBar(
                title: Text(title),
              ),
              body: Center(
                child: Text(
                  snapshot.hasData
                      ? 'Hello ${snapshot.data!.login}!'
                      : 'Retrieving viewer login details...',
                ),
              ),
            );
          },
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Future<CurrentUser> viewerDetail(String accessToken) async {
  final gitHub = GitHub(auth: Authentication.withToken(accessToken));
  return gitHub.users.getCurrentUser();
}

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.

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

É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_client project
$ flutter create -t plugin --platforms=linux,macos,windows window_to_front

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_lints: ^1.0.0

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

$ flutter pub add --path ../window_to_front window_to_front
Resolving dependencies...
  js 0.6.3 (0.6.4 available)
  path 1.8.0 (1.8.1 available)
  source_span 1.8.1 (1.8.2 available)
  test_api 0.4.8 (0.4.9 available)
+ window_to_front 0.0.1 from path ..\window_to_front
Changed 1 dependency!

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 d'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:github/github.dart';
import 'package:window_to_front/window_to_front.dart';    // Add this

import 'github_oauth_credentials.dart';
import 'src/github_login.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        WindowToFront.activate();                        // and this.
        return FutureBuilder<CurrentUser>(
          future: viewerDetail(httpClient.credentials.accessToken),
          builder: (context, snapshot) {
            return Scaffold(
              appBar: AppBar(
                title: Text(title),
              ),
              body: Center(
                child: Text(
                  snapshot.hasData
                      ? 'Hello ${snapshot.data!.login}!'
                      : 'Retrieving viewer login details...',
                ),
              ),
            );
          },
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Future<CurrentUser> viewerDetail(String accessToken) async {
  final gitHub = GitHub(auth: Authentication.withToken(accessToken));
  return gitHub.users.getCurrentUser();
}

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. C'est beaucoup 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 du projet Flutter et les problèmes attribués.

7. Afficher les dépôts, les demandes d'extraction 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.

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

$ flutter pub add fluttericon
Resolving dependencies...
+ fluttericon 2.0.0
  js 0.6.3 (0.6.4 available)
  material_color_utilities 0.1.3 (0.1.4 available)
  path 1.8.0 (1.8.1 available)
  source_span 1.8.1 (1.8.2 available)
  test_api 0.4.8 (0.4.9 available)
  url_launcher_macos 2.0.2 (2.0.3 available)
Changed 1 dependency!

Widgets pour afficher les résultats à l'écran

Vous allez utiliser le package GitHub que vous avez ajouté précédemment 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 depuis le projet Flutter. 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:github/github.dart';
import 'package:url_launcher/url_launcher.dart';

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

  @override
  _GitHubSummaryState createState() => _GitHubSummaryState();
}

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

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        NavigationRail(
          selectedIndex: _selectedIndex,
          onDestinationSelected: (index) {
            setState(() {
              _selectedIndex = index;
            });
          },
          labelType: NavigationRailLabelType.selected,
          destinations: const [
            NavigationRailDestination(
              icon: Icon(Octicons.repo),
              label: Text('Repositories'),
            ),
            NavigationRailDestination(
              icon: Icon(Octicons.issue_opened),
              label: Text('Assigned Issues'),
            ),
            NavigationRailDestination(
              icon: Icon(Octicons.git_pull_request),
              label: Text('Pull Requests'),
            ),
          ],
        ),
        const VerticalDivider(thickness: 1, width: 1),
        // This is the main content.
        Expanded(
          child: IndexedStack(
            index: _selectedIndex,
            children: [
              RepositoriesList(gitHub: widget.gitHub),
              AssignedIssuesList(gitHub: widget.gitHub),
              PullRequestsList(gitHub: widget.gitHub),
            ],
          ),
        ),
      ],
    );
  }
}

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

  @override
  _RepositoriesListState createState() => _RepositoriesListState();
}

class _RepositoriesListState extends State<RepositoriesList> {
  @override
  initState() {
    super.initState();
    _repositories = widget.gitHub.repositories.listRepositories().toList();
  }

  late Future<List<Repository>> _repositories;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Repository>>(
      future: _repositories,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        var repositories = snapshot.data;
        return ListView.builder(
          itemBuilder: (context, index) {
            var repository = repositories![index];
            return ListTile(
              title:
                  Text('${repository.owner?.login ?? ''}/${repository.name}'),
              subtitle: Text(repository.description),
              onTap: () => _launchUrl(context, repository.htmlUrl),
            );
          },
          itemCount: repositories!.length,
        );
      },
    );
  }
}

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

  @override
  _AssignedIssuesListState createState() => _AssignedIssuesListState();
}

class _AssignedIssuesListState extends State<AssignedIssuesList> {
  @override
  initState() {
    super.initState();
    _assignedIssues = widget.gitHub.issues.listByUser().toList();
  }

  late Future<List<Issue>> _assignedIssues;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Issue>>(
      future: _assignedIssues,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        var assignedIssues = snapshot.data;
        return ListView.builder(
          itemBuilder: (context, index) {
            var assignedIssue = assignedIssues![index];
            return ListTile(
              title: Text(assignedIssue.title),
              subtitle: Text('${_nameWithOwner(assignedIssue)} '
                  'Issue #${assignedIssue.number} '
                  'opened by ${assignedIssue.user?.login ?? ''}'),
              onTap: () => _launchUrl(context, assignedIssue.htmlUrl),
            );
          },
          itemCount: assignedIssues!.length,
        );
      },
    );
  }

  String _nameWithOwner(Issue assignedIssue) {
    final endIndex = assignedIssue.url.lastIndexOf('/issues/');
    return assignedIssue.url.substring(29, endIndex);
  }
}

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

  @override
  _PullRequestsListState createState() => _PullRequestsListState();
}

class _PullRequestsListState extends State<PullRequestsList> {
  @override
  initState() {
    super.initState();
    _pullRequests = widget.gitHub.pullRequests
        .list(RepositorySlug('flutter', 'flutter'))
        .toList();
  }

  late Future<List<PullRequest>> _pullRequests;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<PullRequest>>(
      future: _pullRequests,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('${snapshot.error}'));
        }
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        var pullRequests = snapshot.data;
        return ListView.builder(
          itemBuilder: (context, index) {
            var pullRequest = pullRequests![index];
            return ListTile(
              title: Text(pullRequest.title ?? ''),
              subtitle: Text('flutter/flutter '
                  'PR #${pullRequest.number} '
                  'opened by ${pullRequest.user?.login ?? ''} '
                  '(${pullRequest.state?.toLowerCase() ?? ''})'),
              onTap: () => _launchUrl(context, pullRequest.htmlUrl ?? ''),
            );
          },
          itemCount: pullRequests!.length,
        );
      },
    );
  }
}

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

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:github/github.dart';
import 'package:window_to_front/window_to_front.dart';

import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
import 'src/github_summary.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

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

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

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

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

d5c9bebf448a2519.png

8. Étapes suivantes

Félicitations !

Vous avez terminé l'atelier de programmation et développé une application de bureau Flutter qui accède à l'API GitHub. Vous avez utilisé une API authentifiée avec OAuth et 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. Enfin, pour voir une approche totalement différente de Flutter et GitHub, consultez le flux d'activité GitHub de GroovinChip.