title: Flutter with GraphQL Client for FraiseQL description: Complete guide for querying FraiseQL servers from Flutter mobile applications. keywords: ["debugging", "implementation", "best-practices", "deployment", "graphql", "tutorial"] tags: ["documentation", "reference"]
Flutter with GraphQL Client for FraiseQL¶
Status: ✅ Production Ready Audience: Flutter/Dart developers Reading Time: 25-30 minutes Last Updated: 2026-02-05
Complete guide for querying FraiseQL servers from Flutter mobile applications.
Installation & Setup¶
Prerequisites¶
- Flutter 3.0+
- Dart 3.0+
- FraiseQL server running
- iOS Xcode 14+, Android API level 21+
Add Dependencies¶
# pubspec.yaml
dependencies:
flutter:
SDK: flutter
graphql: ^5.0.0
graphql_flutter: ^6.0.0
hive: ^2.2.0
hive_flutter: ^1.1.0
get_it: ^7.5.0
riverpod: ^2.3.0
flutter_riverpod: ^2.3.0
dev_dependencies:
flutter_test:
SDK: flutter
build_runner: ^2.4.0
Run flutter pub get
Initialize GraphQL & Hive¶
// main.dart
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
await Hive.initFlutter();
// Initialize GraphQL
await initHiveForFlutter();
final HttpLink httpLink = HttpLink(
'http://localhost:8000/graphql', // FraiseQL endpoint
);
final WebSocketLink wsLink = WebSocketLink(
'ws://localhost:8000/graphql',
config: const SocketClientConfig(
autoReconnect: true,
inactivityTimeout: Duration(seconds: 30),
),
);
Link link = Link.from([
// Split between WebSocket (subscriptions) and HTTP (queries/mutations)
split(
(request) {
final operationName = request.operation.operationName;
return operationName == 'subscribe';
},
wsLink,
httpLink,
),
]);
final ValueNotifier<GraphQLClient> client = ValueNotifier(
GraphQLClient(
cache: GraphQLCache(store: HiveStore()),
link: link,
),
);
runApp(MyApp(client: client));
}
class MyApp extends StatelessWidget {
final ValueNotifier<GraphQLClient> client;
const MyApp({required this.client});
@override
Widget build(BuildContext context) {
return GraphQLProvider(
client: client,
child: CacheProvider(
child: MaterialApp(
title: 'FraiseQL App',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
),
),
);
}
}
Queries¶
Basic Query¶
import 'package:graphql_flutter/graphql_flutter.dart';
const String GET_USERS = r'''
query GetUsers {
users {
id
name
email
}
}
''';
class UserList extends StatelessWidget {
const UserList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(GET_USERS),
fetchPolicy: FetchPolicy.cacheAndNetwork,
),
builder: (QueryResult result, {fetchMore, refetch}) {
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (result.hasException) {
return Center(
child: Text('Error: ${result.exception.toString()}'),
);
}
final List users = result.data?['users'] ?? [];
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user['name']),
subtitle: Text(user['email']),
);
},
);
},
);
}
}
Query with Variables¶
const String GET_USER_BY_ID = r'''
query GetUserById($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
''';
class UserDetail extends StatelessWidget {
final String userId;
const UserDetail({required this.userId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(GET_USER_BY_ID),
variables: {'id': userId},
fetchPolicy: FetchPolicy.networkOnly,
),
builder: (QueryResult result, {fetchMore, refetch}) {
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final user = result.data?['user'];
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user['name'], style: Theme.of(context).textTheme.headlineMedium),
Text(user['email']),
const SizedBox(height: 16),
Text('Posts', style: Theme.of(context).textTheme.titleMedium),
...List<Widget>.from(
(user['posts'] as List).map(
(post) => ListTile(
title: Text(post['title']),
),
),
),
],
),
);
},
);
}
}
Pagination Pattern¶
const String GET_POSTS_PAGINATED = r'''
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
createdAt
}
postsCount
}
''';
class PostListPaginated extends StatefulWidget {
const PostListPaginated({Key? key}) : super(key: key);
@override
State<PostListPaginated> createState() => _PostListPaginatedState();
}
class _PostListPaginatedState extends State<PostListPaginated> {
int page = 0;
final pageSize = 10;
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(GET_POSTS_PAGINATED),
variables: {
'limit': pageSize,
'offset': page * pageSize,
},
),
builder: (QueryResult result, {fetchMore, refetch}) {
if (result.isLoading && page == 0) {
return const Center(child: CircularProgressIndicator());
}
final posts = result.data?['posts'] as List? ?? [];
final total = result.data?['postsCount'] as int? ?? 0;
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post['title']),
subtitle: Text(post['createdAt']),
);
},
),
),
if (posts.length < total)
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
setState(() => page++);
},
child: const Text('Load More'),
),
),
],
);
},
);
}
}
Mutations¶
Basic Mutation¶
const String CREATE_POST = r'''
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
}
}
''';
class CreatePostForm extends StatefulWidget {
const CreatePostForm({Key? key}) : super(key: key);
@override
State<CreatePostForm> createState() => _CreatePostFormState();
}
class _CreatePostFormState extends State<CreatePostForm> {
final _titleController = TextEditingController();
final _contentController = TextEditingController();
@override
Widget build(BuildContext context) {
return Mutation(
options: MutationOptions(
document: gql(CREATE_POST),
),
builder: (runMutation, result) {
return SingleChildScrollView(
child: Column(
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: _contentController,
decoration: const InputDecoration(labelText: 'Content'),
minLines: 5,
maxLines: null,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: (result?.isLoading ?? false)
? null
: () {
runMutation({
'title': _titleController.text,
'content': _contentController.text,
});
},
child: Text(
(result?.isLoading ?? false) ? 'Creating...' : 'Create Post',
),
),
if (result?.hasException ?? false)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'Error: ${result!.exception.toString()}',
style: const TextStyle(color: Colors.red),
),
),
],
),
);
},
);
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
}
Update Cache After Mutation¶
const String GET_POSTS = r'''
query GetPosts {
posts { id title }
}
''';
const String ADD_POST = r'''
mutation AddPost($title: String!) {
addPost(title: $title) { id title }
}
''';
class PostListWithAdd extends StatelessWidget {
const PostListWithAdd({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(GET_POSTS),
),
builder: (queryResult, {fetchMore, refetch}) {
return Mutation(
options: MutationOptions(
document: gql(ADD_POST),
update: (cache, result) {
// Update cache with new post
final previousData = cache.readQuery(QueryOptions(
document: gql(GET_POSTS),
));
if (previousData != null) {
final newPost = result?.data?['addPost'];
previousData['posts']?.add(newPost);
cache.writeQuery(
QueryOptions(document: gql(GET_POSTS)),
previousData,
);
}
},
),
builder: (runMutation, mutationResult) {
return Column(
children: [
ElevatedButton(
onPressed: () => runMutation({'title': 'New Post'}),
child: const Text('Add Post'),
),
Expanded(
child: ListView.builder(
itemCount: queryResult.data?['posts']?.length ?? 0,
itemBuilder: (context, index) {
final post =
queryResult.data?['posts']?[index];
return ListTile(title: Text(post['title']));
},
),
),
],
);
},
);
},
);
}
}
Subscriptions¶
Real-Time Subscriptions¶
const String ON_POST_CREATED = r'''
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
''';
class PostFeed extends StatelessWidget {
const PostFeed({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Subscription(
options: SubscriptionOptions(
document: gql(ON_POST_CREATED),
),
builder: (result) {
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (result.hasException) {
return Center(
child: Text('Error: ${result.exception.toString()}'),
);
}
final post = result.data?['postCreated'];
if (post == null) {
return const Center(child: Text('Waiting for posts...'));
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(post['title'],
style: Theme.of(context).textTheme.headlineSmall),
Text('by ${post['author']['name']}'),
],
),
),
);
},
);
}
}
Error Handling¶
Handle Different Error Types¶
class SafeQueryWidget extends StatelessWidget {
final String query;
const SafeQueryWidget({required this.query, Key? key}) : super(key: key);
String _getErrorMessage(Exception? exception) {
if (exception == null) return 'Unknown error';
if (exception.toString().contains('SocketException')) {
return 'Connection error. Check your network.';
}
if (exception.toString().contains('Unauthorized')) {
return 'Authentication failed. Please log in again.';
}
if (exception.toString().contains('Forbidden')) {
return 'You do not have permission to access this.';
}
return exception.toString();
}
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(query),
errorPolicy: ErrorPolicy.all,
),
builder: (result, {fetchMore, refetch}) {
if (result.hasException) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_getErrorMessage(result.exception)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: refetch,
child: const Text('Retry'),
),
],
),
);
}
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return const Center(child: Text('Success'));
},
);
}
}
State Management with Riverpod¶
Query Provider¶
import 'package:flutter_riverpod/flutter_riverpod.dart';
final graphqlClientProvider = Provider((ref) {
final httpLink = HttpLink('http://localhost:8000/graphql');
return GraphQLClient(
cache: GraphQLCache(),
link: httpLink,
);
});
final usersProvider = FutureProvider((ref) async {
final client = ref.watch(graphqlClientProvider);
const query = r'''
query GetUsers {
users { id name email }
}
''';
final result = await client.query(
QueryOptions(document: gql(query)),
);
return result.data?['users'] as List<dynamic>;
});
class UserListWithRiverpod extends ConsumerWidget {
const UserListWithRiverpod({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text('Error: $error')),
data: (users) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index]['name']),
);
},
);
},
);
}
}
Local Caching with Hive¶
Setup Hive¶
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
await Hive.initFlutter();
// Register adapters for custom objects
Hive.registerAdapter(UserAdapter());
Hive.registerAdapter(PostAdapter());
runApp(const MyApp());
}
// Define models
@HiveType(typeId: 0)
class User extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
String name;
@HiveField(2)
String email;
User({required this.id, required this.name, required this.email});
}
@HiveType(typeId: 1)
class Post extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
String title;
Post({required this.id, required this.title});
}
Cache GraphQL Results¶
class CachedUserList extends StatefulWidget {
const CachedUserList({Key? key}) : super(key: key);
@override
State<CachedUserList> createState() => _CachedUserListState();
}
class _CachedUserListState extends State<CachedUserList> {
@override
void initState() {
super.initState();
_loadFromCache();
}
Future<void> _loadFromCache() async {
final userBox = Hive.box<User>('users');
// Users are already in memory from box
}
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(GET_USERS),
fetchPolicy: FetchPolicy.cacheAndNetwork,
),
builder: (result, {fetchMore, refetch}) {
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final users = result.data?['users'] as List? ?? [];
// Save to Hive cache
final userBox = Hive.box<User>('users');
for (var user in users) {
userBox.put(
user['id'],
User(
id: user['id'],
name: user['name'],
email: user['email'],
),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(title: Text(user['name']));
},
);
},
);
}
}
Testing¶
Mock GraphQL Responses¶
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('UserList', () {
testWidgets('displays users', (WidgetTester tester) async {
final mockLink = MockLink(
requests: [
MockedResponse(
request: QueryOptions(document: gql(GET_USERS)),
result: QueryResult(
data: {
'users': [
{'id': '1', 'name': 'Alice', 'email': 'alice@example.com'},
{'id': '2', 'name': 'Bob', 'email': 'bob@example.com'},
]
},
),
),
],
);
final client = GraphQLClient(
cache: GraphQLCache(),
link: mockLink,
);
await tester.pumpWidget(
GraphQLProvider(
client: ValueNotifier(client),
child: MaterialApp(
home: Scaffold(
body: UserList(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
expect(find.text('Bob'), findsOneWidget);
});
});
}
App Store / Play Store Deployment¶
Build iOS App¶
# Build iOS release
flutter build ios --release
# Upload to App Store (requires Apple Developer account)
# Use Xcode or fastlane
Build Android App¶
# Build Android release APK
flutter build apk --release
# Build Android App Bundle for Play Store
flutter build appbundle --release
# Upload to Play Store (requires Google Play Developer account)
# Use Google Play Console or fastlane
See Also¶
Related Guides:
- React Native Guide - Mobile web alternative
- Real-Time Patterns - Subscription architecture
- Authentication & Authorization - Securing queries
Flutter & GraphQL Documentation: