Menulis aplikasi desktop Flutter

1. Pengantar

Flutter adalah toolkit UI Google untuk membuat aplikasi yang cantik dan dikompilasi secara native dari satu codebase untuk seluler, web, dan desktop. Dalam codelab ini, Anda akan mem-build aplikasi desktop Flutter yang mengakses API GitHub untuk mengambil repositori, masalah yang ditetapkan, dan permintaan pull Anda. Untuk menyelesaikan tugas ini, Anda akan membuat dan menggunakan plugin untuk berinteraksi dengan API native dan aplikasi desktop, serta menggunakan pembuatan kode untuk mem-build library klien jenis aman bagi API GitHub.

Yang akan Anda pelajari

  • Cara membuat aplikasi desktop Flutter
  • Cara mengautentikasi menggunakan OAuth2 di desktop
  • Cara menggunakan paket Dart GitHub
  • Cara membuat plugin Flutter untuk diintegrasikan dengan API native

Yang akan Anda build

Dalam codelab ini, Anda akan mem-build aplikasi desktop yang dilengkapi dengan integrasi GitHub menggunakan Flutter SDK. Aplikasi Anda akan melakukan tindakan berikut:

  • Mengautentikasi ke GitHub
  • Mengambil data dari GitHub
  • Membuat plugin Flutter untuk Windows, macOS, dan/atau Linux
  • Mengembangkan hot reload UI Flutter menjadi aplikasi desktop native

Berikut screenshot dari aplikasi desktop yang akan Anda build yang dijalankan di Windows.

a456fca6e2997992.png

Codelab ini berfokus pada penambahan kemampuan akses OAuth2 dan GitHub ke aplikasi desktop Flutter. Konsep dan blok kode yang tidak relevan akan dibahas secara sekilas dan disediakan sehingga Anda cukup menyalin dan menempelkannya.

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin melihat ringkasan yang bagus. Saya sedikit paham soal topik ini, tetapi saya perlu mengingat kembali. Saya sedang mencari kode contoh untuk digunakan dalam project saya. Saya sedang mencari penjelasan tentang hal spesifik.

2. Menyiapkan lingkungan Flutter Anda

Anda harus mengembangkan di platform tempat Anda berencana untuk men-deploy. Jadi, jika Anda ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai.

Mengembangkan untuk semua sistem operasi memerlukan dua software agar dapat menyelesaikan lab ini: Flutter SDK dan editor.

Selain itu, ada persyaratan spesifik per sistem operasi yang dibahas secara mendetail di flutter.dev/desktop.

3. Memulai

Mulai mengembangkan aplikasi desktop dengan Flutter

Anda perlu mengonfigurasi dukungan desktop dengan perubahan konfigurasi satu kali.

$ 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

Untuk memastikan bahwa Flutter untuk desktop telah diaktifkan, jalankan perintah berikut.

$ 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

Jika baris desktop yang ditampilkan di output sebelumnya tidak sesuai, pertimbangkan hal berikut:

  • Apakah platform tempat pengembangan dilakukan sudah sesuai?
  • Apakah flutter config yang berjalan mencantumkan macOS sebagai diaktifkan dengan enable-[os]-desktop: true?
  • Apakah flutter channel yang berjalan mencantumkan dev atau master sebagai saluran saat ini? Hal ini diperlukan karena kode tidak akan berjalan di saluran stable atau beta.

Cara mudah untuk mulai menulis Flutter untuk aplikasi desktop adalah dengan menggunakan alat command line Flutter untuk membuat project Flutter. Atau, IDE Anda dapat menyediakan alur kerja untuk membuat project Flutter melalui UI-nya.

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

Untuk menyederhanakan codelab ini, hapus file dukungan web, iOS, dan Android. File tersebut tidak dibutuhkan oleh Flutter untuk aplikasi desktop. Dengan menghapus file tersebut, Anda dapat membantu mencegah ketidaksengajaan dalam menjalankan varian yang salah selama codelab ini.

Untuk macOS dan Linux:

$ rm -r android ios web

Untuk Windows:

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

Untuk memastikan semuanya berfungsi, jalankan aplikasi Flutter boilerplate sebagai aplikasi desktop seperti yang ditunjukkan di bawah. Atau, buka project ini di IDE Anda, lalu gunakan alatnya untuk menjalankan aplikasi. Setelah melakukan langkah sebelumnya, satu-satunya opsi yang tersedia adalah menjalankannya sebagai aplikasi desktop.

$ 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=/

