注册

GraphQL在Flutter中的基本用法

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使用为主,就不再赘述了。


作者:CameIIia
链接:https://juejin.cn/post/7110596046144667655
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册