Flutter'da SQLite ile Lokal Veritabanı Kullanımı

Mobil uygulama geliştirirken, kullanıcıların internet bağlantısı olmasa bile verilere erişebilmesi (offline-first yaklaşımı) harika bir kullanıcı deneyimi sunar. Kullanıcı ayarları, favori içerikler veya uygulamaya özgü kayıtlar gibi verileri cihazın kendi hafızasında tutmak için lokal veritabanlarına ihtiyaç duyarız.

Flutter ekosisteminde Hive, Isar ve Shared Preferences gibi farklı veri saklama yöntemleri olsa da, ilişkisel veritabanı (RDBMS) mantığını ve SQL sorgularını kullanmak isteyenler için standart çözüm SQLite'tır. Flutter'da bu işlemleri sqflite paketi ile gerçekleştiririz.

Bu makalede, bir "Öğrenci Takip Sistemi" senaryosu üzerinden veritabanı oluşturma, veri ekleme, okuma, güncelleme ve silme (CRUD) işlemlerini adım adım inceleyeceğiz.

1. Gerekli Paketlerin Kurulumu

Projeye başlamadan önce pub.dev üzerinden gerekli paketleri pubspec.yaml dosyamıza eklememiz gerekiyor. Veritabanı işlemleri için sqflite, veritabanı dosyasının cihazdaki yolunu bulmak için ise path paketini kullanacağız.

Terminalinize şu komutu yazarak paketleri projeye dahil edebilirsiniz:

Bash:
flutter pub add sqflite path

Veya pubspec.yaml dosyanızın dependencies kısmına manuel olarak ekleyebilirsiniz:

YAML:
dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.3.0
  path: ^1.8.3

2. Veri Modelinin Oluşturulması

Veritabanına kaydedeceğimiz verileri Dart nesnelerine dönüştürmek kodumuzu çok daha yönetilebilir kılar. Bir Student (Öğrenci) modeli oluşturalım.

Veritabanına yazarken (Map'e dönüştürme) ve veritabanından okurken (Map'ten nesneye dönüştürme) kullanacağımız yardımcı metodları modele ekliyoruz.

Dart:
// Geliştirici: Nuri TIRAŞ
// Dosya: student_model.dart

class Student {
  final int? id;
  final String name;
  final String grade;
  final int number;

  Student({
    this.id,
    required this.name,
    required this.grade,
    required this.number,
  });

  // Veritabanına kaydetmek için nesneyi Map'e çevirir
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'grade': grade,
      'number': number,
    };
  }

  // Veritabanından gelen Map'i nesneye çevirir
  factory Student.fromMap(Map<String, dynamic> map) {
    return Student(
      id: map['id'],
      name: map['name'],
      grade: map['grade'],
      number: map['number'],
    );
  }
}

3. Veritabanı Yardımcı Sınıfı (DatabaseHelper)

Uygulamanın farklı yerlerinden veritabanına aynı anda erişmeye çalışmak çakışmalara yol açabilir. Bunu önlemek için Singleton tasarım desenini kullanarak sadece tek bir veritabanı bağlantısı (instance) oluşturan bir DatabaseHelper sınıfı yazacağız.

Bu sınıf, tüm CRUD (Create, Read, Update, Delete) operasyonlarını yönetecek.

Dart:
// Geliştirici: Nuri TIRAŞ
// Dosya: database_helper.dart

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'student_model.dart';

class DatabaseHelper {
  // Singleton kurulumu
  DatabaseHelper._init();
  static final DatabaseHelper instance = DatabaseHelper._init();

  static Database? _database;

