Menulis aplikasi desktop Flutter

Flutter adalah toolkit UI Google untuk mem-build aplikasi yang cantik dan dikompilasi secara native untuk seluler, web, dan desktop dari satu codebase. 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 GraphQL dari Flutter dengan pembuatan kode
  • 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 GraphQL API menggunakan Flutter SDK. Aplikasi Anda akan melakukan tindakan berikut:

  • Mengautentikasi ke GitHub
  • Mengambil data dari GitHub API v4
  • 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.

775e773e58e53e85.png

Codelab ini berfokus untuk menambahkan kemampuan GraphQL ke aplikasi desktop Flutter. Blok kode dan konsep yang tidak relevan tidak akan dibahas secara mendalam, serta telah disediakan sebelumnya agar Anda cukup menyalin dan menempelnya.

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin melihat ringkasan yang bagus. Saya tahu beberapa hal tentang topik ini, tetapi saya ingin mengingat-ingat kembali. Saya sedang mencari kode contoh untuk project saya. Saya sedang mencari penjelasan tentang hal spesifik.

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.

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_graphql_client
Creating project github_graphql_client...
[Eliding listing of created files]
Wrote 127 files.

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

  $ cd github_graphql_client
  $ flutter run

To enable null safety, type:

  $ cd github_graphql_client
  $ dart migrate --apply-changes

Your application code is in github_graphql_client/lib/main.dart.

Aktifkan keamanan null dengan memigrasikan project seperti berikut di macOS dan Linux:

$ cd github_graphql_client
$ dart migrate --apply-changes

Demikian pula di Windows:

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

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_graphql_client> rmdir android
PS C:\src\github_graphql_client> rmdir ios
PS C:\src\github_graphql_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 macOS in debug mode...
Building macOS application...
Activating Dart DevTools...                                         4.2s
Syncing files to device macOS...                                    55ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:56370/WURs0AbCsEY=/

Flutter DevTools, a Flutter debugger and profiler, on macOS is available at:
http://127.0.0.1:56397?uri=http%3A%2F%2F127.0.0.1%3A56370%2FWURs0AbCsEY%3D%2F

💪 Running with sound null safety 💪

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.

ea232028115f24c.png

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

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 menambahkan entri ke file pubspec.yaml, sebagai berikut:

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.1        # Add this line,
  oauth2: ^2.0.0       # and this line,
  url_launcher: ^6.0.2 # and this one too.

dev_dependencies:
  flutter_test:
    sdk: flutter
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

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

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

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

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

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

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 GitHub GraphQL API.

Pengantar GraphQL

Menurut graphql.org, GraphQL memberikan deskripsi data API yang lengkap dan mudah dipahami, serta memberi klien kemampuan untuk meminta hal sesuai kebutuhan saja. Hal ini sangat membantu para developer karena mereka dapat mengajukan kueri API terfokus yang mengisi bagian UI tertentu.

GitHub v4 API didefinisikan dalam hal GraphQL, sehingga dapat menjadi tempat berlatih yang baik untuk menjelajahi GraphQL dengan data nyata. GitHub menyediakan GitHub GraphQL Explorer dengan teknologi GraphQL, yang memberi Anda cara untuk mengajukan kueri GraphQL terhadap GitHub GraphQL API. Untuk informasi selengkapnya tentang cara menggunakan GitHub GraphQL Explorer, buka Menggunakan Explorer di GitHub.

Dalam codelab ini, Anda akan menggunakan paket gql untuk membuat kode marshalling jenis aman untuk kueri yang di-build di Explorer.

Menambahkan dependensi lainnya

Untuk menggunakan pembuatan kode guna mem-build library klien GraphQL, Anda memerlukan build_runner dan paket gql dalam jumlah besar. Mulailah dengan menambahkan dependensi ini ke file pubspec.yaml Anda:

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  gql: ^0.13.0-0          # Add from here
  gql_exec: ^0.3.0-0      #
  gql_link: ^0.4.0-0      #
  gql_http_link: ^0.4.0-0 # to here.
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0   # Add this line,
  gql_build: ^0.2.0-0     # and this one.
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

Mengambil GitHub GraphQL Schema

GitHub memublikasikan skema yang mendeskripsikan API-nya. Anda meng-cache skema ini agar alat pembuatan kode dapat menggunakannya untuk membuat library klien jenis aman untuk kueri Anda.

Untuk macOS dan Linux:

