KEMBAR78
FlutterでGraphQLを扱う | PDF
FlutterでGraphQLを扱う
FlutterKaigi2021
2021/11/29 HironobuIga(@iganin_dev)
GraphQL
Flutter + GraphQL
自己紹介
• HironobuIga(@iganin_dev)


• Software Engineer


• iOS, Flutter, Android etc
アジェンダ
• GraphQLの概要


• FlutterのGraphQL クライアントライブラリ


• Graphqlライブラリについて


• gql_buildでのコード自動生成


• プロジェクト構成例
GraphQLの概要
RESTful APIの問題
• Under Fetching


• 同一画面内で複数のリクエストが発生


• 画面に表示したい情報より1リクエストで取得できる情報が少ない


• Over Fetching


• リクエスト内に必要のない情報が含まれてしまう


• APIから返却される情報の量があらかじめ決まっているため
GraphQLのメリット
• Under Fetchingを抑えることができる


• Over Fetchingを抑えることができる


• 型情報を活用できる


• 強力なキャッシュ機構を備えたクライアントを使用できる


• etc
GraphQLのキーワード
• Schema


• Query, Mutation


• Fragment


• Cache
Schema
• APIで使用できる型や操作の定義一覧


• APIの仕様
定義できる型
• スカラー型


• String, Int, Float, Boolean, ID


• 独自のカスタムスカラー型も定義可能


• オブジェクト型


• 列挙型(enum)


• 配列
定義できる型
• ユニオン型


• 複数の型のうち一つを返却


• インターフェース型


• インターフェース型に準拠した型が持つフィールドを定義
Schema
interface Person
{

name: String
!

}

type User implements Person
{

id: ID
!

name: String
!

}

type Article
{

id: ID
!

title: String
!

}

union Content = User |Articl
e

type Query
{

user(id: ID!): Use
r

}

type Mutation
{

changeUserName(input: ChangeUserNameInput
)

: ChangeUserNamePayload
 

}

Schemaのサンプル
Query
• データの取得操作


• REST APIにおける GET に相当
Queryの特徴 1
type User
{

id: ID
!

name: String
!

address: String
!

}

type Query
{

user(id: ID!): Use
r

}

query GetUserA($id: ID!)
{

user(id: $id)
{

i
d

nam
e

}

}

query GetUserB($id: ID!)
{

user(id: $id)
{

i
d

nam
e

addres
s

}

}
定義されている返却値から
取得するデータを選択でき
る
Queryの特徴 2
type User {…
}

type Article {…
}

Query
{

user(id: ID!): User
!

users(limit: Int, nextToken: String): [User!]
!

articles(limit: Int, nextToken: String): [Article!]
!

}

query userPage
(

$currentUserId: ID!
,

$userNextToken: String
,

$articleNextToken: Strin
g

)
{

user(id: $currentUserId) {…
}

users(limit: 50, nextToken: $userNextToken) {…
}

articles(limit: 50, nextToken: $articleNextToken) {…
}

}
複数のQueryを1回のquery
でまとめて実行することが
できる
Mutation
• データの変更操作


• REST APIにおける PUT, POST, DELETEに相当
Mutationの特徴
type User { …
}

type Mutation
{

changeUserName
(

input: ChangeUserNameInpu
t

): ChangeUserNamePayloa
d

}

input ChangeUserNameInput
{

id: ID
!

name: String
!

}

type ChangeUserNamePayload
{

id: ID
!

name: String
!

}

mutation ChangeUserName($input: ChangeUserNameInput!)
{

changeUserName(input: $input)
{

nam
e

}

}
引数はinput型として渡すこ
とが多い
Fragment
type User
{

id: ID
!

name: String
!

address: String
!

}

query SampleQueryA($userId: ID!)
{

user(id: $id)
{

i
d

nam
e

}

articles { …
}

}

query SampleQueryB($userId: ID!)
{

user(id: $id)
{

i
d

nam
e

}

news { …
}

}
複数のQuery, Mutationで同
様のデータを取得する


     


