apk-kebutuhan-harian

Penjelasan Kode Flutter: Aplikasi Produktif

Ingin coba langsung aplikasi ini? Klik demo interaktif berikut:

Buka Demo Aplikasi Flutter di Zapp.page

Tentang Aplikasi

Aplikasi ini adalah tools produktif berbasis Flutter Web yang memiliki dua fitur utama:

  1. To-Do List – mencatat tugas dan menandainya jika selesai.
  2. Jadwal Mingguan – mencatat kegiatan harian setiap hari dalam seminggu.
Note: Aplikasi menggunakan localStorage lewat package dart:html untuk menyimpan data secara lokal di browser.

Fitur & Teknologi

  • Animasi Menu – menggunakan AnimationController & CurvedAnimation.
  • To-Do Interaktif – tambah, ubah, hapus tugas; gunakan checkbox untuk menandai selesai.
  • Jadwal Harian – atur kegiatan untuk setiap hari (Senin–Minggu).
  • Responsive UI – UI bersih dengan tema teal dan gradien lembut.
  • Persistensi Data – data disimpan di browser sehingga tetap ada meskipun reload.

Kode Dart Lengkap

Berikut adalah implementasi lengkapnya. Kamu bisa menyalinnya ke main.dart di project Flutter Web.


// *IMPORTS & MAIN*
import 'dart:convert';
import 'dart:html' as html;
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Aplikasi Produktif',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      useMaterial3: true,
    ),
    home: const HomePage(),
  );
}

// *HOME PAGE*
class HomePage extends StatefulWidget {
  const HomePage({super.key});
  @override
  State createState() => _HomePageState();
}

class _HomePageState extends State with SingleTickerProviderStateMixin {
  bool _isExpanded = false;
  late final AnimationController _controller;
  late final Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: const Duration(milliseconds: 300), vsync: this);
    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
  }

  @override
  void dispose() { _controller.dispose(); super.dispose(); }

  void _toggleExpansion() {
    setState(() {
      _isExpanded ? _controller.reverse() : _controller.forward();
      _isExpanded = !_isExpanded;
    });
  }

  Widget _buildMenuButton(IconData icon, String label, Widget page) => SizedBox(
    width: 250,
    child: ElevatedButton.icon(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.teal,
        foregroundColor: Colors.white,
        padding: const EdgeInsets.symmetric(vertical: 16),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      ),
      icon: Icon(icon),
      label: Text(label, style: const TextStyle(fontSize: 16)),
      onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => page)),
    ),
  );

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('Beranda'), centerTitle: true,
      backgroundColor: Colors.teal, foregroundColor: Colors.white,
    ),
    body: Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.teal.shade50, Colors.blue.shade50],
          begin: Alignment.topLeft, end: Alignment.bottomRight,
        ),
      ),
      child: Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          if (_isExpanded) ...[
            _buildMenuButton(Icons.check_circle, 'To-Do List', const TodoPage()),
            const SizedBox(height: 20),
            _buildMenuButton(Icons.calendar_month, 'Jadwal Mingguan', const JadwalPage()),
            const SizedBox(height: 20),
          ],
          SizedBox(
            width: 250,
            child: ElevatedButton.icon(
              style: ElevatedButton.styleFrom(
                backgroundColor: _isExpanded ? Colors.teal.shade700 : Colors.teal,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              ),
              icon: Icon(_isExpanded ? Icons.close : Icons.menu),
              label: Text(_isExpanded ? 'Tutup Menu' : 'Buka Menu', style: const TextStyle(fontSize: 16)),
              onPressed: _toggleExpansion,
            ),
          ),
        ]),
      ),
    ),
  );
}

// *TODO PAGE*
class TodoPage extends StatefulWidget {
  const TodoPage({super.key});
  @override
  State createState() => _TodoPageState();
}

class _TodoPageState extends State {
  static const _cacheKey = 'todoCacheV1';
  List todos = [];

  @override
  void initState() { super.initState(); _loadFromCache(); }

