Persist Data with Local Storage
Learn to persist data with local storage in this mobile app development tutorial. Understand the core concepts, apply hands-on exercises, and compare options to choose the right approach.
Focus: persist data with local storage
You’ve just built a beautiful mobile app with Python — screens, navigation, maybe even a fancy custom widget. But the moment you close the app and reopen it, everything resets. That’s the pain of not persisting data: every launch is a blank slate. In this lesson, you’ll learn how to persist data with local storage, so your app remembers user settings, scores, notes, or any small piece of state — without needing a server. By the end, you’ll be able to save and load data on your device using kivy, json, and platform-specific file paths, and you’ll know which storage option fits your use case.
The problem this lesson solves
Mobile apps are inherently stateless on launch — the operating system reclaims memory when your app closes, and your in-memory Python objects (lists, dicts, instances) vanish. If you’re building a to-do list, a fitness tracker, or a note-taking app, losing all user data every time is a dealbreaker.
Consider these real scenarios:
- Fitness tracker: user logs their morning run, closes the app, and opens it later — their stats must still be there.
- Grocery list: user adds items while at the store, then checks them off later — the items must survive app restarts.
- Settings screen: user toggles dark mode — that preference should persist across sessions.
The core problem is simple: your app needs a way to write data to the device’s filesystem or database and read it back when the app starts again. This lesson focuses on local storage — the simplest, most accessible form of persistence for mobile apps written in Python with frameworks like Kivy or BeeWare.
Core concept / mental model
Think of local storage as a notebook your app keeps on the device. Every time you need to remember something, you write it down on a dedicated page. When you need that information again, you open the notebook and read the page.
The “notebook” here is a file (usually a plain-text file like JSON or CSV) or a lightweight database like SQLite. For simple data, a JSON file is perfect because JSON maps naturally to Python dictionaries and lists.
Here’s the mental model in three steps:
- Serialize: turn your Python objects into a string (JSON, for example) that can be saved to a file.
- Write: save that string to a location the OS allows you to write to (your app’s data directory).
- Deserialize: read the file on app startup and convert the string back into Python objects.
The beauty of local storage is that it’s offline, fast, and private — no network requests, no latency, and your data stays on the device. For most small-scale app needs, it’s the ideal starting point.
How it works step by step
Let’s break down the process of persisting data with local storage into concrete steps.
Step 1: Find the right file path
On Android, iOS, and desktop, you can’t just write to any directory. You need to use a platform-appropriate location. In Python with Kivy, you can use the kivy.utils.platform and app.user_data_dir from a Kivy App instance to get a directory that is guaranteed writable.
- On Android:
/data/data/<package>/files(viauser_data_dir) - On iOS:
<app>/Documents - On macOS/Linux/Windows:
~/.local/share/<app>or%APPDATA%
Step 2: Serialize your data to JSON
Use Python’s built-in json module. Convert your data dictionary to a string with json.dumps(). Make sure your data is JSON-serializable (dicts, lists, strings, ints, floats, booleans, None).
Step 3: Write to file
Open the file in write mode ('w') and write the JSON string. Always use with open() to ensure the file is closed properly.
Step 4: Read and deserialize on startup
When your app starts (e.g., in the build() method of a Kivy app), read the file if it exists, parse the JSON with json.loads(), and populate your app’s state.
Step 5: Update data and save periodically
Whenever your data changes, call your save function again. This keeps the file up-to-date. For maximum reliability, save after every critical mutation.
Hands-on walkthrough
Let’s build a simple counter app that persists the count. This example uses Kivy’s App object and user_data_dir.
Example 1: Save and load data in a Kivy app
import json
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.utils import platform
from os.path import join
class CounterApp(App):
def build(self):
self.count = self.load_data()
layout = BoxLayout(orientation='vertical')
self.label = Label(text=f"Count: {self.count}")
increment_btn = Button(text='Increment')
increment_btn.bind(on_press=self.increment)
reset_btn = Button(text='Reset')
reset_btn.bind(on_press=self.reset)
layout.add_widget(self.label)
layout.add_widget(increment_btn)
layout.add_widget(reset_btn)
return layout
def get_data_file(self):
return join(self.user_data_dir, 'counter.json')
def load_data(self):
try:
with open(self.get_data_file(), 'r') as f:
return json.load(f).get('count', 0)
except (FileNotFoundError, json.JSONDecodeError):
return 0
def save_data(self):
data = {'count': self.count}
with open(self.get_data_file(), 'w') as f:
json.dump(data, f)
def increment(self, instance):
self.count += 1
self.label.text = f"Count: {self.count}"
self.save_data()
def reset(self, instance):
self.count = 0
self.label.text = f"Count: {self.count}"
self.save_data()
if __name__ == '__main__':
CounterApp().run()
Expected output: When you run this app, it shows a counter. Every time you tap Increment, the count increases and is saved to counter.json. When you close and reopen the app, the count is restored from the file.
Example 2: Using a generic local_storage helper
For larger apps, encapsulate saving/loading logic in a helper module.
# local_storage.py
import json
from os.path import join, exists
def save_json(app, filename, data):
with open(join(app.user_data_dir, filename), 'w') as f:
json.dump(data, f, indent=2)
def load_json(app, filename, default=None):
path = join(app.user_data_dir, filename)
if not exists(path):
return default
try:
with open(path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return default
Then use it in your app:
# main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from local_storage import save_json, load_json
class NotesApp(App):
def build(self):
self.notes = load_json(self, 'notes.json', default=[])
layout = BoxLayout(orientation='vertical')
self.label = Label(text=f"Notes: {len(self.notes)}")
add_btn = Button(text='Add note')
add_btn.bind(on_press=self.add_note)
layout.add_widget(self.label)
layout.add_widget(add_btn)
return layout
def add_note(self, instance):
note = f"Note {len(self.notes)+1}"
self.notes.append(note)
save_json(self, 'notes.json', self.notes)
self.label.text = f"Notes: {len(self.notes)}"
if __name__ == '__main__':
NotesApp().run()
Example 3: Async file I/O with asyncio (advanced)
For large data, consider writing in a separate thread to avoid UI lag.
import asyncio
import aiofiles
from kivy.app import App
from kivy.clock import Clock
class AsyncPersistApp(App):
async def save_async(self, data):
path = self.get_data_file()
async with aiofiles.open(path, 'w') as f:
await f.write(json.dumps(data))
def save_in_thread(self, data):
# Run async save in a thread so UI stays responsive
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self.save_async(data))
Pro tip: Always use
user_data_dirfrom Kivy’sApp, not a hardcoded path — it guarantees the right platform-specific directory and makes your app portable.
Compare options / when to choose what
Now that you know the basics, let’s compare the main local storage solutions for Python mobile apps:
| Method | Best for | Pros | Cons |
|---|---|---|---|
| JSON file | Simple key-value data, app settings, small lists | Easy, human-readable, built-in json module |
Not great for huge data, no queries |
| SQLite | Structured data, relational queries, larger datasets | ACID compliance, complex queries, mature | More boilerplate, requires SQL knowledge |
Key-Value stores (e.g., plyer storage, pathlib + file) |
Very simple preferences, small fragments | Minimal code, fast | Limited to flat data |
For most small to medium apps, I recommend starting with JSON files — they’re easy to debug, and you can inspect the saved data during development. If your app grows to thousands of records and you need to filter or sort, migrate to SQLite.
Rule of thumb: If your data fits in a dictionary or list and you rarely query it, use JSON. If you need relational queries or transactions, use SQLite.
Troubleshooting & edge cases
Here are common pitfalls when you persist data with local storage, and how to fix them.
1. FileNotFoundError on first launch
Your app tries to read a file that doesn’t exist yet.
Fix: Always check if the file exists before opening it, or wrap the read in a try/except.
2. json.JSONDecodeError after manual editing
If the file is corrupted or edited incorrectly, json.loads() will fail.
Fix: Catch this exception and fall back to a default value (e.g., empty list).
3. Data not saving due to wrong file path
Hardcoding paths like /sdcard/data.json may fail on Android due to permissions. Always use user_data_dir.
4. UI freezes when writing large data
Synchronous file I/O can block the main thread. Use threading or async I/O for large payloads.
5. Saving too often (performance)
Writing to disk on every keystroke can be slow. Debounce with a system that saves after a short idle time.
What you learned & what's next
You now understand how to persist data with local storage — from choosing the right file path, serializing to JSON, writing and reading, to troubleshooting common issues. You can apply this to your own mobile apps built with Kivy. You’ve completed a hands-on exercise that saves and loads a counter, and you know when to use JSON vs. SQLite.
Next lesson in this track will cover SQLite integration for more complex data requirements. You’ll build on your local storage knowledge to handle structured data with queries.
Keep this pattern in mind: find the path, serialize, write, read, deserialize. It will serve you in every app you build.
Now go ahead and add persistence to your app — your users will thank you!
Practice recap
Mini-exercise: Extend the counter app to save a list of all previous counts (e.g., [1,2,3...]) and display them on a label. Run the app, increment a few times, close it, and reopen — verify the list persists. Then add a 'Clear History' button that empties the list and saves.
Common mistakes
- Using a hardcoded file path like
/sdcard/data.json— fails on Android due to permissions; always useuser_data_dir. - Assuming the file exists on first launch — always wrap
open()in atry/except FileNotFoundErroror check withos.path.exists(). - Saving every millisecond without debouncing — leads to poor performance and battery drain; save on changes or after a debounce delay.
Variations
- Use
sqlite3for structured data that requires queries and transactions. - Leverage
plyer's storage API for a cross-platform key-value store. - Use
picklefor arbitrary Python objects, but be aware it's not human-readable and has security risks.
Real-world use cases
- Fitness tracker saving daily step counts and workout logs offline.
- Note-taking app storing user notes as JSON files on the device.
- Game saving high scores and level progress to persist player state.
Key takeaways
- Local storage allows your app to remember data between launches by writing to the device filesystem.
- Use
app.user_data_dirto get the correct writable directory on any platform. - Serialize Python objects to JSON with
json.dumps()and write to a file; on startup, load and parse withjson.loads(). - Always handle missing or corrupted files gracefully to avoid crashes.
- Choose JSON for simple data and SQLite for larger, relational datasets.
- Keep your UI responsive by using async or threaded I/O for large data.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.