Fetch data from REST APIs, handle async UI with FutureBuilder, and persist data locally with shared_preferences, Hive, and SQLite.
The http package is the simplest way to make network requests in Flutter. Add it to pubspec.yaml.
# pubspec.yaml dependencies: http: ^1.2.1
// Import in your Dart files import 'dart:convert'; import 'package:http/http.dart' as http;
<!-- android/app/src/main/AndroidManifest.xml --> <uses-permission android:name="android.permission.INTERNET" />
<!-- ios/Runner/Info.plist --> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
NSExceptionDomains rather than allowing all arbitrary loads.// ---- Model class with fromJson factory ----
class Post {
final int id;
final String title;
final String body;
final int userId;
const Post({
required this.id,
required this.title,
required this.body,
required this.userId,
});
// Parse from JSON map
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
userId: json['userId'] as int,
);
}
// Convert to JSON map
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'body': body,
'userId': userId,
};
}
// ---- Fetch a single object ----
Future<Post> fetchPost(int id) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
final response = await http.get(url);
if (response.statusCode == 200) {
final Map<String, dynamic> json = jsonDecode(response.body);
return Post.fromJson(json);
} else {
throw Exception('Failed to load post (${response.statusCode})');
}
}
// ---- Fetch a list ----
Future<List<Post>> fetchPosts() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(url);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
// ---- POST — create a new resource ----
Future<Post> createPost(String title, String body) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer YOUR_TOKEN_HERE',
},
body: jsonEncode({
'title': title,
'body': body,
'userId': 1,
}),
);
if (response.statusCode == 201) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create post: ${response.statusCode}');
}
}
// ---- PUT — full update ----
Future<Post> updatePost(int id, String title, String body) async {
final response = await http.put(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'id': id, 'title': title, 'body': body, 'userId': 1}),
);
if (response.statusCode == 200) return Post.fromJson(jsonDecode(response.body));
throw Exception('Update failed');
}
// ---- PATCH — partial update ----
Future<void> patchTitle(int id, String newTitle) async {
await http.patch(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': newTitle}),
);
}
// ---- DELETE ----
Future<void> deletePost(int id) async {
final response = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
);
if (response.statusCode != 200) {
throw Exception('Delete failed: ${response.statusCode}');
}
}
// ---- GET with query parameters ----
Future<List<Post>> searchPosts(String query) async {
final url = Uri.parse('https://api.example.com/posts').replace(
queryParameters: {'q': query, 'limit': '10', 'page': '1'},
);
// URL becomes: https://api.example.com/posts?q=flutter&limit=10&page=1
final response = await http.get(url);
// ...
}
FutureBuilder integrates async operations directly into the widget tree. It handles loading, error, and data states automatically.
class PostListScreen extends StatelessWidget {
const PostListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Posts')),
body: FutureBuilder<List<Post>>(
// The future to wait for
future: fetchPosts(),
// Builder is called whenever the future state changes
builder: (context, snapshot) {
// State 1: Still loading
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// State 2: Error occurred
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Error: ${snapshot.error}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {}, // trigger rebuild to retry
child: const Text('Retry'),
),
],
),
);
}
// State 3: Data arrived
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No posts found.'));
}
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
leading: CircleAvatar(child: Text('${post.id}')),
title: Text(post.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(post.body, maxLines: 2, overflow: TextOverflow.ellipsis),
);
},
);
},
),
);
}
}
initState(), not directly in build().// Custom exception class
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => 'ApiException: $message (HTTP $statusCode)';
}
// Robust fetch with detailed error handling
Future<List<Post>> fetchPostsSafe() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
).timeout(const Duration(seconds: 10)); // timeout
switch (response.statusCode) {
case 200:
final data = jsonDecode(response.body) as List;
return data.map((j) => Post.fromJson(j)).toList();
case 401:
throw ApiException('Unauthorized — please log in again', statusCode: 401);
case 403:
throw ApiException('Forbidden', statusCode: 403);
case 404:
throw ApiException('Resource not found', statusCode: 404);
case 500:
throw ApiException('Server error — please try later', statusCode: 500);
default:
throw ApiException('Unexpected error', statusCode: response.statusCode);
}
} on TimeoutException {
throw ApiException('Request timed out — check your connection');
} on SocketException {
throw ApiException('No internet connection');
} on FormatException {
throw ApiException('Invalid response format from server');
}
}
// Show error in UI with SnackBar
void _handleError(BuildContext context, Object e) {
final message = e is ApiException ? e.message : 'An error occurred';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red.shade700,
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {},
textColor: Colors.white,
),
),
);
}
Use shared_preferences for simple persistent key-value data: tokens, settings, flags, last-seen timestamps.
# pubspec.yaml dependencies: shared_preferences: ^2.3.1
import 'package:shared_preferences/shared_preferences.dart';
class PrefsService {
// Always use late and initialize in main() or on first access
static late SharedPreferences _prefs;
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// ---- Write ----
static Future<void> setString(String key, String value) =>
_prefs.setString(key, value);
static Future<void> setInt(String key, int value) =>
_prefs.setInt(key, value);
static Future<void> setBool(String key, bool value) =>
_prefs.setBool(key, value);
static Future<void> setStringList(String key, List<String> value) =>
_prefs.setStringList(key, value);
// ---- Read ----
static String? getString(String key) => _prefs.getString(key);
static int? getInt(String key) => _prefs.getInt(key);
static bool getBool(String key) => _prefs.getBool(key) ?? false;
static List<String>? getStringList(String key) => _prefs.getStringList(key);
// ---- Remove / Clear ----
static Future<void> remove(String key) => _prefs.remove(key);
static Future<void> clear() => _prefs.clear();
// ---- Typed helpers ----
static Future<void> saveToken(String token) => setString('auth_token', token);
static String? getToken() => getString('auth_token');
static bool isLoggedIn() => getToken() != null;
static Future<void> logout() => remove('auth_token');
}
// Usage in main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PrefsService.init();
runApp(const MyApp());
}
// Usage anywhere
await PrefsService.saveToken('eyJhbGciOi...');
final token = PrefsService.getToken();
print(PrefsService.isLoggedIn()); // true
Hive is a fast, pure-Dart key-value database. It stores objects in typed boxes and is much faster than SQLite for simple object storage.
# pubspec.yaml dependencies: hive_flutter: ^1.1.0 dev_dependencies: hive_generator: ^2.0.1 build_runner: ^2.4.9
import 'package:hive_flutter/hive_flutter.dart';
// Initialize in main()
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter(); // sets app documents dir automatically
// Register adapters (for typed objects)
Hive.registerAdapter(TodoAdapter());
// Open boxes before using them
await Hive.openBox<String>('settings');
await Hive.openBox<Todo>('todos');
runApp(const MyApp());
}
// ---- Using a simple string box ----
final settingsBox = Hive.box<String>('settings');
settingsBox.put('theme', 'dark');
final theme = settingsBox.get('theme', defaultValue: 'light');
settingsBox.delete('theme');
// ---- Typed objects with HiveType and HiveField annotations ----
// Run: flutter pub run build_runner build
@HiveType(typeId: 0)
class Todo extends HiveObject {
@HiveField(0) late String id;
@HiveField(1) late String title;
@HiveField(2) late bool isDone;
Todo({required this.id, required this.title, this.isDone = false});
}
// CRUD on a typed box
final box = Hive.box<Todo>('todos');
// Create
box.put('todo_1', Todo(id: 'todo_1', title: 'Buy milk'));
// Read all
final allTodos = box.values.toList();
// Update
final todo = box.get('todo_1')!;
todo.isDone = true;
todo.save(); // HiveObject.save() updates in-place
// Delete
box.delete('todo_1');
// Listen to changes with ValueListenableBuilder
ValueListenableBuilder(
valueListenable: box.listenable(),
builder: (context, Box<Todo> box, child) {
return ListView.builder(
itemCount: box.length,
itemBuilder: (context, i) => ListTile(title: Text(box.getAt(i)!.title)),
);
},
)
Use sqflite for relational data with multiple tables, complex queries, or when migrating from a web backend that uses SQL.
# pubspec.yaml dependencies: sqflite: ^2.3.3+1 path: ^1.9.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
static Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
static Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'myapp.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
isDone INTEGER NOT NULL DEFAULT 0,
created TEXT NOT NULL
)
''');
},
onUpgrade: (db, oldVersion, newVersion) async {
// Handle schema migrations here
if (oldVersion < 2) {
await db.execute('ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0');
}
},
);
}
// INSERT
static Future<int> insertTodo(Map<String, dynamic> todo) async {
final db = await database;
return db.insert('todos', todo, conflictAlgorithm: ConflictAlgorithm.replace);
}
// SELECT ALL
static Future<List<Map<String, dynamic>>> getTodos() async {
final db = await database;
return db.query('todos', orderBy: 'created DESC');
}
// SELECT WHERE
static Future<List<Map<String, dynamic>>> getPending() async {
final db = await database;
return db.query('todos', where: 'isDone = ?', whereArgs: [0]);
}
// UPDATE
static Future<int> updateTodo(int id, Map<String, dynamic> updates) async {
final db = await database;
return db.update('todos', updates, where: 'id = ?', whereArgs: [id]);
}
// DELETE
static Future<int> deleteTodo(int id) async {
final db = await database;
return db.delete('todos', where: 'id = ?', whereArgs: [id]);
}
// RAW QUERY
static Future<List<Map<String, dynamic>>> search(String q) async {
final db = await database;
return db.rawQuery('SELECT * FROM todos WHERE title LIKE ?', ['%$q%']);
}
}
// Usage
final id = await DatabaseHelper.insertTodo({
'title': 'Buy groceries',
'isDone': 0,
'created': DateTime.now().toIso8601String(),
});
final todos = await DatabaseHelper.getTodos();
Keep HTTP logic separate from UI in a dedicated service class. Widgets only call service methods and react to results.
// lib/services/api_service.dart
class ApiService {
static const _baseUrl = 'https://jsonplaceholder.typicode.com';
// Reusable headers
static Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${PrefsService.getToken() ?? ''}',
};
// Generic GET
static Future<T> get<T>(String path, T Function(dynamic json) fromJson) async {
final response = await http.get(
Uri.parse('$_baseUrl$path'),
headers: _headers,
).timeout(const Duration(seconds: 15));
_checkStatus(response);
return fromJson(jsonDecode(response.body));
}
// Generic POST
static Future<T> post<T>(
String path,
Map<String, dynamic> body,
T Function(dynamic json) fromJson,
) async {
final response = await http.post(
Uri.parse('$_baseUrl$path'),
headers: _headers,
body: jsonEncode(body),
).timeout(const Duration(seconds: 15));
_checkStatus(response);
return fromJson(jsonDecode(response.body));
}
static void _checkStatus(http.Response response) {
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException('HTTP ${response.statusCode}', statusCode: response.statusCode);
}
}
// ---- Specific API methods ----
static Future<List<Post>> fetchPosts() =>
get('/posts', (json) => (json as List).map((j) => Post.fromJson(j)).toList());
static Future<Post> fetchPost(int id) =>
get('/posts/$id', (json) => Post.fromJson(json));
static Future<Post> createPost(String title, String body) =>
post('/posts', {'title': title, 'body': body, 'userId': 1}, (json) => Post.fromJson(json));
}
// ---- Widget uses the service ----
class PostsScreen extends StatelessWidget {
const PostsScreen({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Post>>(
future: ApiService.fetchPosts(), // clean call, no HTTP code here
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (ctx, i) => ListTile(title: Text(snapshot.data![i].title)),
);
},
);
}
}