Flutter 如何接入 GraphQL

本文将带着大家创建一个以 GraphQL 为后端的 Flutter 项目。

项目依赖

创建项目

创建一个全新的 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.1010.0.0.10, 这个需要自己查询。

我们定义一个方法 Future<PostsData> _createPostsData(GraphQLClient client),它通过 clientGraphQL 服务器请求 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,
    };
}

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335