- 16 Haz 2022
- 53
- 39
bu konuda Flutter ile to do app yapımını detaya girmeden başlangıç seviyesinde olmayan kişilerin incelemesi için lokal veritabanı kullanımı açısından iyi bir örnek proje anlatacağım öncelikle yapacağım örnek uygulamanın özellikleri:
- Hive lokal veritabanını kullanarak görevleri uygulama kapansa da hatırlaması
- Efektif kodlama (tek satırla NoSQL veritabanını değiştirebilme örneğin Hive veritabanı yerine Secure Storage vb geçişlerin tek satırda yapılabilmesi)
- Sade tasarım
- Görevlerin kaydırılarak silinebilmesi
- Görevler arasında arama yapılabilmesi
gibi özellikler olacak uygulamamıza başlayalım.
son hal:
arama yapılabilir halde olacak:
öncelikle pubspec.yaml'a eklenmesi gereken package'lar:
son hal:
arama yapılabilir halde olacak:
öncelikle pubspec.yaml'a eklenmesi gereken package'lar:
Kod:
cupertino_icons: ^1.0.2
flutter_datetime_picker: ^1.5.1
uuid: ^3.0.7
intl: ^0.18.0
hive: ^2.2.3
hive_flutter: ^1.1.0
get_it: ^7.2.0
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
hive_generator: ^2.0.0
build_runner: ^2.3.3
projemiz lib altında bu şekilde klasör yapısı:
kodların yanına mümkün olduğunca // açıp açıklamaya çalıştım
main.dart ile başlayalım:
Kod:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:to_do_project/data/local_storage.dart';
import 'package:to_do_project/models/task_model.dart';
import 'package:to_do_project/pages/home_page.dart';
final locator = GetIt.instance; //global bir locator nesnesi tanımladık get_it package kullanarak
void setup(){
locator.registerSingleton<LocalStorage>(HiveLocalStorage());
//yarın öbür gün hive yerine başka bir sey kullanırsak hivelocalstorage kısmını degistirecegiz mantık bu
}
Future<void> setupHive() async{ //Hive ayarlarımızı şöyle bir topladık
await Hive.initFlutter(); //runAppden önce Hive ı başlatıyoruz
Hive.registerAdapter(TaskAdapter()); //adapterımızı da register etmiş olduk
var taskBox = await Hive.openBox<Task>('tasks'); //icinde taskların bulundugu kutu açılmış oluyor artık
for (var task in taskBox.values) {
if(task.createdAt.day != DateTime.now().day){
taskBox.delete(task.id);
}
}
}
void main() async{
//mainde uzun süren runappden önce çalışmasını istediğimiz şeyler için
WidgetsFlutterBinding.ensureInitialized();
//status barı kaldırmak
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( //durum cubugu ile ilgili stil bilgileri icin
statusBarColor: Colors.transparent, //durum cubugu rengini beyaz yaptık
),
);
await setupHive();
setup();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData( //themedata ile uygulamanın temasını renkler vb şeylerini themedata diyerek merkezi yonetim yapılabiliyor
primarySwatch: Colors.blue,
appBarTheme: const AppBarTheme( //appbartemasını buradan ayarlayabiliyoruz
elevation: 0,
backgroundColor: Colors.white, //arkaplan beyaz olsun dedik
iconTheme: IconThemeData( //iconların temasını ayarlayabiliriz bu şekilde
color: Colors.black, //iconların rengi siyah olsun dedik
),
),
),
home: const HomePage(),
);
}
}
model sınıfımızı oluşturalım task_model sınıfı daha sonra Hive package kullandığımız için otomatik olarak .g lisi oluşuyor
Kod:
/*
* bu sınıf bir model sınıftır tasklar yani uygulamamızdaki görevler için oluşturulmuş bir model sınıftır
*
* */
import 'package:hive/hive.dart';
import 'package:uuid/uuid.dart';
part 'task_model.g.dart';
@HiveType(typeId: 1)
class Task extends HiveObject{
//Klasik öğrendiğimiz hive a çevirme işlemini yapıyoruz fakat burada ekstradan extends hiveobject dedik bunun da faydası şu
//update ve delete işlemleri çok daha kolay oluyor extends HiveObject dersek.
@HiveField(0)
late final String id;
@HiveField(1)
late String name;
@HiveField(2)
late final DateTime createdAt;
@HiveField(3)
late bool isCompleted;
//sınıfımızın constructor'ını oluşturduk
Task(
{required this.id,
required this.name,
required this.createdAt,
required this.isCompleted});
/*
factory kullanımını hatırlayalım.
factory default constructor'dan farklı olarak bu sınıftan nesne üretmek için kullanılan bir fonksiyon gibidir.
yani Task.create yaptığımızda şu şu parametreyi isteyip return diyerek bir nesne döndürüyoruz istediğimiz ayarda
*/
factory Task.create({required String name, required DateTime createdAt}) {
return Task(
id: const Uuid().v1(),
name: name,
createdAt: createdAt,
isCompleted: false);
}
}
home_page sayfamızı oluşturalım main.dart içerisinde direkt olarak yazmak yerine home_page diye bir stful yapı oluşturup oraya tasarımımızı yapıyoruz
Kod:
import 'package:flutter/material.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
import 'package:to_do_project/data/local_storage.dart';
import 'package:to_do_project/main.dart';
import 'package:to_do_project/models/task_model.dart';
import 'package:to_do_project/widgets/custom_search_delegate.dart';
import 'package:to_do_project/widgets/task_list_item.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late List<Task>
_allTasks; //Task sınıfından nesneler tutabilen bir List oluşturduk
late LocalStorage _localStorage;
/*initState
* burada initState metodu sadece bir kez çalışır uygulama başlatıldığında
* çalıştığında olacakları yazıyoruz bir kez çalışır sadece bu metod
* */
@override
void initState(){
// TODO: implement initState
super.initState();
_localStorage = locator<LocalStorage>(); //locatori kullanarak localstorage turunde bir veri bekledigimizi belirttik
_allTasks = <Task>[]; //allTasks adlı List'e boş bir atama yaptık
_getAllTaskFromDb();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: GestureDetector(
onTap: () {
_showAddTaskBottomSheet(context); //metodu kullanıyoruz
},
child: const Text(
"Bugün Neler Yapacaksın",
style: TextStyle(color: Colors.black),
),
),
centerTitle: false,
actions: [
IconButton(onPressed: () {
_showSearchPage();
}, icon: const Icon(Icons.search)),
IconButton(
onPressed: () {
_showAddTaskBottomSheet(context);
},
icon: const Icon(Icons.add)),
],
),
/*
* Tasarım olarak body kısmına bir ListView.builder tasarımı düşündük
* ListView.builder bize context, index veriyor index 0 dan başlar öyle gider
* bir değişken oluşturduk oankiTaskIndex diye onu da allTasks listesinin 0.indexi olsun dedik o öyle gider 0,1,2 diye
* ListView.builder'ın uzunluğunu da belirleyecek olan şey zaten allTasks listesinin uzunluğu kadar olacak öyle ayarladık
* sonra her seferinde ListTile döndürecek her bir index için çünkü ListView.builder'ın olayı bu
*
* başlangıçta isNotEmtpy falan yapmamızın sebebi eğer _allTasks listesi boş değilse yani bir şeyler varsa tasarımı göster
* yook eğer bir şeyler yoksa yani boşsa : dan sonra yazdığımız Center içerisinde bir Text widget gösterecek hadi görev ekle diye
* */
body: _allTasks.isNotEmpty
? ListView.builder(
itemBuilder: (context, index) {
var _oankiTaskElemani = _allTasks[index];
/*
* Dismissible çok güzel bir widget sağa kaydırmaya yarıyor içerisindeki widgetı
* sağa kaydırıldığında onDismissed özelliği var oraya direction yazmak yeterli
* onDismissed edildiğinde olacakları yazdık _allTasks listemizin o anki elemanini sil demiş olduk listeden
* Dismissible widgetı bizden bir key bekler her seferinde farklı olmalı bu key. Bu keye de oankielemanin id sini verdik
* */
return Dismissible(
background: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.delete,
color: Colors.grey,
),
SizedBox(
width: 8,
),
Text("Bu görev silindi"),
],
),
key: Key(_oankiTaskElemani.id),
onDismissed: (direction){
_allTasks.removeAt(index);
_localStorage.deleteTask(task: _oankiTaskElemani);
setState(() {});
},
child: TaskItem(task: _oankiTaskElemani,), //oankiTaskElemanini TaskItem sınıfından nesne oluşturuyoruz
/*
* TaskItem sınıfı bizden bir Task sınıfı türünde nesne bekliyor zaten
* biz de oankiTaskElemani ni yolladık
* TaskItem sınıfına gidip orada neler olduğuna bakabilirsin*/
);
},
itemCount: _allTasks.length,
)
: const Center(
child: Text("Hadi görev ekle"),
),
);
}
//goreveklebottomı adını bu sekilde ing koydugumuz bir metod olusturduk
void _showAddTaskBottomSheet(BuildContext context) {
showModalBottomSheet(
//guzel bir widget biraz karartarak alltan bir pencere acıyor gibi
context: context,
builder: (context) {
return Container(
//bir tasarım döndürüyoruz
/*
* bu tasarımda bir container koyduk öncelikle containera padding verdik yani içindeki itemlerle arasında
* kendi arasında boşluk bırakması için. burada şöyle bir ayar var
* MediaQuery den yararlanıyoruz viewInsets diye bir ozelligi var viewInsets.bottom ifadesi bize
* klavye açıldığında klavyenin kapladığı alanı veriyor bizde bu şekilde padding verdiğimizde
* klavye açıldığında Containerımız ve içerisindeki her şey tabiki alttan o kadar yukarı çıkıyor baya güzel*/
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
width: MediaQuery.of(context).size.width,
child: ListTile(
title: TextField(
autofocus: true, //direkt olarak imlecin yazilabilir halde olmasini sagliyor
style: const TextStyle(fontSize: 20),
decoration: const InputDecoration(
hintText: "Görev Nedir ?",
border: InputBorder
.none, //InputBorder.none dedik çünkü altta çizgi vardı o kalktı
),
onSubmitted: (value) {
//görev nedir text kutucuguna bir seyler yazildıktan klavyeden tamama basınca
Navigator.of(context).pop(); //geri dön demek
if (value.length > 3) {
/*
* flutter date time picker adlı package ı yükledik zaman tarih vb. seçme penceresi açıyor bize*/
DatePicker.showTimePicker(
context,
showSecondsColumn: false,
onConfirm: (time) async{
//yeni görev ekliyoruz
/*
* burada yapılan şey şu Task diye bir sınıf var bu sınıfın factory metoduyla nesne üretiyoruz
* bu sınıf da zaten Görev sınıfı yani oluşturduğumuz model sınıftan nesne üretiyoruz mevzu bu
* sonra bu nesneyi ürettiğimiz bu nesneyi allTasks diye bir listin içine atıyoruz olay bu*/
var addNewTask =
Task.create(name: value, createdAt: time);
_allTasks.insert(0, addNewTask);
await _localStorage.addTask(task: addNewTask);
setState(() {});
},
); //showSecondsColumn false yaptık ki saniye gözükmesin
}
},
),
),
);
},
);
}
//bir tane fonksiyon olusturuyoruz allTasks e localStorage sınıfından getAllTask metodunu kullanacak sonra setstate çakacak
void _getAllTaskFromDb() async{
_allTasks = await _localStorage.getAllTask();
setState(() {
});
}
void _showSearchPage() async{ //arama ile ilgili işlemler icin
await showSearch(context: context, delegate: CustomSearchDelegate(allTasks: _allTasks));
_getAllTaskFromDb();
}
}
itemlerin listelenmesi ile ilgili task_list_item.dart dosyamız
Kod:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:to_do_project/data/local_storage.dart';
import 'package:to_do_project/main.dart';
import 'package:to_do_project/models/task_model.dart';
class TaskItem extends StatefulWidget {
Task task;
TaskItem({Key? key, required this.task}) : super(key: key);
@override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
final TextEditingController _taskNameController =
TextEditingController(); //bir kontroller tanımladık textfield için
late LocalStorage _localStorage;
@override
void initState() {
// TODO: implement initState
super.initState();
_localStorage = locator<LocalStorage>();
}
@override
Widget build(BuildContext context) {
_taskNameController.text =
widget.task.name; //sayfa açıldığında bu atama gerçekleşsin
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(.2), blurRadius: 10),
]),
child: ListTile(
leading: GestureDetector(
onTap: () {
widget.task.isCompleted = !widget.task.isCompleted;
_localStorage.updateTask(task: widget.task);
setState(() {});
},
child: Container(
//bir container içine bir icon koyduk ve box decoration falan verdik
decoration: BoxDecoration(
color: widget.task.isCompleted ? Colors.green : Colors.white,
border: Border.all(
color: Colors.grey,
width: 0.8,
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
),
),
),
title: widget.task.isCompleted
? Text(
widget.task.name,
style: const TextStyle(
decoration: TextDecoration.lineThrough, color: Colors.grey),
)
: TextField(
controller: _taskNameController,
minLines: 1,
maxLines: null, //sığmama sorunu çözülüyor maksimum satırı null veriyoruz genisletiyoruz yani esneklik
textInputAction: TextInputAction.done, //klavyede yeni alt satıra geç değil de tik işaretinin çıkması için
decoration: const InputDecoration(border: InputBorder.none),
onSubmitted: (yeniDeger){
if(yeniDeger.length>3){
//eger textfielda yazılan yazı 3 ten buyukse artık gelen nesnenin ismine bu yeni degeri ata klavyeden ok basılınca
widget.task.name = yeniDeger;
_localStorage.updateTask(task: widget.task);
}
},
),
trailing: Text(
//intl package ile tarih formatlandırma yapabiliyoruz dokümantasyonda var bir seyler
DateFormat('hh:mm a').format(widget.task.createdAt),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
),
);
}
}
ve en önemli kısım olan lokal veritabanımızı abstract bir sınıftan türetme mantığı ile efektif bir şekilde lokal veritabanları arasında değişim yapılabilecek halde oluşturuyoruz ve metodların içerisini dolduruyoruz
Kod:
//öyle bir kod yazmalıyız ki lokal veritabanı değiştiğinde kodlarımız bundan etkilenmesin.
//soyut sınıf içindeki kodları yazmadıgımız yapılarla yapıyorduk bunu
import 'package:hive/hive.dart';
import 'package:to_do_project/models/task_model.dart';
abstract class LocalStorage{
Future<void> addTask({required Task task}); //parametre olarak task sınıfından nesne alabilen metodumuz addTask metodu
Future<Task?> getTask({required String id}); //string türünde bir id alan ve geriye Task sınıfından nesne döndüren metodumuz
Future<List<Task>> getAllTask(); //Task sınıfından nesneleri tutabilen bir list döndüren getAllTask metodumuz
Future<bool> deleteTask({required Task task}); //bool bir deger donduren parametre olarak Task sınıfından bir nesne isteyen metodumuz
Future<Task> updateTask({required Task task}); //ve yine Task sınıfından nesne isteyen parametre olarak ve geriye bir Task sınıfından nesne döndüren metodumuz
}
class MockData extends LocalStorage{
@override
Future<void> addTask({required Task task}) {
// TODO: implement addTask
throw UnimplementedError();
}
@override
Future<bool> deleteTask({required Task task}) {
// TODO: implement deleteTask
throw UnimplementedError();
}
@override
Future<List<Task>> getAllTask() {
// TODO: implement getAllTask
throw UnimplementedError();
}
@override
Future<Task> getTask({required String id}) {
// TODO: implement getTask
throw UnimplementedError();
}
@override
Future<Task> updateTask({required Task task}) {
// TODO: implement updateTask
throw UnimplementedError();
}
//falanfilan flana filan mesela
} //mesela yani
class HiveLocalStorage extends LocalStorage{
late Box<Task> _taskBox; //Task nesneleri tutabilen bir hive kutusu tanımladık
HiveLocalStorage(){
_taskBox = Hive.box<Task>('tasks'); //constructor da _taskBox a bu kutuyu koyduk mainde açtığımız şey bu aynı olmalı
}
@override
Future<void> addTask({required Task task}) async{
await _taskBox.put(task.id, task);
}
@override
Future<bool> deleteTask({required Task task}) async{
await task.delete(); //hiveobject sayesinde böyle kolayca yapıyoruz öbür türlü _taskBox.delete(task.id); şeklinde yazardık
return true;
}
@override
Future<List<Task>> getAllTask() async{
List<Task> _allTask = <Task>[]; //Task nesneleri tutabilen boş bir list oluşturduk
_allTask = _taskBox.values.toList(); //toList diyerek butun degerleri allTask listesine koyduk
if(_allTask.isNotEmpty){ //eger allTask listesinin eleman sayisi 0 dan buyukse
_allTask.sort((Task a, Task b)=> b.createdAt.compareTo(a.createdAt)); //kendi içinde siralama yap tarihlerine gore dedik
}
return _allTask;
}
@override
Future<Task?> getTask({required String id}) async{
if(_taskBox.containsKey(id)){
return _taskBox.get(id);
}else{
return null;
}
}
@override
Future<Task> updateTask({required Task task}) async{
await task.save();
return task;
}
} //bizim olayımız bu tabiki hive ı kullanacagız cok sistematik yaptık
son olarak arama muhabbetini çözdüğümüz custom_search_delegate.dart ımızı dolduralım
Kod:
import 'package:flutter/material.dart';
import 'package:to_do_project/data/local_storage.dart';
import 'package:to_do_project/main.dart';
import 'package:to_do_project/models/task_model.dart';
import 'task_list_item.dart';
class CustomSearchDelegate extends SearchDelegate {
late final List<Task> allTasks;
CustomSearchDelegate({required this.allTasks});
@override
List<Widget>? buildActions(BuildContext context) {
//arama kısmının sağ tarafındaki ikonları
return [
IconButton(onPressed: () {
query.isEmpty ? null : query =
''; // kullanıcının arama yapmak istedigi deger bossa sorguyu '' bosalt
}, icon: const Icon(Icons.clear)),
];
}
@override
Widget? buildLeading(BuildContext context) {
//en bastaki ikonları
return GestureDetector(
onTap: () {
close(context,
null); //bunlar hep flutter sayesinde close metodu falan query mevzusu falan
},
child: const Icon(Icons.arrow_back_ios, color: Colors.red, size: 24,));
}
@override
Widget buildResults(BuildContext context) {
//cıkacak sonucların gosterilis sekli
List<Task> filteredList = allTasks.where((gorev) =>
gorev.name.toLowerCase().contains(query.toLowerCase())).toList();
return filteredList.length > 0 ? ListView.builder(
itemBuilder: (context, index) {
var _oankiTaskElemani = filteredList[index];
/*
* Dismissible çok güzel bir widget sağa kaydırmaya yarıyor içerisindeki widgetı
* sağa kaydırıldığında onDismissed özelliği var oraya direction yazmak yeterli
* onDismissed edildiğinde olacakları yazdık _allTasks listemizin o anki elemanini sil demiş olduk listeden
* Dismissible widgetı bizden bir key bekler her seferinde farklı olmalı bu key. Bu keye de oankielemanin id sini verdik
* */
return Dismissible(
background: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.delete,
color: Colors.grey,
),
SizedBox(
width: 8,
),
Text("Bu görev silindi"),
],
),
key: Key(_oankiTaskElemani.id),
onDismissed: (direction) async{
filteredList.removeAt(index);
await locator<LocalStorage>().deleteTask(task: _oankiTaskElemani);
},
child: TaskItem(task: _oankiTaskElemani,), //oankiTaskElemanini TaskItem sınıfından nesne oluşturuyoruz
/*
* TaskItem sınıfı bizden bir Task sınıfı türünde nesne bekliyor zaten
* biz de oankiTaskElemani ni yolladık
* TaskItem sınıfına gidip orada neler olduğuna bakabilirsin*/
);
},
itemCount: filteredList.length,
) : const Center(child: Text("Aradığınızı bulamadık"),);
}
@override
Widget buildSuggestions(BuildContext context) {
//kullanici bir harf falan yazarsa veya bir sey yazmazsa gosterilecekler
return Container();
}
}
hepsi bu kadar kodların içerisinde // ile açıklamalar mevcut bazı açıklamalar gereksiz veya anlatmak için yanlış denilebilecek düzeyde. kodlar tabii çok daha efektif hale getirilebilirdi çok daha fazla dosyalara parçalanarak ancak sadece örnek olması açısından daha çok lokal veritabanı kullanımına yönelik bir örnek bir projeydi.