  void _saveToCache() {
    html.window.localStorage[_cacheKey] = jsonEncode(todos);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Perubahan tersimpan'), duration: Duration(milliseconds: 900))
    );
  }

  void _loadFromCache() {
    final raw = html.window.localStorage[_cacheKey];
    if (raw != null) setState(() => todos = List.from(jsonDecode(raw)));
  }

  void _addTodo() {
    String input = '';
    showDialog(
      context: context, builder: (context) => AlertDialog(
        title: const Text('Tambah To-Do'),
        content: TextField(autofocus: true, decoration: const InputDecoration(hintText: 'Tulis tugas...', border: OutlineInputBorder()), onChanged: (val) => input = val),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
          ElevatedButton(onPressed: () {
            if (input.trim().isNotEmpty) {
              setState(() => todos.add({'task': input.trim(), 'done': false}));
              _saveToCache();
            }
            Navigator.pop(context);
          }, child: const Text('Tambah')),
        ],
      )
    );
  }

  void _renameTodo(int idx) {
    String input = todos[idx]['task'];
    showDialog(
      context: context, builder: (context) => AlertDialog(
        title: const Text('Ubah To-Do'),
        content: TextField(controller: TextEditingController(text: input), autofocus: true, decoration: const InputDecoration(border: OutlineInputBorder()), onChanged: (val) => input = val),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
          ElevatedButton(onPressed: () {
            if (input.trim().isNotEmpty) {
              setState(() => todos[idx]['task'] = input.trim());
              _saveToCache();
            }
            Navigator.pop(context);
          }, child: const Text('Simpan')),
        ],
      )
    );
  }

  void _deleteTodo(int idx) {
    showDialog(
      context: context, builder: (context) => AlertDialog(
        title: const Text('Konfirmasi'),
        content: const Text('Yakin ingin menghapus to-do ini?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
          ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () {
            setState(() => todos.removeAt(idx));
            _saveToCache();
            Navigator.pop(context);
          }, child: const Text('Hapus', style: TextStyle(color: Colors.white))),
        ],
      )
    );
  }

  Widget _buildTodoItem(int idx) {
    final todo = todos[idx];
    return Card(
      color: Colors.teal.shade50,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      elevation: 2,
      child: ListTile(
        leading: Checkbox(
          value: todo['done'],
          onChanged: (val) { setState(() => todos[idx]['done'] = val ?? false); _saveToCache(); },
        ),
        title: Text(
          todo['task'],
          style: TextStyle(decoration: todo['done'] ? TextDecoration.lineThrough : null, color: todo['done'] ? Colors.grey : Colors.black),
        ),
        trailing: PopupMenuButton(
          icon: const Icon(Icons.more_vert),
          onSelected: (v) { if (v == 'rename') _renameTodo(idx); else if (v == 'delete') _deleteTodo(idx); },
          itemBuilder: (c) => const [
            PopupMenuItem(value: 'rename', child: Text('Rename')),
            PopupMenuItem(value: 'delete', child: Text('Delete')),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('To-Do List'), centerTitle: true, backgroundColor: Colors.teal, foregroundColor: Colors.white),
    body: Container(
      decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.teal.shade50, Colors.blue.shade50], begin: Alignment.topLeft, end: Alignment.bottomRight)),
      child: ListView.builder(itemCount: todos.length, padding: const EdgeInsets.all(12), itemBuilder: (c, i) => _buildTodoItem(i)),
    ),
    floatingActionButton: FloatingActionButton(backgroundColor: Colors.teal, onPressed: _addTodo, child: const Icon(Icons.add, color: Colors.white)),
  );
}

// *JADWAL PAGE*
class JadwalPage extends StatefulWidget {
  const JadwalPage({super.key});
  @override
  State createState() => _JadwalPageState();
}

class _JadwalPageState extends State {
  static const _cacheKey = 'jadwalCacheV1';
  final List hari = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
  final Map> jadwal = {};

  @override
  void initState() {
    super.initState();
    hari.forEach((h) => jadwal[h] = []);
    _loadFromCache();
  }