  // Veritabanı bağlantısını getirir, yoksa oluşturur
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB('school.db');
    return _database!;
  }

  // Veritabanı dosyasını cihazda oluşturma ve yolları belirleme
  Future<Database> _initDB(String filePath) async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, filePath);

    return await openDatabase(
      path,
      version: 1,
      onCreate: _createDB,
    );
  }

  // Tabloları oluşturma (SQL Sorgusu)
  Future _createDB(Database db, int version) async {
    const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
    const textType = 'TEXT NOT NULL';
    const intType = 'INTEGER NOT NULL';

    await db.execute('''
CREATE TABLE students (
  id $idType,
  name $textType,
  grade $textType,
  number $intType
  )
''');
  }

  // CREATE: Yeni öğrenci ekleme
  Future<int> insertStudent(Student student) async {
    final db = await instance.database;
    return await db.insert('students', student.toMap());
  }

  // READ: Tüm öğrencileri listeleme
  Future<List<Student>> getAllStudents() async {
    final db = await instance.database;
    
    // orderBy ile isme göre alfabetik sıralama yapıyoruz
    final result = await db.query('students', orderBy: 'name ASC');
    
    return result.map((json) => Student.fromMap(json)).toList();
  }

  // UPDATE: Öğrenci bilgilerini güncelleme
  Future<int> updateStudent(Student student) async {
    final db = await instance.database;
    return await db.update(
      'students',
      student.toMap(),
      where: 'id = ?',
      whereArgs: [student.id], // Güvenlik için whereArgs kullanılır (SQL Injection önlemi)
    );
  }

  // DELETE: Öğrenci silme
  Future<int> deleteStudent(int id) async {
    final db = await instance.database;
    return await db.delete(
      'students',
      where: 'id = ?',
      whereArgs: [id],
    );
  }
  
  // Veritabanı bağlantısını kapatma
  Future close() async {
    final db = await instance.database;
    db.close();
  }
}

4. Kullanıcı Arayüzü (UI) ile Entegrasyon

Arka plan işlemlerini hallettikten sonra, bu verileri Flutter ekranında göstermek oldukça kolaydır. Veritabanından gelen asenkron verileri beklerken ekranda bir yüklenme ikonu göstermek için FutureBuilder widget'ı biçilmiş kaftandır.

İşte basit bir liste ekranı kullanımı:

Dart:
// Geliştirici: Nuri TIRAŞ
// Dosya: student_list_screen.dart

import 'package:flutter/material.dart';
import 'database_helper.dart';
import 'student_model.dart';

class StudentListScreen extends StatefulWidget {
  @override
  _StudentListScreenState createState() => _StudentListScreenState();
}

class _StudentListScreenState extends State<StudentListScreen> {
  late Future<List<Student>> _studentsFuture;

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

  // Veritabanından verileri tekrar çeker ve arayüzü günceller
  void _refreshStudents() {
    setState(() {
      _studentsFuture = DatabaseHelper.instance.getAllStudents();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Öğrenci Listesi'),
      ),
      body: FutureBuilder<List<Student>>(
        future: _studentsFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Bir hata oluştu: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return const Center(child: Text('Kayıtlı öğrenci bulunamadı.'));
          }

          final students = snapshot.data!;

          return ListView.builder(
            itemCount: students.length,
            itemBuilder: (context, index) {
              final student = students[index];
              return ListTile(
                leading: CircleAvatar(child: Text(student.grade)),
                title: Text(student.name),
                subtitle: Text('Okul No: ${student.number}'),
                trailing: IconButton(
                  icon: const Icon(Icons.delete, color: Colors.red),
                  onPressed: () async {
                    await DatabaseHelper.instance.deleteStudent(student.id!);
                    _refreshStudents(); // Silme işleminden sonra listeyi yenile
                  },
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // Örnek bir veri ekleme işlemi
          final newStudent = Student(
            name: 'Ali Yılmaz',
            grade: '11-A',
            number: 1453,
          );
          await DatabaseHelper.instance.insertStudent(newStudent);
          _refreshStudents();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Sonuç ve İpuçları

Flutter'da sqflite paketi, arka planda C dilinde yazılmış oldukça performanslı olan standart SQLite kütüphanesini kullanır. Bu sayede karmaşık sorgular (JOIN, GROUP BY vb.) çalıştırabilir, binlerce satır veriyi saniyeler içinde filtreleyebilirsiniz.

Önemli Best-Practice Kuralları:

  1. Veritabanı sorgularını hiçbir zaman doğrudan UI kodunun içinde (örneğin butonun onPressed fonksiyonunda raw SQL olarak) yazmayın. Daima DatabaseHelper gibi aracı sınıflar (Repository pattern) kullanın.

  2. SQL Injection saldırılarından korunmak için sorgularınızda where: 'id = $id' yerine where: 'id = ?', whereArgs: [id] yapısını tercih edin.

  3. Uygulamanız kapandığında bellek sızıntılarını (memory leak) önlemek için gerekli durumlarda veritabanı bağlantısını close() metodu ile kapatmayı unutmayın.

Yorumlar

Bu blogdaki popüler yayınlar

Dart Programlama Dil Uygulama Sınavı Çalışma Soruları

Dart Uygulama Sınavı: Pardus ETAP 23 Kurulum Otomasyonu

Pardus ETAP 23 İçin Flutter ile Dijital "Öğrenci Seçici" Uygulaması