Greg Young — винахідник CQRS вже 17 жовтня на Highload fwdays онлайн конференції
×Закрыть

Джентльменский набор инструментов для работы с 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, не дожидаясь имплементации на бэке.

Что для этого потребуется:

Практика

Создаем новый 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 указывает файл, который будет взят за основу схемы. Так как у нас нет такого файла — за основу будет взят дефолтный мок, поставляемый с сервером.

Попробуем кое-что добавить: после 20-й строки пишем 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

Спустя 10-20 секунд генерируется два файла:

  • graphql_api.dart;
  • graphql_api.g.dart.

flutter_graphql

Теперь попробуем добыть список компаний и посмотреть, как все работает в связке.

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

LinkedIn

6 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Судя из этого примера это какой-то mvc, а есть вариант с интеграцией graph ql вместе clean architecture? Где data layer будет отвечать за получение данных и выдавать это на UI.

Так «clean architecture» заставляет вас делать все на infrastructure layer, Flutter здесь никаким боком, это совсем другой слой.
В этой статье как раз рассматривается связка graphql + Flutter, без разделения на слои.

Хотелось бы более вразумительного описания указанной темы. Есть люди, которые не знают, что такое Flutter, а в статье это малопонятно рассказано

В первых 5 абзацах в избытке приведены ссылки для ознакомления с тем что такое Флаттер, чем он крут, сравнение флаттера и реакт нейтив. Описание этих аспектов выходит за рамки этой статьи.

Дякую за статтю і демо graphql-faker!
P.S. Я додав «id» філди в дефолтну схему в версії v2.0.0-rc.17

В комментариях оставляйте ссылки на свои любимые библиотеки для работы с GraphQL.

www.apollographql.com

Подписаться на комментарии