$ mkdir -p lib/third_party/github_graphql_schema
$ curl -o ./lib/third_party/github_graphql_schema/schema.docs.graphql \
  https://docs.github.com/public/schema.docs.graphql

Untuk Windows:

PS C:\src\github_graphql_client> mkdir .\lib\third_party\github_graphql_schema\
PS C:\src\github_graphql_client> curl -o .\lib\third_party\github_graphql_schema\schema.docs.graphql https://docs.github.com/public/schema.docs.graphql

Dua perintah sebelumnya membuat schema.docs.graphql yang digunakan pipeline pembuatan kode untuk memeriksa jenis kueri Anda dan membuat library klien jenis aman. Anda juga memerlukan kueri. Mulailah dengan kueri default yang digunakan oleh GitHub GraphQL Explorer untuk memulai, dengan satu perubahan kecil. Anda harus memberi nama pada kueri agar generator kode dapat membuat library klien jenis aman Anda.

lib/src/github_gql/github_queries.graphql

query ViewerDetail {
  viewer {
    login
  }
}

Mengonfigurasi build_runner

Untuk mengonfigurasi build_runner, Anda perlu menambahkan aturan ke build.yaml. Dalam hal ini, Anda mengonfigurasi cara kode dibuat oleh paket gql dari GitHub GraphQL Schema dan kueri yang Anda buat di Explorer.

build.yaml

targets:
  $default:
    builders:
      gql_build|ast_builder:
        enabled: true
      gql_build|req_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|serializer_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|schema_builder:
        enabled: true
      gql_build|data_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql
      gql_build|var_builder:
        enabled: true
        options:
          schema: github_graphql_client|lib/third_party/github_graphql_schema/schema.docs.graphql

Paket build_runner cukup kompleks, sehingga tidak cukup untuk dibahas di sini. Untuk melihat pembahasan detailnya, tonton Pembuatan kode dengan sistem build Dart oleh Kevin Moore di YouTube.

Setelah semua komponennya sudah lengkap, Anda dapat menjalankan build_runner untuk membuat library klien GraphQL.

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

Melalui lib/third_party/github_graphql_schema/ dan lib/src/github_gql/,, Anda akan melihat bahwa Anda sekarang memiliki banyak kode yang baru dibuat.

Menggabungkan lagi semuanya

