Local Data Storage with Flutter

Tayyip Guzel
Flutter Students Club
10 min readFeb 23, 2022

Herkese Selamlar,

Bu yazımda mobil uygulamaların olmazsa olmazı veri depolama işlemlerinden bahsedeceğim. Kullandığım teknolojileri bol teknik kod dahilinde sizlere aktarmaya çalışacağım. Keyifli okumalar dilerim.

Anlatacağım konuların başlıkları ile başlayalım🔥

  • Path Provider
  • Shared Prefeferences
  • Secure Storage
  • Hive
  • SQLite
  • Floor

görüldüğü gibi harika 6 adet konu başlığımız bulunuyor. Haydi başlayalım..

Path Provider

Bu oluşturduğum başlıklar aslında pub.dev içerisinde bulunan paket isimleridir. Flutter ile direkt olarak dosya sistemine erişemiyoruz. Bu yüzden bu tarz plugin ve paketlerden yararlanıyoruz. Bu paketi de Flutter ekibi geliştirmektedir.

Bu paketin genel amacı dosya sistemine erişmek, veri yazıp , veri okumaktır. Bu paket ile internet üzerinden pdf,mp4 dosyaları indirebilirsiniz. Biz ise bu paket ile dosya dizininde bir txt dosyayı oluşturup bunun içine veri yazmayı ve yazdığımız veriyi okumayı göreceğiz.

Lets code🔥

İlk olarak paket ekleyerek başlayalım. Vs code kullanıyorsanız benim çok beğendiğim bir paket ekleme yöntemi var. Sizlerle de bunu paylaşmak istiyorum. Editör üzerinde ctrl+shift+p tuşlarına basın.

Command Palette

Burada Dart:Add Dependency kısmını seçiyoruz.

Command Palette

Burada istediğimiz paketin adını seçiyoruz ve paketi pubspec.yaml içerisine ekliyor o da yetmezmiş gibi flutter pub get komutunu da çalıştırıyor ve paket kullanıma hazır hale geliyor. 🚀

Android Studio veya IntelliJ kullananlar için bilindik yöntemler ile paketi projeye dahil ediyoruz.

dependencies:
path_provider: ^2.0.9

flutter pub get komutunu çalıştırınız.

