GraphQL在Flutter中的基本用法

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

GraphQL是一个用于API的查询语言,它可以使客户端准确地获得所需的数据,没有任何冗余。在Flutter项目中怎么使用Graphql呢?我们需要借助graphql-flutter插件

Tip: 这里以4.0.1为例

1. ​添加依赖

首先添加到pubspec.yaml

image.png

然后我们再看看graphql-flutter(API)有什么,以及我们该怎么用。

2.重要API

GraphQLClient

  • 仿apollo-client,通过配置LinkCache构造客户端实例
  • 像apollo-client一样通过构造不同Link来丰富client实例的功能

image.png

client实例方法几乎跟apollo-client一致,如querymutatesubscribe,也有些许差别的方法watchQuerywatchMutation 等,后面具体介绍使用区别

Link

graphql-flutter里基于Link实现了一些比较使用的类,如下

HttpLink

  • 设置请求地址,默认header等

image.png

AuthLink

  • 通过函数的形式设置Authentication

image.png

ErrorLink

  • 设置错误拦截

image.png

DedupeLink

  • 请求去重

GraphQLCache

  • 配置实体缓存,官方推荐使用 HiveStore 配置持久缓存

image.png

  • HiveStore在项目中关于环境是Web还是App需要作判断,所以我们需要一个方法

image.png

综上各个Link以及Cache构成了Client,我们稍加对这些API做一个封装,以便在项目复用。

3.基本封装

  • 代码及释义如下
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:graphql/client.dart';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:path_provider/path_provider.dart'
    show getApplicationDocumentsDirectory;
import 'package:path/path.dart' show join;
import 'package:hive/hive.dart' show Hive;

class Gql {
  final String source;
  final String uri;
  final String token;
  final Map<String, String> header;

  HttpLink httpLink;
  AuthLink authLink;
  ErrorLink errorLink;

  GraphQLCache cache;
  GraphQLClient client;

  String authHeaderKey = 'token';
  String sourceKey = 'source';

  Gql({
    @required this.source,
    @required this.uri,
    this.token,
    this.header = const {},
  }) {
    // 设置url,复写传入header
    httpLink = HttpLink(uri, defaultHeaders: {
      sourceKey: source,
      ...header,
    });
    // 通过复写getToken动态设置auth
    authLink = AuthLink(getToken: getToken, headerKey: authHeaderKey);
    // 错误拦截
    errorLink = ErrorLink(
      onGraphQLError: onGraphQLError,
      onException: onException,
    );
    // 设置缓存
    cache = GraphQLCache(store: HiveStore());

    client = GraphQLClient(
      link: Link.from([
        DedupeLink(), // 请求去重
        errorLink,
        authLink,
        httpLink,
      ]),
      cache: cache,
    );
  }

  static Future<void> initHiveForFlutter({
    String subDir,
    Iterable<String> boxes = const [HiveStore.defaultBoxName],
  }) async {
    if (!kIsWeb) { // 判断App获取path,初始化
      var appDir = await getApplicationDocumentsDirectory(); // 获取文件夹路径
      var path = appDir.path;
      if (subDir != null) {
        path = join(path, subDir);
      }
      Hive.init(path);
    }

    for (var box in boxes) {
      await Hive.openBox(box);
    }
  }

  FutureOr<String> getToken() async => null;

  void _errorsLoger(List<GraphQLError> errors) {
    errors.forEach((error) {
      print(error.message);
    });
  }

  // LinkError处理函数
  Stream<Response> onException(
      Request req,
      Stream<Response> Function(Request) _,
      LinkException exception,
      ) {
    if (exception is ServerException) {  // 服务端错误
      _errorsLoger(exception.parsedResponse.errors);
    }

    if (exception is NetworkException) { // 网络错误
      print(exception.toString());
    }

    if (exception is HttpLinkParserException) { // http解析错误
      print(exception.originalException);
      print(exception.response);
    }

    return _(req);
  }

  // GraphqlError
  Stream<Response> onGraphQLError(
      Request req,
      Stream<Response> Function(Request) _,
      Response res,
      ) {
    // print(res.errors);
    _errorsLoger(res.errors); // 处理返回错误
    return _(req);
  }
}
复制代码

4. 基本使用

  • main.dart
void main() async {

  await Gql.initHiveForFlutter(); // 初始化HiveBox

  runApp(App());
}
复制代码
  • clent.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';


const codeMessage = {
  401: '登录失效,',
  403: '用户已禁用',
  500: '服务器错误',
  503: '服务器错误',
};

// 通过复写,实现错误处理与token设置
class CustomGgl extends Gql {
  CustomGgl({
    @required String source,
    @required String uri,
    String token,
    Map<String, String> header = const {},
  }) : super(source: source, uri: uri, token: token, header: header);

  String authHeaderKey = 'token';

  @override
  FutureOr<String> getToken() async {  // 设置token
    final sharedPref = await SharedPreferences.getInstance();
    return sharedPref.getString(authHeaderKey);
  }

  @override
  Stream<Response> onGraphQLError( // 错误处理并给出提示
      Request req,
      Stream<Response> Function(Request) _,
      Response res,
      ) {
    res.errors.forEach((error) {
      final num code = error.extensions['exception']['status'];
      Toast.error(message: codeMessage[code] ?? error.message);
      print(error);
    });
    return _(req);
  }
}

// 创建ccClient
final Gql ccGql = CustomGgl(
  source: 'cc',
  uri: 'https://xxx/graphql',
  header: {
    'header': 'xxxx',
  },
);
复制代码
  • demo.dart
import 'package:flutter/material.dart';

import '../utils/client.dart';
import '../utils/json_view/json_view.dart';
import '../models/live_bill_config.dart';
import '../gql_operation/gql_operation.dart';

class GraphqlDemo extends StatefulWidget {
  GraphqlDemo({Key key}) : super(key: key);

  @override
  _GraphqlDemoState createState() => _GraphqlDemoState();
}

class _GraphqlDemoState extends State<GraphqlDemo> {
  ObservableQuery observableQuery;
  ObservableQuery observableMutation;

  Map<String, dynamic> json;
  num pageNum = 1;
  num pageSize = 10;

  @override
  void initState() {
    super.initState();

    Future.delayed(Duration(), () {
      initObservableQuery();
      initObservableMutation();
    });
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Wrap(
            spacing: 10.0,
            runSpacing: 10.0,
            children: [
              RaisedButton(
                onPressed: getLiveBillConfig,
                child: Text('Basic Query'),
              ),
              RaisedButton(
                onPressed: sendPhoneAuthCode,
                child: Text('Basic Mutation'),
              ),
              RaisedButton(
                onPressed: () {
                  pageNum++;

                  observableQuery.fetchMore(FetchMoreOptions(
                    variables: {
                      'pageNum': pageNum,
                      'pageSize': pageSize,
                    },
                    updateQuery: (prev, newData) => newData,
                  ));
                },
                child: Text('Watch Query'),
              ),
              RaisedButton(
                onPressed: () {
                  observableMutation.fetchResults();
                },
                child: Text('Watch Mutation'),
              ),
            ],
          ),
          Divider(),
          if (json != null)
            SingleChildScrollView(
              child: JsonView.map(json),
              scrollDirection: Axis.horizontal,
            ),
        ],
      ),
    );
  }


  @override
  dispose() {
    super.dispose();

    observableQuery.close();
  }

  void getLiveBillConfig() async {
    Toast.loading();

    try {
      final QueryResult result = await ccGql.client.query(QueryOptions(
        document: gql(LIVE_BILL_CONFIG),
        fetchPolicy: FetchPolicy.noCache,
      ));

      final liveBillConfig =
      result.data != null ? result.data['liveBillConfig'] : null;
      if (liveBillConfig == null) return;

      setState(() {
        json = LiveBillConfig.fromJson(liveBillConfig).toJson();
      });
    } finally {
      if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
  }


  void sendPhoneAuthCode() async {
    Toast.loading();

    try {
      final QueryResult result = await ccGql.client.mutate(MutationOptions(
        document: gql(SEND_PHONE_AUTH_CODE),
        fetchPolicy: FetchPolicy.cacheAndNetwork,
        variables: {
          'phone': '15883300888',
          'authType': 2,
          'platformName': 'Backend'
        },
      ));

      setState(() {
        json = result.data;
      });
    } finally {
      if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
  }

  void initObservableQuery() {
    observableQuery = ccGql.client.watchQuery(
      WatchQueryOptions(
        document: gql(GET_EMPLOYEE_CONFIG),
        variables: {
          'pageNum': pageNum,
          'pageSize': pageSize,
        },
      ),
    );

    observableQuery.stream.listen((QueryResult result) {
      if (!result.isLoading && result.data != null) {
        if (result.isLoading) {
          Toast.loading();
          return;
        }

        if (Toast.loadingType == ToastType.loading) Toast.dismiss();
        setState(() {
          json = result.data;
        });
      }
    });
  }

  void initObservableMutation() {
    observableMutation = ccGql.client.watchMutation(
      WatchQueryOptions(
        document: gql(LOGIN_BY_AUTH_CODE),
        variables: {
          'phone': '15883300888',
          'authCodeType': 2,
          'authCode': '5483',
          'statisticInfo': {'platformName': 'Backend'},
        },
      ),
    );

    observableMutation.stream.listen((QueryResult result) {
      if (!result.isLoading && result.data != null) {
        if (result.isLoading) {
          Toast.loading();
          return;
        }

        if (Toast.loadingType == ToastType.loading) Toast.dismiss();
        setState(() {
          json = result.data;
        });
      }
    });
  }
}
复制代码

总结

这篇文章介绍了如何在Flutter项目中简单快速的使用GraphQL。并实现了一个简单的Demo。但是上面demo将UI和数据绑定在一起,导致代码耦合性很高。在实际的公司项目中,我们都会将数据和UI进行分离,常用的做法就是将GraphQL的 ValueNotifier client 调用封装到VM层中,然后在Widget中把VM数据进行绑定操作。网络上已经有大量介绍Provider|Bloc|GetX的文章,这里以介绍GraphQL使用为主,就不再赘述了。

猜你喜欢

转载自juejin.im/post/7110596046144667655