Escribe una aplicación para computadoras en Flutter

1. Introducción

Flutter es el kit de herramientas de IU de Google diseñado para crear aplicaciones atractivas compiladas de forma nativa que funcionen en dispositivos móviles, la Web y computadoras a partir de una base de código única. En este codelab, compilarás una app de escritorio de Flutter que accederá a las API de GitHub para recuperar tus repositorios, problemas asignados y solicitudes de extracción. Para completar esta tarea, crearás y usarás complementos a fin de interactuar con aplicaciones de escritorio y API nativas, y usarás la generación de código con el objetivo de compilar bibliotecas cliente de tipo seguro para las API de GitHub.

Qué aprenderás

  • Cómo crear una aplicación de escritorio de Flutter
  • Cómo autenticarte con OAuth2 en computadoras de escritorio
  • Cómo usar el paquete de GitHub de Dart
  • Cómo crear un complemento de Flutter para que se integre con las API nativas

Qué compilarás

En este codelab, compilarás una aplicación para computadoras que incluya una integración de GitHub con el SDK de Flutter. Tu app hará lo siguiente:

  • Autenticarse en GitHub
  • Recuperar datos desde GitHub
  • Crear un complemento de Flutter para Windows, macOS o Linux
  • Desarrollar una recarga en caliente de la IU de Flutter en una aplicación nativa para computadoras

A continuación, se muestra una captura de pantalla de la aplicación para computadoras que compilarás, ejecutándose en Windows.

a456fca6e2997992.png

Este codelab se enfoca en agregar capacidades de acceso de OAuth2 y GitHub a una app para computadoras en Flutter. Los conceptos que no son relevantes y los bloques de código se pasan por alto y se te brindan para que solo copies y pegues.

¿Qué te gustaría aprender en este codelab?

Desconozco el tema y me gustaría obtener una buena descripción general. Tengo algunos conocimientos sobre este tema, pero me gustaría repasarlos. Estoy buscando código de ejemplo para usar en mi proyecto. Estoy buscando una explicación sobre un tema específico.

2. Configura tu entorno de Flutter

Debes desarrollar contenido en la plataforma donde tengas pensado realizar la implementación. Por lo tanto, si deseas desarrollar una app de escritorio para Windows, debes desarrollarla en Windows para obtener acceso a la cadena de compilación correcta.

Desarrollar contenido para todos los sistemas operativos requiere dos tipos de software si deseas completar este lab: el SDK de Flutter y un editor.

Además, se exigen requisitos específicos de sistema operativo que se describen con detalle en flutter.dev/desktop.

3. Primeros pasos

Comienza a desarrollar aplicaciones para computadoras con Flutter

Debes configurar la compatibilidad con computadoras de escritorio mediante un cambio único de configuración.

$ 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

A fin de confirmar que se haya habilitado Flutter para computadoras de escritorio, ejecuta el siguiente comando:

$ flutter devices
1 connected device:

Windows (desktop) • windows    • windows-x64    • Microsoft Windows [Version 10.0.19041.508]
macOS (desktop)   • macos      • darwin-x64     • macOS 11.2.3 20D91 darwin-x64
Linux (desktop)   • linux      • linux-x64      • Linux

Si no ves la línea correcta para computadora de escritorio que se muestra en el resultado anterior, ten en cuenta lo siguiente:

  • ¿Estás desarrollando contenido en la plataforma de destino?
  • ¿El elemento flutter config en ejecución muestra macOS como habilitado con enable-[os]-desktop: true?
  • ¿El elemento flutter channel en ejecución muestra dev o master como el canal actual? Es obligatorio, ya que el código no se ejecutará en los canales stable o beta.

Una manera fácil de comenzar a escribir código de Flutter para apps de escritorio es usar la herramienta de línea de comandos de Flutter a fin de crear un proyecto en ese framework. De manera alternativa, tu IDE puede brindar un flujo de trabajo para crear un proyecto de Flutter a través de su IU.

$ flutter create github_client
Creating project github_client...
Running "flutter pub get" in github_client...                    1,103ms
Wrote 128 files.

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

  $ cd github_client
  $ flutter run

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

A fin de simplificar este codelab, borra los archivos de compatibilidad para la Web, iOS y Android, , ya que no se necesitan para una aplicación de escritorio de Flutter. Borrar los archivos evita ejecutar por accidente la variante incorrecta durante este codelab.

