Caching and Offline Mode
When building API-driven Flutter apps, you often face challenges with unreliable networks or slow connections. To provide a seamless experience, you can implement local caching and an offline mode. Caching stores API data locally, so your app can quickly display information without needing to fetch it from the server every time. Offline mode ensures your app remains usable even when the device loses internet connectivity. Together, these techniques improve both speed and reliability for users.
- Memory cache: stores data in RAM for fast, temporary access; data is lost when the app closes.
- File-based cache: saves data to the device's storage (such as with the
path_providerpackage); data persists between app sessions. - Database cache: uses a local database (like
sqfliteorhive) to store structured data for offline access.
- Time-based expiration: set a time limit after which cached data is considered stale and must be refreshed from the API.
- Manual refresh: allow users to trigger a refresh (such as with a pull-to-refresh gesture or button).
- Conditional requests: use HTTP headers like
ETagorLast-Modifiedto check if data has changed on the server before updating the cache.
main.dart
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131// main.dart import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Caching Example', home: PostsPage(), ); } } class PostsPage extends StatefulWidget { @override _PostsPageState createState() => _PostsPageState(); } class _PostsPageState extends State<PostsPage> { List<dynamic> _posts = []; bool _loading = true; bool _offline = false; @override void initState() { super.initState(); _loadPosts(); } Future<File> _getCacheFile() async { final dir = await getApplicationDocumentsDirectory(); return File('${dir.path}/posts_cache.json'); } Future<void> _saveToCache(List<dynamic> posts) async { final file = await _getCacheFile(); await file.writeAsString(jsonEncode(posts)); } Future<List<dynamic>?> _readFromCache() async { try { final file = await _getCacheFile(); if (await file.exists()) { final contents = await file.readAsString(); return jsonDecode(contents); } } catch (_) {} return null; } Future<void> _loadPosts() async { setState(() { _loading = true; _offline = false; }); try { final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')); if (response.statusCode == 200) { final posts = jsonDecode(response.body); setState(() { _posts = posts; }); await _saveToCache(posts); } else { throw Exception('Failed to load'); } } catch (_) { // Fallback to cache final cached = await _readFromCache(); if (cached != null) { setState(() { _posts = cached; _offline = true; }); } } setState(() { _loading = false; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Posts'), actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: _loadPosts, ), ], ), body: _loading ? Center(child: CircularProgressIndicator()) : _posts.isEmpty ? Center(child: Text('No posts available')) : Column( children: [ if (_offline) Container( color: Colors.amber, padding: EdgeInsets.all(8), child: Text('You are offline. Showing cached data.'), ), Expanded( child: ListView.builder( itemCount: _posts.length, itemBuilder: (context, index) { final post = _posts[index]; return ListTile( title: Text(post['title']), subtitle: Text(post['body']), ); }, ), ), ], ), ); } }
The code above uses a simple file-based cache to store API responses locally. When the app loads, it tries to fetch fresh data from the API. If the network request fails, it falls back to show cached data. Cache invalidation and refresh are handled by allowing the user to refresh manually (with the refresh button), which attempts to fetch new data and overwrite the cache. This approach ensures users see up-to-date information when online, but always have access to the last known data if offline.
Thanks for your feedback!
Ask AI
Ask AI
Ask anything or try one of the suggested questions to begin our chat
Awesome!
Completion rate improved to 6.67
Caching and Offline Mode
Swipe to show menu
When building API-driven Flutter apps, you often face challenges with unreliable networks or slow connections. To provide a seamless experience, you can implement local caching and an offline mode. Caching stores API data locally, so your app can quickly display information without needing to fetch it from the server every time. Offline mode ensures your app remains usable even when the device loses internet connectivity. Together, these techniques improve both speed and reliability for users.
- Memory cache: stores data in RAM for fast, temporary access; data is lost when the app closes.
- File-based cache: saves data to the device's storage (such as with the
path_providerpackage); data persists between app sessions. - Database cache: uses a local database (like
sqfliteorhive) to store structured data for offline access.
- Time-based expiration: set a time limit after which cached data is considered stale and must be refreshed from the API.
- Manual refresh: allow users to trigger a refresh (such as with a pull-to-refresh gesture or button).
- Conditional requests: use HTTP headers like
ETagorLast-Modifiedto check if data has changed on the server before updating the cache.
main.dart
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131// main.dart import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Caching Example', home: PostsPage(), ); } } class PostsPage extends StatefulWidget { @override _PostsPageState createState() => _PostsPageState(); } class _PostsPageState extends State<PostsPage> { List<dynamic> _posts = []; bool _loading = true; bool _offline = false; @override void initState() { super.initState(); _loadPosts(); } Future<File> _getCacheFile() async { final dir = await getApplicationDocumentsDirectory(); return File('${dir.path}/posts_cache.json'); } Future<void> _saveToCache(List<dynamic> posts) async { final file = await _getCacheFile(); await file.writeAsString(jsonEncode(posts)); } Future<List<dynamic>?> _readFromCache() async { try { final file = await _getCacheFile(); if (await file.exists()) { final contents = await file.readAsString(); return jsonDecode(contents); } } catch (_) {} return null; } Future<void> _loadPosts() async { setState(() { _loading = true; _offline = false; }); try { final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')); if (response.statusCode == 200) { final posts = jsonDecode(response.body); setState(() { _posts = posts; }); await _saveToCache(posts); } else { throw Exception('Failed to load'); } } catch (_) { // Fallback to cache final cached = await _readFromCache(); if (cached != null) { setState(() { _posts = cached; _offline = true; }); } } setState(() { _loading = false; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Posts'), actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: _loadPosts, ), ], ), body: _loading ? Center(child: CircularProgressIndicator()) : _posts.isEmpty ? Center(child: Text('No posts available')) : Column( children: [ if (_offline) Container( color: Colors.amber, padding: EdgeInsets.all(8), child: Text('You are offline. Showing cached data.'), ), Expanded( child: ListView.builder( itemCount: _posts.length, itemBuilder: (context, index) { final post = _posts[index]; return ListTile( title: Text(post['title']), subtitle: Text(post['body']), ); }, ), ), ], ), ); } }
The code above uses a simple file-based cache to store API responses locally. When the app loads, it tries to fetch fresh data from the API. If the network request fails, it falls back to show cached data. Cache invalidation and refresh are handled by allowing the user to refresh manually (with the refresh button), which attempts to fetch new data and overwrite the cache. This approach ensures users see up-to-date information when online, but always have access to the last known data if offline.
Thanks for your feedback!