Tutorial · 6 min read · 0 dilihat

Belajar Flutter #4: Integrasi REST API dan Parsing JSON

Setelah membangun multi-screen app di episode 3, saatnya kita ambil langkah lebih jauh — mengintegrasikan data real dari internet ke aplikasi Flutter kamu. Di episode ini, kita akan belajar cara fetch data dari REST API, parsing response JSON jadi model Dart yang rapi, dan menampilkannya di UI dengan state management yang proper. Kami akan pakai http package (yang simple dan ringan) dan JSONPlaceholder sebagai fake API. Dengan fokus pada praktik langsung dan error handling yang solid, kamu akan siap move ke optimization dan deploy di episode 5 nanti.

IKHSAN MAULANA

IKHSAN MAULANA

Web, Android, and RPA Development

Belajar Flutter #4: Integrasi REST API dan Parsing JSON

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.0

Setelah itu jalankan:

flutter pub get

Package 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!

Share this article:

IKHSAN MAULANA

Tentang Penulis

IKHSAN MAULANA

Web, Android, and RPA Development

I am an experienced IT programmer specializing in Web Development (Laravel/PHP), Android (Dart/Flutter), and RPA (UiPath). I love building clean, efficient solutions that solve real-world problems. With 4+ years of hands...

Download CV

Sebelum download, boleh kenalan dulu? Form ini opsional — kosongin juga gak apa-apa, langsung klik Download.