En macOS y Linux:

$ rm -r android ios web

En Windows:

PS C:\src\github_client> rmdir android
PS C:\src\github_client> rmdir ios
PS C:\src\github_client> rmdir web

Para asegurarte de que todo funcione correctamente, ejecuta la aplicación estándar de Flutter como una aplicación de escritorio, de la manera que se muestra más adelante. De forma alternativa, abre este proyecto en tu IDE y usa sus herramientas para ejecutar la aplicación. Gracias al paso anterior, ejecutar la aplicación como una de escritorio debería ser la única opción 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=/

Ahora, deberías ver en la pantalla la siguiente ventana de la aplicación. Haz clic en el botón de acción flotante para asegurarte de que el incrementador funcione como se espera. También puedes probar la recarga en caliente cambiando el color del tema o modificando el comportamiento del método _incrementCounter en lib/main.dart.

A continuación, se muestra la aplicación ejecutándose en Windows.

bee40fe7a8e69791.png

En la siguiente sección, te autenticarás en GitHub con OAuth2.

4. Agrega autenticación

Autentícate en una computadora

Si usas Flutter en Android, iOS o la Web, cuentas con una variedad de opciones respecto de los paquetes de autenticación. Sin embargo, desarrollar contenido para computadoras de escritorio cambia la ecuación. En la actualidad, debes compilar una integración de autenticación desde cero, pero esto cambiará a medida que los autores de paquetes implementen Flutter para ofrecer compatibilidad con computadoras de escritorio.

Registra una aplicación de OAuth de GitHub

Para compilar una aplicación de escritorio que use las API de GitHub, primero debes autenticarte. Existen varias opciones disponibles, pero para brindar la mejor experiencia del usuario, te recomendamos que dirijas al usuario mediante el flujo de acceso de OAuth2 de GitHub en su navegador. De esa manera, se permite el control de la autenticación de dos factores y la integración sin esfuerzo de administradores de contraseñas.

A fin de registrar una aplicación para el flujo de OAuth2 de GitHub, ve a github.com y sigue las instrucciones solo del primer paso de Cómo compilar apps de OAuth de GitHub. Los siguientes pasos son importantes cuando tienes una aplicación para lanzar, pero no mientras realizas un codelab.

Para completar las instrucciones de Cómo crear una app de OAuth, en el paso 8, se te pedirá que proporciones la URL de devolución de llamada de autorización. Para una app de escritorio, introduce http://localhost/ como la URL de devolución de llamada. El flujo de OAuth2 de GitHub se configuró de tal manera que definir una URL de devolución de llamada de localhost habilita cualquier puerto y permite poner en funcionamiento un servidor web en un puerto alto, local y efímero. De esta manera, se evita solicitarle al usuario que copie el token de código de OAuth en la aplicación como parte del proceso de OAuth.

A continuación, se muestra una captura de pantalla de ejemplo de la manera en que se completa el formulario para crear una aplicación de OAuth de GitHub:

be454222e07f01d9.png

Después de registrar una app de OAuth en la interfaz de administrador de GitHub, recibirás un ID de cliente y un secreto de cliente. Si necesitas esos valores más adelante, puedes recuperarlos desde la configuración de desarrollador de GitHub. Necesitarás estas credenciales en tu aplicación para crear una URL válida de autorización de OAuth2. Podrás usar el paquete de Dart oauth2 a fin de controlar el flujo de OAuth2 y el complemento de Flutter url_launcher para habilitar el lanzamiento del navegador web del usuario.

Agrega oauth2 y url_launcher a pubspec.yaml

Para incluir dependencias de paquete en tu aplicación, ejecuta flutter pub add de la siguiente manera:

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

Este primer comando agrega el paquete http para realizar llamadas HTTP de manera coherente en un sistema multiplataforma. Luego, agrega el paquete oauth2 de la siguiente manera.

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

Por último, agrega el paquete 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!

Incluye credenciales de cliente

Agrega credenciales de cliente a un archivo nuevo, lib/github_oauth_credentials.dart, de la siguiente manera:

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

Copia y pega tus credenciales de cliente del paso anterior en este archivo.

Compila el flujo de OAuth2 para computadoras