毎回書くのは手間となる
Fragment
type User
{

id: ID
!

name: String
!

address: String
!

}

Fragment UserFragment on User
{

i
d

nam
e

}

query SampleQueryA($userId: ID!)
{

user(id: $id)
{

…UserFragmen
t

}

articles { …
}

}

query SampleQueryB($userId: ID!)
{

user(id: $id)
{

…UserFragmen
t

}

news { …
}

}
Fragmentで反復して使う
データのまとまりを定義で
きる
GraphQLのリクエスト
• 同一エンドポイント


• 例: https://sampledomain.com/graphql


• POSTメソッド


• REST API クライアントでも実行可能
GraphQL


リクエスト
curl


-X POST


-H "Content-Type: application/json"


--data '{ "query": "{ country(code: "JP") { code name
} } " }'


https://countries.trevorblades.com/
{

"data":
{

"country":
{

"code": "JP"
,

"name":"Japan
"

}

}

}
REST API クライアントの
POST通信でも実行可能
引用: https://countries.trevorblades.com/
REST APIのキャッシュ
• リクエストのURLがキャッシュのKeyとして使われることが多い


• GraphQLの場合、URLのエンドポイントが全て同じ


• GraphQLリクエストのキャッシュは少し行いづらい


• またGraphQLの型情報を有効に活かすのも難しい
GraphQLクライアント
• キャッシュを有効活用するための仕組み


• 整備されたリクエストの仕組み
GraphQLクライアントのキャッシュ機構
• 型とIDによる正規化が行われている


• __typename:idをキーとした Key Value形式のCache


• {“__typename:id”: Object }
キャッシュ
Query SampleQueryA
{

users
{

Results
{

__typenam
e

i
d

nam
e

}

}

}

Query SampleQueryB
{

user(id: "1")
{

__typenam
e

I
d

nam
e

}

}
同一の型とIDのデータであ
れば、別queryの結果でも同
一のCacheとして保存され
る
GraphQLクライアントのキャッシュ機構
• typename, id による正規化


• 別リクエストで取得した結果を効率的に活用できる


• 最新の状態のCacheを取得できる
GraphQLの概要
• GraphQLの利点


• Schema,Query, Mutation, Fragment


• Cache機構
FlutterのGraphQL


クライアントライブラリ
GraphQLクライアントは何を選ぶ?


Apollo…?
Apollo


Flutterは未対応
FlutterのGraphQLクライアントライブラリ
• graphql_
fl
utter(graphql)


• artemis


• ferry
GraphQLクライアントの検討項目
• キャッシュなどのクライアント機能の豊富さ


• コードの自動生成の有無と方法


• sound null safetyのサポート状況


• メンテナンス状況


• (UIコンポーネントの対応)
graphql_
fl
utter(graphql)
graphql_
fl
utter(graphql)
• (おそらく)最も有名なFlutterのGraphQLクライアント


• インメモリ/永続化 キャッシュのサポート


• 比較的事例が豊富


• コードの自動生成は未対応


• 他のコード自動生成ライブラリと組み合わせることはできる
• モノレポで管理されている


• graphql - GraphQLクライアント


• graphql_
fl
utter - クライアント + Query, Mutation Widget


• graphql, graphql_
fl
utterどちらも sound null safery対応済み


• graphql_
fl
utterは対応済みのラベルがないが、pub outdated で確
認すると対応済みとなっている
graphql_
fl
utter(graphql)
graphql_
fl
utter(graphql)
• アクティブメンテナが不在の状況


• 📣 no active maintainer 📣


• https://github.com/zino-app/graphql-
fl
utter/issues/894


• apollo-clientにて引き継ぎ依頼されたが、引き継ぎされず


• Take over existing
fl
utter GraphQL client


• https://github.com/apollographql/apollo-client/issues/8332
artemis
artemis
• コード自動生成機能が充実


