Connecting Google Sheets to Your Mobile App: A Complete Guide
Learn how to use Google Sheets as a backend for your iOS and Android apps with SheetsJSON, including offline caching and real-time updates.
Building a mobile app but dreading the backend setup? What if you could use Google Sheets as your data source and have it work seamlessly with iOS and Android? In this guide, we’ll show you exactly how to do that.
Why Use Sheets as a Mobile Backend?
For many apps, especially in early stages, a full backend is overkill:
- Content apps – News, blogs, event listings
- Product catalogs – Inventory, menus, directories
- Reference apps – Guides, tutorials, FAQs
- Internal tools – Company directories, schedules
Google Sheets + SheetsJSON gives you:
- ✅ Instant API without server setup
- ✅ Non-technical team members can update content
- ✅ Real-time sync when data changes
- ✅ Zero DevOps maintenance
Architecture Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Google Sheet │────▶│ SheetsJSON │────▶│ Mobile App │
│ (Your Data) │ │ (API Layer) │ │ (iOS/Android) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- You manage data in Google Sheets
- SheetsJSON provides a REST API
- Your mobile app fetches JSON data
Setting Up Your Sheet
First, create a well-structured spreadsheet. Let’s build a restaurant menu:
| id | name | description | price | category | image_url | available | spicy_level |
|---|---|---|---|---|---|---|---|
| 1 | Margherita Pizza | Classic tomato and mozzarella | 12.99 | pizza | https://… | true | 0 |
| 2 | Spicy Arrabbiata | Penne with spicy tomato sauce | 14.99 | pasta | https://… | true | 2 |
| 3 | Caesar Salad | Romaine, croutons, parmesan | 9.99 | salads | https://… | true | 0 |
Best Practices for Mobile
- Keep payloads small – Mobile data is precious
- Use image URLs – Don’t embed base64 images
- Include IDs – Essential for caching and updates
-
Boolean flags –
available,featured, etc. - Categories – Enable filtering in the app
Connecting to SheetsJSON
- Sign up at SheetsJSON
- Connect your Google account
- Select your menu spreadsheet
- Copy your API endpoint and key
Your endpoint will look like:
https://api.sheetsjson.com/api/sheets/your-account/menu
iOS Implementation (Swift)
Basic Network Layer
import Foundation
class MenuAPI {
private let baseURL = "https://api.sheetsjson.com/api/sheets/your-account/menu"
private let apiKey = "your-api-key"
func fetchMenu() async throws -> [MenuItem] {
var request = URLRequest(url: URL(string: baseURL)!)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw MenuError.fetchFailed
}
let result = try JSONDecoder().decode(MenuResponse.self, from: data)
return result.data
}
}
struct MenuResponse: Codable {
let data: [MenuItem]
}
struct MenuItem: Codable, Identifiable {
let id: Int
let name: String
let description: String
let price: Double
let category: String
let imageUrl: String
let available: Bool
let spicyLevel: Int
enum CodingKeys: String, CodingKey {
case id, name, description, price, category, available
case imageUrl = "image_url"
case spicyLevel = "spicy_level"
}
}
SwiftUI View
import SwiftUI
struct MenuView: View {
@State private var menuItems: [MenuItem] = []
@State private var isLoading = true
@State private var selectedCategory: String? = nil
private let api = MenuAPI()
var filteredItems: [MenuItem] {
guard let category = selectedCategory else { return menuItems }
return menuItems.filter { $0.category == category }
}
var categories: [String] {
Array(Set(menuItems.map { $0.category })).sorted()
}
var body: some View {
NavigationView {
VStack {
// Category filter
ScrollView(.horizontal, showsIndicators: false) {
HStack {
FilterChip(title: "All", isSelected: selectedCategory == nil) {
selectedCategory = nil
}
ForEach(categories, id: \.self) { category in
FilterChip(title: category.capitalized,
isSelected: selectedCategory == category) {
selectedCategory = category
}
}
}
.padding(.horizontal)
}
// Menu list
if isLoading {
ProgressView()
} else {
List(filteredItems) { item in
MenuItemRow(item: item)
}
}
}
.navigationTitle("Menu")
.task {
await loadMenu()
}
.refreshable {
await loadMenu()
}
}
}
func loadMenu() async {
do {
menuItems = try await api.fetchMenu()
isLoading = false
} catch {
print("Error loading menu: \(error)")
}
}
}
Offline Caching with UserDefaults
class CachedMenuAPI {
private let api = MenuAPI()
private let cacheKey = "cached_menu"
private let cacheTimestampKey = "cached_menu_timestamp"
private let cacheValiditySeconds: TimeInterval = 300 // 5 minutes
func fetchMenu() async throws -> [MenuItem] {
// Try cache first
if let cached = getCachedMenu(), isCacheValid() {
return cached
}
// Fetch fresh data
do {
let items = try await api.fetchMenu()
cacheMenu(items)
return items
} catch {
// Return stale cache if network fails
if let cached = getCachedMenu() {
return cached
}
throw error
}
}
private func getCachedMenu() -> [MenuItem]? {
guard let data = UserDefaults.standard.data(forKey: cacheKey) else { return nil }
return try? JSONDecoder().decode([MenuItem].self, from: data)
}
private func cacheMenu(_ items: [MenuItem]) {
if let data = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(data, forKey: cacheKey)
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: cacheTimestampKey)
}
}
private func isCacheValid() -> Bool {
let timestamp = UserDefaults.standard.double(forKey: cacheTimestampKey)
return Date().timeIntervalSince1970 - timestamp < cacheValiditySeconds
}
}
Android Implementation (Kotlin)
Data Classes
data class MenuResponse(
val data: List<MenuItem>
)
data class MenuItem(
val id: Int,
val name: String,
val description: String,
val price: Double,
val category: String,
@SerializedName("image_url") val imageUrl: String,
val available: Boolean,
@SerializedName("spicy_level") val spicyLevel: Int
)
Retrofit Service
interface MenuService {
@GET("api/sheets/your-account/menu")
suspend fun getMenu(
@Header("Authorization") auth: String = "Bearer your-api-key"
): MenuResponse
}
object RetrofitClient {
private const val BASE_URL = "https://api.sheetsjson.com/"
val menuService: MenuService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MenuService::class.java)
}
}
ViewModel with Caching
class MenuViewModel(application: Application) : AndroidViewModel(application) {
private val _menuItems = MutableLiveData<List<MenuItem>>()
val menuItems: LiveData<List<MenuItem>> = _menuItems
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val prefs = application.getSharedPreferences("menu_cache", Context.MODE_PRIVATE)
private val gson = Gson()
fun loadMenu() {
viewModelScope.launch {
_isLoading.value = true
// Try cache first
getCachedMenu()?.let { cached ->
_menuItems.value = cached
}
// Fetch fresh data
try {
val response = RetrofitClient.menuService.getMenu()
_menuItems.value = response.data
cacheMenu(response.data)
} catch (e: Exception) {
Log.e("MenuViewModel", "Error fetching menu", e)
}
_isLoading.value = false
}
}
private fun getCachedMenu(): List<MenuItem>? {
val json = prefs.getString("menu_data", null) ?: return null
return try {
gson.fromJson(json, Array<MenuItem>::class.java).toList()
} catch (e: Exception) {
null
}
}
private fun cacheMenu(items: List<MenuItem>) {
prefs.edit()
.putString("menu_data", gson.toJson(items))
.putLong("cache_time", System.currentTimeMillis())
.apply()
}
}
Jetpack Compose UI
@Composable
fun MenuScreen(viewModel: MenuViewModel = viewModel()) {
val menuItems by viewModel.menuItems.observeAsState(emptyList())
val isLoading by viewModel.isLoading.observeAsState(false)
var selectedCategory by remember { mutableStateOf<String?>(null) }
val categories = menuItems.map { it.category }.distinct().sorted()
val filteredItems = selectedCategory?.let { cat ->
menuItems.filter { it.category == cat }
} ?: menuItems
LaunchedEffect(Unit) {
viewModel.loadMenu()
}
Column {
// Category chips
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
FilterChip(
selected = selectedCategory == null,
onClick = { selectedCategory = null },
label = { Text("All") }
)
}
items(categories) { category ->
FilterChip(
selected = selectedCategory == category,
onClick = { selectedCategory = category },
label = { Text(category.capitalize()) }
)
}
}
// Menu list
if (isLoading && menuItems.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn {
items(filteredItems) { item ->
MenuItemCard(item)
}
}
}
}
}
React Native Implementation
import React, { useState, useEffect } from 'react';
import { View, FlatList, RefreshControl, AsyncStorage } from 'react-native';
const API_URL = 'https://api.sheetsjson.com/api/sheets/your-account/menu';
const API_KEY = 'your-api-key';
const CACHE_KEY = 'menu_cache';
export default function MenuScreen() {
const [menuItems, setMenuItems] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadMenu();
}, []);
const loadMenu = async () => {
// Load from cache first
try {
const cached = await AsyncStorage.getItem(CACHE_KEY);
if (cached) {
setMenuItems(JSON.parse(cached));
setLoading(false);
}
} catch (e) {}
// Fetch fresh data
try {
const response = await fetch(API_URL, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const { data } = await response.json();
setMenuItems(data);
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(data));
} catch (error) {
console.error('Error fetching menu:', error);
}
setLoading(false);
setRefreshing(false);
};
return (
<FlatList
data={menuItems}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <MenuItemCard item={item} />}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={() => {
setRefreshing(true);
loadMenu();
}} />
}
/>
);
}
Best Practices for Production
1. Implement Proper Caching
- Cache responses locally for offline access
- Use ETags or timestamps to check for updates
- Show cached data immediately, then refresh
2. Handle Errors Gracefully
enum NetworkState<T> {
case loading
case success(T)
case error(Error)
case offline(cached: T?)
}
3. Optimize Images
- Use image URLs in your sheet
- Implement lazy loading in your app
- Consider using a CDN for images
4. Secure Your API Key
- Don’t hardcode keys in your app
- Use environment variables or secure storage
- Consider a lightweight backend proxy for sensitive apps
5. Monitor Usage
- Track API calls in Sheets JSON dashboard
- Set up alerts for rate limits
- Plan for scaling if needed
Conclusion
Using Google Sheets as a mobile backend is a powerful pattern for many apps. It dramatically reduces development time, empowers non-technical team members to update content, and eliminates server maintenance.
Get started with SheetsJSON and have your mobile app connected to a spreadsheet backend in minutes.
Questions? Check out our documentation or reach out to support. Happy building! 📱
Ready to transform your sheets?
Turn your Google Sheets into powerful JSON APIs in seconds.
Get Started Free