Compila un widget que contenga el flujo de OAuth2 para computadoras de escritorio. Se aplica una lógica razonablemente complicada, ya que debes ejecutar un servidor web temporal, redireccionar al usuario a un extremo en GitHub en su navegador web, esperar a que el usuario complete el flujo de autorización en su navegador y controlar una llamada de redireccionamiento de GitHub que incluye el código (que luego debe convertirse en un token de OAuth2 con una llamada diferente a los servidores de la 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;
}

Vale la pena dedicar un poco de tiempo a avanzar con este código, ya que demuestra algunas de las capacidades de usar Flutter y Dart en computadoras de escritorio. Sí, el código es complicado, pero muchas funcionalidades se encapsulan en un widget relativamente fácil de usar.

Este widget expone un servidor web temporal y realiza solicitudes HTTP seguras. En macOS, se deben solicitar ambas capacidades mediante archivos de autorización.

Cambia las autorizaciones de cliente y servidor (solo en macOS)

Realizar solicitudes web y ejecutar un servidor web como una app de escritorio para macOS requiere cambios en las autorizaciones para la aplicación. Si necesitas más información al respecto, consulta Autorizaciones y zona de pruebas de apps.

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>

También, debes modificar las autorizaciones de lanzamiento para las compilaciones de producción.

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>

Combínalo todo

Configuraste una aplicación nueva de OAuth, se configuró el proyecto con los paquetes y los complementos necesarios, creaste un widget para encapsular el flujo de autenticación de OAuth y habilitaste la app de modo que actúe como cliente de red y servidor en macOS mediante autorizaciones. Con todos estos bloques necesarios de compilación implementados, puedes combinarlos en el archivo 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,
    );
  }
}

Cuando ejecutas esta aplicación de Flutter, inicialmente, se te muestra un botón para iniciar el flujo de acceso de OAuth de GitHub. Después de hacer clic en el botón, completa el flujo de acceso en el navegador web para verificar que accediste con la app.

Ahora que completaste la autenticación de OAuth, puedes comenzar a usar el paquete de GitHub.

5. Accede a GitHub

Conéctate a GitHub

Con el flujo de autenticación de OAuth, obtuviste el token necesario para acceder a tus datos en GitHub. Para facilitar esta tarea, usarás el paquete github, que está disponible en pub.dev.

Agrega más dependencias

Ejecuta el siguiente comando:

$ flutter pub add github

Usa las credenciales de OAuth con el paquete de GitHub

El GithubLoginWidget que creaste en el paso anterior proporciona un HttpClient que puede interactuar con la API de GitHub. En este paso, usarás las credenciales que contiene HttpClient para acceder a la API de GitHub con el paquete de GitHub, como se muestra a continuación:

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

Vuelve a combinarlo todo

Es momento de integrar el cliente de GitHub en el archivo 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();
}

Después de ejecutar esta aplicación de Flutter, se mostrará un botón que inicia el flujo de acceso de OAuth de GitHub. Después de hacer clic en el botón, completa el flujo de acceso en tu navegador web. Ya accediste a la app.

En el próximo paso, eliminarás una molestia en la base de código actual. Volverás a colocar la aplicación en primer plano después de autenticarla en el navegador web.

6. Crea un complemento de Flutter para Windows, macOS y Linux

Elimina las molestias

Actualmente, el código tiene un aspecto molesto. Después del flujo de autenticación, cuando GitHub autentica tu aplicación, quedas mirando una página del navegador web. Sería ideal que regreses automáticamente a la aplicación. Con el fin de corregir este problema, se necesita crear un complemento de Flutter para las plataformas de computadoras de escritorio.

Crea un complemento de Flutter para Windows, macOS y Linux

Para que la aplicación regrese automáticamente al frente de la pila de ventanas de la aplicación una vez que se completa el flujo de OAuth, se requiere código nativo. En macOS, la API que necesitas es el método de instancia NSApplicationactivate(ignoringOtherApps:). Para Linux, usaremos gtk_window_present y para Windows, recurriremos a Stack Overflow. Para poder llamar a estas API, debes crear un complemento de Flutter.

Puedes usar flutter para crear un proyecto nuevo de complemento.

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

Confirma que el archivo pubspec.yaml generado se vea de la siguiente manera.

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

