Flutter Web REST API — Materi & Proyek (main.dart + panduan)).
Aktifitas Pembelajaran:
• Rencana pembelajaran (tujuan, alur 7JP, penilaian, aktivitas nyata)
• Petunjuk singkat cara pakai di zapp.run (tambahkan http:
di pubspec.yaml
)
• Kode lengkap lib/main.dart
— aplikasi ToDo Manager (Flutter Web) yang menunjukkan operasi REST API: GET, POST, PUT, DELETE menggunakan JSONPlaceholder (public demo API).
Beberapa hal yang perlu kamu ketahui langsung:
-
JSONPlaceholder cocok untuk praktik karena menerima POST/PUT/DELETE tetapi tidak menyimpan perubahan permanen — bagus untuk belajar konsep HTTP/REST.
-
Untuk membuat proyek jalan di zapp.run: buat project Flutter Web → tambahkan http: ^0.13.6
di pubspec.yaml
→ copy-paste main.dart
dari canvas → run (web).
-
Aplikasi sudah termasuk: tampil list (GET), tambah item (POST), edit (PUT), hapus (DELETE), toggle completed (PUT), refresh (GET).
Saran Pengembangan:
-
tambahkan versi yang terhubung ke Google Sheets / Firebase (nyata & persistensi), atau
-
ubah resource dari JSONPlaceholder ke backend sederhana (contoh Node/Flask) + skrip quick-start?
Berikut Full Codingan:
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'REST API Demo - ToDo Manager',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: const HomePage(),
);
}
}
class TodoItem {
int id;
String title;
bool completed;
TodoItem({required this.id, required this.title, required this.completed});
factory TodoItem.fromJson(Map<String, dynamic> json) => TodoItem(
id: json['id'] is int ? json['id'] : int.parse(json['id'].toString()),
title: json['title'] ?? '',
completed: json['completed'] ?? false,
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'completed': completed,
};
}
class ApiService {
// JSONPlaceholder base URL
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
// GET: ambil daftar todos (contoh resource)
static Future<List<TodoItem>> fetchTodos({int limit = 20}) async {
final url = Uri.parse('$baseUrl/todos?_limit=$limit');
final res = await http.get(url);
if (res.statusCode == 200) {
final List list = jsonDecode(res.body);
return list.map((e) => TodoItem.fromJson(e)).toList();
} else {
throw Exception('Failed to load todos: ${res.statusCode}');
}
}
// POST: buat todo baru
static Future<TodoItem> createTodo(String title) async {
final url = Uri.parse('$baseUrl/todos');
final res = await http.post(url,
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode({'title': title, 'completed': false, 'userId': 1}));
if (res.statusCode == 201) {
return TodoItem.fromJson(jsonDecode(res.body));
} else {
throw Exception('Failed to create todo: ${res.statusCode}');
}
}
// PUT: update todo
static Future<TodoItem> updateTodo(TodoItem todo) async {
final url = Uri.parse('$baseUrl/todos/${todo.id}');
final res = await http.put(url,
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(todo.toJson()));
if (res.statusCode == 200) {
return TodoItem.fromJson(jsonDecode(res.body));
} else {
throw Exception('Failed to update todo: ${res.statusCode}');
}
}
// DELETE: hapus todo
static Future<bool> deleteTodo(int id) async {
final url = Uri.parse('$baseUrl/todos/$id');
final res = await http.delete(url);
// JSONPlaceholder returns 200 with empty body
return res.statusCode == 200 || res.statusCode == 204;
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<List<TodoItem>> _futureTodos;
final TextEditingController _newTodoController = TextEditingController();
List<TodoItem> _todos = [];
bool _loadingAction = false;
@override
void initState() {
super.initState();
_futureTodos = ApiService.fetchTodos(limit: 30);
_futureTodos.then((value) => setState(() => _todos = value)).catchError((e) {
if (kDebugMode) print(e);
});
}
Future<void> _refresh() async {
setState(() => _futureTodos = ApiService.fetchTodos(limit: 30));
final newList = await _futureTodos;
setState(() => _todos = newList);
}
Future<void> _createTodo() async {
final title = _newTodoController.text.trim();
if (title.isEmpty) return;
setState(() => _loadingAction = true);
try {
final created = await ApiService.createTodo(title);
// JSONPlaceholder returns id = 201 etc; append to local list for UX
setState(() {
_todos.insert(0, created);
_newTodoController.clear();
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Berhasil membuat todo (POST)')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error create: $e')));
} finally {
setState(() => _loadingAction = false);
}
}
Future<void> _toggleCompleted(TodoItem todo) async {
final updated = TodoItem(id: todo.id, title: todo.title, completed: !todo.completed);
setState(() => _loadingAction = true);
try {
final res = await ApiService.updateTodo(updated);
setState(() {
final idx = _todos.indexWhere((t) => t.id == todo.id);
if (idx != -1) _todos[idx] = res;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Berhasil update (PUT)')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error update: $e')));
} finally {
setState(() => _loadingAction = false);
}
}
Future<void> _editTodoDialog(TodoItem todo) async {
final controller = TextEditingController(text: todo.title);
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Todo (PUT)'),
content: TextField(
controller: controller,
decoration: const InputDecoration(labelText: 'Judul'),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Batal')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Simpan')),
],
),
);
if (result == true) {
final newTitle = controller.text.trim();
if (newTitle.isEmpty) return;
setState(() => _loadingAction = true);
try {
final updated = TodoItem(id: todo.id, title: newTitle, completed: todo.completed);
final res = await ApiService.updateTodo(updated);
setState(() {
final idx = _todos.indexWhere((t) => t.id == todo.id);
if (idx != -1) _todos[idx] = res;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Berhasil menyimpan perubahan')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error save: $e')));
} finally {
setState(() => _loadingAction = false);
}
}
}
Future<void> _deleteTodoConfirm(TodoItem todo) async {
final yes = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Hapus Todo (DELETE)?'),
content: Text('Yakin ingin menghapus: "${todo.title}" ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Batal')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Hapus')),
],
),
);
if (yes == true) {
setState(() => _loadingAction = true);
try {
final ok = await ApiService.deleteTodo(todo.id);
if (ok) {
setState(() => _todos.removeWhere((t) => t.id == todo.id));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Berhasil dihapus')));
} else {
throw Exception('Delete failed');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error delete: $e')));
} finally {
setState(() => _loadingAction = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('REST API Demo — ToDo Manager'),
actions: [
IconButton(onPressed: _refresh, icon: const Icon(Icons.refresh), tooltip: 'GET (refresh)'),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _newTodoController,
decoration: const InputDecoration(labelText: 'Tambah todo baru (POST)', border: OutlineInputBorder()),
onSubmitted: (_) => _createTodo(),
),
),
const SizedBox(width: 8),
_loadingAction
? const SizedBox(width: 48, height: 48, child: Center(child: CircularProgressIndicator()))
: ElevatedButton(onPressed: _createTodo, child: const Text('Tambah')),
],
),
),
Expanded(
child: _todos.isEmpty
? FutureBuilder<List<TodoItem>>(
future: _futureTodos,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (snapshot.hasError) return Center(child: Text('Error: ${snapshot.error}'));
final list = snapshot.data ?? [];
if (list.isEmpty) return const Center(child: Text('Tidak ada data'));
return _buildList(list);
},
)
: _buildList(_todos),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// Show short info dialog about HTTP methods
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Metode HTTP ringkas'),
content: const Text('GET = membaca data\nPOST = membuat data baru\nPUT = memperbarui data (seluruh resource)\nDELETE = menghapus data'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Tutup'))],
),
);
},
child: const Icon(Icons.info_outline),
),
);
}
Widget _buildList(List<TodoItem> list) {
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: list.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final todo = list[index];
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
child: ListTile(
leading: Checkbox(value: todo.completed, onChanged: (_) => _toggleCompleted(todo)),
title: Text(todo.title, maxLines: 2, overflow: TextOverflow.ellipsis),
subtitle: Text('ID: ${todo.id}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(onPressed: () => _editTodoDialog(todo), icon: const Icon(Icons.edit)),
IconButton(onPressed: () => _deleteTodoConfirm(todo), icon: const Icon(Icons.delete)),
],
),
),
);
},
);
}
}