Flutter with Supabase
catatan apps dengan flutter dan supabase
Berikut adalah tutorial lengkap untuk membangun aplikasi Flutter dengan Supabase dari awal.
1. Pengaturan Proyek Supabase
Pertama, siapkan backend Anda di Supabase.
1.1. Buat Proyek Baru
Buka supabase.com dan buat akun atau login.
Klik "New Project" dan berikan nama serta kata sandi database yang kuat.
Tunggu beberapa menit hingga proyek Anda siap.
1.2. Dapatkan API Keys
Di dasbor proyek Anda, buka Project Settings (ikon roda gigi).
Pilih menu API.
Anda akan memerlukan Project URL dan anon public Key. Simpan keduanya untuk nanti.
1.3. Atur Tabel Database
Kita akan membuat dua tabel: profiles
untuk data pengguna dan notes
untuk catatan.
Buka Table Editor (ikon tabel) di menu sebelah kiri.
Klik "New table".
Buat tabel
profiles
:Nama tabel:
profiles
PENTING: Hapus centang pada "Enable Row Level Security (RLS)" untuk sementara. Kita akan mengaktifkannya nanti.
Tambahkan kolom-kolom berikut:
id
(tipeuuid
, Primary Key, defaultuuid_generate_v4()
). Atur ini sebagai Foreign Key keauth.users.id
.username
(tipetext
).avatar_url
(tipetext
, nullable).updated_at
(tipetimestampz
, nullable, defaultnow()
).
Klik Save.
Buat tabel
notes
:Nama tabel:
notes
Hapus centang pada "Enable Row Level Security (RLS)".
Tambahkan kolom-kolom berikut:
id
(tipeint8
, Primary Key, is Identity).user_id
(tipeuuid
). Atur ini sebagai Foreign Key keauth.users.id
.title
(tipetext
).content
(tipetext
).image_url
(tipetext
, nullable).created_at
(tipetimestampz
, defaultnow()
).
Klik Save.
cara menambah foreign key
1.4. Atur Penyimpanan (Storage)
Kita butuh dua bucket untuk menyimpan gambar.
Buka Storage (ikon kotak) di menu sebelah kiri.
Buat bucket untuk avatar:
Klik "New Bucket".
Nama bucket:
avatars
Biarkan sebagai Public bucket.
Buat bucket untuk gambar catatan:
Klik "New Bucket".
Nama bucket:
notes-images
Biarkan sebagai Public bucket.
setting permission atau policy storage






untuk uji coba, sementara disable confirmasi email


