本文将带着大家创建一个以 GraphQL 为后端的 Flutter 项目。
项目依赖
- Flutter 开发环境 - 参考《如何安装Flutter》
- 已安装 Flutter 插件的代码编辑器 - 参考《配置编辑器》
- 一个正在运行 GraphQL 的后端 - 参考《10 分钟搭建一个 GraphQL 后端项目》
- GraphQL 后端的局域网 IP - 参考《怎样在命令行下查看局域网内IP地址》
创建项目
创建一个全新的 Flutter 项目 tinylearn_client
:
$ flutter create tinylearn_client
使用代码编辑器打开项目:
点击文件 lib/main.dart
,清理代码,删除注释,将其改为:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(),
);
}
}
运行后得到:
接入 GraphQL
安装依赖库 graphql_flutter:
pubspec.yaml:
dependencies:
flutter:
sdk: flutter
+ graphql_flutter: ^3.0.1
运行:
$ flutter pub get
安装完毕后,我们先回顾上一集 《10 分钟搭建一个 GraphQL 后端项目》 的内容:
这个页面是我们在上集中提到的 “Postman”。 图片中间部分就是服务器返回的 JSON。我们先来解析这些 JSON。
大家可以用自己喜欢的方式解析 JSON,这里我使用在线工具 quicktype 生成模型,详情可参见 《Flutter 如何使用在线转码工具将 JSON 转为 Model》。
将解析代码存放到新文件 lib/PostsData.dart
中:
// To parse this JSON data, do
//
// final postsData = postsDataFromJson(jsonString);
import 'dart:convert';
class PostsData {
final List<Post> posts;
PostsData({
this.posts,
});
factory PostsData.fromJson(String str) => PostsData.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory PostsData.fromMap(Map<String, dynamic> json) => PostsData(
posts: json["posts"] == null ? null : List<Post>.from(json["posts"].map((x) => Post.fromMap(x))),
);
Map<String, dynamic> toMap() => {
"posts": posts == null ? null : List<dynamic>.from(posts.map((x) => x.toMap())),
};
}
class Post {
final String id;
final int created;
final String content;
Post({
this.id,
this.created,
this.content,
});
factory Post.fromJson(String str) => Post.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory Post.fromMap(Map<String, dynamic> json) => Post(
id: json["id"] == null ? null : json["id"],
created: json["created"] == null ? null : json["created"],
content: json["content"] == null ? null : json["content"],
);
Map<String, dynamic> toMap() => {
"id": id == null ? null : id,
"created": created == null ? null : created,
"content": content == null ? null : content,
};
}
打开 lib/main.dart
文件,加入 GraphQL 网络请求相关代码:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:tinylearn_client/PostsData.dart';
...
final _uri = 'http://10.0.0.10:4444/'; // 这里 IP 为服务器局域网 IP 地址,端口为 4444,大家需要根据网络环境自己配置
class _MyHomePageState extends State<MyHomePage> {
Future<PostsData> _postsData;
@override
void initState() {
final client = GraphQLClient(
cache: InMemoryCache(),
link: HttpLink(uri: _uri)
);
_postsData = _createPostsData(client);
super.initState();
}
Future<PostsData> _createPostsData(GraphQLClient client) async {
final result = await client.query(QueryOptions(
documentNode: gql(r'''
query {
posts {
id
created
content
}
}
'''),
));
if (result.hasException) throw result.exception;
return PostsData.fromMap(result.data);
}
...
}
首先,配置一个 GraphQLClient
, 通过这个 client
我们可以向 GraphQL 服务器发送请求。
final _uri = 'http://10.0.0.10:4444/';
class _MyHomePageState extends State<MyHomePage> {
...
@override
void initState() {
final client = GraphQLClient(
cache: InMemoryCache(),
link: HttpLink(uri: _uri)
);
...
}
...
}
注意,这里 final _uri = 'http://10.0.0.10:4444/';
是 GraphQL 服务器的地址。我们使用上一篇文章《10 分钟搭建一个 GraphQL 后端项目》 中写好的后台,就需要将它配置成后台的局域网 IP 加端口号:http://${backendLanIP}:4444/
而 backendLanIP
可能是 192.169.0.10
或 10.0.0.10
, 这个需要自己查询。
我们定义一个方法 Future<PostsData> _createPostsData(GraphQLClient client)
,它通过 client
向 GraphQL 服务器请求 PostsData
数据:
...
class _MyHomePageState extends State<MyHomePage> {
Future<PostsData> _postsData;
@override
void initState() {
final client = ...;
_postsData = _createPostsData(client);
super.initState();
}
Future<PostsData> _createPostsData(GraphQLClient client) async {
final result = await client.query(QueryOptions(
documentNode: gql(r'''
query {
posts {
id
created
content
}
}
'''),
));
if (result.hasException) throw result.exception;
return PostsData.fromMap(result.data);
}
...
}
请求的参数来自于 “Postmain” 左侧的配置。
下一步,我们来绘制页面,页面将会消费这个请求,并将结果以列表的形式展示出来:
...
Future<PostsData> _createPostsData(GraphQLClient client) async { ... }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FutureBuilder<PostsData>(
future: _postsData,
builder: (context, snapshot) {
if (snapshot.hasData) {
final List<Post> posts = snapshot.data.posts;
return _buildListView(posts);
}
if (snapshot.hasError) {
return Text('${snapshot.error}', textAlign: TextAlign.center);
}
return CircularProgressIndicator();
},
),
),
);
}
ListView _buildListView(List<Post> posts) {
return ListView.separated(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return _postListTile(post);
},
separatorBuilder: (context, index) => Divider(),
);
}
ListTile _postListTile(Post post) {
return ListTile(
title: Text(post.content),
subtitle: Text(post.created.time.toString()),
);
}
...
最后,加入一个拓展方法将服务器返回的 int
时间戳转为 DateTime
时间类型。
...
class MyHomePage extends StatefulWidget {
...
}
extension Time on int {
DateTime get time => DateTime.fromMillisecondsSinceEpoch(this);
}
运行:
大功告成!
未完待续
这次我们打通了 GraphQL 的前后端,并演示了一些基本功能。后面,我们会先介绍 GraphQL 的设计理念,以及它的优势与劣势。然后,将这些理念投入到实战中,制作一个功能完善的前后端产品。
大家有什么想法,欢迎给我留言。
源码:
lib/main.dart:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:tinylearn_client/PostsData.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
final _uri = 'http://10.0.0.105:4444/';
class _MyHomePageState extends State<MyHomePage> {
Future<PostsData> _postsData;
@override
void initState() {
final client = GraphQLClient(
cache: InMemoryCache(),
link: HttpLink(uri: _uri)
);
_postsData = _createPostsData(client);
super.initState();
}
Future<PostsData> _createPostsData(GraphQLClient client) async {
final result = await client.query(QueryOptions(
documentNode: gql(r'''
query {
posts {
id
created
content
}
}
'''),
));
if (result.hasException) throw result.exception;
return PostsData.fromMap(result.data);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FutureBuilder<PostsData>(
future: _postsData,
builder: (context, snapshot) {
if (snapshot.hasData) {
final List<Post> posts = snapshot.data.posts;
return _buildListView(posts);
}
if (snapshot.hasError) {
return Text('${snapshot.error}', textAlign: TextAlign.center);
}
return CircularProgressIndicator();
},
),
),
);
}
ListView _buildListView(List<Post> posts) {
return ListView.separated(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return _postListTile(post);
},
separatorBuilder: (context, index) => Divider(),
);
}
ListTile _postListTile(Post post) {
return ListTile(
title: Text(post.content),
subtitle: Text(post.created.time.toString()),
);
}
}
extension Time on int {
DateTime get time => DateTime.fromMillisecondsSinceEpoch(this);
}
lib/PostData.dart:
// To parse this JSON data, do
//
// final postsData = postsDataFromJson(jsonString);
import 'dart:convert';
class PostsData {
final List<Post> posts;
PostsData({
this.posts,
});
factory PostsData.fromJson(String str) => PostsData.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory PostsData.fromMap(Map<String, dynamic> json) => PostsData(
posts: json["posts"] == null ? null : List<Post>.from(json["posts"].map((x) => Post.fromMap(x))),
);
Map<String, dynamic> toMap() => {
"posts": posts == null ? null : List<dynamic>.from(posts.map((x) => x.toMap())),
};
}
class Post {
final String id;
final int created;
final String content;
Post({
this.id,
this.created,
this.content,
});
factory Post.fromJson(String str) => Post.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory Post.fromMap(Map<String, dynamic> json) => Post(
id: json["id"] == null ? null : json["id"],
created: json["created"] == null ? null : json["created"],
content: json["content"] == null ? null : json["content"],
);
Map<String, dynamic> toMap() => {
"id": id == null ? null : id,
"created": created == null ? null : created,
"content": content == null ? null : content,
};
}