Flutter Web REST API — Materi & Proyek (main.dart + panduan)).

 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)),

              ],

            ),

          ),

        );

      },

    );

  }

}


Share:

Tidak ada komentar:

Posting Komentar

Blogger Tricks

Blogger Themes