2. Pengaturan Proyek Flutter
Sekarang kita siapkan aplikasi Flutter.
2.1. Buat Proyek Flutter
Buka terminal dan jalankan:
Bash
flutter create flutter_supabase_crud
cd flutter_supabase_crud
2.2. Tambahkan Dependensi
Buka file pubspec.yaml
dan tambahkan dependensi berikut:
YAML
dependencies:
flutter:
sdk: flutter
supabase_flutter: ^2.5.3 # Versi terbaru mungkin berbeda
get: ^4.6.6
get_storage: ^2.1.1
image_picker: ^1.1.2
path_provider: ^2.1.3 # Dibutuhkan oleh supabase_flutter
Jalankan flutter pub get
di terminal.
2.3. Struktur Folder
Buat struktur folder berikut di dalam folder lib
:
lib/
├── main.dart
├── models/
│ ├── note_model.dart
│ └── profile_model.dart
├── screens/
│ ├── auth/
│ │ ├── login_screen.dart
│ │ └── register_screen.dart
│ ├── home/
│ │ ├── notes_list_screen.dart
│ │ └── note_form_screen.dart
│ ├── profile/
│ │ └── profile_screen.dart
│ └── splash_screen.dart
├── services/
│ └── supabase_service.dart
├── utils/
│ ├── app_routes.dart
│ └── constants.dart
└── widgets/
└── custom_input_field.dart
3. Konfigurasi Awal & Kode
Mari kita mulai menulis kode.
3.1. Simpan API Key (File Terpisah)
Buat file lib/utils/constants.dart
untuk menyimpan kredensial Supabase.
Dart
// lib/utils/constants.dart
// GANTI DENGAN URL DAN ANON KEY DARI PROYEK SUPABASE ANDA
const String supabaseUrl = 'https://YPUR_PROJECT_ID.supabase.co';
const String supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY';
3.2. Inisialisasi di main.dart
main.dart
Inisialisasi Supabase dan GetStorage.
Dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../utils/app_routes.dart';
import '../../utils/constants.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi GetStorage
await GetStorage.init();
// Inisialisasi Supabase
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey,
);
runApp(const MyApp());
}
// Helper global untuk akses mudah ke client Supabase
final supabase = Supabase.instance.client;
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Supabase',
theme: ThemeData(
primarySwatch: Colors.teal,
useMaterial3: true,
),
initialRoute: AppRoutes.splash,
getPages: AppRoutes.routes,
debugShowCheckedModeBanner: false,
);
}
}
3.3. Navigasi dengan GetX
Atur semua rute navigasi di satu tempat.
Dart
// lib/utils/app_routes.dart
import 'package:get/get.dart';
import '../../screens/auth/login_screen.dart';
import '../../screens/auth/register_screen.dart';
import '../../screens/home/note_form_screen.dart';
import '../../screens/home/notes_list_screen.dart';
import '../../screens/profile/profile_screen.dart';
import '../../screens/splash_screen.dart';
class AppRoutes {
static const String splash = '/';
static const String login = '/login';
static const String register = '/register';
static const String profile = '/profile';
static const String notes = '/notes';
static const String noteForm = '/note-form';
static final routes = [
GetPage(name: splash, page: () => const SplashScreen()),
GetPage(name: login, page: () => const LoginScreen()),
GetPage(name: register, page: () => const RegisterScreen()),
GetPage(name: profile, page: () => const ProfileScreen()),
GetPage(name: notes, page: () => const NotesListScreen()),
GetPage(name: noteForm, page: () => NoteFormScreen()),
];
}
4. Models (Struktur Data)
4.1. Profile Model
Dart
// lib/models/profile_model.dart
class Profile {
final String id;
final String username;
final String? avatarUrl;
Profile({
required this.id,
required this.username,
this.avatarUrl,
});
factory Profile.fromJson(Map<String, dynamic> json) {
return Profile(
id: json['id'],
username: json['username'],
avatarUrl: json['avatar_url'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'username': username,
'avatar_url': avatarUrl,
};
}
}
4.2. Note Model
Dart
// lib/models/note_model.dart
class Note {
final int id;
final String userId;
final String title;
final String content;
final String? imageUrl;
final DateTime createdAt;
Note({
required this.id,
required this.userId,
required this.title,
required this.content,
this.imageUrl,
required this.createdAt,
});
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['id'],
userId: json['user_id'],
title: json['title'],
content: json['content'],
imageUrl: json['image_url'],
createdAt: DateTime.parse(json['created_at']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'title': title,
'content': content,
'image_url': imageUrl,
'created_at': createdAt.toIso8601String(),
};
}
}
5. Service (Logika Supabase)
Pusatkan semua interaksi dengan Supabase di sini.
Dart
// lib/services/supabase_service.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../main.dart'; // Untuk akses `supabase` client
class SupabaseService {
// Upload Gambar (Bekerja untuk Web dan Mobile)
Future<String> uploadImage(
File file, String bucketName, String fileName) async {
final bytes = await file.readAsBytes();
await supabase.storage.from(bucketName).uploadBinary(
fileName,
bytes,
fileOptions: const FileOptions(cacheControl: '3600', upsert: false),
);
// Mendapatkan URL publik dari gambar yang diupload
final String publicUrl =
supabase.storage.from(bucketName).getPublicUrl(fileName);
return publicUrl;
}
// Versi lain untuk web menggunakan XFile
Future<String> uploadImageBytes(
Uint8List bytes, String bucketName, String fileName) async {
await supabase.storage.from(bucketName).uploadBinary(
fileName,
bytes,
fileOptions: const FileOptions(cacheControl: '3600', upsert: false),
);
final String publicUrl =
supabase.storage.from(bucketName).getPublicUrl(fileName);
return publicUrl;
}
// --- CRUD Catatan ---
Future<List<Map<String, dynamic>>> getNotes() async {
final userId = supabase.auth.currentUser!.id;
final data = await supabase
.from('notes')
.select()
.eq('user_id', userId)
.order('created_at', ascending: false);
return data;
}
Future<void> addNote({
required String title,
required String content,
String? imageUrl,
}) async {
final userId = supabase.auth.currentUser!.id;
await supabase.from('notes').insert({
'user_id': userId,
'title': title,
'content': content,
'image_url': imageUrl,
});
}
Future<void> updateNote({
required int id,
required String title,
required String content,
String? imageUrl,
}) async {
final updates = {
'title': title,
'content': content,
'image_url': imageUrl,
};
await supabase.from('notes').update(updates).eq('id', id);
}
Future<void> deleteNote(int id) async {
await supabase.from('notes').delete().eq('id', id);
}
// --- Profil Pengguna ---
Future<Map<String, dynamic>?> getProfile() async {
final userId = supabase.auth.currentUser!.id;
final data =
await supabase.from('profiles').select().eq('id', userId).single();
return data;
}
Future<void> updateProfile({
required String username,
String? avatarUrl,
}) async {
final userId = supabase.auth.currentUser!.id;
final updates = {
'id': userId,
'username': username,
'avatar_url': avatarUrl,
'updated_at': DateTime.now().toIso8601String(),
};
await supabase.from('profiles').upsert(updates);
}
}
6. Screens (Tampilan Aplikasi)
6.1. Splash Screen (Pengecekan Sesi)
supabase_flutter
secara otomatis menyimpan sesi. Kita hanya perlu mengecek apakah sesi ada atau tidak.
Dart
// lib/screens/splash_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../main.dart'; // Untuk akses `supabase`
import '../../utils/app_routes.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
// Tunggu frame pertama selesai render sebelum navigasi
WidgetsBinding.instance.addPostFrameCallback((_) {
_redirect();
});
}
Future<void> _redirect() async {
// Tunggu sedikit untuk menampilkan splash screen
await Future.delayed(const Duration(seconds: 1));
// `mounted` check
if (!mounted) return;
final session = supabase.auth.currentSession;
if (session != null) {
Get.offAllNamed(AppRoutes.profile);
} else {
Get.offAllNamed(AppRoutes.login);
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('Memuat...'),
],
),
),
);
}
}
6.2. Login Screen
Dart
// lib/screens/auth/login_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../main.dart';
import '../../utils/app_routes.dart';
import '../../widgets/custom_input_field.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
Future<void> _signIn() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
await supabase.auth.signInWithPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
if (mounted) {
Get.offAllNamed(AppRoutes.profile);
}
} on AuthException catch (e) {
Get.snackbar('Error', e.message,
backgroundColor: Colors.red, colorText: Colors.white);
} catch (e) {
Get.snackbar('Error', 'Terjadi kesalahan tidak terduga',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lock, size: 80, color: Colors.teal),
const SizedBox(height: 20),
CustomInputField(
controller: _emailController,
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !GetUtils.isEmail(value)) {
return 'Masukkan email yang valid';
}
return null;
},
),
const SizedBox(height: 16),
CustomInputField(
controller: _passwordController,
labelText: 'Password',
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password minimal 6 karakter';
}
return null;
},
),
const SizedBox(height: 24),
_isLoading
? const CircularProgressIndicator()
: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _signIn,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('Login'),
),
),
TextButton(
onPressed: () => Get.toNamed(AppRoutes.register),
child: const Text('Belum punya akun? Daftar di sini'),
),
],
),
),
),
),
);
}
}

