Google Sheets as a REST API in Flutter (Dart)
Fetch and display Google Sheet data in a Flutter app using SheetsAPI, the http package, typed models, and FutureBuilder.
3 min read
Flutter makes it easy to fetch JSON from a REST API and display it in a list or grid. SheetsAPI turns any Google Sheet into a typed JSON endpoint, so you can drive a Flutter app from a spreadsheet without writing any backend code.
Prerequisites
- Flutter 3.19+ / Dart 3.3+
- A Google Sheet with a header row
- A SheetsAPI account and a
YOUR_USER_KEYkey from the dashboard
1. Add the http package
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.2.1Run flutter pub get.
2. Define models
// lib/models/sheet_response.dart
class SheetMeta {
final int total;
final int limit;
final int offset;
const SheetMeta({
required this.total,
required this.limit,
required this.offset,
});
factory SheetMeta.fromJson(Map<String, dynamic> json) => SheetMeta(
total: json['total'] as int,
limit: json['limit'] as int,
offset: json['offset'] as int,
);
}
class SheetResponse<T> {
final List<T> data;
final SheetMeta meta;
const SheetResponse({required this.data, required this.meta});
}
class Product {
final String name;
final String price;
final String category;
const Product({
required this.name,
required this.price,
required this.category,
});
factory Product.fromJson(Map<String, dynamic> json) => Product(
name: json['name'] as String? ?? '',
price: json['price'] as String? ?? '',
category: json['category'] as String? ?? '',
);
}3. SheetsAPI service
// lib/services/sheets_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/sheet_response.dart';
const _base = 'https://sheetsapi.gkit.mreshank.com/api/spreadsheets';
class SheetsService {
final String userKey;
final String? apiKey;
const SheetsService({required this.userKey, this.apiKey});
Map<String, String> get _headers => {
if (apiKey != null) 'Authorization': 'Bearer $apiKey',
};
Future<SheetResponse<T>> query<T>({
required String sheet,
required T Function(Map<String, dynamic>) fromJson,
int limit = 20,
int offset = 0,
String? search,
String? sort,
String? fields,
}) async {
final params = {
'limit': '$limit',
'offset': '$offset',
if (search != null) 'search': search,
if (sort != null) 'sort': sort,
if (fields != null) 'fields': fields,
};
final uri = Uri.https(
'sheetsapi.gkit.mreshank.com',
'/api/spreadsheets/$userKey/$sheet',
params,
);
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw Exception('SheetsAPI ${response.statusCode}: ${response.body}');
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final rawData = json['data'] as List<dynamic>;
return SheetResponse<T>(
data: rawData
.map((item) => fromJson(item as Map<String, dynamic>))
.toList(),
meta: SheetMeta.fromJson(json['meta'] as Map<String, dynamic>),
);
}
}Replace YOUR_USER_KEY with your actual key. For private sheets, pass apiKey.
4. Display data with FutureBuilder
// lib/screens/products_screen.dart
import 'package:flutter/material.dart';
import '../models/sheet_response.dart';
import '../services/sheets_service.dart';
final _service = SheetsService(
userKey: 'YOUR_USER_KEY',
apiKey: 'sk_your_api_key_here',
);
class ProductsScreen extends StatefulWidget {
const ProductsScreen({super.key});
@override
State<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState extends State<ProductsScreen> {
late Future<SheetResponse<Product>> _future;
@override
void initState() {
super.initState();
_load();
}
void _load({String? search}) {
_future = _service.query<Product>(
sheet: 'Products',
fromJson: Product.fromJson,
limit: 50,
sort: 'name',
search: search,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: FutureBuilder<SheetResponse<Product>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final resp = snapshot.data!;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text('${resp.meta.total} products'),
),
Expanded(
child: ListView.builder(
itemCount: resp.data.length,
itemBuilder: (context, i) {
final p = resp.data[i];
return ListTile(
title: Text(p.name),
subtitle: Text(p.category),
trailing: Text(p.price),
);
},
),
),
],
);
},
),
);
}
}5. Add search with a TextField
class _ProductsScreenState extends State<ProductsScreen> {
late Future<SheetResponse<Product>> _future;
final _searchCtrl = TextEditingController();
@override
void initState() {
super.initState();
_load();
}
void _load({String? search}) {
setState(() {
_future = _service.query<Product>(
sheet: 'Products',
fromJson: Product.fromJson,
sort: 'name',
search: search != null && search.isNotEmpty ? 'name:$search' : null,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: 'Search products…',
border: InputBorder.none,
),
onChanged: (q) => _load(search: q),
),
),
body: FutureBuilder<SheetResponse<Product>>(
future: _future,
builder: (context, snapshot) {
// ... same as before
return const SizedBox.shrink();
},
),
);
}
}6. Pagination with a ListView
class _PaginatedProductsState extends State<PaginatedProducts> {
final List<Product> _items = [];
int _offset = 0;
bool _loading = false;
bool _done = false;
@override
void initState() {
super.initState();
_fetchNext();
}
Future<void> _fetchNext() async {
if (_loading || _done) return;
setState(() => _loading = true);
final resp = await _service.query<Product>(
sheet: 'Products',
fromJson: Product.fromJson,
limit: 30,
offset: _offset,
sort: 'name',
);
setState(() {
_items.addAll(resp.data);
_offset += 30;
_done = _offset >= resp.meta.total;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollEndNotification>(
onNotification: (n) {
if (n.metrics.extentAfter < 200) _fetchNext();
return false;
},
child: ListView.builder(
itemCount: _items.length + (_done ? 0 : 1),
itemBuilder: (context, i) {
if (i == _items.length) {
return const Center(child: CircularProgressIndicator());
}
final p = _items[i];
return ListTile(title: Text(p.name), trailing: Text(p.price));
},
),
);
}
}Query parameter reference
| Parameter | Example | Description |
|---|---|---|
limit | 50 | Rows per page (default: 20, max: 500) |
offset | 0 | Row offset for pagination |
search | category:books | Filter by field:value |
sort | price or -price | Ascending / descending |
fields | name,price | Return only named columns |
Summary
| Concern | Approach |
|---|---|
| HTTP | package:http |
| Models | Dart classes with fromJson factories |
| Display | FutureBuilder + ListView.builder |
| Search | TextField.onChanged → rebuild Future |
| Pagination | NotificationListener<ScrollEndNotification> |
SheetsAPI speaks plain JSON - no Google SDK or OAuth credentials needed in the client.