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.
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?
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 denganenable-[os]-desktop: true
? - Apakah
flutter channel
yang berjalan mencantumkandev
ataumaster
sebagai saluran saat ini? Hal ini diperlukan karena kode tidak akan berjalan di saluranstable
ataubeta
.
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.
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:
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:
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.