  void _showSavedSnack([String msg = 'Perubahan tersimpan']) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating, duration: const Duration(milliseconds: 900)),
    );
  }

  void _saveToCache() {
    final data = hari.fold>>({}, (map, h) {
      map[h] = List.from(jadwal[h]!);
      return map;
    });
    html.window.localStorage[_cacheKey] = jsonEncode(data);
    _showSavedSnack();
  }

  void _loadFromCache() {
    final raw = html.window.localStorage[_cacheKey];
    if (raw != null) {
      final decoded = jsonDecode(raw) as Map;
      setState(() {
        hari.forEach((h) {
          jadwal[h] = List.from(decoded[h] ?? []);
        });
      });
    }
  }

  void _tambahJadwal(String h) {
    String input = '';
    showDialog(context: context, builder: (_) => AlertDialog(
      title: Text('Tambah Jadwal untuk $h'),
      content: TextField(autofocus: true, decoration: const InputDecoration(hintText: 'Masukkan mata pelajaran atau kegiatan', border: OutlineInputBorder()), onChanged: (val) => input = val), actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
      ElevatedButton(onPressed: () {
        if (input.trim().isNotEmpty) {
          setState(() => jadwal[h]!.add(input.trim()));
          _saveToCache();
        }
        Navigator.pop(context);
      }, child: const Text('Tambah')),
    ]));
  }

  void _renameJadwal(String h, int idx) {
    final ctrl = TextEditingController(text: jadwal[h]![idx]);
    showDialog(context: context, builder: (_) => AlertDialog(
      title: const Text('Ubah Jadwal'),
      content: TextField(controller: ctrl, autofocus: true, decoration: const InputDecoration(border: OutlineInputBorder())), actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
      ElevatedButton(onPressed: () {
        final val = ctrl.text.trim();
        if (val.isNotEmpty) {
          setState(() => jadwal[h]![idx] = val);
          _saveToCache();
        }
        Navigator.pop(context);
      }, child: const Text('Simpan')),
    ]));
  }

  void _hapusJadwal(String h, int idx) {
    showDialog(context: context, builder: (_) => AlertDialog(
      title: const Text('Konfirmasi'),
      content: const Text('Yakin ingin menghapus jadwal ini?'), actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
      ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () {
        setState(() => jadwal[h]!.removeAt(idx));
        _saveToCache();
        Navigator.pop(context);
      }, child: const Text('Hapus', style: TextStyle(color: Colors.white))),
    ]));
  }

  Widget _buildCard(String h) => Card(
    color: Colors.teal.shade50,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    elevation: 3,
    child: Padding(
      padding: const EdgeInsets.all(12),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
          Text(h, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.teal)),
          IconButton(onPressed: () => _tambahJadwal(h), icon: const Icon(Icons.add_circle, color: Colors.teal), tooltip: 'Tambah'),
        ]),
        const Divider(),
        if (jadwal[h]!.isEmpty)
          const Center(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Belum ada jadwal', style: TextStyle(color: Colors.grey))))
        else
          ...jadwal[h]!.asMap().entries.map((e) {
            final i = e.key, item = e.value;
            return ListTile(
              contentPadding: EdgeInsets.zero,
              title: Text(item),
              trailing: PopupMenuButton(
                icon: const Icon(Icons.more_vert),
                onSelected: (v) { if (v == 'rename') _renameJadwal(h, i); else if (v == 'delete') _hapusJadwal(h, i); },
                itemBuilder: (_) => const [
                  PopupMenuItem(value: 'rename', child: Text('Rename')),
                  PopupMenuItem(value: 'delete', child: Text('Delete')),
                ],
              ),
            );
          }).toList(),
      ]),
    ),
  );

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Jadwal Mingguan'), centerTitle: true, backgroundColor: Colors.teal, foregroundColor: Colors.white),
    body: Container(
      decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.teal.shade50, Colors.blue.shade50], begin: Alignment.topLeft, end: Alignment.bottomRight)),
      child: ListView.builder(itemCount: hari.length, padding: const EdgeInsets.all(12), itemBuilder: (context, index) => _buildCard(hari[index])),
    ),
  );
}

Tips: Jika kamu ingin blog lebih keren, bisa tambahkan plugin syntax highlighter seperti PrismJS atau Highlight.js di Blogger untuk membuat code-nya benar-benar berdiri—warna-warnanya akan mirip seperti di IDE seperti VS Code.

Komentar

Postingan Populer