Este complemento se configuró para macOS, Linux y Windows. Ahora, puedes agregar el código Swift que permite que la ventana aparezca delante de todo. Edita macos/Classes/WindowToFrontPlugin.swift de la siguiente manera:

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

Para hacer lo mismo en el complemento de Linux, reemplaza el contenido de linux/window_to_front_plugin.cc con lo siguiente:

../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);
}

Para hacer lo mismo en el complemento de Windows, reemplaza el contenido de windows/window_to_front_plugin.cc con lo siguiente:

..\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));
}

Agrega el código para permitir que la funcionalidad nativa que creamos anteriormente esté disponible en el mundo 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.
}

Este complemento de Flutter está completo, y puedes volver a editar el proyecto github_graphql_client.

$ cd ../github_client

Agrega dependencias

El complemento de Flutter que recién creaste es excelente, pero así solo no sirve de nada. Debes agregarlo como una dependencia en tu aplicación de Flutter para poder usarlo.

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

Observa la ruta que se especifica para la dependencia window_to_front: como se trata de un paquete local en lugar de uno publicado en pub.dev, debes especificar una ruta de acceso en lugar de un número de versión.

Vuelve a combinarlo todo otra vez

Es momento de integrar window_to_front en tu archivo lib/main.dart. Solo debemos agregar una importación y una llamada al código nativo en el momento adecuado.

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

Una vez que ejecutes esta aplicación de Flutter, aparecerá una app de apariencia similar, pero si haces clic en el botón, notarás una diferencia de comportamiento. Si colocas la app delante del navegador web con el que te estás autenticando, cuando hagas clic en el botón Acceder, tu aplicación se moverá detrás del navegador web, pero una vez que hayas completado el flujo de autenticación en el navegador, la aplicación aparecerá nuevamente delante de todo. Mucho mejor.

En la siguiente sección, compilarás sobre lo que ya tengas, a fin de crear un cliente de GitHub para computadoras que te brinde información sobre lo que tienes en GitHub. Inspeccionarás la lista de repositorios de la cuenta, las solicitudes de extracción del proyecto de Flutter y los problemas asignados.

7. Consulta repositorios, solicitudes de extracción y problemas asignados

Avanzaste bastante con la compilación de esta aplicación, pero, aun así, lo único que hace es indicar tus datos de acceso. Es probable que esperes un poco más de un cliente de GitHub para computadoras de escritorio. A continuación, agregarás la capacidad de mostrar una lista de repositorios, solicitudes de extracción y problemas asignados.

Agrega una última dependencia

Para renderizar los datos que se muestran en las consultas anteriores, usarás un paquete adicional, fluttericon, para mostrar con mayor facilidad los íconos Octicons de GitHub.

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

Usa widgets para renderizar los resultados en la pantalla

Usarás el paquete de GitHub que agregaste antes para propagar un widget NavigationRail con vistas de tus repositorios, problemas asignados y solicitudes de extracción desde el proyecto de Flutter. En la documentación del sistema de diseño de Material.io, se explica cómo los rieles de navegación brindan movimiento ergonómico entre destinos principales en aplicaciones.

Crea un archivo nuevo y complétalo con el siguiente contenido.

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

Agregaste mucho código nuevo en este paso. El aspecto positivo es que se trata de un código de Flutter bastante común, con widgets que se usan para separar la responsabilidad de diferentes inquietudes. Revisa este código durante unos minutos antes de continuar con el siguiente paso para lograr que todo funcione correctamente.

Combínalo todo por última vez

Es momento de integrar GitHubSummary en tu archivo lib/main.dart. En este caso, los cambios son bastante importantes, pero consisten, sobre todo, en eliminaciones. Reemplaza el contenido de tu archivo lib/main.dart con lo siguiente:

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

Ejecuta la aplicación. Debería aparecer algo similar a lo siguiente:

d5c9bebf448a2519.png

8. Próximos pasos

¡Felicitaciones!

Completaste el codelab y creaste una aplicación para computadoras en Flutter que accede a la API de GitHub. Utilizaste una API autenticada con OAuth y API nativas mediante un complemento que también creaste.

Para obtener más información sobre Flutter en computadoras, visita flutter.dev/desktop. Por último, para conocer una opinión totalmente diferente sobre Flutter y GitHub, consulta GitHub‑Activity‑Feed de GroovinChip.