Джентльменский набор инструментов для работы с 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» в телеграме — нас много, и мы рады помочь вам с решением проблем :)