• Releaseバージョンはnull safety未対応


• 最新リリース 2021/2/18(11/19時点)


• Pre Release(Beta)はnull safety対応


• 最新リリース 2021/10/27(11/19時点)


• 開発はBetaブランチにて非常にアクティブ
artemis
• Artemis Client


• Cacheの機構がない


• 機能は非常に限定的


• 通信のみなら大丈夫だが、Cacheを活用したい場合は他のクライアン
トを併用した方が良さそう
ferry
ferry
• graphql_
fl
utterと同じく豊富なキャッシュ機能


• コード自動生成(ferry_generator)


• null safety対応済み
ferry
• graphql_
fl
utterと同様にUIで簡単に使用できるferry_
fl
utter


• ドキュメントが丁寧


• Clientからの返却値はStreamに統一


• 開発はアクティブ
graphql_
fl
utter artemis ferry
クライアント機能 ○ △ ○
コード自動生成 ない ○ ○
Null Safety ○ ○(※) ○
メンテナンス △ ○ ○
※ artemisのnull safetyサポートは Pre Release の Beta版以降であることに注意
ライブラリの選定(私見)
• クライアントライブラリとしてはferryかgraphql_
fl
utter(graphql)


• graphql_
fl
utter(graphql)を使用する場合


• コード自動生成をサポートするならartemisやgql_buildなどを併用
graphqlライブラリについて
Link
• GraphQL の通信部分の仕組み


• リクエストへの処理 - 認証、ロギングなど


• レスポンスの処理 - エラーハンドリングなど
Operation Link Link
Terminal


Link
Server
キャッシュ
• アプリが閉じたら消える In Memory Cache


• アプリが閉じても残り続ける Persistent Cache


• DBとしては Hiveが使用されていることが多い
キャッシュの正規化
• 通信結果はデフォルトでは正規化されて保存


• __typename:id をキーとする


• { “ __trypename:id”: Object }


のKey Value形式で保存されている
GraphQLクライアントの初期化
• Linkの作成


• クライアントで使用するLinkを定義


• Cacheの設定


• In Memory Cache / Persistent Cache
初期化
final httpLink = HttpLink
(

'https://api.github.com/graphql'
,

)
;

final authLink = AuthLink
(

getToken: () async =>
 

'Bearer $YOUR_PERSONAL_ACCESS_TOKEN'
,

)
;

final link = authLink.concat(httpLink)
;

final store = await HiveStore.open(path: 'path')
;

final GraphQLClient client = GraphQLClient
(

cache: GraphQLCache(store: store)
,

link: link
,

);
初期化コードサンプル
Operation
• Operationの型


• Query: QueryOptions


• Mutation: MutationOptions


• Document - 実行するQuery文


• Vars - Operationの引数


• Policy - キャッシュやエラーの扱い
Fetch Policy
• Operationをどのように行うか


• Cacheを使用するかどうか


• レスポンス結果をCacheに格納するか


• Cacheのみか通信も行うか
Operation
const readRepositories = r''
'

query ReadRepositories($nRepositories: Int!)
{

viewer
{

repositories(last: $nRepositories)
{

nodes
{

__typenam
e

i
d

nam
e

}

}

}

}

'''
;

final QueryOptions options = QueryOptions
(

document: gql(readRepositories)
,

variables: <String, dynamic>
{

'nRepositories': nRepositories
,

}
,

fetchPolicy: FetchPolicy.cacheFirst
,

)
;

final QueryResult result = await client.query(options);
documentを生成


QueryOptions生成


Clientからリクエスト
graphqlクライアント
• Link


• Cache


• Operation


• Fetch Policy
gql_buildでのコード自動生成
コード自動生成をしない場合
• QueryをStringで作成


• gqlを用いてQuery StringをASTに変換


• QueryOptions, MutationOptionsに使用
コード自動生成
しない場合
const readRepositories = r''
'

query ReadRepositories($nRepositories: Int!)
{

viewer
{

repositories(last: $nRepositories)
{

nodes
{

__typenam
e

i
d

nam
e

}

}

}

}

'''
;