6.3. Register Screen
Dart
// lib/screens/auth/register_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../main.dart';
import '../../widgets/custom_input_field.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _usernameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
Future<void> _signUp() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
final AuthResponse res = await supabase.auth.signUp(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
data: {'username': _usernameController.text.trim()},
);
if (res.user != null) {
await supabase.from('profiles').insert({
'id': res.user!.id,
'username': _usernameController.text.trim(),
});
}
if (mounted) {
Get.snackbar(
'Sukses',
'Pendaftaran berhasil! Silakan cek email Anda untuk konfirmasi.',
backgroundColor: Colors.green,
colorText: Colors.white,
);
Get.back(); // Kembali ke halaman login
}
} on AuthException catch (e) {
Get.snackbar('Error', e.message,
backgroundColor: Colors.red, colorText: Colors.white);
} catch (e) {
Get.snackbar('Error', 'Terjadi kesalahan tidak terduga: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Daftar')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.person_add, size: 80, color: Colors.teal),
const SizedBox(height: 20),
CustomInputField(
controller: _usernameController,
labelText: 'Username',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Masukkan username';
}
return null;
},
),
const SizedBox(height: 16),
CustomInputField(
controller: _emailController,
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !GetUtils.isEmail(value)) {
return 'Masukkan email yang valid';
}
return null;
},
),
const SizedBox(height: 16),
CustomInputField(
controller: _passwordController,
labelText: 'Password',
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password minimal 6 karakter';
}
return null;
},
),
const SizedBox(height: 24),
_isLoading
? const CircularProgressIndicator()
: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _signUp,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('Daftar'),
),
),
],
),
),
),
),
);
}
}

