Finansal harcamalarınızı takip etmek, bütçenizi yönetmek ve gereksiz giderleri kısmak için etkili bir araca mı ihtiyacınız var? Dart Nesne Yönelimli Programlama (OOP) ilkelerini kullanarak sıfırdan bir gider yöneticisi uygulaması geliştirmeye ne dersiniz? Bu rehber, Dart’ın gücünü keşfederken, finansal takibinizi kolaylaştıracak pratik bir uygulama oluşturmanıza yardımcı olacak. Adım adım ilerleyecek, temel OOP kavramlarından başlayıp ileri seviye veri yönetimine kadar pek çok konuya değineceğiz.
Modern yaşamın karmaşıklığı içinde, finansal durumumuzu kontrol altında tutmak hiç bu kadar önemli olmamıştı. Her geçen gün artan harcamalar, sabit kalan gelirler ve hayat pahalılığı, bireyleri ve aileleri bütçelerini daha dikkatli yönetmeye itiyor. Ancak, hangi harcamaların gerçekten gerekli olduğunu, nereye ne kadar para harcadığımızı net bir şekilde görmek genellikle zordur. İşte tam da bu noktada bir gider yöneticisi uygulaması devreye girer. Bir gider yöneticisi, finansal alışkanlıklarımızı şeffaf hale getirerek, para akışımızı anlamamıza, gereksiz harcamaları tespit etmemize ve uzun vadeli finansal hedeflerimize ulaşmamıza yardımcı olan paha biçilmez bir araçtır.
Peki, böyle bir uygulamayı Dart Nesne Yönelimli Programlama (OOP) prensipleriyle geliştirmek neden bu kadar mantıklı? Dart, özellikle Flutter ile mobil ve web uygulamaları geliştirmek için popüler bir dil olmasına rağmen, temiz, okunabilir ve bakımı kolay kod yazmak için güçlü OOP yetenekleri sunar. Bu, karmaşık finansal verileri düzenli sınıflar ve nesneler halinde modellememize olanak tanır. Örneğin, her bir gideri bir nesne olarak düşünebiliriz; bu nesnenin bir miktarı, bir tarihi, bir açıklaması ve bir kategorisi olabilir. Bu sayede, finansal verileriniz sadece bir sayı yığını olmaktan çıkar, anlamlı ve yönetilebilir bilgi parçacıklarına dönüşür. Uygulamanız büyüdükçe veya yeni özellikler eklemek istediğinizde (örneğin, bütçeleme, raporlama, farklı para birimleri desteği), OOP’nin sağladığı modüler yapı sayesinde kodunuzu kolayca genişletebilir ve sürdürebilirsiniz. Bu ders serisinin dokuzuncu bölümü olarak, önceki bilgilerinizin üzerine inşa ederek, daha sofistike ve kullanıcı dostu bir gider yöneticisi oluşturma yolunda önemli adımlar atacağız. Bu bölümde, uygulamanın temel mimarisini ve veri modellerini Dart OOP’nin gücünü kullanarak nasıl oluşturacağımızı derinlemesine inceleyeceğiz. Finansal özerkliğinize giden yolda kodlamanın tadını çıkarın!
Dart’ta Nesne Yönelimli Programlamaya Hızlı Bir Bakış: Temel İlkeler Nelerdir?
Nesne Yönelimli Programlama (OOP), yazılım geliştirmede kullanılan güçlü bir paradigmadır. Gerçek dünyadaki varlıkları yazılım nesneleri olarak modellememizi sağlar, böylece kodumuzu daha anlaşılır, esnek ve bakımı kolay hale getiririz. Dart da güçlü bir OOP dilidir ve bu paradigmanın temel ilkelerini tamamen destekler. Bir gider yöneticisi gibi finansal bir uygulamayı geliştirirken, bu ilkeler bize büyük avantajlar sağlar. Her bir harcamanın veya gelirin bir nesne olarak düşünüldüğü, kategorilere ayrıldığı ve üzerinde işlemler yapıldığı bir sistem hayal edin. İşte tam bu noktada OOP’nin gücü ortaya çıkar.
Sınıflar ve Nesneler: Gerçek Dünyayı Kodlamak Nasıl Mümkün?
OOP’nin temel yapı taşları sınıflar ve nesnelerdir. Bir sınıf (Class), belirli bir türdeki nesnelerin özelliklerini (nitelikler/alanlar) ve davranışlarını (metotlar/fonksiyonlar) tanımlayan bir taslak veya şablondur. Örneğin, bir “Gider” sınıfı, her giderin sahip olması gereken özellikleri (miktar, açıklama, tarih, kategori) ve yapabileceği eylemleri (kaydetme, düzenleme, silme) tanımlayabilir. Bir nesne (Object) ise bu sınıfın somut bir örneğidir. Yani, “Gider” sınıfından türetilmiş bir nesne, belirli bir harcamayı (örneğin, “Market alışverişi, 200 TL, 15 Mart 2023, Gıda” gibi) temsil eder. Bu ayrım, kodunuzu düzenli ve modüler tutmanın anahtarıdır. Her nesne, kendi iç durumunu korur ve bu durum üzerinde tanımlanmış metotlar aracılığıyla işlem yapar.
Kapsülleme (Encapsulation), bir sınıfın iç detaylarını dış dünyadan gizleme prensibidir. Bir nesnenin verilerine (alanlarına) doğrudan erişimi kısıtlar ve bu verilere sadece o nesnenin kendi metotları aracılığıyla erişilmesini sağlar. Bu, veri bütünlüğünü korur ve kodun beklenmedik şekillerde değiştirilmesini önler. Dart’ta bu, genellikle özel (private) alanlar tanımlayarak ve bu alanlara erişim için getter/setter metotları veya uygun arayüzler sağlayarak yapılır. Bir giderin miktarını direkt olarak değiştirmek yerine, bir gider.miktarGuncelle(yeniMiktar) metodu kullanmak, hem verinin geçerliliğini kontrol etmenizi sağlar hem de gelecekteki değişiklikleri kolaylaştırır.
Kalıtım ve Arayüzler: Kod Tekrarını Azaltmanın Yolları Nelerdir?
Kalıtım (Inheritance), bir sınıfın (alt sınıf/türetilmiş sınıf) başka bir sınıfın (üst sınıf/temel sınıf) özelliklerini ve davranışlarını miras almasını sağlayan bir OOP ilkesidir. Bu, kod tekrarını azaltır ve hiyerarşik bir yapı oluşturarak kodun yeniden kullanılabilirliğini artırır. Örneğin, bir genel “İşlem” (Transaction) sınıfınız olabilir ve hem “Gider” hem de “Gelir” sınıfları bu “İşlem” sınıfından miras alabilir. Bu sayede, tarih veya miktar gibi ortak özellikleri her iki sınıfta da tekrar tekrar tanımlamak yerine bir kez tanımlamış olursunuz. Dart’ta kalıtım extends anahtar kelimesiyle sağlanır. Böylece, ortak işlevsellik tek bir yerde yönetilir ve tüm alt sınıflar otomatik olarak bu işlevsellikten faydalanır.
Çok Biçimlilik (Polymorphism), farklı sınıflara ait nesnelerin aynı arayüz üzerinden işlenebilmesi yeteneğidir. Yani, bir temel sınıf türünden bir referans, o temel sınıftan türetilmiş farklı alt sınıf nesnelerini işaret edebilir. Bu, esnek ve genişletilebilir kod yazmanıza olanak tanır. Gider yöneticisi örneğinde, bir List içinde hem Gider hem de Gelir nesnelerini tutabilir ve her birini aynı şekilde işleyebilirsiniz.
Soyutlama (Abstraction), sadece gerekli bilgileri gösterip, gereksiz detayları gizleme prensibidir. Dart’ta soyut sınıflar (abstract class) ve arayüzler (implements anahtar kelimesiyle belirtilen normal sınıflar veya abstract class‘lar) kullanılarak uygulanır. Bir soyut sınıf, diğer sınıfların miras alabileceği ve belirli metotları uygulamak zorunda kalacağı bir şablon sunar. Bu, farklı veri depolama mekanizmalarını (örneğin, yerel depolama, bulut depolama) tek bir VeriDepolayici arayüzü altında toplayarak kodunuzu daha esnek hale getirebilir. Her bir depolama türü, bu arayüzü uygulayarak kendi kaydetme ve yükleme mantığını içerebilir. Bu sayede, uygulamanızın temel veri yönetimi mantığını değiştirmeden farklı depolama çözümleri arasında geçiş yapabilirsiniz. Bu OOP prensipleri, gider yöneticisi uygulamamızın sağlam ve sürdürülebilir bir temele sahip olmasını sağlayacaktır.
Gider Yöneticisi Uygulamasında Temel Modelleri Oluşturmak: Hangi Sınıflara İhtiyacımız Var?
Bir gider yöneticisi uygulamasının kalbi, harcamaları ve ilgili verileri nasıl modelediğimizdir. Dart’ın Nesne Yönelimli Programlama (OOP) yetenekleri sayesinde, gerçek dünyadaki kavramları (giderler, kategoriler vb.) kodumuzda anlamlı sınıflara dönüştürebiliriz. Bu, hem kodun okunabilirliğini artırır hem de gelecekteki değişiklikleri ve genişletmeleri çok daha kolay hale getirir. Uygulamamız için ilk olarak temel veri modellerini tasarlamamız gerekiyor. Genellikle, bir giderin kendisi ve ait olduğu kategori gibi ana bileşenler öne çıkar.
Gider Sınıfı Tasarımı: Bir Gideri Neler Tanımlar?
Her bir gideri temsil edecek bir Gider sınıfı, uygulamamızın en merkezi parçalarından biridir. Bir gideri benzersiz kılan ve anlamlı kılan özellikleri düşünelim: miktarı, ne zaman yapıldığı, ne için yapıldığı (açıklama) ve hangi kategoriye ait olduğu. Bu özellikler, Gider sınıfımızın alanlarını (properties) oluşturacaktır.
İşte Gider sınıfının Dart’taki temel yapısı:
import 'package:uuid/uuid.dart'; // Benzersiz ID'ler için
import 'package:intl/intl.dart'; // Tarih formatlama için
class Gider {
final String id;
final double miktar;
final String aciklama;
final DateTime tarih;
final String kategori; // Şimdilik basit bir string olarak tutalım
Gider({
String? id,
required this.miktar,
required this.aciklama,
required this.tarih,
required this.kategori,
}) : id = id ?? const Uuid().v4(); // Eğer ID verilmezse yeni bir ID oluştur.
// Gider nesnesini kolayca yazdırmak için toString metodu
@override
String toString() {
final dateFormat = DateFormat('dd/MM/yyyy');
return 'ID: $id, Miktar: ${miktar.toStringAsFixed(2)} TL, Açıklama: $aciklama, Tarih: ${dateFormat.format(tarih)}, Kategori: $kategori';
}
// Gider nesnesini JSON formatına dönüştürme
Map toJson() {
return {
'id': id,
'miktar': miktar,
'aciklama': aciklama,
'tarih': tarih.toIso8601String(), // ISO 8601 formatında tarih saklama
'kategori': kategori,
};
}
// JSON'dan Gider nesnesi oluşturma
factory Gider.fromJson(Map json) {
return Gider(
id: json['id'] as String,
miktar: json['miktar'] as double,
aciklama: json['aciklama'] as String,
tarih: DateTime.parse(json['tarih'] as String),
kategori: json['kategori'] as String,
);
}
}
Bu Gider sınıfında şunları görüyoruz:
id: Her gideri benzersiz şekilde tanımlamak için bir ID kullanıyoruz. Bu, daha sonra bir gideri kolayca bulmamızı, güncellememizi veya silmemizi sağlar.uuidpaketi, bu tür benzersiz ID'leri üretmek için kullanışlıdır.miktar: Giderin sayısal değerini temsil eder (örneğin,double).aciklama: Giderin ne ile ilgili olduğunu anlatan kısa bir metin (örneğin,String).tarih: Giderin ne zaman yapıldığını belirten birDateTimenesnesi.kategori: Giderin hangi kategoriye ait olduğunu gösteren birString. İlerleyen bölümlerde bunu daha sofistike birKategorinesnesine dönüştürebiliriz.
Ayrıca, nesneleri daha okunabilir hale getirmek için bir toString() metodu ve verileri depolamak için gerekli olan toJson() ve fromJson() metotlarını ekledik. Bu metotlar, Gider nesnelerini JSON formatına dönüştürmeyi ve JSON'dan yeniden oluşturmayı sağlayarak, verilerimizi kalıcı hale getirmemizde kilit rol oynayacaklardır.
Kategori Sınıfı ve İlişkisel Yapılar: Giderleri Gruplamak Neden Önemli?
Giderleri sadece miktarı ve açıklamasıyla takip etmek yeterli değildir. Genellikle, harcamalarımızı belirli gruplara ayırarak bütçe takibi ve analiz yapmak isteriz. Örneğin, "Yiyecek", "Ulaşım", "Eğlence", "Fatura" gibi kategoriler, finansal alışkanlıklarımızı daha iyi anlamamızı sağlar. Bu nedenle, bir Kategori sınıfı tanımlamak da uygulamamız için oldukça önemlidir.
Kategori sınıfının temel yapısı şu şekilde olabilir:
import 'package:uuid/uuid.dart';
class Kategori {
final String id;
final String ad;
final double? butceLimiti; // Her kategoriye özel bir bütçe limiti olabilir
Kategori({
String? id,
required this.ad,
this.butceLimiti,
}) : id = id ?? const Uuid().v4();
@override
String toString() {
return 'ID: $id, Ad: $ad, Bütçe Limiti: ${butceLimiti != null ? butceLimiti.toStringAsFixed(2) + ' TL' : 'Yok'}';
}
// Kategori nesnesini JSON formatına dönüştürme
Map toJson() {
return {
'id': id,
'ad': ad,
'butceLimiti': butceLimiti,
};
}
// JSON'dan Kategori nesnesi oluşturma
factory Kategori.fromJson(Map json) {
return Kategori(
id: json['id'] as String,
ad: json['ad'] as String,
butceLimiti: json['butceLimiti'] as double?,
);
}
}
Burada Kategori sınıfı şunları içerir:
id: Her kategoriyi benzersiz şekilde tanımlamak için.ad: Kategorinin adı (örneğin, "Gıda", "Ulaşım").butceLimiti: Bu kategori için belirlenen opsiyonel bir bütçe limiti. Bu, kullanıcıların belirli harcama alanlarında kendilerini sınırlamalarına yardımcı olabilir.
Şu anda Gider sınıfındaki kategori alanını basit bir String olarak tuttuk. Ancak, gerçek bir uygulamada, Gider sınıfının Kategori sınıfından bir nesneye referans vermesi daha iyi bir ilişkisel modelleme olacaktır. Bu, kategorilerin yönetilmesini (ekleme, düzenleme, silme) ve giderlerle tutarlı bir şekilde ilişkilendirilmesini kolaylaştırır. Örneğin:
// Gider sınıfını güncelleyelim
// import 'package:uuid/uuid.dart';
// import 'package:intl/intl.dart';
// class Gider {
// final String id;
// final double miktar;
// final String aciklama;
// final DateTime tarih;
// final Kategori kategori; // Kategori nesnesine referans
// Gider({
// String? id,
// required this.miktar,
// required this.aciklama,
// required this.tarih,
// required this.kategori,
// }) : id = id ?? const Uuid().v4();
// @override
// String toString() {
// final dateFormat = DateFormat('dd/MM/yyyy');
// return 'ID: $id, Miktar: ${miktar.toStringAsFixed(2)} TL, Açıklama: $aciklama, Tarih: ${dateFormat.format(tarih)}, Kategori: ${kategori.ad}';
// }
// // toJson ve fromJson metodları da Kategori nesnesini içerecek şekilde güncellenmeli
// Map toJson() {
// return {
// 'id': id,
// 'miktar': miktar,
// 'aciklama': aciklama,
// 'tarih': tarih.toIso8601String(),
// 'kategori': kategori.toJson(), // Kategori nesnesini de JSON'a dönüştür
// };
// }
// factory Gider.fromJson(Map json) {
// return Gider(
// id: json['id'] as String,
// miktar: json['miktar'] as double,
// aciklama: json['aciklama'] as String,
// tarih: DateTime.parse(json['tarih'] as String),
// kategori: Kategori.fromJson(json['kategori'] as Map), // JSON'dan Kategori oluştur
// );
// }
// }
Bu şekilde, Gider ve Kategori sınıfları arasında daha güçlü ve anlamlı bir ilişki kurmuş oluruz. Bu yapı, hem veri bütünlüğünü sağlar hem de uygulama içinde verilerle çalışmayı çok daha tutarlı hale getirir. Örneğin, bir kategorinin adını değiştirdiğinizde, o kategoriye ait tüm giderlerin de otomatik olarak yeni kategori adına sahip olmasını sağlamak daha kolaylaşır. Bu modelleme, uygulamamızın temelini oluşturacak ve Dart OOP'nin gücünü en iyi şekilde kullanmamızı sağlayacaktır.
Veri Yönetimi ve Kalıcılık: Gider Bilgilerini Nasıl Saklarız?
Uygulamamızın en kritik ihtiyaçlarından biri, kullanıcıların girdiği gider bilgilerini kalıcı olarak saklayabilmektir. Kullanıcı uygulamayı kapattığında tüm verilerin kaybolması, bir gider yöneticisi için kabul edilemez bir durumdur. Bu nedenle, verilerimizi bir dosyaya (veya daha gelişmiş sistemlerde bir veritabanına) yazma ve oradan okuma mekanizmaları oluşturmamız gerekir. Bu bölümde, Dart'ın dosya okuma/yazma yeteneklerini ve nesnelerimizi veri formatına dönüştürmek için JSON'ı nasıl kullanacağımızı inceleyeceğiz. Bu sayede, uygulamanız her açıldığında kaldığınız yerden devam edebilecek, tüm giderleriniz ve kategorileriniz güvenle saklanabilecektir.
Giderleri JSON Formatında Saklamak: Neden ve Nasıl?
JSON (JavaScript Object Notation), hafif, okunabilir ve veri alışverişi için yaygın olarak kullanılan bir formattır. Nesne yönelimli programlamadaki nesneleri temsil etmek için doğal bir yapısı vardır ve Dart'ta nesneleri JSON'a dönüştürmek (serileştirme) ve JSON'dan nesneye dönüştürmek (deserileştirme) oldukça kolaydır. JSON kullanmak, verilerinizi insanlar tarafından okunabilir bir formatta saklamanızı sağlar ve gelecekte farklı platformlarla (web arayüzleri, diğer uygulamalar, API'ler) veri alışverişi yapmanız gerektiğinde büyük esneklik sunar. Dart'ın dart:convert kütüphanesi, bu işlemleri yapmak için güçlü araçlar sağlar.
Öncelikle, tüm giderlerimizi ve kategorilerimizi yöneten bir sınıf oluşturalım. Bu sınıf, verileri bellekte tutacak ve bunları diske kaydetme/yükleme işlemlerini yönetecek. Aşağıdaki örnekte, temel bir GiderYonetici sınıfı ve dosya işlemlerini göreceksiniz:
import 'dart:convert'; // JSON işlemleri için
import 'dart:io'; // Dosya işlemleri için
import 'package:path_provider/path_provider.dart'; // Uygulama dizini için (Flutter/mobil)
import 'package:uuid/uuid.dart';
import 'package:intl/intl.dart';
// Yukarıdaki Gider ve Kategori sınıflarını buraya eklediğinizi varsayalım
class GiderYonetici {
List _giderler = [];
List _kategoriler = [];
static const String _giderDosyasi = 'giderler.json';
static const String _kategoriDosyasi = 'kategoriler.json';
// Getter'lar
List get giderler => [..._giderler]; // Değiştirilemez bir kopya döndür
List get kategoriler => [..._kategoriler];
GiderYonetici() {
_kategoriler = [ // Varsayılan kategoriler
Kategori(ad: 'Gıda'),
Kategori(ad: 'Ulaşım'),
Kategori(ad: 'Eğlence'),
Kategori(ad: 'Fatura'),
Kategori(ad: 'Giyim'),
];
}
// Dosya yolunu bulma (uygulamanın çalıştığı ortama göre değişebilir)
Future _getLocalPath() async {
// Bu örnek bir Flutter uygulaması için path_provider kullanır.
// Konsol uygulaması için './' veya spesifik bir dizin kullanabilirsiniz.
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
// Verileri dosyaya kaydetme
Future verileriKaydet() async {
try {
final path = await _getLocalPath();
final giderDosyasi = File('$path/$_giderDosyasi');
final kategoriDosyasi = File('$path/$_kategoriDosyasi');
// Giderleri JSON formatına dönüştür ve kaydet
final giderlerJson = jsonEncode(_giderler.map((g) => g.toJson()).toList());
await giderDosyasi.writeAsString(giderlerJson);
// Kategorileri JSON formatına dönüştür ve kaydet
final kategorilerJson = jsonEncode(_kategoriler.map((k) => k.toJson()).toList());
await kategoriDosyasi.writeAsString(kategorilerJson);
print('Veriler başarıyla kaydedildi.');
} catch (e) {
print('Verileri kaydederken hata oluştu: $e');
}
}
// Verileri dosyadan yükleme
Future verileriYukle() async {
try {
final path = await _getLocalPath();
final giderDosyasi = File('$path/$_giderDosyasi');
final kategoriDosyasi = File('$path/$_kategoriDosyasi');
// Giderleri yükle
if (await giderDosyasi.exists()) {
final giderlerString = await giderDosyasi.readAsString();
final List decodedGiderler = jsonDecode(giderlerString);
_giderler = decodedGiderler.map((json) => Gider.fromJson(json as Map)).toList();
}
// Kategorileri yükle
if (await kategoriDosyasi.exists()) {
final kategorilerString = await kategoriDosyasi.readAsString();
final List decodedKategoriler = jsonDecode(kategorilerString);
_kategoriler = decodedKategoriler.map((json) => Kategori.fromJson(json as Map)).toList();
}
print('Veriler başarıyla yüklendi.');
} catch (e) {
print('Verileri yüklerken hata oluştu: $e');
}
}
// Gider ekleme
void giderEkle(Gider gider) {
_giderler.add(gider);
print('Gider eklendi: $gider');
}
// Kategori ekleme
void kategoriEkle(Kategori kategori) {
if (!_kategoriler.any((k) => k.ad.toLowerCase() == kategori.ad.toLowerCase())) {
_kategoriler.add(kategori);
print('Kategori eklendi: $kategori');
} else {
print('Bu kategori zaten mevcut.');
}
}
// Belirli bir kategoriye ait giderleri filtreleme
List giderleriFiltrele(String kategoriAdi) {
return _giderler.where((g) => g.kategori.toLowerCase() == kategoriAdi.toLowerCase()).toList();
}
// Gider silme (ID ile)
bool giderSil(String id) {
final initialLength = _giderler.length;
_giderler.removeWhere((g) => g.id == id);
return _giderler.length < initialLength;
}
}
path_provider paketi genellikle Flutter uygulamalarında dosya yollarını yönetmek için kullanılır. Konsol tabanlı bir Dart uygulaması için, Directory.current.path kullanarak uygulamanın çalıştığı dizine erişebilirsiniz. Bu, uygulamaların farklı ortamlarda sorunsuz çalışmasını sağlamak için önemlidir.
Yukarıdaki kodda:
_getLocalPath(): Verileri saklayacağımız dizinin yolunu belirler. Mobil uygulamalar içinpath_providerpaketi kullanılırken, masaüstü veya konsol uygulamaları için farklı yaklaşımlar gerekebilir.verileriKaydet():_giderlerve_kategorilerlistelerindeki her bir nesneyitoJson()metoduyla JSON'a dönüştürür, ardından tüm bu JSON nesnelerini bir listeye alıpjsonEncodeile tek bir JSON string'ine çevirir ve bir dosyaya yazar.verileriYukle(): Dosyadan JSON string'ini okur,jsonDecodeile Dart nesnelerine (List) çevirir ve her biriniGider.fromJson()veyaKategori.fromJson()metotları ile kendi sınıf nesnelerine dönüştürerek listelere ekler.try-catchblokları, dosya okuma/yazma sırasında oluşabilecek hataları yönetmek için kullanılır, böylece uygulama beklenmedik bir şekilde çökmez.
Bu yapı sayesinde, gider yöneticisi uygulamamız kullanıcıların verilerini güvenle saklayabilir ve her açılışta bu verilere erişebilir. Bu, uygulamanın temel işlevselliği için vazgeçilmez bir adımdır ve kullanıcı deneyimini önemli ölçüde artırır. Ayrıca, JSON formatı sayesinde verilerin taşınabilirliği ve diğer sistemlerle entegrasyonu da oldukça kolaylaşır. Bu model, Dart OOP'nin nesne modelleme ve serileştirme gücünü bir araya getirerek, sağlam bir veri yönetim katmanı oluşturmanın pratik bir örneğidir.
Uygulamayı Geliştirme ve Genişletme: İleri Seviye Özellikler Eklemek Nasıl Mümkün?
Temel Gider ve Kategori sınıflarımızı oluşturup verileri kaydetme ve yükleme mekanizmalarını kurduktan sonra, uygulamamızı daha kullanışlı hale getirecek ileri seviye özellikler eklemeye başlayabiliriz. Bu aşamada, Nesne Yönelimli Programlamanın (OOP) bize sunduğu esneklik ve genişletilebilirlik prensiplerini daha derinlemesine kullanacağız. Uygulamamıza raporlama, filtreleme, güncelleme gibi işlevler eklemek, kullanıcı deneyimini zenginleştirirken, kodumuzun modüler yapısını korumamızı sağlayacaktır.
Farklı Raporlama Yöntemleri ve Veri Analizi: Giderlerden Anlam Çıkarmak
Bir gider yöneticisinin en değerli özelliklerinden biri, kullanıcının harcama alışkanlıklarını analiz edebilmesi ve anlamlı raporlar sunabilmesidir. Kullanıcılar, belirli bir dönemdeki toplam harcamalarını, en çok para harcadıkları kategorileri veya aylık bütçe limitlerine ne kadar yaklaştıklarını görmek isteyebilirler. Bu tür raporlama işlevlerini, GiderYonetici sınıfımıza metotlar ekleyerek veya daha ileri giderek, Çok Biçimlilik (Polymorphism) kullanarak farklı raporlama stratejileri oluşturarak geliştirebiliriz.
İlk olarak, GiderYonetici sınıfımıza bazı temel analiz metotları ekleyelim:
// GiderYonetici sınıfının devamı...
class GiderYonetici {
// ... (önceki alanlar ve metotlar) ...
// Belirli bir kategoriye ait toplam gideri hesaplama
double kategoriyeGoreToplamGider(String kategoriAdi) {
return _giderler
.where((g) => g.kategori.toLowerCase() == kategoriAdi.toLowerCase())
.fold(0.0, (toplam, gider) => toplam + gider.miktar);
}
// Belirli bir tarih aralığındaki toplam gideri hesaplama
double tarihAraligiToplamGider(DateTime baslangic, DateTime bitis) {
return _giderler
.where((g) => g.tarih.isAfter(baslang.subtract(Duration(days: 1))) && g.tarih.isBefore(bitis.add(Duration(days: 1))))
.fold(0.0, (toplam, gider) => toplam + gider.miktar);
}
// En çok harcama yapılan kategorileri bulma (basit bir örnek)
Map enCokHarcamaYapilanKategoriler() {
final Map kategoriHarcamalari = {};
for (var gider in _giderler) {
kategoriHarcamalari.update(gider.kategori, (value) => value + gider.miktar, ifAbsent: () => gider.miktar);
}
// Harcaması en yüksek olanı üste al
var sortedList = kategoriHarcamalari.entries.toList()
..sort((e1, e2) => e2.value.compareTo(e1.value));
return Map.fromEntries(sortedList);
}
// Gider güncelleme (ID ile)
bool giderGuncelle(String id, {double? miktar, String? aciklama, DateTime? tarih, String? kategori}) {
final index = _giderler.indexWhere((g) => g.id == id);
if (index != -1) {
final existingGider = _giderler[index];
_giderler[index] = Gider(
id: existingGider.id,
miktar: miktar ?? existingGider.miktar,
aciklama: aciklama ?? existingGider.aciklama,
tarih: tarih ?? existingGider.tarih,
kategori: kategori ?? existingGider.kategori,
);
print('Gider güncellendi: ${_giderler[index]}');
return true;
}
return false;
}
}
Bu metotlar, kullanıcıların genel harcama eğilimlerini anlamalarına yardımcı olacak temel raporlar sunar. Ancak, daha karmaşık raporlar (örneğin, aylık bütçe takibi, gider grafikleri) için daha soyut bir yaklaşım benimseyebiliriz. Bu, Soyutlama (Abstraction) ve Çok Biçimlilik (Polymorphism) ilkelerinin gücünü gösterir.
Farklı raporlama türleri için bir arayüz (veya soyut sınıf) tanımlayabiliriz:
// Raporlama Stratejisi Arayüzü
abstract class RaporlamaStratejisi {
String raporUret(List giderler);
}
// Aylık Özeti Raporu
class AylikOzetRaporu implements RaporlamaStratejisi {
@override
String raporUret(List giderler) {
final Map aylikToplamlar = {};
for (var gider in giderler) {
final ayYil = DateFormat('MM/yyyy').format(gider.tarih);
aylikToplamlar.update(ayYil, (value) => value + gider.miktar, ifAbsent: () => gider.miktar);
}
StringBuffer rapor = StringBuffer('--- Aylık Gider Özeti ---\n');
aylikToplamlar.forEach((ayYil, toplam) {
rapor.writeln('$ayYil: ${toplam.toStringAsFixed(2)} TL');
});
return rapor.toString();
}
}
// Kategoriye Göre Detaylı Rapor
class KategoriDetayRaporu implements RaporlamaStratejisi {
@override
String raporUret(List giderler) {
final Map kategoriToplamlari = {};
for (var gider in giderler) {
kategoriToplamlari.update(gider.kategori, (value) => value + gider.miktar, ifAbsent: () => gider.miktar);
}
StringBuffer rapor = StringBuffer('--- Kategoriye Göre Gider Özeti ---\n');
kategoriToplamlari.forEach((kategori, toplam) {
rapor.writeln('$kategori: ${toplam.toStringAsFixed(2)} TL');
});
return rapor.toString();
}
}
Bu yapıyı kullanarak, GiderYonetici sınıfı, bir RaporlamaStratejisi nesnesini alıp rapor üretebilir:
// GiderYonetici sınıfının içine eklenecek metot
String raporUret(RaporlamaStratejisi strateji) {
return strateji.raporUret(_giderler);
}
Şimdi, farklı raporlar oluşturmak için sadece yeni bir raporlama stratejisi sınıfı yazmamız yeterli olacaktır, GiderYonetici sınıfının içini değiştirmemize gerek kalmaz. Bu, Açık/Kapalı prensibine (Open/Closed Principle) uygun, bakımı kolay ve genişletilebilir bir tasarımdır. Bu sayede uygulamanız, sürekli olarak yeni raporlama ihtiyaçlarına adaptasyon sağlayabilirken, mevcut kodunuzun kararlılığını korur.
Bu ileri düzey yaklaşımlar, Dart OOP'nin gücünü kullanarak uygulamalarınızı daha modüler, test edilebilir ve sürdürülebilir hale getirmenizi sağlar. Kullanıcıların ihtiyaçları değiştikçe veya yeni analiz yöntemleri ortaya çıktıkça, bu esnek yapı sayesinde uygulamanızı kolayca adapte edebilir ve geliştirebilirsiniz.
Mobil Uyumluluk ve Kullanıcı Deneyimi: Kodumuz Her Yerde Nasıl Çalışır?
Dart, Flutter sayesinde mobil uygulama geliştirmede oldukça popülerdir ve yazdığımız OOP modelleri direkt olarak Flutter uygulamalarında kullanılabilir. Ancak, eğer verilerimizi web üzerinde de sunmak istersek veya bir Dart backend'i (örneğin, Dart Frog veya Shelf ile) kullanarak basit bir HTML arayüzü oluşturuyorsak, bu HTML içeriğinin mobil cihazlarda da düzgün görünmesi kritik öneme sahiptir. Kullanıcı deneyimi, ekran boyutuna bakılmaksızın tutarlı ve erişilebilir olmalıdır.
Bu bölümde, Dart ile oluşturduğumuz gider verilerini basit bir web sayfasında nasıl sergileyebileceğimizi ve bu sayfanın mobil cihazlarda nasıl daha iyi bir deneyim sunabileceğini CSS medya sorguları (media queries) kullanarak göstereceğiz. Bu, Dart'ın esnekliğinin ve verilerinizin farklı platformlarda nasıl kullanılabileceğinin bir kanıtıdır. Unutmayın ki bu bir Flutter UI örneği değil, Dart'ın ürettiği bir HTML çıktısının responsivliğini gösterme örneğidir.
Gider Verilerini HTML'de Gösterme ve Mobil İçin Optimize Etme
Varsayalım ki Dart backend'iniz, GiderYonetici sınıfınızdaki verileri alıp aşağıdaki gibi basit bir HTML tablosu oluşturuyor. Bu tablo, kullanıcıların giderlerini liste halinde görmesini sağlar. Bu HTML'in mobil cihazlarda da okunaklı olması için CSS medya sorgularını kullanacağız.
Gider Yöneticisi Raporu
Gider Raporu
ID
Miktar
Açıklama
Tarih
Kategori
a1b2c3d4
150.00 TL
Market Alışverişi
2023-10-26
Gıda
e5f6g7h8
50.00 TL
Benzin
2023-10-25
Ulaşım
i9j0k1l2
80.00 TL
Sinema Bileti
2023-10-24
Eğlence
Yukarıdaki HTML ve CSS kodunda, @media (max-width: 768px) medya sorgusu, ekran genişliği 768 piksel veya daha az olduğunda devreye girer. Bu durumda:
- Tablo elemanları (
table,thead,tbody,th,td,tr) blok seviyesinde gösterilir. Bu, normalde yan yana duran sütunların alt alta gelmesini sağlar. - Tablo başlıkları (
thead tr) gizlenir. - Her bir satır (
tr) bir blok elemanına dönüşür ve aralarında boşluk bırakılır. - Her bir hücre (
td), içeriğinin yanında gerçek başlığını gösteren bir:beforesözde-elemanı alır. Bu sayede, mobil ekranda bile hangi bilginin neye ait olduğu açıkça anlaşılır.
etiketi, web sayfalarınızın mobil cihazlarda doğru şekilde ölçeklenmesi için hayati öneme sahiptir. Bu etiketi kullanmak, sayfanın genişliğinin cihazın genişliğine eşit olmasını ve başlangıç yakınlaştırma seviyesinin 1.0 olmasını sağlar.
Bu yöntem, Dart ile elde ettiğiniz verileri sadece konsol çıktıları olarak değil, aynı zamanda kullanıcı dostu ve erişilebilir web arayüzleri aracılığıyla da sunabileceğinizi gösterir. Bu esneklik, Dart'ın ve OOP prensiplerinin, uygulamanızın farklı platformlarda başarılı olmasına nasıl katkıda bulunduğunun harika bir örneğidir. Geliştirdiğiniz finansal model ne kadar sağlam olursa olsun, son kullanıcının bu verilere kolayca erişip etkileşime geçebilmesi, uygulamanızın başarısı için kritik bir faktördür.
Sonuç: Dart ile Gider Yönetimi Maceranızın Devamı Nasıl Olacak?
Bu makalede, Dart Nesne Yönelimli Programlama (OOP) ilkelerini kullanarak başlangıç seviyesinde bir gider yöneticisi uygulaması geliştirmeyi adım adım inceledik. İlk olarak, finansal takibin neden önemli olduğunu ve OOP'nin bu karmaşık verileri nasıl daha iyi modelleyebileceğini tartıştık. Ardından, Dart'taki temel OOP kavramlarını – sınıflar, nesneler, kapsülleme, kalıtım ve çok biçimlilik – gerçek dünya senaryolarıyla ilişkilendirerek ele aldık. Gider ve Kategori gibi ana veri modellerimizi nasıl tasarlayacağımızı gördük, bu sınıfların alanlarını ve davranışlarını belirledik. Veri kalıcılığını sağlamak için JSON serileştirme ve dosya işlemleriyle ilgili pratik örnekler sunarak, uygulamamızın verileri güvenle saklayıp geri yüklemesini sağladık. İleri seviye özellikler eklemek için raporlama stratejileri ve dinamik veri analizi metotlarını inceledik, böylece uygulamanın genişletilebilirliğini ve sürdürülebilirliğini artırdık. Son olarak, Dart tarafından üretilen verilerin mobil uyumlu bir HTML arayüzünde nasıl görüntülenebileceğini CSS medya sorguları ile örneklendirerek, uygulamanızın farklı platformlardaki erişilebilirliğine değindik.
Bu rehber, Dart ile güçlü, bakımı kolay ve esnek bir gider yöneticisi uygulamasının temelini atmanıza yardımcı olmayı amaçladı. Ancak bu sadece başlangıç! Öğrendiğiniz bu temel prensipleri kullanarak uygulamanızı çok daha ileri taşıyabilirsiniz. Örneğin, kullanıcı arayüzü olarak Flutter ile zengin bir mobil uygulama geliştirebilir, verilerinizi yerel dosya yerine bir SQLite veritabanında (sqflite paketi ile) veya Firebase gibi bulut tabanlı bir çözümde saklayabilirsiniz. Ayrıca, daha karmaşık bütçeleme kuralları, otomatik harcama etiketleme, çoklu para birimi desteği gibi özellikler ekleyerek uygulamanızın yeteneklerini genişletebilirsiniz. Unutmayın, en iyi öğrenme yolu uygulamaktır. Bu bilgileri kendi projelerinizde kullanarak Dart ve OOP konusundaki becerilerinizi pekiştirmeye devam edin. Kodlamaya devam edin, finansal bilgeliğinizi artırın!
Sıkça Sorulan Sorular (SSS)
- Dart neden Nesne Yönelimli Programlama (OOP) için iyi bir seçimdir?
- Dart, temiz ve sezgisel bir sözdizimine sahiptir, bu da OOP kavramlarını öğrenmeyi kolaylaştırır. Ayrıca, tamamen nesne yönelimli bir dil olması (her şey bir nesnedir), güçlü kapsülleme, kalıtım, çok biçimlilik ve soyutlama gibi temel OOP prensiplerini doğal bir şekilde uygulamanızı sağlar. Flutter ile entegrasyonu sayesinde mobil ve web uygulamalarında OOP modellerini doğrudan kullanabilmesi de büyük bir avantajdır.
- Bu gider yöneticisi uygulamasını mobil uygulamaya dönüştürebilir miyim?
- Kesinlikle! Bu makalede oluşturduğumuz
Gider,KategoriveGiderYoneticigibi sınıflar, Dart dilinde yazıldığı için Flutter ile geliştirilecek bir mobil uygulamanın arka plan iş mantığı (business logic) olarak doğrudan kullanılabilir. Tek yapmanız gereken, Flutter widget'larını kullanarak bu sınıflar üzerinden kullanıcı arayüzünü (UI) tasarlamak ve etkileşimleri sağlamaktır. Veri kalıcılığı içinpath_providerzaten Flutter için düşünülmüştür, ancaksqflitegibi yerel veritabanı çözümlerini de tercih edebilirsiniz. - Verilerimi JSON yerine bir veritabanında saklamak daha mı iyidir?
- Daha büyük ve karmaşık uygulamalar için genellikle bir veritabanı (SQL veya NoSQL) kullanmak daha iyidir. JSON dosyaları küçük ölçekli uygulamalar ve basit veri yapıları için yeterli olsa da, milyonlarca gider kaydını yönetmek, karmaşık sorgular çalıştırmak veya çok kullanıcılı bir ortamda veri bütünlüğünü sağlamak için veritabanları (örneğin, SQLite, PostgreSQL, MongoDB) daha performanslı ve güvenilirdir. Veritabanları, aynı zamanda veri doğrulama, indeksleme ve gelişmiş raporlama yetenekleri de sunar.
- Makale başlığındaki "Bölüm 9" ne anlama geliyor?
- "Bölüm 9", bu makalenin bir ders serisinin veya vaka çalışmasının devamı niteliğinde olduğunu gösterir. Bu, okuyucunun önceki bölümlerdeki temel bilgileri zaten edinmiş olabileceğini varsayar, ancak bu makale yeni başlayanlar için de anlaşılır olacak şekilde temel OOP kavramlarını tekrar ederek ve adım adım örneklerle ilerleyerek bağımsız bir rehber niteliğindedir. Bu bölümde, gider yöneticisi uygulamasının temel veri modellemesi ve kalıcılık katmanının daha gelişmiş yönlerine odaklanılmıştır.