final QueryOptions options = QueryOptions
(

document: gql(readRepositories)
,

variables: <String, dynamic>
{

'nRepositories': nRepositories
,

}
,

fetchPolicy: FetchPolicy.cacheFirst
,

)
;

final QueryResult result = await client.query(options)
;

String部分で補完が効かない


引数が自動補完されない


Responseをパースする型を
自分で作成する必要がある
gql_build
• graphqlファイルからコードを自動生成


• Query, Mutation, Fragment


• カスタムスカラー使用可能
gql_buildでのコード自動生成
• Schemaを配置


• graphqlファイルを作成・配置


• build.yamlファイルを作成・編集


• build_runnerでの実行
コード自動生成でのTips
• Schemaの取得


• get-graphql-schema ライブラリを使用する


• Fragment利用時の注意点


• 利用するファイルでImport文を書く必要がある


• # import ‘fragment/sample/sample.fragment.graphql’
自動生成された
コードの利用例
final getUserQuery = GGetUserReq((builder) => builde
r

..vars.id = userI
d

)
;

final queryOptions = QueryOptions
(

document: getUserQuery.operation.document
,

variables: getUserQuery.vars.toJson()
,

fetchPolicy: FetchPolicy.cacheFirst
,

)
;

final data = await gqlClient.query(queryOptions)
;

final user = GGetUserData.fromJson(data);
自動生成されたコードの利用
• queryからクラスを自動生成


• 引数に型を付与できる -> query文との相違の発生を避けられる


• レスポンスを自動生成されたクラスにパースできる
コード自動生成
• gql_buildの簡単な使い方


• コード自動生成に関するTips


• 自動生成されたコードの使用
Project構成案
Project構成


Fragment Colocation


※まだこの方法ではプロダクト開発できていません
サンプルプロジェクト


https://github.com/HironobuIga/
graphql_sample_kaigi_2021
Fragment Colocation
• UIコンポーネントに必要な情報をFragmentとして1対1で定義する


• 宣言的UI に対応した 宣言的データフェッチング
Fragment


Colocation
query TopScreenQuery($searchWord: String!)
{

items(searchWord: $searchWord)
{

…HeaderFragmen
t

items
{

…ItemFragmen
t

}

…FooterFragmen
t

}

}

fragment HeaderFragment on Result
{

searchWor
d

}

Fragment ItemFragment on Item
{

nam
e

imageUr
l

}

Fragment FooterFragment on ItemsResult
{

coun
t

totalItemCount
 

}
Fragment Colocationの例


Header, List, Footer


があるような画面
Fragment Colocationの利点
• Over Fetchingを抑制しやすい


• UIコンポーネントに必要なデータのみを取得できる


• Under Fetchingを抑制しやすい


• 1画面に必要な情報をqueryでまとめて取得できる


• UIやデータ取得の修正が行いやすい
使用するライブラリ案
• Ferry


• コードの自動生成


• レスポンスデータを自動的に自動生成したクラスへ変換


• ferry_
fl
utterによるUIでのGraphQLの簡易的な利用


• 返却値がstreamに統一されているので、Reactiveな反映が容易
Riverpodと組み合わせる
• ClientのDIに使用する


• Mock可能なようにするためにリソース取得種別ごとにレイヤーを設
ける


• Query


• Mutation
DI
Provider clientProvider = Provider<Client>((_)
{

throw Exception()
;

})
;

void main() async
{

runApp(child: CircularProgressIndicator())
;

final client = await initClient()
;

runApp
(

ProviderScope
(

overrides:
[

clientProvider.overrideWithValue(client
)

]
,

child: App()
,

)
,

)
;

}
永続化キャッシュHiveStore
の初期化は非同期処理