Path provider ile platform bağımsız bir şekilde dosya dizinine erişebiliyoruz. Android ve IOS için 2 farklı kısıma da erişim sağlanabiliyor. Temporary Directory cache olarak da bildiğimiz önbellek dosyalarının bulunduğu kısım diyebiliriz. Document Directory ise yalnızca uygulamanın erişebildiği dizindir. Biz Document Directory `e veri yazacağız.

Path provider ile dosyaya veri yazarken ve okurken 4 adımda ilerliyoruz.

1- Dosyayı yazacağımız yerel dizine ulaşmak

2- Dosyanın bulunduğu kısımdan bir referans oluşturmak

3- Veriyi yazmak

4- Yazdığımız veriyi okumak

Local Path

Bu şekilde yerel dizine ulaşıyoruz. directory.path bize dosya yolunu veriyor.

Local File

Bu kısımda ise yukarıda oluşturduğumuz fonksiyonu da kulanarak o dosya dizininde bize bir referans oluşturuyoruz. Bu referansı kullanarak dosyaya yazma ve okuma işlemlerini gerçekleştireceğiz.

Burada kullandığımız File ve Directory sınıfları dart:io kitaplığına ait sınıflardır.

Writing Data

Buradaki örnekte Textfieldüzerinden aldığım String değeri dosyaya yazıyoruz.

Asenkron işlemler yaptığımız yerlerde try-catch bloklarını kullanıyoruz.

Son olarak da bu yazığımız veriyi okumalıyız.

Reading Data

Okuma işlemini gerçekleştirirken yine referansımızı kullanarak okuyoruz ve okuduğum değeri de ekranda Text widget’a yazdırıyorum. Örneğin tamamını görmeniz için github reposuna tıklayınız.

Shared Prefeferences

Bu konunun en çok kullanılan paketlerinden biri ile devam ediyoruz. Gördüğüm bir çok projede bu yapıya yer veriliyor. Basit verileri key-value çiftleri olarak kaydediyor.

int, double, String ,bool , List<String> tipinde verileri saklar.

Örnek vermek gerekirse bir onboard ekranımız mevcut ve biz bunu sadece uygulama ilk açıldığında göstermek istiyoruz. Bunu uygulamanın ilk kez açıldığını bizim bir yerde tutmamız gerekiyor. Firebase veya database tarafında bunu tutmamız çok doğru olmaz bunun yerine SharedPreferences içerisinde tutmamız daha kolay olacaktır.

Uygulama için kritik verilerin saklanması önerilmez.⚠️

Platformlara bağlı olarak kayıt ettiği lokasyonlar şu şekilde;

Android-Shared Preferences

IOS-NSUserDefaults

Web-Local Storage

Windows- AppData Directory

Native olarak bu alanlarda çalışmış kişiler için bu lokasyonlar pek tanıdık gelecektir.

Shared Preferences için sevdiğim bir kullanım yöntemi mevcut ve bunu sizinle de paylaşmak istiyorum. İlk olarak Helper yazmamız gerekiyor. Ben helper diyorum fakat siz bunu manager olarak da duymuş olabilirsiniz veya util olarak da duymuş olabilirsiniz.

Shared Preferences Helper

Bu yapıyı üzerinde anlatılacak çok güzel konular bulunuyor. İlk olarak Singleton pattern kullanarak bu sınıfı oluşturdum. Peki nedir bu singleton pattern?

Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.

Refactoring.guru sitesinden alıntıladığım bu cümlede de dediği gibi oluşturduğumuz sınıfın tek bir erişim noktası olmasını istiyorsak ve bu sınıfı birçok yerde kullanacaksak yapmamız gereken şey tam olarak budur. Ayrıntı için inceleyiniz.

Shared Preferences kullanımına gelirsek bu sınıfın içerisinde getInstance() metodunu çağırmamız gerekiyor ve geri dönen Shared Preferences nesnesi ile de işlemlerimizi yapabiliriz. Bu metodu da Helper sınıfının isimlendirilmiş kurucusunda(named constructor) çağırıyoruz.

Sırada ise kayıt edilecek ve okunacak veriler için bize uygun fonksiyonları yazmaya.. Bunun için bir interface yazıp bu fonksiyonları içerinde belirtip Helper sınıfına kalıtım verebiliriz veya bu şekilde de kullanabiliriz. Bu örneği de yine github reposu üzerinden iceleyebilirsiniz.

Secure Storage

Secure Storage , Shared ile çok benzer yapıdadır. Adından da anlaşıldığı gibi güvenlidir. Şifreleme ile verileri saklar. IOS için Keychain serivisini, Android için ise Keystore servisini kullanır. Secure storage 5.0.0 ile birlikte Android için EncryptedSharedPreferences özelliğininde kullanabiliyoruz.

Şimdi paketi projemize dahil edelim.

dependencies:
flutter_secure_storage: ^5.0.2

flutter pub get komutunu çalıştırınız.

Keystore Android 4.3(API 18)’den önceki sürümler için çalışmaz.

Bunun için proje dizini içinde android/app/build.gradle içerisinde bulunan minSdkVersion 18 olmalıdır.

android {
...

defaultConfig {
...
minSdkVersion 18
...
}

}

minSdkVersion’u 18 yaptıktan sonra projeyi tekrar başlatmayı unutmayınız.

Linux ve Web için farklı configurasyonlar da bulunmaktadır. Daha fazla detay için paket sayfasına gidiniz.

Secure Storageiçin helper sınıfımızı yazıyoruz. Shared Preferences’a benzer bir yapı kuracağız.

Secure Storage Helper

Secure Storage için FlutterSecureStorage ‘den nesne üretiyoruz ve bunu yine sınıfımızın isimlendirilmiş kurucusu(named constructor) içerisinde yapıyoruz.

Secure storage içerisinde sadece String veriler tutuluyor.

Kullanımları anlatırken sadece okuma ve yazma işlemlerinden bahsediyorum fakat silme, hepsini silme, hepsini okuma gibi fonksiyonlar da bulunuyor. Bunları da deneyerek öğrenebilir ve kullanabilirsiniz. 🤞

Örnek kullanımları github reposu üzerinden incelemeyi unutmayınız.

Hive

Yukarda anlattığım yapıları veri saklama olarak düşünebiliriz. Hive’e geldiğimizde ise daha çok database gibi düşünebiliriz. NoSQL olan Hive key-value çiftlerini tutmaktadır.

Öne çıkaran özelliklerden birisi ise saf Dart dili ile yazılmış olmasıdır. Bu yüzden hafif ve hızlı bir veritabanıdır.

Hive sadece bir değil birçok paketten oluşmaktadır. Eğer Dart uygulaması yapıyorsanız hive, Flutter uygulaması yapıyorsanız hive_flutter ve yardımcı paket olarakta hive_generator’ı kullabilirsiniz.

bağımlılıklarımızı pubspec.yaml içerisine ekleyelim.

dependencies:
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^1.1.2
build_runner: ^2.1.7

flutter pub get komutunu çalıştırınız.

Hive int, String, Map, Datetime gibi primitive tipteki verileri saklayabilir.

Hive kullanmadan önce init fonksiyonunu çağırmamız gerekmektedir.

Bunun için Helper metodu içierisinde kurucu içerisinde bu fonksiyonu çağırıyoruz.

await Hive.initFlutter('/hive');

Burada fonksiyon içinde verdiğimiz String parametre zorunlu alan değildir. Buraya dosya dizini içinde yeni dosya yolu vermek için kullanıyoruz.

İnit fonksiyonunu çağıdıktan sonra box yapılarına geçelim. Veritabanına veri yazmak için box açmamız gerekmektedir. Bunu SQL veritabanında bulunan tablo gibi düşünebiliriz. box yapılarında generic olarak içerisine hangi türde veri geleceğini belirtebiliyoruz.

Helper sınıfı içerisinde openBox() fonksiyonunu yazıyoruz. Açtığımız box ile işlemler yapmamız gerekiyor ve bunun için helper içinde initialBox adından bir box nesnesi oluşturuyorum ve changeInitialBox() fonksiyonu ile de açığımız box ‘ı initialBox nesnesi içine atıyoruz ve bu box için yapılacak işlemleri de helper sınıfı içerisine yazalım.

Eğer box ile başka bir işiniz kalmadıysa onu close() metodu ile kapatın.

Hive Helper

Yukarıdaki helper sınıfını detaylı inceleyerek daha ayrıntılı anlayabiliriz. Hive içinde verileri kaydederken Shared Preferences ‘de olduğu gibi tipe göre kaydetmiyoruz, put() metodu dynamic değerler alabilmektedir ama bu şekilde kullanmak, tipe göre fonksiyon yazmak işimizi kolaylaştıracaktır.

Bu sınıfı kullanarak bir örnek gerçekleştirelim.

Screenshot

Bu şekilde çok basit bir tasarım yaptım. İsterseniz siz daha güzel ekranlar hazırlayıp orada da deneyebilirsiniz.

Stateful widget oluşturuyoruz ve sayfanın tasarımını yapıyoruz.

Textfield, ElevatedButton ve sonrasında da bir Divider ekliyoruz. Tekrar ElevatedButton ve Text widgetlarını sırası ile bir Column içinde yerleştiriyoruz.

Write butonu ile Textfield içerisinde girilen String değeri açtığımız bir box içerisine kaydediyoruz ve Read butonu ile de veriyi aşağıda bulunan Text içine yerleştiriyoruz.

late final HiveHelper hiveHelper;late final TextEditingController tfController;String data = '';

Bu tanımlamaları yapıyoruz ve initState() fonksiyonu içerisinde de atamalarını yapıyoruz.

@overridevoid initState() {hiveHelper = HiveHelper.instance;tfController = TextEditingController();super.initState();}

Şimdi ekranımızı yazalım. Scaffold içinde body kısmına geliyoruz.

Hive Screen

Write butonu içinde “names” isimli box nesnesini açıyoruz ve initialBox olarak bu box ‘ı seçiyoruz. Şimdi de Textfield içerisinden gelecek olan değeri key değeri ile kaydediyoruz.

Read butonunda ise verdiğimiz key ile kayıt edilen veriyi alıyoruz ve ekrana yazdırıyoruz.

Screenshot

Yazdığımız veriyi Text içinde görüntüledik.🎉

Hive için çok fazla anlatılacak başlık bulunuyor fakat daha fazla ayrıntıya inerek bu kısımı çok uzatmak istemiyorum. Ayrıntılı bilgi için hive dökümanını incelemenizde fayda var.

SQLite

SQLite Flutter ile kullanabileceğimiz SQL özelliklerini de içerisinde barındıran bir veritabanıdır. Insert, query, update, delete sorguları yapabilmekteyiz. Hızlı ve güvenilirdir. Android , IOS ve MacOS desteklemektedir.

Güncel olarak 20.02.2022 tarihinde web desteği bulunmamaktadır.

Daha önce SQL dili kullandıysanız bildiğimiz sorgular basit bir proje geliştireceğiz. Haydi paketimizi dahil edelim.

dependencies:
sqflite: ^2.0.2
path: ^1.8.1

flutter pub get komutunu çalıştırınız.

Path paketini de helper sınıfımız içerisinde join() metodu için kullanıyoruz.

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

sonrasında Helper sınıfımızın bulunduğu klasöre importlarımızı ekliyoruz.

Şimdi yapacağımız örnekten biraz konuşalım. Student adında bir modelimiz olacak ve biz Student modeli için ad, okul adı ve yaş bilgisini alıp veritabanına kayıt edeceğiz. Kayıt ettikten sonrada tekrardan kayıtlı olan veriyi çekip kontrolleri sağlayacağız.

Student sınıfımızı aşağıda göründüğü gibi oluşturalım.

Student

toMap fonksiyonu ve fromMap kurucu fonksiyonu ile modelimizi veritabanına kayıt edebileceğimiz Map<> formatına dönüştürmekteyiz.

Factory constructor hakkında detaylı bilgi için linke tıklayınız.

toString() fonksiyonu ile de sınıfımızı String türünde yazdırabilmekteyiz.

Şimdi de Student sınıfımız için Helper sınıfı yazalım ve kullanımı kolaylaştıralım.

SQLite Helper

Buradaki kodları biraz detaylandıralım. İlk olarak yine Singleton bir sınıf oluşturduk ve kurucu fonksiyon içerisinde _init() fonksiyonu yazdık. Burada bunu yapmamın amacı kurucu fonksiyon içerisinde await işlemini gerçekleştiremiyoruz. Bunun için _init() içerisinde openDatabase() fonksiyonunu çağırdık.

openDatabase() içerisinde String dosya uzantısı , ve onCreate() fonksiyonlarını kullandık. Path paketini burada kullanıyoruz.

Join fonksiyonu verilen String değerleri birleştirmektedir.

onCreate() fonksiyonu ile veritabanımızda SQL sorguları ile tablomuzu oluşturduk.

SQLite içerisinde veri tipleri INTEGER, TEXT, REAL, NULL’dır.

Veritabanı hazır olduğuna göre insert, select, update ve delete fonksiyonlarını hazırlayalım.

SQLite Helper Fonksiyonları

Şimdi örneğimizi hazırlayalım. Öreğimizde 3 adet Textfield içerisinden ad, okul adı, yaş bilgilerini alıp ElevatedButton yardımı ile veritabanına kayıt edeceğiz. Kayıt ettiğimiz verileri ElevatedButton ile çekip Snackbar ile ekranda göstereceğiz.

Screenshot

Bu şeklilde yine basit bir ekran tasarlıyoruz ve anlattığım kurgu ile bu işlemleri yapmaya çalışınız. Yapamadığınız yerde github reposu içinde proje ile karşılaştırmalar yapabilirsiniz.

Kayıt edip, tekrar okuduktan sonra da şu şekilde bir görüntü ortaya çıkmaktadır.

Screenshot

Sqlite için daha fazla detaylı bilgiye de şu kaynaklardan ulaşabilirsiniz.

Floor

Floor Flutter için geliştirilmiş bir SQLite kütüphanesidir. Eğer bir modeli veritabanına kaydetmek istiyorsanız Floor bu iş için uygunudur. Native Android geliştirmesi yaptıysanız Room ile benzer mantığa sahiptir.

Floor, Entity DAO ve Database yapılarından oluşmaktadır. Entity modellerimizi kapsar. DAO ise modellerimize erişimi sağlar(Database Access Object). Database ise verilerimizi sakladığımız yer olarak düşünebiliriz.

Önce paketlerimizi pubspec.yaml içerisine ekleyelim.

dependencies:
floor: ^1.2.0
dev_dependencies:
floor_generator: ^1.2.0
build_runner: ^2.1.7

flutter pub get komutunu çalıştırınız.

Geliştireceğimiz uygulamada çalışan sınıfı olacak. Bu sınıfı veritabanına kayıt etmeyi ve kayıtlı verileri çekmeyi anlatacağım.

Şimdi çalışan sınıfımızı oluşturalım.

Employee Model

Çalışanların isim ve cinsiyet tiplerini tutacağız. Farklı bir kullanımı göstermek için cinsiyeti enum tipinde tanımladım. Floor içerisinde enum tipinde veriler tutamayız bunun için enum’ın index değerini int olarak tutabiliriz. Şimdi bu sınıfı nasıl Entity işaretliyebileceğimizi göstereyim.

Employee Model

Burada belirli annotationlar kullanıyoruz. @entity sınıfı işaretler.

@PrimaryKey ise sınıfın primaryKey ‘ini işaretler ve otomatik arttırabiliyoruz.

@ignore ile de veritabanına kaydedemeyeceğimiz tipleri işaretleriz.

Enum, Datetime gibi yapılar veritabanında saklanamaz.

Sınıfımız veritabanına kayıt edilmek için hazırlandı. Bu sınıf için Dao ‘yu oluşturalım.

Database Access Object

Dao içerisinde sınıf ile ilgili yapacağımıc CRUD fonksiyonlarını yazıyoruz. Query yazıldığı gibi delete , insert , update gibi annotationlar da vardır.

Future fonksiyonlar ile bunları yaparken Stream ile de sürekli verileri dinleyebiliyoruz.

Sırada Database’i oluşturmak var. Database oluşurken build runner’ı çalıştırıp code generation yapacağız. Haydi hazırlayalım..

Database için dart dosyası açtıktan sonra bu import içeriğini sayfanın en üstüne ekleyin.

Database İmports

part satırında proje hata verecektir ama sıkntı yok. Part içinde database yazan yeri kendi database.dart dosyanızın ismi ile değiştirmeyi unutmayınız.

Bu eklemelerden sonra Database sınıfımızı oluşturalım.

Database

Database’i olşutururken yine @Database annotation`u ile işaretliyoruz. Bu işaretlemede version için 1 değerini giriyoruz. entities içine ise Entitiy olarak işaretlediğimiz sınıfları ekliyoruz.

