Hari pertama saya mau bikin aplikasi mobile, saya langsung bingung harus mulai dari mana. React Native? Flutter? Kotlin? SwiftUI? Pilihannya banyak banget dan masing-masing punya fanboy-nya sendiri. Setelah coba beberapa framework, saya akhirnya jatuh cinta sama Flutter dan saya mau cerita kenapa, plus cara bikin aplikasi pertama kamu: Todo List.
Kenapa Todo List? Karena ini projek sempurna buat pemula. Kamu belajar state management, input handling, layout design, dan CRUD operations semuanya dalam satu aplikasi sederhana. Nggak perlu bikin Uber clone di hari pertama.
Flutter itu framework dari Google yang pakai bahasa Dart. Kalau kamu pernah pakai Java atau JavaScript, Dart bakal terasa familiar. Beberapa alasan saya pilih Flutter untuk project pertama:
Sebelum mulai coding, kita perlu install Flutter SDK dulu. Saya pakai Ubuntu, tapi langkahnya mirip untuk macOS dan Windows.
# Download Flutter SDK (Linux) git clone https://github.com/flutter/flutter.git -b stable # Tambahkan ke PATH echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.bashrc source ~/.bashrc # Verifikasi instalasi flutter doctor Perintah flutter doctor akan cek semua dependencies yang kamu butuhkan. Kalau ada yang belum ke-install, dia bakal kasih tahu persis apa yang kurang. Biasanya yang sering missing adalah Android SDK atau Chrome untuk web development.
VS Code lebih ringan dan saya pribadi lebih suka pakai ini. Install extension Flutter dan Dart dari marketplace. Kalau kamu mau pakai Android Studio juga nggak masalah tinggal install plugin Flutter-nya.
# Buat project baru flutter create todo_app # Masuk ke direktori project cd todo_app # Jalankan di browser (paling gampang buat pemula) flutter run -d chrome Sekarang kamu udah punya aplikasi Flutter pertama yang jalan di browser. Coba ubah teks di lib/main.dart dan lihat perubahannya.
Sebelum kita mulai coding, penting untuk paham struktur project Flutter:
lib/main.dart Entry point aplikasi kamu. Semua kode utama ada di sini.pubspec.yaml File konfigurasi, seperti package.json di Node.js. Di sini kamu declare dependencies dan assets.android/ dan ios/ Konfigurasi native untuk masing-masing platform.test/ Directory untuk unit tests.Simpel kan? Flutter nggak bikin kamu pusing dengan konfigurasi yang ribet. Sekarang mari kita coding!
Kita mulai dari halaman utama. Buka lib/main.dart dan ganti seluruh isinya dengan kode berikut:
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Todo List App', debugShowCheckedModeBanner: false, theme: ThemeData( colorSchemeSeed: Colors.indigo, useMaterial3: true, brightness: Brightness.light, ), home: const TodoListScreen(), ); } } Ini adalah boilerplate awal. Kita setup MaterialApp dengan tema Material 3 yang modern. debugShowCheckedModeBanner: false buat ilangin tulisan "DEBUG" di pojok kanan atas.
Setiap todo item punya beberapa data: teks, status selesai, dan waktu dibuat. Kita buat class sederhana untuk ini:
class TodoItem { String title; bool isCompleted; DateTime createdAt; TodoItem({ required this.title, this.isCompleted = false, DateTime? createdAt, }) : createdAt = createdAt ?? DateTime.now(); } Class ini sangat sederhana cuma 3 properties. isCompleted default-nya false karena setiap todo baru belum selesai. createdAt otomatis diisi waktu saat todo dibuat.
Sekarang kita buat halaman utama yang menampilkan list todo dan form untuk menambah baru:
class TodoListScreen extends StatefulWidget { const TodoListScreen({super.key}); @override State<TodoListScreen> createState() => _TodoListScreenState(); } class _TodoListScreenState extends State<TodoListScreen> { final List<TodoItem> _todos = []; final TextEditingController _controller = TextEditingController(); int _selectedIndex = 0; // List halaman untuk bottom navigation final List<Widget> _pages = []; @override void initState() { super.initState(); _pages.add(_buildTodoList()); _pages.add(_buildCompletedList()); } void _addTodo() { if (_controller.text.trim().isEmpty) return; setState(() { _todos.add(TodoItem(title: _controller.text.trim())); _controller.clear(); }); } void _toggleTodo(int index) { setState(() { _todos[index].isCompleted = !_todos[index].isCompleted; }); } void _deleteTodo(int index) { setState(() { _todos.removeAt(index); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('My Todo List'), centerTitle: true, ), body: _pages[_selectedIndex], floatingActionButton: FloatingActionButton( onPressed: _showAddTodoDialog, child: const Icon(Icons.add), ), bottomNavigationBar: NavigationBar( selectedIndex: _selectedIndex, onDestinationSelected: (index) { setState(() => _selectedIndex = index); }, destinations: const [ NavigationDestination( icon: Icon(Icons.list_alt), label: 'Active', ), NavigationDestination( icon: Icon(Icons.check_circle_outline), label: 'Completed', ), ], ), ); } } Perhatikan beberapa hal penting di sini:
StatefulWidget karena data berubah-ubah. Kalau cuma tampilan statis, StatelessWidget cukup..dispose() kalau sudah nggak dipakai biar nggak memory leak.Ini bagian yang paling seru menampilkan list todo dalam bentuk cards yang bisa di-scroll:
Widget _buildTodoList() { final pendingTodos = _todos .where((todo) => !todo.isCompleted) .toList(); if (pendingTodos.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_task, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'Belum ada todo. Tap + untuk menambah!', style: TextStyle(color: Colors.grey, fontSize: 16), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: pendingTodos.length, itemBuilder: (context, index) { final todo = pendingTodos[index]; return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: Checkbox( value: todo.isCompleted, onChanged: (_) => _toggleTodo(_todos.indexOf(todo)), ), title: Text( todo.title, style: const TextStyle(fontSize: 16), ), subtitle: Text( 'Dibuat: ${_formatDate(todo.createdAt)}', style: const TextStyle(fontSize: 12), ), trailing: IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _deleteTodo(_todos.indexOf(todo)), ), ), ); }, ); } Widget _buildCompletedList() { final completedTodos = _todos .where((todo) => todo.isCompleted) .toList(); if (completedTodos.isEmpty) { return const Center( child: Text( 'Belum ada todo yang selesai.', style: TextStyle(color: Colors.grey, fontSize: 16), ), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: completedTodos.length, itemBuilder: (context, index) { final todo = completedTodos[index]; return Card( color: Colors.grey.shade100, margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: Checkbox( value: todo.isCompleted, onChanged: (_) => _toggleTodo(_todos.indexOf(todo)), ), title: Text( todo.title, style: const TextStyle( fontSize: 16, decoration: TextDecoration.lineThrough, color: Colors.grey, ), ), trailing: IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _deleteTodo(_todos.indexOf(todo)), ), ), ); }, ); } String _formatDate(DateTime date) { return '${date.day}/${date.month}/${date.year}'; } Beberapa konsep penting di bagian ini:
Sekarang kita buat dialog popup saat user menekan tombol (+):
void _showAddTodoDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Tambah Todo Baru'), content: TextField( controller: _controller, autofocus: true, decoration: const InputDecoration( hintText: 'Apa yang mau kamu kerjakan?', border: OutlineInputBorder(), ), onSubmitted: (_) { _addTodo(); Navigator.of(context).pop(); }, ), actions: [ TextButton( onPressed: () { _controller.clear(); Navigator.of(context).pop(); }, child: const Text('Batal'), ), FilledButton( onPressed: () { _addTodo(); Navigator.of(context).pop(); }, child: const Text('Tambah'), ), ], ), ); } Dialog ini pakai AlertDialog bawaan Flutter. autofocus: true bikin keyboard langsung muncul. onSubmitted handle kalau user pencet Enter di keyboard langsung submit tanpa perlu tap tombol.
Todo list yang bagus harusnya nggak hilang kalau aplikasi di-close. Kita pakai package shared_preferences untuk simpan data di local storage:
# Tambahkan di pubspec.yaml dependencies: flutter: sdk: flutter shared_preferences: ^2.2.0 import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; class TodoStorage { static const String _key = 'todos'; static Future<List<TodoItem>> loadTodos() async { final prefs = await SharedPreferences.getInstance(); final jsonString = prefs.getString(_key); if (jsonString == null) return []; final List<dynamic> jsonList = json.decode(jsonString); return jsonList.map((json) => TodoItem( title: json['title'], isCompleted: json['isCompleted'], createdAt: DateTime.parse(json['createdAt']), )).toList(); } static Future<void> saveTodos(List<TodoItem> todos) async { final prefs = await SharedPreferences.getInstance(); final jsonList = todos.map((todo) => { 'title': todo.title, 'isCompleted': todo.isCompleted, 'createdAt': todo.createdAt.toIso8601String(), }).toList(); await prefs.setString(_key, json.encode(jsonList)); } } Sederhana tapi powerful. Kita convert list todo ke JSON string dan simpan di SharedPreferences. Kalau mau scale lebih besar, bisa pakai database seperti SQLite atau Hive yang lebih performant untuk data kompleks.
Supaya aplikasi kelihatan lebih pro, tambahkan beberapa style improvements:
// Di bagian theme theme: ThemeData( colorSchemeSeed: Colors.indigo, useMaterial3: true, brightness: Brightness.light, inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), cardTheme: CardTheme( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), Dengan InputDecorationTheme dan CardTheme, semua input dan card di seluruh aplikasi bakal punya style yang konsisten. Nggak perlu set borderRadius satu per satu di setiap widget.
Container, Row, Column, Stack, ListView dulu.const supaya Flutter bisa optimize performa.Setelah aplikasi selesai dan diuji, berikut langkah-langkah deploy ke Play Store:
# Build APK untuk testing flutter build apk --release # Build App Bundle untuk Play Store (recommended) flutter build appbundle --release File output ada di build/app/outputs/. Untuk App Bundle, kamu perlu setup signing dulu. Google sekarang wajibkan AAB (Android App Bundle) untuk upload ke Play Store APK nggak diterima lagi untuk app baru.
Biaya publish ke Play Store cuma $25 sekali seumur hidup murah banget dibanding potensi penghasilan dari aplikasi yang sukses.
Beberapa tools yang saya pakai sehari-hari saat development Flutter:
Flutter itu framework yang sangat approachable untuk pemula. Dalam waktu beberapa jam, kamu udah bisa bikin aplikasi Todo List yang fungsional dan terlihat profesional. Yang paling penting adalah mulai jangan tunggu sempurna baru mulai coding.
Dari Todo List ini, kamu bisa eksplor lebih jauh: tambahkan fitur kategori, reminder, atau sinkronisasi ke cloud. Setiap fitur baru bakal ajarin kamu konsep Flutter yang lebih advanced.
Sekarang giliran kamu! Sudah coba Flutter belum? Atau masih ragu pindah dari framework mobile lain? Tulis pengalaman kamu di kolom komentar saya penasaran framework mobile apa yang jadi pilihan kamu dan kenapa!