Back to Blog
7 min read SheetsJSON Team

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)  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
  1. You manage data in Google Sheets
  2. SheetsJSON provides a REST API
  3. 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

  1. Keep payloads small – Mobile data is precious
  2. Use image URLs – Don’t embed base64 images
  3. Include IDs – Essential for caching and updates
  4. Boolean flagsavailable, featured, etc.
  5. Categories – Enable filtering in the app

Connecting to SheetsJSON

  1. Sign up at SheetsJSON
  2. Connect your Google account
  3. Select your menu spreadsheet
  4. 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