HTTP Requests & APIs
Most real apps aren't just static screens — they talk to a server. Flutter doesn't include networking in the core framework itself; instead, the community-maintained http package (installed exactly the way shown in Packages & pubspec.yaml) is the standard, lightweight way to make requests. This lesson covers making a GET request, Dart's async/await syntax that makes waiting for the network feel like ordinary sequential code, and FutureBuilder for tying that async work directly into your UI.
Adding the http Package
Add it under dependencies: in pubspec.yaml, then run flutter pub get,
and import it wherever you need to make a request.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
import 'package:http/http.dart' as http;
Making a GET Request
http.get sends a GET request to the given Uri and returns a
Response once the server replies — statusCode tells you if
it succeeded (200 means OK), and body holds the raw
response text, typically JSON.
Future<void> fetchArticles() async {
final response = await http.get(Uri.parse('https://api.example.com/articles'));
if (response.statusCode == 200) {
print('Success: ${response.body}');
} else {
print('Request failed with status: ${response.statusCode}');
}
}
Why Network Calls Are Asynchronous
A network request can take anywhere from a few milliseconds to several seconds, depending on the server and the user's connection. Flutter renders your UI on a single main thread — if a network call blocked that thread while waiting for a reply, the entire app would freeze: no scrolling, no button taps, no animations, until the response arrived. async/ await avoids this entirely: the function pauses at the await and hands control back to the event loop, so the UI stays fully responsive, and resumes exactly where it left off once the response is ready.
Future<T>: What It Means
A function marked to return Future<T> doesn't hand back a T immediately — it hands back a promise that a T will be available later, once whatever asynchronous work it's doing finishes. Marking the function itself async is what allows await to be used inside its body.
Future<String> fetchGreeting() async {
final response = await http.get(Uri.parse('https://api.example.com/greeting'));
return response.body; // wrapped in a Future automatically
}
FutureBuilder: Driving UI From an Async Operation
Manually calling setState in three different places to track loading, error, and success
gets messy fast. FutureBuilder is purpose-built for this: give it a
Future and a builder function, and it calls your builder
again automatically every time that future's state changes — while it's still running, once it
completes with data, or once it completes with an error.
class ArticleScreen extends StatefulWidget {
const ArticleScreen({super.key});
@override
State<ArticleScreen> createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
late final Future<String> _articleFuture;
@override
void initState() {
super.initState();
_articleFuture = fetchGreeting(); // started once, not on every rebuild
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _articleFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); // loading
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}')); // error
} else {
return Center(child: Text(snapshot.data!)); // data
}
},
);
}
}
initState (as above), not directly inside build — creating it inside build would kick off a brand-new network request on every single rebuild.Error Handling With try/catch
Beyond checking statusCode, the request itself can throw — a lost connection, a DNS
failure, a timeout. Wrapping the call in try/catch
lets you handle that gracefully instead of crashing.
Future<String> fetchGreetingSafely() async {
try {
final response = await http.get(Uri.parse('https://api.example.com/greeting'));
if (response.statusCode != 200) {
throw Exception('Server returned ${response.statusCode}');
}
return response.body;
} catch (e) {
throw Exception('Failed to load greeting: $e');
}
}
POST Requests
Sending data to a server works similarly, using http.post with a JSON-encoded body and a
content-type header so the server knows how to parse it.
import 'dart:convert';
Future<void> createArticle(String title, String body) async {
final response = await http.post(
Uri.parse('https://api.example.com/articles'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': title, 'body': body}),
);
if (response.statusCode == 201) {
print('Article created');
} else {
print('Failed: ${response.statusCode}');
}
}
response.body still needs to be parsed into usable Dart objects — see JSON Serialization next.