Integrasi REST API adalah skill wajib untuk developer mobile modern. Hampir semua aplikasi production butuh fetch data dari backend — entah list produk, user profile, atau notifikasi terbaru. Di episode ini, kita bahas step-by-step mulai dari mempersiapkan project, fetch data, parsing JSON, sampai menampilkan dengan UI yang responsif.
Sebelum melanjut, pastikan kamu sudah comfortable dengan state management menggunakan StatefulWidget dari episode 3. Kita akan mengembangkan konsep itu dengan menambahkan layer API communication dan model parsing.
Persiapan Project dan Package
Langkah pertama adalah menambahkan http package ke project. Buka pubspec.yaml dan tambahkan dependency:
dependencies:
flutter:
sdk: flutter
http: ^1.1.0Setelah itu jalankan:
flutter pub getPackage http adalah pilihan populer untuk pemula karena simple dan tidak terlalu opinionated. Alternatif lain adalah Dio (lebih feature-rich) atau Chopper (code generation), tapi untuk tutorial ini kita stick dengan http.
Jangan lupa — kalau kamu target Android 9 ke atas, pastikan file AndroidManifest.xml sudah punya izin internet. Biasanya sudah default, tapi cek di android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />Membuat Model Class untuk Parsing JSON
Sebelum fetch data, kita perlu membuat model class yang mewakili struktur response API. Ini penting untuk type safety dan mudah di-maintain.
Kita akan pakai JSONPlaceholder — fake API gratis yang bagus untuk belajar. Endpoint /posts mengembalikan response seperti ini:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident",
"body": "quia et suscipit..."
}Sekarang buat file baru lib/models/post.dart:
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({
required this.userId,
required this.id,
required this.title,
required this.body,
});
// Factory constructor untuk parse dari JSON
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
// Method untuk convert ke JSON (berguna kalau kirim ke backend)
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'body': body,
};
}
}Pattern fromJson dan toJson ini adalah best practice di Dart ecosystem. Kalau project besar, kamu bisa automate dengan package json_serializable, tapi untuk pemula hardcode dulu jadi paham mekanismenya.
Membuat API Service dan Fetch Data
Sekarang buat service class untuk handle semua HTTP request. Ini memisahkan logic API dari UI, jadi cleaner dan reusable.
Buat file lib/services/api_service.dart:
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/post.dart';
class ApiService {
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
// Method untuk fetch list posts
static Future<List<Post>> fetchPosts() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/posts'),
).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
// Parse response body (JSON array)
final List<dynamic> jsonData = jsonDecode(response.body);
// Convert setiap item jadi Post object
final List<Post> posts = jsonData
.map((item) => Post.fromJson(item as Map<String, dynamic>))
.toList();
return posts;
} else {
throw Exception(
'Failed to load posts. Status code: ${response.statusCode}',
);
}
} on http.ClientException {
throw Exception('Network error. Check your internet connection.');
} catch (e) {
throw Exception('Error: $e');
}
}
// Method untuk fetch single post by id
static Future<Post> fetchPostById(int id) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/posts/$id'),
).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load post.');
}
} catch (e) {
throw Exception('Error: $e');
}
}
}
Perhatikan beberapa hal: kita pakai Uri.parse untuk handle special characters, .timeout() untuk prevent hanging requests, dan error handling yang comprehensive.
Menampilkan Data dengan FutureBuilder dan ListView
Sekarang kita display data di UI. FutureBuilder adalah widget yang cocok untuk async operations. Buat file lib/screens/posts_screen.dart:
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/post.dart';
class PostsScreen extends StatefulWidget {
const PostsScreen({Key? key}) : super(key: key);
@override
State<PostsScreen> createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
late Future<List<Post>> _futurePost;
@override
void initState() {
super.initState();
_futurePost = ApiService.fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Posts from API'),
centerTitle: true,
),
body: FutureBuilder<List<Post>>(
future: _futurePost,
builder: (context, snapshot) {
// State: Loading
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
// State: Error
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
'Error: ${snapshot.error}',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_futurePost = ApiService.fetchPosts();
});
},
child: const Text('Retry'),
),
],
),
);
}
// State: Success
if (snapshot.hasData) {
final posts = snapshot.data ?? [];
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
title: Text(
post.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
post.body,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
'User ID: ${post.userId} | Post ID: ${post.id}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
);
},
);
}
return const Center(child: Text('No data'));
},
),
);
}
}
FutureBuilder memiliki 4 state utama yang perlu kita handle — waiting (loading), hasError, hasData (success), dan default. Ini sudah cover semua skenario real-world.
Handling Loading dan Error State dengan Proper
Perhatikan implementasi error state di atas — kita pakai icon, message yang informatif, dan tombol retry. Ini best practice yang improve user experience significantly. Beberapa tips penting:
- Selalu show loading indicator saat fetch berlangsung — jangan biarkan user bingung
- Display error message yang jelas, bukan technical jargon yang serem
- Sediakan retry button agar user bisa coba ulang tanpa restart app
- Gunakan .timeout() di ApiService untuk prevent indefinite hanging
- Test dengan network throttle (chrome dev tools atau android studio) untuk pastikan loading state terlihat
Bonus: Refresh Data dan Pull-to-Refresh
Untuk enhance UX, kita bisa tambahkan pull-to-refresh. Wrap ListView dengan RefreshIndicator:
RefreshIndicator(
onRefresh: () async {
setState(() {
_futurePost = ApiService.fetchPosts();
});
// Wait untuk future complete
await _futurePost;
},
child: FutureBuilder<List<Post>>(
// ... rest of FutureBuilder
),
)
Sekarang user bisa pull down untuk refresh data — familiar pattern dari hampir semua app modern.
Kesimpulan
Di episode ini kita sudah cover workflow lengkap integrasi API di Flutter — dari setup package http, membuat model dengan fromJson/toJson pattern, fetch data di service class, sampai display dengan proper state management di UI. FutureBuilder + RefreshIndicator adalah kombinasi powerful yang bisa handle 90% use case production apps.
Next step — di episode 5 kita akan optimize app ini, bahas build for production, code obfuscation, dan submit ke Play Store. Stay tuned!