Sekarang waktunya untuk mengintegrasikan semua komponen GraphQL ke dalam file lib/main.dart.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'github_oauth_credentials.dart';
import 'src/github_gql/github_queries.data.gql.dart';
import 'src/github_gql/github_queries.req.gql.dart';
import 'src/github_login.dart';

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

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        final link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: httpClient,
        );
        return FutureBuilder<GViewerDetailData_viewer>(
          future: viewerDetail(link),
          builder: (context, snapshot) {
            return Scaffold(
              appBar: AppBar(
                title: Text(title),
              ),
              body: Center(
                child: Text(
                  snapshot.hasData
                      ? 'Hello ${snapshot.data!.login}!'
                      : 'Retrieving viewer login details...',
                ),
              ),
            );
          },
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Future<GViewerDetailData_viewer> viewerDetail(Link link) async {
  final req = GViewerDetail((b) => b);
  final result = await link
      .request(Request(
        operation: req.operation,
        variables: req.vars.toJson(),
      ))
      .first;
  final errors = result.errors;
  if (errors != null && errors.isNotEmpty) {
    throw QueryException(errors);
  }
  return GViewerDetailData.fromJson(result.data!)!.viewer;
}

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

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.

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

Sekarang, migrasikan plugin Anda ke keamanan null, lalu lepaskan aplikasi contoh. Untuk macOS atau Linux:

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

Demikian pula untuk Windows:

PS C:\src> cd window_to_front
PS C:\src\window_to_front> dart migrate --apply-changes
PS C:\src\window_to_front> rmdir example

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

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  gql: ^0.13.0-0
  gql_exec: ^0.3.0-0
  gql_link: ^0.4.0-0
  gql_http_link: ^0.4.0-0
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2
  window_to_front:             # Add this dependency, from here
    path: '../window_to_front' # to here.

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0
  gql_build: ^0.2.0-0
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

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:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:window_to_front/window_to_front.dart'; // Add this,
import 'github_oauth_credentials.dart';
import 'src/github_gql/github_queries.data.gql.dart';
import 'src/github_gql/github_queries.req.gql.dart';
import 'src/github_login.dart';

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

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, httpClient) {
        WindowToFront.activate();                      // and this.
        final link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: httpClient,
        );
        return FutureBuilder<GViewerDetailData_viewer>(
          future: viewerDetail(link),
          builder: (context, snapshot) {
            return Scaffold(
              appBar: AppBar(
                title: Text(title),
              ),
              body: Center(
                child: Text(
                  snapshot.hasData
                      ? 'Hello ${snapshot.data!.login}!'
                      : 'Retrieving viewer login details...',
                ),
              ),
            );
          },
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Future<GViewerDetailData_viewer> viewerDetail(Link link) async {
  final req = GViewerDetail((b) => b);
  final result = await link
      .request(Request(
        operation: req.operation,
        variables: req.vars.toJson(),
      ))
      .first;
  final errors = result.errors;
  if (errors != null && errors.isNotEmpty) {
    throw QueryException(errors);
  }
  return GViewerDetailData.fromJson(result.data!)!.viewer;
}

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

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 yang diotorisasi, 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.

Mengkueri repositori, permintaan pull, dan masalah menggunakan GraphQL

Agar dapat menampilkan informasi dari GitHub, Anda perlu mengambil informasi tersebut. Jadi, tambahkan kueri GraphQL berikut ke file gabungan:

lib/src/github_gql/github_queries.graphql

query ViewerDetail {
  viewer {
    login
  }
}

// Add everything below here.

query PullRequests($count: Int!) {
  viewer {
    pullRequests(
      first: $count
      orderBy: { field: CREATED_AT, direction: DESC }
    ) {
      edges {
        node {
          repository {
            nameWithOwner
            url
          }
          author {
            login
            url
          }
          number
          url
          title
          updatedAt
          url
          state
          isDraft
          comments {
            totalCount
          }
          files {
            totalCount
          }
        }
      }
    }
  }
}

query AssignedIssues($query: String!, $count: Int!) {
  search(query: $query, type: ISSUE, first: $count) {
    edges {
      node {
        ... on Issue {
          __typename
          repository {
            nameWithOwner
            url
          }
          number
          url
          title
          author {
            login
            url
          }
          labels(last: 10) {
            nodes {
              name
              color
            }
          }
          comments {
            totalCount
          }
        }
      }
    }
  }
}

query Repositories($count: Int!) {
  viewer {
    repositories(
      first: $count
      orderBy: { field: UPDATED_AT, direction: DESC }
    ) {
      nodes {
        name
        description
        isFork
        isPrivate
        stargazers {
          totalCount
        }
        url
        issues {
          totalCount
        }
        owner {
          login
          avatarUrl
        }
      }
    }
  }
}

Untuk membuat ulang library klien GraphQL, jalankan perintah berikut:

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

Menambahkan satu dependensi terakhir

Untuk merender data yang ditampilkan dari kueri di atas, sebaiknya gunakan paket tambahan, fluttericon, agar mudah menampilkan Octicons GitHub.

pubspec.yaml

name: github_graphql_client
description: Github client using Github API V4 (GraphQL)
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  fluttericon: ^2.0.0    # Add this dependency
  gql: ^0.13.0-0
  gql_exec: ^0.3.0-0
  gql_link: ^0.4.0-0
  gql_http_link: ^0.4.0-0
  http: ^0.13.1
  oauth2: ^2.0.0
  url_launcher: ^6.0.2
  window_to_front:
    path: '../window_to_front'

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0
  gql_build: ^0.2.0-0
  pedantic: ^1.11.0

flutter:
  uses-material-design: true

Widget untuk merender hasil ke layar

Anda akan menggunakan kueri GraphQL yang Anda buat di atas untuk mengisi widget NavigationRail dengan tampilan repositori, masalah yang ditetapkan, dan permintaan pull. 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:gql_exec/gql_exec.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:gql_link/gql_link.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'github_gql/github_queries.data.gql.dart';
import 'github_gql/github_queries.req.gql.dart';

class GitHubSummary extends StatefulWidget {
  GitHubSummary({required http.Client client})
      : _link = HttpLink(
          'https://api.github.com/graphql',
          httpClient: client,
        );
  final HttpLink _link;
  @override
  _GitHubSummaryState createState() => _GitHubSummaryState();
}

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

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

class RepositoriesList extends StatefulWidget {
  const RepositoriesList({required this.link});
  final Link link;
  @override
  _RepositoriesListState createState() => _RepositoriesListState(link: link);
}

class _RepositoriesListState extends State<RepositoriesList> {
  _RepositoriesListState({required Link link}) {
    _repositories = _retreiveRespositories(link);
  }
  late Future<List<GRepositoriesData_viewer_repositories_nodes>> _repositories;

  Future<List<GRepositoriesData_viewer_repositories_nodes>>
      _retreiveRespositories(Link link) async {
    final req = GRepositories((b) => b..vars.count = 100);
    final result = await link
        .request(Request(
          operation: req.operation,
          variables: req.vars.toJson(),
        ))
        .first;
    final errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GRepositoriesData.fromJson(result.data!)!
        .viewer
        .repositories
        .nodes!
        .asList();
  }

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

class AssignedIssuesList extends StatefulWidget {
  const AssignedIssuesList({required this.link});
  final Link link;
  @override
  _AssignedIssuesListState createState() =>
      _AssignedIssuesListState(link: link);
}

class _AssignedIssuesListState extends State<AssignedIssuesList> {
  _AssignedIssuesListState({required Link link}) {
    _assignedIssues = _retrieveAssignedIssues(link);
  }

  late Future<List<GAssignedIssuesData_search_edges_node__asIssue>>
      _assignedIssues;

  Future<List<GAssignedIssuesData_search_edges_node__asIssue>>
      _retrieveAssignedIssues(Link link) async {
    final viewerReq = GViewerDetail((b) => b);
    var result = await link
        .request(Request(
          operation: viewerReq.operation,
          variables: viewerReq.vars.toJson(),
        ))
        .first;
    var errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    final _viewer = GViewerDetailData.fromJson(result.data!)!.viewer;

    final issuesReq = GAssignedIssues((b) => b
      ..vars.count = 100
      ..vars.query = 'is:open assignee:${_viewer.login} archived:false');

    result = await link
        .request(Request(
          operation: issuesReq.operation,
          variables: issuesReq.vars.toJson(),
        ))
        .first;
    errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GAssignedIssuesData.fromJson(result.data!)!
        .search
        .edges!
        .map((e) => e.node)
        .whereType<GAssignedIssuesData_search_edges_node__asIssue>()
        .toList();
  }

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

class PullRequestsList extends StatefulWidget {
  const PullRequestsList({required this.link});
  final Link link;
  @override
  _PullRequestsListState createState() => _PullRequestsListState(link: link);
}

class _PullRequestsListState extends State<PullRequestsList> {
  _PullRequestsListState({required Link link}) {
    _pullRequests = _retrievePullRequests(link);
  }
  late Future<List<GPullRequestsData_viewer_pullRequests_edges_node>>
      _pullRequests;

  Future<List<GPullRequestsData_viewer_pullRequests_edges_node>>
      _retrievePullRequests(Link link) async {
    final req = GPullRequests((b) => b..vars.count = 100);
    final result = await link
        .request(Request(
          operation: req.operation,
          variables: req.vars.toJson(),
        ))
        .first;
    final errors = result.errors;
    if (errors != null && errors.isNotEmpty) {
      throw QueryException(errors);
    }
    return GPullRequestsData.fromJson(result.data!)!
        .viewer
        .pullRequests
        .edges!
        .map((e) => e.node)
        .whereType<GPullRequestsData_viewer_pullRequests_edges_node>()
        .toList();
  }

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

class QueryException implements Exception {
  QueryException(this.errors);
  List<GraphQLError> errors;
  @override
  String toString() {
    return 'Query Exception: ${errors.map((err) => '$err').join(',')}';
  }
}

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

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:window_to_front/window_to_front.dart';
import 'github_oauth_credentials.dart';
import 'src/github_login.dart';
import 'src/github_summary.dart';

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

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

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

  @override
  Widget build(BuildContext context) {
    return GithubLoginWidget(
      builder: (context, client) {
        WindowToFront.activate();
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: GitHubSummary(client: client),
        );
      },
      githubClientId: githubClientId,
      githubClientSecret: githubClientSecret,
      githubScopes: githubScopes,
    );
  }
}

Jalankan aplikasi, lalu Anda akan menerima pesan seperti ini:

775e773e58e53e85.png

Selamat!

Anda telah menyelesaikan codelab dan mem-build aplikasi Flutter desktop yang mengakses GitHub GraphQL API. Anda telah menggunakan API yang diautentikasi menggunakan OAuth, membuat library klien jenis aman, dan menggunakan API native melalui plugin buatan Anda juga.

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