Anda sekarang akan melihat jendela aplikasi berikut di layar. Lanjutkan dan klik tombol tindakan mengambang (FAB) untuk memastikan bahwa incrementer bekerja sesuai harapan. Anda juga dapat mencoba hot reload dengan mengubah warna tema dengan atau mengubah perilaku metode _incrementCounter di lib/main.dart.

Berikut aplikasi yang dijalankan di Windows.

bee40fe7a8e69791.png

Di bagian berikutnya, Anda akan mengautentikasi di GitHub menggunakan OAuth2.

4. Menambahkan autentikasi

Autentikasi di desktop

Jika Anda menggunakan Flutter di Android, iOS, atau web, Anda memiliki banyak opsi sehubungan dengan paket autentikasi. Namun, kasusnya berbeda jika Anda mengembangkan untuk desktop. Saat ini, Anda harus mem-build integrasi autentikasi dari awal, tetapi ini akan berubah jika pembuat paket telah menerapkan Flutter untuk dukungan desktop.

Mendaftarkan aplikasi OAuth GitHub

Untuk mem-build aplikasi desktop yang menggunakan API GitHub, pertama-tama Anda perlu mengautentikasi. Ada beberapa opsi yang tersedia, tetapi pengalaman yang terbaik adalah mengarahkan pengguna melalui alur login OAuth2 GitHub di browsernya. Dengan begitu, penanganan autentikasi 2 langkah dan integrasi pengelola sandi menjadi mudah.

Untuk mendaftarkan aplikasi ke alur OAuth2 GitHub, buka github.com lalu ikuti petunjuk Membuat Aplikasi OAuth di GitHub hanya sampai langkah pertama. Langkah-langkah berikut penting jika Anda memiliki aplikasi untuk diluncurkan, bukan selama codelab.

Saat proses penyelesaian Membuat Aplikasi OAuth, Langkah 8 meminta Anda untuk memberikan URL callback Otorisasi. Untuk aplikasi desktop, masukkan http://localhost/ sebagai URL callback. Alur OAuth2 GitHub telah disiapkan sedemikian rupa sehingga saat URL callback localhost ditetapkan, port apa pun dapat digunakan dan Anda dapat mendirikan server web pada port tinggi lokal yang singkat. Dengan begitu, Anda tidak perlu meminta pengguna untuk menyalin token kode OAuth ke dalam aplikasi sebagai bagian dari proses OAuth.

Berikut contoh screenshot tentang cara mengisi formulir untuk membuat aplikasi OAuth GitHub:

be454222e07f01d9.png

Setelah mendaftarkan aplikasi OAuth di antarmuka admin GitHub, Anda akan menerima client ID dan rahasia klien. Jika Anda membutuhkan nilai ini di lain waktu, Anda dapat mengambilnya dari setelan developer GitHub. Anda memerlukan kredensial ini di aplikasi agar dapat membuat URL otorisasi OAuth2 yang valid. Anda akan menggunakan paket Dart oauth2 untuk menangani alur OAuth2, dan plugin Flutter url_launcher untuk memungkinkan peluncuran browser web pengguna.

Menambahkan oauth2 dan url_launcher ke pubspec.yaml

Anda dapat menambahkan dependensi paket untuk aplikasi Anda dengan menjalankan flutter pub add sebagai berikut:

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

Perintah pertama ini menambahkan paket http untuk membuat panggilan HTTP lintas platform yang konsisten. Selanjutnya, tambahkan paket oauth2 sebagai berikut.

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

Terakhir, tambahkan paket 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!

Menyertakan kredensial klien

Tambahkan kredensial klien ke file baru, lib/github_oauth_credentials.dart, sebagai berikut:

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

Salin dan tempel kredensial klien dari langkah sebelumnya ke dalam file ini.

Mem-build alur OAuth2 desktop

Build widget agar menampung alur OAuth2 desktop. Ini adalah bagian logika yang cukup rumit, karena Anda harus menjalankan server web sementara, mengalihkan pengguna ke endpoint GitHub di browser webnya, menunggu pengguna menyelesaikan alur otorisasi di browsernya, dan menangani panggilan pengalihan dari GitHub yang berisi kode (yang kemudian harus dikonversi menjadi token OAuth2 dengan panggilan terpisah ke server GitHub API).

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

Sebaiknya luangkan beberapa waktu untuk mengerjakan kode ini karena kode ini menunjukkan beberapa kemampuan Flutter dan Dart saat digunakan di desktop. Ya, kodenya memang rumit, tetapi banyak fungsi yang dienkapsulasi dalam widget yang relatif mudah digunakan.