Database içine başka Dao’ları da ekleyebiliriz.

Sırada build runner ile code generation yapmamız gerekiyor. Bunun için hazır bir kod bulunuyor.

flutter pub run build_runner build --delete-conflicting-outputs

Kendi IDE terminalinize bu kodu yapıştırıp,çalıştırınız.

Build runner çalıştığında çok fazla sorunla karşılaşabilirsiniz :) isimlendirmeleri, annotationları, importları eksiksiz yaptığınızıdan emin olun.

Yapılacak son bir adım kaldı. Database’i oluşturmak için bir helper sınıfı yazacağız. Database nesnesine bu sınıfın instance’ı üzerinden erişeceğiz.

Database Helper

Burada oluşturduğumuz database yapısına erişimek için LocalDatabase tipinde nesnemiz bulunuyor. Bu nesneyi Helper snıfının constructor’ında dolduruyoruz. Bu işlem Sqlite içinde bulunana openDatabase fonksiyonuna karşılık gelmektedir. Bu işlemi de tamamladıktan sonra geriye ekran tasarımlarını da halledip, deneyimlemek kalıyor.

Ekranda sayfanın üzerinde kayıt alanı ve sayfanın altında Listview içerisinde verilerin gösterildiği bir alan bulunacak. Verileri kayıt edip, Listview içerisinde görmeyi bekliyoruz.

Screenshot

Burada Save butonu ile çalışan kayıt ediyoruz. Read butonu ile de kayıttaki verileri çekiyoruz.

Burada getEmployee() fonkisyonu ile verileri çekiyoruz.

Get employee

Scaffold içinde body kısmını aşağıda bulunan kodlar ile doldurunuz.

Body

Floor ile ilgili anlataklarım bu kadardı. Ayrıntılı bilgi için linke tıklayınız.

Ekranların çiziminin ayrınıtısı için github reposunu incelemeyi unutmayınız.

Giphy.com

Umarım anlaşılır ve güzel bir içerik olmuştur. Sonraki yazılarda görüşmek üzere.. 💙

--

--