6.4. Profile Screen
Dart
// lib/screens/profile/profile_screen.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import '../../main.dart';
import '../../models/profile_model.dart';
import '../../services/supabase_service.dart';
import '../../utils/app_routes.dart';
import 'dart:io';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final SupabaseService _supabaseService = SupabaseService();
Profile? _profile;
bool _isLoading = true;
final _usernameController = TextEditingController();
@override
void initState() {
super.initState();
_loadProfile();
}
Future<void> _loadProfile() async {
setState(() {
_isLoading = true;
});
try {
final data = await _supabaseService.getProfile();
if (data != null) {
setState(() {
_profile = Profile.fromJson(data);
_usernameController.text = _profile!.username;
});
}
} catch (e) {
Get.snackbar('Error', 'Gagal memuat profil: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _updateProfile() async {
if (_usernameController.text.isEmpty) {
Get.snackbar('Error', 'Username tidak boleh kosong',
backgroundColor: Colors.red, colorText: Colors.white);
return;
}
setState(() {
_isLoading = true;
});
try {
await _supabaseService.updateProfile(username: _usernameController.text);
Get.snackbar('Sukses', 'Profil berhasil diperbarui',
backgroundColor: Colors.green, colorText: Colors.white);
_loadProfile();
} catch (e) {
Get.snackbar('Error', 'Gagal memperbarui profil: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _uploadAvatar() async {
final picker = ImagePicker();
final imageFile = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 300,
maxHeight: 300,
);
if (imageFile == null) return;
setState(() {
_isLoading = true;
});
try {
final fileName =
'${supabase.auth.currentUser!.id}/${DateTime.now().millisecondsSinceEpoch}.jpg';
String imageUrl;
if (kIsWeb) {
final imageBytes = await imageFile.readAsBytes();
imageUrl = await _supabaseService.uploadImageBytes(
imageBytes,
'avatars',
fileName,
);
} else {
final file = File(imageFile.path);
imageUrl = await _supabaseService.uploadImage(
file,
'avatars',
fileName,
);
}
await _supabaseService.updateProfile(
username: _usernameController.text,
avatarUrl: imageUrl,
);
_loadProfile();
Get.snackbar('Sukses', 'Avatar berhasil diupload',
backgroundColor: Colors.green, colorText: Colors.white);
} catch (e) {
Get.snackbar('Error', 'Gagal mengupload avatar: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _signOut() async {
await supabase.auth.signOut();
Get.offAllNamed(AppRoutes.login);
}
@override
void dispose() {
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profil Saya'),
actions: [
IconButton(onPressed: _signOut, icon: const Icon(Icons.logout))
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _profile == null
? const Center(child: Text('Gagal memuat profil.'))
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
GestureDetector(
onTap: _uploadAvatar,
child: CircleAvatar(
radius: 60,
backgroundImage: (_profile?.avatarUrl != null)
? NetworkImage(_profile!.avatarUrl!)
: null,
child: (_profile?.avatarUrl == null)
? const Icon(Icons.person, size: 60)
: null,
),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: _uploadAvatar,
icon: const Icon(Icons.camera_alt),
label: const Text('Ganti Avatar'),
),
const SizedBox(height: 20),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _updateProfile,
icon: const Icon(Icons.save),
label: const Text('Simpan Perubahan'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12)),
),
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => Get.toNamed(AppRoutes.notes),
icon: const Icon(Icons.note),
label: const Text('Lihat Catatan Saya'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12)),
),
),
],
),
),
);
}
}