Widget ini mengekspos server web sementara dan membuat permintaan HTTP yang aman. Di macOS, kedua kemampuan ini perlu diminta melalui file hak.

Mengubah hak klien dan server (khusus macOS)

Untuk membuat permintaan web dan menjalankan server web sebagai aplikasi desktop macOS, Anda harus mengubah hak atas aplikasi. Untuk info selengkapnya, buka Hak dan App Sandbox.

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>

Anda juga perlu mengubah hak Rilis untuk build produksi.

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>

Menyatukan semuanya

Anda telah mengonfigurasi aplikasi OAuth baru, project telah dikonfigurasi dengan paket dan plugin yang diperlukan, widget telah diotorisasi agar mengenkapsulasi alur autentikasi OAuth, dan aplikasi telah dimungkinkan untuk bertindak sebagai server sekaligus klien jaringan di macOS melalui hak. Jika semua elemen penyusun di atas sudah siap, Anda dapat menggabungkan semuanya dalam file 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,
    );
  }
}

Saat menjalankan aplikasi Flutter ini, awalnya Anda akan diberi tombol untuk memulai alur login OAuth GitHub. Setelah mengklik tombol tersebut, selesaikan alur login di browser web untuk melihat apakah Anda telah login ke aplikasi.

Setelah Anda memahami autentikasi OAuth, Anda dapat mulai menggunakan paket GitHub.

5. Mengakses GitHub

Menghubungkan ke GitHub

Dengan alur autentikasi OAuth, Anda telah memperoleh token yang diperlukan untuk mengakses data di GitHub. Untuk memfasilitasi tugas ini, Anda akan menggunakan paket github, yang tersedia di pub.dev.

Menambahkan dependensi lainnya

Jalankan perintah berikut:

$ flutter pub add github

Menggunakan kredensial OAuth dengan paket GitHub

GithubLoginWidget yang Anda buat pada langkah sebelumnya menyediakan HttpClient yang dapat berinteraksi dengan GitHub API. Pada langkah ini, Anda akan menggunakan kredensial yang terdapat dalam HttpClient untuk mengakses GitHub API menggunakan paket GitHub seperti yang ditunjukkan di bawah:

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

Menyatukan lagi semuanya

Kini saatnya untuk mengintegrasikan klien GitHub ke dalam file 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();
}

Setelah Anda menjalankan aplikasi Flutter ini, tombol yang memulai alur login OAuth GitHub akan ditampilkan. Setelah mengklik tombol tersebut, selesaikan alur login di browser web Anda. Anda sekarang sudah login ke aplikasi.

Di langkah berikutnya, Anda akan menghilangkan gangguan pada code base saat ini. Anda akan mengembalikan aplikasi ke latar depan setelah mengautentikasi aplikasi di browser web.

6. Membuat plugin Flutter untuk Windows, macOS, dan Linux

Menghilangkan gangguan

Saat ini, kode memiliki aspek yang mengganggu. Setelah alur autentikasi selesai, dan saat GitHub telah mengautentikasi aplikasi, Anda akan dibiarkan berada di halaman browser web. Seharusnya, Anda dikembalikan ke aplikasi secara otomatis. Untuk mengatasi hal ini, Anda harus membuat plugin Flutter untuk platform desktop.

Membuat plugin Flutter untuk Windows, macOS, dan Linux

Untuk membuat aplikasi otomatis muncul di depan stack jendela aplikasi setelah alur OAuth selesai, Anda memerlukan beberapa kode native. Untuk macOS, API yang dibutuhkan adalah metode instance activate(ignoringOtherApps:) NSApplication, untuk Linux, kita akan menggunakan gtk_window_present, dan untuk Windows menggunakan Stack Overflow. Agar dapat memanggil API tersebut, Anda perlu membuat plugin Flutter.

Anda dapat menggunakan flutter untuk membuat project plugin baru.

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

Pastikan pubspec.yaml yang dibuat terlihat seperti ini.

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

Plugin ini dikonfigurasikan untuk macOS, Linux, dan Windows. Sekarang, Anda dapat menambahkan kode Swift yang memunculkan jendela ke depan. Lakukan edit pada macos/Classes/WindowToFrontPlugin.swift, sebagai berikut:

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

Untuk melakukan ini di plugin Linux, ganti konten linux/window_to_front_plugin.cc dengan kode berikut ini:

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