この場合ProviderScopeで
Overrideするのがおすすめ
DI
class ItemListScreen extends ConsumerWidget
{

@overrid
e

Widget build(BuildContext context, WidgetRef ref)
{

final client = ref.watch(clientProvider)
;

return Scaffold
(

appBar: AppBar
(

title: Text(‘all items’)
,

)
,

body: Container()
,

)
,

)
;

}
使用する際は通常と同様
Query
class SampleChangeNotifier extends ChangeNotifier
{

SampleChangeNotifier(this._read, this.id)
{

_load()
;

}

final Reader _read
;

final String id
;

late final client = _read(clientProvider)
;

AsyncValue<GSampleData> _sampleData = AsyncValue.loading()
;

AsyncValue<GSampleData> get sampleData => _sampleData
;

StreamSubscription? subscription
;

Future<void> _load() async
{

final request =GSampleDataReq
(

(builder) => builde
r

..vars.id = i
d

)
;

subscription = client.request(request).listen((event)
{

if (event.hasErrors)
{

final error = _parseError()
;

_sampleData = AsyncValue.error(error)
;

} else
{

_sampleData = AsyncValue.data(event.data!)
;

}

notifyListeners()
;

})
;

}

void reLoad()
{

_sampleData = AsyncValue.loading()
;

final request = GSampleDataReq
(

(builder) => builde
r

..vars.id = i
d

..fetchPolicy = FetchPolicy.CacheFirs
t

)
;

client.requestController.add(request)
;

}

~~~~~~
~

}
ChangeNoti
fi
erを使用


AsnycValueによってデータ
自体に通信状態を持たせる
Query UI
class ListScreen extends ConsumerWidget
{

@overrid
e

Widget build(BuildContext context, WidgetRef ref)
{

final data = ref.watch
(

sampleChangeNotifierProvide
r

).sampleData
;

 

return Scaffold
(

body: data.when
(

data: (data) => DataView(data: data)
,

loading: () => Center
(

child: CircularProgressIndicator(
)

)
,

error: (context, error) => ErrorView()
,

)

)
;

}

}
Mutation
class SampleMutationChangeNotifier extends ChangeNotifier
{

SampleMutationChangeNotifier(this._read)
;

final Reader _read
;

late final client = _read(clientProvider)
;

AsyncValue<void> _status = AsyncValue.data(null)
;

AsyncValue<void> get status => _status
;

final _requestId = "SampleMutation"
;

Future<void> doSomething({required String id}) async
{

final request = GAddStarReq((builder) => builde
r

..vars.input.id = i
d

..requestId = _requestI
d

)
;

_status = AsyncValue.loading()
;

notifyListeners()
;

final result = await client.request(request).first
;

if (result.hasErrors)
{

final error = _parseError()
;

_status = AsyncValue.error(error)
;

} else
{

_status = AsyncValue.data(null)
;

}

notifyListeners()
;

}

}
Mutationの操作もProvider
側に記述することができる


変更は自動的にUIに反映さ
れる
フォルダ構成
li
b

networ
k

clien
t

graphql_clien
t

mutatio
n

sample_mutatio
n

sample_mutation_provider.dar
t

graphq
l

sample_mutation.graphq
l

scree
n

sample_scree
n

sample_screen_query_provider.dar
t

sample_screen.dar
t

graphq
l

screen.graphq
l

vie
w

sample_vie
w

…

componen
t

sample_componen
t

…

network, mutation


screen, view


provider (アプリ内状態)
プロジェクト構成
• Fragment Colocationに従った構成にすることでGraphQLのメリット
を享受できる


• RiverpodでDIやMock可能な構成にできる
まとめ
まとめ
• GraphQLの概要


• 特徴、利点、GraphQLクライアントを使うメリット


• FlutterのGraphQLクライアントライブラリの確認・比較


• graphqlライブラリ、gql_buildでのコード自動生成


• Project構成
ご静聴ありがとうございました

FlutterでGraphQLを扱う