This episode connects your app to the outside world: HTTP requests with the http or dio package, JSON deserialization with dart:convert and json_serializable, async programming with Future and Stream, and production-ready loading states, error handling, and retry.

A useful app almost always reads from and sends data to a server. Episode 8 opens that door: you'll learn to make HTTP requests, parse JSON into Dart models, understand async programming with Future and Stream, and handle loading, errors, and retries like a production app. Start by installing the package:
flutter pub add httpflutter pub add http adds the http package to pubspec.yaml and immediately runs pub get. For larger needs, the dio package offers interceptors, timeouts, and upload progress — we'll compare the two in a moment.
With the http package, a simple request is only a few lines:
import 'package:http/http.dart' as http;
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
print(response.body);
} else {
throw Exception('Gagal: ${response.statusCode}');
}await http.get(Uri.parse(...)) returns a Response. Always check statusCode before processing body. Notice that the URL is always wrapped in Uri.parse — http.get accepts a Uri, not a raw string.
dio adds features on top of http: interceptors for logging, base options for the base URL, and cleaner timeout handling. For apps with many endpoints and auth needs, dio saves a lot of boilerplate:
flutter pub add diodart:convert parses raw JSON, then you map it to a Dart model:
class Post {
const Post({required this.id, required this.title});
final int id;
final String title;
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
);
}
}Post.fromJson(json) maps JSON keys to model fields. Use jsonDecode(response.body) to turn the string into a Map before calling fromJson.
Writing fromJson by hand gets tedious when you have many models. The json_serializable package generates it from annotations:
flutter pub add json_annotation
flutter pub add dev:build_runner dev:json_serializableRun the codegen:
dart run build_runner builddart run build_runner build generates .g.dart files containing the fromJson and toJson implementations derived from the @JsonSerializable() annotation.
Future represents an operation that produces one value later. The combination of async and await makes asynchronous code read like synchronous code:
Future<Post> ambilPost(int id) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Post.fromJson(json);
}A function typed Future<Post> tells the caller the result isn't ready yet. await waits for the result without blocking the UI — the key to comfortable asynchronous programming in Flutter.
Stream represents a continuous flow of data — for example notifications or progress. In the UI, StreamBuilder re-renders every time the stream sends new data:
StreamBuilder<int>(
stream: streamKoneksi,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Ping: ${snapshot.data} ms');
}
return const Text('Menunggu data...');
},
)StreamBuilder calls builder again every time streamKoneksi sends a new value. snapshot.hasData and snapshot.data give access to the latest value.
Use FutureBuilder to show loading, error, and data in a single widget:
FutureBuilder<Post>(
future: ambilPost(1),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Terjadi kesalahan: ${snapshot.error}');
}
return Text(snapshot.data!.title);
},
)ConnectionState.waiting indicates the process is still running — show a spinner. snapshot.hasError holds error handling, and snapshot.data is used once the data is ready.
Don't give up on a single network failure. Build a retry helper with a limit:
Future<T> cobaUlang<T>(Future<T> Function() aksi, {int maks = 3}) async {
for (var i = 0; i < maks; i++) {
try {
return await aksi();
} catch (e) {
if (i == maks - 1) rethrow;
await Future.delayed(Duration(milliseconds: 500 * (i + 1)));
}
}
throw StateError('Tidak terjangkau');
}cobaUlang calls the action until it succeeds or reaches maks attempts. The delay Duration(milliseconds: 500 * (i + 1)) gives the server time to recover — without hammering it too aggressively.
Key takeaways:
http for small projects; dio for interceptor and timeout needs.statusCode and wrap URLs with Uri.parse.fromJson or json_serializable codegen.Future for a single result; Stream for continuous data.FutureBuilder handles loading, error, and data in one place.In the next episode 9 we discuss persistent storage — local storage with shared_preferences and file I/O, local SQLite databases with sqflite and Drift, caching strategies and offline support, and secure storage for tokens and secrets. Your app's data no longer disappears when the app closes.