Untuk melakukan ini di plugin Windows, ganti konten windows/window_to_front_plugin.cc dengan kode berikut:

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

Tambahkan kode untuk membuat fungsi native yang kita buat di atas tersedia untuk pengguna Flutter di seluruh dunia.

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

Plugin Flutter ini sudah selesai, dan Anda dapat kembali mengedit project github_graphql_client.

$ cd ../github_client

Menambahkan dependensi

Plugin Flutter yang baru saja Anda buat sudah cukup bagus, tetapi plugin ini belum bisa digunakan jika berdiri sendiri. Anda harus menambahkannya sebagai dependensi di aplikasi Flutter agar dapat digunakan.

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

Catat jalur yang ditetapkan untuk dependensi window_to_front: karena ini adalah paket lokal, bukan paket yang dipublikasikan ke pub.dev, dan yang Anda tetapkan adalah jalur, bukan nomor versi.

Menggabungkan lagi semuanya, sekali lagi

Kini saatnya mengintegrasikan window_to_front ke dalam file lib/main.dart. Kita hanya perlu menambahkan file impor dan memanggil kode native pada waktu yang tepat.

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

Setelah menjalankan aplikasi Flutter ini, Anda akan melihat aplikasi yang mirip, tetapi perilakunya ternyata berbeda saat Anda mengklik tombol. Jika Anda menempatkan aplikasi di atas browser web yang digunakan untuk mengautentikasi, saat mengklik tombol Login, aplikasi akan dipindah ke belakang browser web, dan setelah Anda menyelesaikan alur autentikasi di browser, aplikasi akan muncul lagi di latar depan. Ini jauh lebih rapi.

Di bagian berikutnya, Anda akan mem-build pada basis yang Anda miliki, untuk membuat klien GitHub desktop yang memberikan insight tentang hal yang Anda miliki di GitHub. Anda akan memeriksa daftar repositori pada akun, permintaan pull dari project Flutter, dan masalah yang ditetapkan.

7. Melihat repositori, permintaan pull, dan masalah yang ditetapkan

Anda sudah cukup jauh dalam proses mem-build aplikasi ini, tetapi tindakan yang dapat dilakukan aplikasi hanyalah memberi tahu Anda untuk login. Sedikit lagi Anda akan dapat membuat klien GitHub desktop. Selanjutnya, Anda akan menambahkan kemampuan untuk mencantumkan repositori, permintaan pull, dan masalah yang ditetapkan.

Menambahkan satu dependensi terakhir

Untuk merender data yang ditampilkan dari kueri di atas, sebaiknya gunakan paket tambahan, fluttericon, agar mudah menampilkan Octicons 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!

Widget untuk merender hasil ke layar

Anda akan menggunakan paket GitHub yang ditambahkan sebelumnya untuk mengisi widget NavigationRail dengan tampilan repositori, masalah yang ditetapkan, dan permintaan pull dari project Flutter. Dokumentasi sistem desain Material.io menjelaskan cara Kolom samping navigasi memberikan gerakan ergonomis di antara tujuan utama dalam aplikasi.

Buat file baru, lalu isi dengan konten berikut.

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

Anda telah menambahkan banyak kode baru di sini. Sisi baiknya adalah semua kode Flutter ini cukup normal, dengan widget yang digunakan untuk memisahkan tanggung jawab karena berbagai alasan. Luangkan beberapa waktu untuk meninjau kode ini sebelum melanjutkan ke langkah berikutnya untuk membuat semuanya berjalan.

Menggabungkan semuanya untuk yang terakhir kali

Kini saatnya mengintegrasikan GitHubSummary ke dalam file lib/main.dart. Perubahan kali ini cukup besar, tetapi sebagian besar perubahan tersebut berupa penghapusan. Ganti konten file lib/main.dart Anda dengan kode berikut.

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

Jalankan aplikasi, lalu Anda akan menerima pesan seperti ini:

d5c9bebf448a2519.png

8. Langkah berikutnya

Selamat!

Anda sudah menyelesaikan codelab dan mem-build aplikasi Flutter desktop yang dapat mengakses API GitHub. Anda telah menggunakan API yang diautentikasi menggunakan OAuth dan menggunakan API native melalui plugin buatan Anda juga.

Untuk mempelajari Flutter di desktop lebih lanjut, buka flutter.dev/desktop. Terakhir, untuk melihat pembahasan tentang Flutter dan GitHub dari perspektif lain, buka GitHub-Activity-Feed GroovinChip.