استفاده از SQLite در فلاتر (Flutter) — به زبان ساده
نگهداری دائمی دادهها برای کاربران مختلف حائز اهمیت بالایی است، چون ممکن است برای آنها دشوار باشد که هر بار دادههای یکسان را مجدداً وارد کنند یا هر نوبت منتظر شبکه بمانند تا دادههای یکسانی مجدداً بارگذاری شوند. در موقعیتهایی مانند این بهتر است دادهها به صورت محلی ذخیره شوند. در این مقاله به بررسی استفاده از SQLite در فلاتر میپردازیم.
SQLite چیست؟
SQLite یکی از محبوبترین روشهای ذخیرهسازی دادهها به صورت محلی است. در این مقاله از بسته sqlite برای اتصال به SQLite استفاده میکنیم. sqlite یکی از پرکاربردترین و مدرنترین بستهها برای اتصال به پایگاههای داده SQLite در فلاتر محسوب میشود. مراحل کار به صورت زیر است.
1. افزودن وابستگی به پروژه
در پروژه خود به فایل pubspec.yaml بروید و به دنبال گزینه dependencies بگردید. زیر این بخش، آخرین نسخه از sqflite و path_provider را اضافه کنید:
dependencies: flutter: sdk: flutter sqflite: any path_provider: any
دقت کنید که ما از بسته path_provider برای دریافت مکانهایی که بیشتر استفاده میشوند، مانند TemporaryDirectory و ApplicationDocumentsDirectory استفاده میکنیم.
2. ایجاد کلاینت پایگاه داده
اکنون در پروژه خود یک فایل جدید به نام Database.dart ایجاد کنید. سپس در این فایل جدید ایجاد شده یک «سینگلتون» (singleton) میسازیم. اگر میپرسید چه نیازی به سینگلتون داریم، باید پاسخ دهیم که ما از الگوی سینگلتون استفاده میکنیم تا مطمئن شویم که تنها یک وهله از کلاس داریم و یک نقطه دسترسی سراسری به آن ایجاد کنیم.
یک «سازنده خصوصی» (private constructor) ایجاد کنید که بتوان صرفاً درون کلاس از آن استفاده کرد:
class DBProvider { DBProvider._(); static final DBProvider db = DBProvider._(); }
در مرحله بعد یک شیء پایگاه دادهای ایجاد میکنیم و یک getter برای آن میسازیم که در صورت عدم مقداردهی اولیه آن را Initialize میکند (این وضعیت به نام مقداردهی اولیه کُند یا lazy initialization) نامیده میشود.
static Database _database; Future<Database> get database async { if (_database != null) return _database; // if _database is null we instantiate it _database = await initDB(); return _database; }
اگر هیچ شیئی به پایگاه داده انتساب نیابد، از تابع initDB برای ایجاد پایگاه داده استفاده میکنیم. در این تابع مسیر مورد نیاز برای ذخیرهسازی پایگاه داده و ایجاد جدولهای مطلوب را دریافت میکنیم:
static Database _database; Future<Database> get database async { if (_database != null) return _database; // if _database is null we instantiate it _database = await initDB(); return _database; }
دقت کنید که نام پایگاه داده TestDB و تنها جدولی که در آن داریم Client است. اگر متوجه نمیشوید که چه اتفاقی در کد فوق میافتد، میبایست ابتدا کمی بیشتر با SQL آشنا شوید. به این منظور پیشنهاد میکنیم از مجموعه مطالب «آموزش دستورهای SQL – مجموعه مقالات جامع وبلاگ فرادرس» استفاده کنید.
3. ایجاد کلاس مدل
دادههای درون پایگاه داده به صورت نگاشتهای Dart درمیآیند. بنابراین در ابتدا باید کلاسهای مدل را با متدهای toMap و fromMap ایجاد کنیم. در این نوشته قصد نداریم روش انجام دستی این کار را توضیح دهیم. اگر نمیدانید این کار را باید چگونه انجام دهید، برای مطالعه بیشتر در این خصوص، میتوانید از این مقاله (+) کمک بگیرید.
برای ایجاد کلاسهای مدل از سرویسهای این وبسایت (+) کمک میگیریم. مدل ما به شکل زیر است:
/// ClientModel.dart import 'dart:convert'; Client clientFromJson(String str) { final jsonData = json.decode(str); return Client.fromJson(jsonData); } String clientToJson(Client data) { final dyn = data.toJson(); return json.encode(dyn); } class Client { int id; String firstName; String lastName; bool blocked; Client({ this.id, this.firstName, this.lastName, this.blocked, }); factory Client.fromJson(Map<String, dynamic> json) => new Client( id: json["id"], firstName: json["first_name"], lastName: json["last_name"], blocked: json["blocked"], ); Map<String, dynamic> toJson() => { "id": id, "first_name": firstName, "last_name": lastName, "blocked": blocked, }; }
عملیات CRUD
عملیات CRUD به چهار عمل ایجاد (Create)، خواندن (Read)، بهروزرسانی (Update) و حذف (Delete) گفته میشود که در ادامه هر کدام را توضیح میدهیم.
عملیات ایجاد (Create)
بسته SQFlite دو روش برای مدیریت این عملیات با استفاده از کوئریهای RawSQL یا با استفاده از نام جدول و یک نگاشت ارائه کرده است که شامل دادهها هستند. روش استفاده از rawInsert به صورت زیر است.
newClient(Client newClient) async { final db = await database; var res = await db.rawInsert( "INSERT Into Client (id,first_name)" " VALUES (${newClient.id},${newClient.firstName})"); return res; }
مثال دیگر با استفاده از بزرگترین ID به عنوان ID جدید به صورت زیر است:
newClient(Client newClient) async { final db = await database; //get the biggest id in the table var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM Client"); int id = table.first["id"]; //insert to the table using the new id var raw = await db.rawInsert( "INSERT Into Client (id,first_name,last_name,blocked)" " VALUES (?,?,?,?)", [id, newClient.firstName, newClient.lastName, newClient.blocked]); return raw; }
عملیات خواندن (Read)
برای دریافت کلاینت بر اساس id به صورت زیر عمل میکنیم:
getClient(int id) async { final db = await database; var res =await db.query("Client", where: "id = ?", whereArgs: [id]); return res.isNotEmpty ? Client.fromMap(res.first) : Null ; }
در کد فوق، یک کوئری را با یک id با استفاده از whereArgs به عنوان آرگومان ارائه میکنیم. سپس در صورتی که فهرست خالی نباشد، نخستین نتیجه را بازمیگردانیم و در غیر این صورت مقدار null بازگشت میدهیم.
دریافت همه کلاینتها با یک شرط
در این مثال از rawQuery استفاده کرده و فهرست نتایج را به یک فهرست از شیءهای Client نگاشت میکنیم:
getAllClients() async { final db = await database; var res = await db.query("Client"); List<Client> list = res.isNotEmpty ? res.map((c) => Client.fromMap(c)).toList() : []; return list; }
مثال: تنها کلاینتهای مسدود شده را دریافت میکنیم:
getBlockedClients() async { final db = await database; var res = await db.rawQuery("SELECT * FROM Client WHERE blocked=1"); List<Client> list = res.isNotEmpty ? res.toList().map((c) => Client.fromMap(c)) : null; return list; }
عملیات بهروزرسانی (Update)
برای بهروزرسانی یک کلاینت موجود به صورت زیر عمل میکنیم:
updateClient(Client newClient) async { final db = await database; var res = await db.update("Client", newClient.toMap(), where: "id = ?", whereArgs: [newClient.id]); return res; }
مثال: مسدودسازی یا رفع انسداد یک کلاینت
blockOrUnblock(Client client) async { final db = await database; Client blocked = Client( id: client.id, firstName: client.firstName, lastName: client.lastName, blocked: !client.blocked); var res = await db.update("Client", blocked.toMap(), where: "id = ?", whereArgs: [client.id]); return res; }
عملیات حذف (Delete)
برای حذف کردن یک کلاینت به صورت زیر عمل میکنیم:
deleteClient(int id) async { final db = await database; db.delete("Client", where: "id =?", whereArgs: [id]); }
برای حذف همه کلاینتها نیز از کد زیر میتوان استفاده کرد:
deleteAll() async { final db = await database; db.rawDelete("Delete * from Client"); }
دمو
در دموی فوق ما یک اپلیکیشن فلاتر ساده برای تعامل با پایگاه داده ایجاد کردهایم. ابتدا با یک طرحبندی برای اپلیکیشن آغاز میکنیم:
Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Flutter SQLite")), body: FutureBuilder<List<Client>>( future: DBProvider.db.getAllClients(), builder: (BuildContext context, AsyncSnapshot<List<Client>> snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data.length, itemBuilder: (BuildContext context, int index) { Client item = snapshot.data[index]; return ListTile( title: Text(item.lastName), leading: Text(item.id.toString()), trailing: Checkbox( onChanged: (bool value) { DBProvider.db.blockClient(item); setState(() {}); }, value: item.blocked, ), ); }, ); } else { return Center(child: CircularProgressIndicator()); } }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () async { Client rnd = testClients[math.Random().nextInt(testClients.length)]; await DBProvider.db.newClient(rnd); setState(() {}); }, ), ); }
نکات:
- FutureBuilder برای دریافت دادهها از پایگاه داده استفاده میشود.
- FAB برای اضافه کردن کلاینت تصادفی به پایگاه داده در زمان کلیک شدن استفاده میشود.
List<Client> testClients = [ Client(firstName: "Raouf", lastName: "Rahiche", blocked: false), Client(firstName: "Zaki", lastName: "oun", blocked: true), Client(firstName: "oussama", lastName: "ali", blocked: false), ];
- در صورتی که دادهای موجود نباشد، یک CircularProgressIndicator نمایش مییابد.
- وقتی که کاربر کادر انتخاب را کلیک کند، بر اساس وضعیت کنونی، کلاینت مسدود یا رفع انسداد میشود.
اکنون افزودن ویژگیهای جدید بسیار ساده است، برای مثال، اگر بخواهیم زمانی که آیتم سوایپ میشود یک کلاینت را حذف کنیم، کافی است ListTile را درون یک ویجت Dismissible به صورت زیر پوشش دهیم:
return Dismissible( key: UniqueKey(), background: Container(color: Colors.red), onDismissed: (direction) { DBProvider.db.deleteClient(item.id); }, child: ListTile(...), );
در تابع OnDismissed از ارائه دهنده پایگاه داده برای فراخوانی متد deleteClient استفاده میکنیم. به عنوان آرگومان نیز id آیتم را ارسال میکنیم.
بازسازی برای استفاده از الگوی BLoC
تا به این جا در این مقاله کارهای زیادی انجام دادهایم؛ اما در یک اپلیکیشن دنیای واقعی، این وضعیت که حالت (State) بخشی از رابط کاربری (UI) باشد وضعیت خوبی محسوب نمیشود.؛ بلکه باید همواره آنها را از هم جدا نگه داریم.
الگوهای زیادی برای مدیریت حالت در فلاتر وجود دارند؛ اما ما از BLoC استفاده میکنیم، چون انعطافپذیری بالایی دارد.
ایجاد BLoC
class ClientsBloc { ClientsBloc() { getClients(); } final _clientController = StreamController<List<Client>>.broadcast(); get clients => _clientController.stream; dispose() { _clientController.close(); } getClients() async { _clientController.sink.add(await DBProvider.db.getAllClients()); } }
نکات
1. getClients دادهها را از پایگاه داده (جدول کلاینت) به صورت ناهمگام دریافت میکند. ما این متد را هر زمان که جدول را بهروزرسانی میکنیم فراخوانی خواهیم کرد و از این رو باید آن را در بدنه سازنده قرار دهیم.
2. ما از سازنده StreamController<T>.broadcast استفاده میکنیم، به طوری که میتوانیم بیش از یک بار نیز به استریم گوش دهیم. در این مثال، این وضعیت تفاوت زیادی ایجاد نمیکند، زیرا ما تنها یک بار به استریم گوش میدهیم؛ اما ممکن است در مواردی بخواهیم بیش از یک بار نیز به یک استریم گوش دهیم.
3. فراموش نکنید که استریم خود را به موقع ببندید. این وضعیت باعث میشود که از نشت حافظه جلوگیری شود. در این مثال، ما آن را با استفاده از متد dispose در ویجت StatefulWidget میبندیم.
اینک برخی متدها را به بلوک خود اضافه میکنیم تا با پایگاه داده تعامل داشته باشند:
blockUnblock(Client client) { DBProvider.db.blockOrUnblock(client); getClients(); } delete(int id) { DBProvider.db.deleteClient(id); getClients(); } add(Client client) { DBProvider.db.newClient(client); getClients(); }
این همه کاری هست که برای ایجاد BLoC نیاز داشتیم. مرحله بعدی این است که روشی برای ارائه bloc خود در ویجت پیدا کنیم. به این منظور باید راهی برای ایجاد امکان دسترسی به bloc از بخشهای متفاوت درخت و همزمان امکان آزادسازی آن از حافظه در زمانهایی که مورد نیاز نیست بیابیم. به این منظور میتوانیم از این کتابخانه (+) استفاده کنیم.
در مورد مثال ما، bloc تنها قرار است از سوی ویجت استفاده شود و از این رو میتوانیم آن را در «ویجتِ باحالت» خود اعلان و dispose کنیم.
final bloc = ClientsBloc(); @override void dispose() { bloc.dispose(); super.dispose(); }
سپس باید به جای FutureBuilder از StreamBuilder استفاده کنیم. دلیل این امر آن است که ما اینک به یک استریم گوش میدهیم.
StreamBuilder<List<Client>>( stream: bloc.clients, ... )
مرحله نهایی این است که کد خود را طوری بازسازی کنیم که متدها را از Bloc و نه مستقیماً از پایگاه داده فراخوانی کنیم:
onDismissed: (direction) { bloc.delete(item.id); },
نتیجه نهایی به صورت زیر خواهد بود:
در نهایت شما میتوانید کد کامل این اپلیکیشن را در این ریپوی گیتهاب (+) مشاهده کنید. اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای طراحی و برنامه نویسی وب
- گوگل فلاتر (Flutter) از صفر تا صد — ساخت اپلیکیشن به کمک ویجت
- آموزش گوگل فلاتر (Flutter ): ساخت اپلیکیشن دستورهای آشپزی
- ویجت Hero در گوگل فلاتر (Flutter) — از صفر تا صد
- آموزش SQL Server Management Studio | کامل، رایگان و گام به گام
==