Джентльменский набор инструментов для работы с Flutter и GraphQL
Приветствую! Последнее время я активно занимаюсь исследованием Flutter на предмет пригодности для продакшена с учетом наших потребностей. Одна из них — использование GraphQL для получения данных. Для этого я параллельно с разработчиками на ReactNative пишу то же приложение на Flutter (кстати, по ссылке — моя статья на DOU, посвященная сравнению Flutter и ReactNative). Я уже вдоволь наигрался и версткой, и анимацией, так что настало время переходить к разбору полетов в области GraphQL.
Вступление
Около 6 месяцев назад, когда я только начинал эксперименты с GraphQL, в экосистеме Flutter все было не так радужно. Основному плагину для работы с GraphQL был год отроду, а пакет artemis для генерации типов был в условном релизе всего пару месяцев. Документация разрознена, целостного решения нет, в общем — печаль.
На днях я снова вернулся к этой теме, и у меня получилось собрать довольно неплохой набор инструментов для работы с GraphQL, который удовлетворяет все мои желания, чем спешу с вами поделиться.
Я буду рассматривать инструменты на примере Android Studio, что также подходит для всего семейства продуктов от JetBrains.
Начнем сначала: если вы не знакомы с Flutter и/или GraphQL (я не знаю, зачем вы открыли эту статью), начните с небольшой вводной:
Увидимся после прочтения :)
Подготовка
Что бы хотелось получить:
- генерацию типов для GraphQL API;
- полноценную работу автокомплита;
- обновление схемы для генерации типов в один клик;
- автодополнение и валидацию GraphQL запросов;
- возможность работать с API, не дожидаясь имплементации на бэке.
Что для этого потребуется:
- Android Studio (или любой другой продукт JetBrains) — взять их можно тут и тут соответственно;
- установить Flutter;
- настроить IDE;
- установить в IDE плагин JS Graphql;
- установить NodeJS (сомневаюсь, что у кого-нибудь ее не будет).
Практика
Создаем новый Flutter проект
File -> New -> New Flutter Project
В открывшемся окне оставляем выбранный пункт по умолчанию — Flutter Application.
Жмем Next до самого конца (ничего не меняя в настройках), и в конце — Finish.
Более красочно процесс описан в документации в разделе Test Drive.
В итоге у нас получится демо приложение с любимым каунтером.
Устанавливаем graphql_faker
npm install -g graphql-faker
или
yarn global add graphql-faker
Запускаем graphql-faker ./fake.schema --open
Запустится сервер, автоматически откроется страница в браузере для редактирования API.
Параметр ./fake.schema
указывает файл, который будет взят за основу схемы. Так как у нас нет такого файла — за основу будет взят дефолтный мок, поставляемый с сервером.
Попробуем кое-что добавить: после id: ID! @fake(type: uuid)
и нажмем кнопку Save, в папке с проектом появится файл fake.schema
с внесенными вами изменениями. Его будет удобно в последующем использовать для старта сервера, закоммитить для шеринга между командой или как вам заблагорассудится.
graphql_faker
будет полезен, если вы хотите быстро запустить проект, поиграть с цветами со структурой схемы, не дожидаясь имплементации бэкенда, запилить интерфейс с большинством работающих фич. Также он позволяет заэкстендить существующую схему. Вариантов применения достаточно много — смотрим документацию за подробностями.
Приятный бонус — проект поддерживает наш земляк Иван Гончаров. Сейчас активно готовится версия v2.0.0, нелишним будет поддержать коллегу пиаром или детальным баг-репортом.
Плагин JSGraphQL
Создаем файл .graphqlconfig
в корне проекта. Заполняем копи/пастой
{ "name": "Schema", "schemaPath": "my.schema.json", "extensions": { "endpoints": { "Default": { "url": "http://localhost:9002/graphql", "introspect": true } } } }
Сохраняем изменения, переходим на вкладку GraphQL, дважды щелкаем на Default, выбираем Get graphql schema from endpoint:
Будет создан файл my.schema.json
, содержащий свежую схему вашего АПИ.
Плагин будет удобен для контроля над ошибками и помощи в автозаполнении при написании запросов. Это легко проверить: создаем папку graphql
в корне проект, в ней — файл employee_data.graphql
, пробуем набрать несложный запрос:
query EmployeeData($id: ID!) { employee(id: $id) { firstName id } }
...и радуемся работе автозаполнения.
Возвращаемся в редактор graphql_faker
, меняем у employee firstName на firstName1, жмем Save, обновляем локальную схему и радуемся отображению ошибки.
Flutter-часть
Нам понадобится два плагина: flutter_graphql
для работы непосредственно с GraphQL и artemis
для генерации типов.
Добавляем в pubspeck.yaml следующие строки:
dependencies: … graphql_flutter: ^3.0.0-beta.3 path_provider: ^1.5.1 equatable: ^1.0.2 json_serializable: ^3.2.3 gql: 0.12.0 dev_dependencies: ... build_runner: ^1.7.2 artemis: ^2.1.4
Выполняем в консоли flutter packages get.
Artemis
Начнем с настройки artemis (подробнее о настройках — здесь).
Создаем файл build.yaml
в корне проекта со следующим содержимым:
targets: $default: sources: - lib/** - graphql/** - my.schema.json builders: artemis: options: schema_mapping: - schema: my.schema.json queries_glob: graphql/*.graphql output: lib/graphql_api.dart
Запускаем генерацию типов
pub run build_runner build
или
flutter pub run build_runner build
Спустя
graphql_api.dart
;graphql_api.g.dart
.
flutter_graphql
Теперь попробуем добыть список компаний и посмотреть, как все работает в связке.
Возвращаемся в редактор graphql_faker
и после id
для компании id: ID! @fake(type: uuid)
— это нам понадобится для корректной работы кеша. Сохраняем. Обновляем локальную схему.
Создаем новый файл graphql/companies_data.graphql
.
targets: $default: sources: - lib/** - graphql/** - my.schema.json builders: artemis: options: schema_mapping: - schema: my.schema.json queries_glob: graphql/*.graphql output: lib/graphql_api.dart
Генерируем типы flutter pub run build_runner build
.
Создаем файл lib/graphql_provider.dart
.
import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:flutter/material.dart'; String uuidFromObject(Object object) { if (object is Map<String, Object>) { final String typeName = object['__typename'] as String; final String id = object['id'].toString(); if (typeName != null && id != null) { return <String>[typeName, id].join('/'); } } return null; } final OptimisticCache cache = OptimisticCache( dataIdFromObject: uuidFromObject, ); ValueNotifier<GraphQLClient> clientFor({ @required String uri, String subscriptionUri, }) { Link link = HttpLink(uri: uri); if (subscriptionUri != null) { final WebSocketLink websocketLink = WebSocketLink( url: subscriptionUri, config: SocketClientConfig( autoReconnect: true, inactivityTimeout: Duration(seconds: 30), ), ); link = link.concat(websocketLink); } return ValueNotifier<GraphQLClient>( GraphQLClient( cache: cache, link: link, ), ); } /// Wraps the root application with the `graphql_flutter` client. /// We use the cache for all state management. class GraphqlProvider extends StatelessWidget { GraphqlProvider({ @required this.child, @required String uri, String subscriptionUri, }) : client = clientFor( uri: uri, subscriptionUri: subscriptionUri, ); final Widget child; final ValueNotifier<GraphQLClient> client; @override Widget build(BuildContext context) { return GraphQLProvider( client: client, child: child, ); } }
В файле main.dart
добавляем функцию host
, которая в зависимости от платформы подправит адрес localhost’a.
String get host { if (Platform.isAndroid) { return '10.0.2.2'; } else { return 'localhost'; } }
А MaterialApp (в том же файле main.dart
) виджет оборачиваем в:
GraphqlProvider( uri: 'http://$host:9002/graphql', child: MaterialApp(...), )
Параметр body
меняем полностью на:
Query( options: QueryOptions( documentNode: CompaniesDataQuery().document, ), builder: ( QueryResult result, { Future<QueryResult> Function() refetch, FetchMore fetchMore, }) { if (result.hasException) { return Text(result.exception.toString()); } if (result.loading) { return const Center( child: CircularProgressIndicator(), ); } final allCompanies = CompaniesData.fromJson(result.data).allCompanies; return ListView.builder( itemBuilder: (_, index) { return ListTile( leading: Icon(Icons.card_travel), title: Text(allCompanies[index].name), subtitle: Text(allCompanies[index].industry), ); }, itemCount: allCompanies.length, ); }, )
Перезапустите приложение, и у вас получится что-то наподобие:
Немного подробностей реализации:
Виджет Query предоставляет нам возможность отправить запрос.
В параметр documentNode
мы передаем непосредственно сам запрос, сгенерированный artemis
.
options: QueryOptions( documentNode: CompaniesDataQuery().document, ),
После того как flutter_graphql
перешел в версии 3 на documentNode
формат, а artemis
добавил генерацию кверей, эти две системы стали замечательно работать вместе.
В функции builder
три стандартных части:
Вывод ошибки
if (result.hasException) { return Text(result.exception.toString()); }
Вывод спиннера
if (result.loading) { return const Center( child: CircularProgressIndicator(), ); }
Вывод содержимого результата
final allCompanies = CompaniesData.fromJson(result.data).allCompanies; return ListView.builder( itemBuilder: (_, index) { return ListTile( leading: Icon(Icons.card_travel), title: Text(allCompanies[index].name), subtitle: Text(allCompanies[index].industry), ); }, itemCount: allCompanies.length, ); },
Итог
После окончания экспериментов, легших в основу этой статьи, я убедился, что работа с GraphQL стала намного удобнее спустя всего полгода. В результате мы получаем достаточно стабильный, автоматизированный и bullet proof набор инструментов, который поможет работать с API с еще большим удовольствием.
Весь код по ссылке.
Поддержите проекты, отмеченные в этой статье, звездочкой, баг-репортом или пулл-реквестом.
В комментариях оставляйте ссылки на свои любимые библиотеки для работы с GraphQL.
А также присоединяйтесь к нашей группе «Art Flutter» в телеграме — нас много, и мы рады помочь вам с решением проблем :)