6.5. Notes List Screen (Read & Delete)
Dart
// lib/screens/home/notes_list_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../models/note_model.dart';
import '../../services/supabase_service.dart';
import '../../utils/app_routes.dart';
class NotesListScreen extends StatefulWidget {
const NotesListScreen({super.key});
@override
State<NotesListScreen> createState() => _NotesListScreenState();
}
class _NotesListScreenState extends State<NotesListScreen> {
final SupabaseService _supabaseService = SupabaseService();
List<Note> _notes = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_fetchNotes();
}
Future<void> _fetchNotes() async {
setState(() {
_isLoading = true;
});
try {
final data = await _supabaseService.getNotes();
setState(() {
_notes = data.map((item) => Note.fromJson(item)).toList();
});
} catch (e) {
Get.snackbar('Error', 'Gagal memuat catatan: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _deleteNote(int id) async {
try {
await _supabaseService.deleteNote(id);
Get.snackbar('Sukses', 'Catatan berhasil dihapus',
backgroundColor: Colors.green, colorText: Colors.white);
_fetchNotes(); // Muat ulang daftar catatan
} catch (e) {
Get.snackbar('Error', 'Gagal menghapus catatan: $e',
backgroundColor: Colors.red, colorText: Colors.white);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Catatan Saya'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _notes.isEmpty
? const Center(
child: Text('Anda belum memiliki catatan. Buat satu!'),
)
: ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) {
final note = _notes[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: note.imageUrl != null
? Image.network(note.imageUrl!, width: 50, height: 50, fit: BoxFit.cover)
: const Icon(Icons.note, size: 40),
title: Text(note.title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(note.content, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () async {
// Tunggu hasil dari halaman edit, jika true, muat ulang
final result = await Get.toNamed(AppRoutes.noteForm, arguments: note);
if (result == true) {
_fetchNotes();
}
},
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _showDeleteConfirmation(note.id),
),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Get.toNamed(AppRoutes.noteForm);
if (result == true) {
_fetchNotes();
}
},
child: const Icon(Icons.add),
),
);
}
void _showDeleteConfirmation(int id) {
Get.defaultDialog(
title: "Hapus Catatan",
middleText: "Apakah Anda yakin ingin menghapus catatan ini?",
textConfirm: "Ya, Hapus",
textCancel: "Batal",
confirmTextColor: Colors.white,
onConfirm: () {
Get.back(); // Tutup dialog
_deleteNote(id);
},
);
}
}

6.6. Note Form Screen (Create & Update)
Dart
// lib/screens/home/note_form_screen.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import '../../main.dart';
import '../../models/note_model.dart';
import '../../services/supabase_service.dart';
import '../../widgets/custom_input_field.dart';
class NoteFormScreen extends StatefulWidget {
const NoteFormScreen({super.key});
@override
State<NoteFormScreen> createState() => _NoteFormScreenState();
}
class _NoteFormScreenState extends State<NoteFormScreen> {
final SupabaseService _supabaseService = SupabaseService();
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _contentController = TextEditingController();
bool _isLoading = false;
Note? _existingNote;
XFile? _imageFile;
String? _existingImageUrl;
@override
void initState() {
super.initState();
// Cek apakah kita sedang mengedit note yang sudah ada
if (Get.arguments is Note) {
_existingNote = Get.arguments as Note;
_titleController.text = _existingNote!.title;
_contentController.text = _existingNote!.content;
_existingImageUrl = _existingNote!.imageUrl;
}
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final imageFile = await picker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
setState(() {
_imageFile = imageFile;
});
}
}
Future<void> _submit() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
String? imageUrl = _existingImageUrl;
// Jika ada gambar baru yang dipilih, upload
if (_imageFile != null) {
final fileName =
'${supabase.auth.currentUser!.id}/${DateTime.now().millisecondsSinceEpoch}.jpg';
if (kIsWeb) {
final imageBytes = await _imageFile!.readAsBytes();
imageUrl = await _supabaseService.uploadImageBytes(
imageBytes, 'notes-images', fileName);
} else {
final file = File(_imageFile!.path);
imageUrl = await _supabaseService.uploadImage(
file, 'notes-images', fileName);
}
}
if (_existingNote != null) {
// Update note
await _supabaseService.updateNote(
id: _existingNote!.id,
title: _titleController.text,
content: _contentController.text,
imageUrl: imageUrl,
);
} else {
// Tambah note baru
await _supabaseService.addNote(
title: _titleController.text,
content: _contentController.text,
imageUrl: imageUrl,
);
}
// Kembalikan `true` untuk menandakan sukses
Get.back(result: true);
} catch (e) {
Get.snackbar('Error', 'Gagal menyimpan catatan: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_existingNote == null ? 'Tambah Catatan' : 'Edit Catatan'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Pratinjau Gambar
_imageFile != null
? (kIsWeb
? Image.network(_imageFile!.path, height: 150, fit: BoxFit.cover)
: Image.file(File(_imageFile!.path), height: 150, fit: BoxFit.cover))
: (_existingImageUrl != null
? Image.network(_existingImageUrl!, height: 150, fit: BoxFit.cover)
: Container(
height: 150,
color: Colors.grey[200],
child: const Icon(Icons.image, size: 50, color: Colors.grey),
)),
const SizedBox(height: 8),
TextButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.image),
label: const Text('Pilih Gambar'),
),
const SizedBox(height: 16),
CustomInputField(
controller: _titleController,
labelText: 'Judul',
validator: (value) => value!.isEmpty ? 'Judul tidak boleh kosong' : null,
),
const SizedBox(height: 16),
CustomInputField(
controller: _contentController,
labelText: 'Isi Catatan',
maxLines: 5,
validator: (value) => value!.isEmpty ? 'Isi catatan tidak boleh kosong' : null,
),
const SizedBox(height: 24),
_isLoading
? const Center(child: CircularProgressIndicator())
: ElevatedButton.icon(
onPressed: _submit,
icon: const Icon(Icons.save),
label: const Text('Simpan Catatan'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16)),
),
],
),
),
),
);
}
}

7. Custom Widget
Widget yang dapat digunakan kembali untuk input form.
Dart
// lib/widgets/custom_input_field.dart
import 'package:flutter/material.dart';
class CustomInputField extends StatelessWidget {
final TextEditingController controller;
final String labelText;
final bool obscureText;
final TextInputType keyboardType;
final String? Function(String?)? validator;
final int? maxLines;
const CustomInputField({
super.key,
required this.controller,
required this.labelText,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.validator,
this.maxLines = 1,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
validator: validator,
maxLines: maxLines,
decoration: InputDecoration(
labelText: labelText,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
filled: true,
fillColor: Colors.grey[100],
),
);
}
}
Jalankan aplikasi Anda dengan perintah flutter run
di terminal.
dapat juga "f5" atau run -> start debugging


Aplikasi sekarang memiliki semua fitur yang diminta: alur otentikasi, manajemen profil dengan gambar, dan CRUD lengkap untuk catatan dengan gambar.
link github https://github.com/triyono777/flutter_supabase_notes_app
apabila menggunakan git clone
jangan lupa
di terminal jalankan "flutter pub get"

apabila ada terkendala sdk silakan ubah berikut sesuai versi sdk kalian

Last updated
Was this helpful?