ساخت اپلیکیشن فلاتر ToDo با SQLite — از صفر تا صد

۳۶ بازدید
آخرین به‌روزرسانی: ۵ دی ۱۳۹۹
زمان مطالعه: ۷ دقیقه

داده‌ها برای کاربران حائز اهمیت بالایی است و از این رو برای آن‌ها راحت نیست که بخواهند به طور مرتب داده‌های تکراری را وارد کنند و یا مکرراً منتظر بمانند تا داده‌های یکسانی از اینترنت بارگذاری شوند. در چنین مواردی بهتر است داده‌ها را به صورت محلی ذخیره کنیم. در این مقاله مراحل ساخت اپلیکیشن فلاتر ToDo با پایگاه داده لوکال SQLite توضیح داده می‌شود. به این منظور از پلاگین sqflite استفاده می‌کنیم.

اهدافی که در این مطلب دنبال می‌کنیم عبارت هستند از:

  • ایجاد لیست ToDo
  • مدیریت عملیات CRUD برای ذخیره و بازیابی داده‌ها

اپلیکیشنی که می‌خواهیم بسازیم شبیه به تصویر زیر است:

ساخت اپلیکیشن فلاتر ToDo

گام 1

قبل از هر چیز باید کلاس مدل را برای آیتم ToDo ایجاد کنیم. ما از عنوان، توضیح، تاریخ و id برای هر آیتم To-Do استفاده می‌کنیم. یک کلاس مدل به صورت todo.dart ایجاد کرده و کد زیر را در آن قرار می‌دهیم:

class Todo {

	int _id;
	String _title;
	String _description;
	String _date;

	Todo(this._title, this._date, [this._description] );

	Todo.withId(this._id, this._title, this._date, [this._description]);

	int get id => _id;

	String get title => _title;

	String get description => _description;

	String get date => _date;


	set title(String newTitle) {
		if (newTitle.length <= 255) {
			this._title = newTitle;
		}
	}
	set description(String newDescription) {
		if (newDescription.length <= 255) {
			this._description = newDescription;
		}
	}

	set date(String newDate) {
		this._date = newDate;
	}

	// Convert a Note object into a Map object
	Map<String, dynamic> toMap() {

		var map = Map<String, dynamic>();
		if (id != null) {
			map['id'] = _id;
		}
		map['title'] = _title;
		map['description'] = _description;
		map['date'] = _date;

		return map;
	}

	// Extract a Note object from a Map object
	Todo.fromMapObject(Map<String, dynamic> map) {
		this._id = map['id'];
		this._title = map['title'];
		this._description = map['description'];
		this._date = map['date'];
	}
}

گام 2

در این مرحله باید عملیات CRUD پایگاه داده SQLite را پیاده‌سازی کنیم. به این منظور یک کلاس مجزا ایجاد کرده و همه عملیات درج، به‌روزرسانی و حذف را با ایجاد جداولی پیاده‌سازی می‌کنیم. پیش از آن باید وابستگی‌ها را اضافه کنیم تا بتوانیم از SQLite در پروژه خود استفاده کنیم. به این منظور به فایل pubspec.yaml بروید و وابستگی‌های زیر را به پروژه اضافه کرده و آن را ذخیره کنید:

dependencies:
  flutter:
    sdk: flutter
  sqflite: any
  path_provider: any
  intl: ^0.15.7

در ادامه برخی تابع‌های مهم را در کلاس database_helper مشاهده می‌کنید:

Future<Database> get database async {

		if (_database == null) {
			_database = await initializeDatabase();
		}
		return _database;
	}

تابع فوق شیء پایگاه داده را ایجاد کرده و یک getter در آن ارائه می‌کند که در صورت عدم ایجاد وهله‌ای از پایگاه داده، از آن برای وهله‌سازی پایگاه داده استفاده می‌کنیم. این کار «مقداردهی با تأخیر» (Lazy Initialization) نامیده می‌شود.

	Future<Database> initializeDatabase() async {

		Directory directory = await getApplicationDocumentsDirectory();
		String path = directory.path + 'todos.db';
		var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb);
		return todosDatabase;
	}

اگر هیچ شیئی به پایگاه داده انتساب نیافته باشد، از تابع initializeDatabase برای ایجاد پایگاه داده بهره می‌گیریم. در این تابع مسیر ذخیره‌سازی پایگاه داده و ایجاد جداول مطلوب را به دست خواهیم آورد. نام پایگاه داده را todos تعیین می‌کنیم:

	void _createDb(Database db, int newVersion) async {

		await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, '
				'$colDescription TEXT, $colDate TEXT)');
	}

سپس جدول‌ها را مانند کد فوق ایجاد می‌کنیم. در ادامه باید تابع‌های درج، به‌روزرسانی و حذف را اضافه کنیم. در ادامه کد کامل مربوط به کلاس database_helper را می‌بینید:

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:todo_list/Models/todo.dart';

class DatabaseHelper {

	static DatabaseHelper _databaseHelper;    // Singleton DatabaseHelper
	static Database _database;                // Singleton Database

	String todoTable = 'todo_table';
	String colId = 'id';
	String colTitle = 'title';
	String colDescription = 'description';
	String colDate = 'date';

	DatabaseHelper._createInstance(); // Named constructor to create instance of DatabaseHelper

	factory DatabaseHelper() {

		if (_databaseHelper == null) {
			_databaseHelper = DatabaseHelper._createInstance(); // This is executed only once, singleton object
		}
		return _databaseHelper;
	}

	Future<Database> get database async {

		if (_database == null) {
			_database = await initializeDatabase();
		}
		return _database;
	}

	Future<Database> initializeDatabase() async {
		// Get the directory path for both Android and iOS to store database.
		Directory directory = await getApplicationDocumentsDirectory();
		String path = directory.path + 'todos.db';

		// Open/create the database at a given path
		var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb);
		return todosDatabase;
	}

	void _createDb(Database db, int newVersion) async {

		await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, '
				'$colDescription TEXT, $colDate TEXT)');
	}

	// Fetch Operation: Get all todo objects from database
	Future<List<Map<String, dynamic>>> getTodoMapList() async {
		Database db = await this.database;

//		var result = await db.rawQuery('SELECT * FROM $todoTable order by $colTitle ASC');
		var result = await db.query(todoTable, orderBy: '$colTitle ASC');
		return result;
	}

	// Insert Operation: Insert a todo object to database
	Future<int> insertTodo(Todo todo) async {
		Database db = await this.database;
		var result = await db.insert(todoTable, todo.toMap());
		return result;
	}

	// Update Operation: Update a todo object and save it to database
	Future<int> updateTodo(Todo todo) async {
		var db = await this.database;
		var result = await db.update(todoTable, todo.toMap(), where: '$colId = ?', whereArgs: [todo.id]);
		return result;
	}


	// Delete Operation: Delete a todo object from database
	Future<int> deleteTodo(int id) async {
		var db = await this.database;
		int result = await db.rawDelete('DELETE FROM $todoTable WHERE $colId = $id');
		return result;
	}

	// Get number of todo objects in database
	Future<int> getCount() async {
		Database db = await this.database;
		List<Map<String, dynamic>> x = await db.rawQuery('SELECT COUNT (*) from $todoTable');
		int result = Sqflite.firstIntValue(x);
		return result;
	}

	// Get the 'Map List' [ List<Map> ] and convert it to 'todo List' [ List<Todo> ]
	Future<List<Todo>> getTodoList() async {

		var todoMapList = await getTodoMapList(); // Get 'Map List' from database
		int count = todoMapList.length;         // Count the number of map entries in db table

		List<Todo> todoList = List<Todo>();
		// For loop to create a 'todo List' from a 'Map List'
		for (int i = 0; i < count; i++) {
			todoList.add(Todo.fromMapObject(todoMapList[i]));
		}

		return todoList;
	}

}

گام 3

اکنون باید صفحه‌ها را برای لیست ToDo پیاده‌سازی کنیم. یک پوشه به نام Screen ایجاد کرده و فایل todo_list.dart را اضافه می‌کنیم.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:todo_list/Models/todo.dart';
import 'package:todo_list/Utils/database_helper.dart';
import 'package:todo_list/Screens/todo_detail.dart';
import 'package:sqflite/sqflite.dart';

class TodoList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return TodoListState();
  }
}

class TodoListState extends State<TodoList> {
  DatabaseHelper databaseHelper = DatabaseHelper();
  List<Todo> todoList;
  int count = 0;

  @override
  Widget build(BuildContext context) {
    if (todoList == null) {
      todoList = List<Todo>();
      updateListView();
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: getTodoListView(),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          debugPrint('FAB clicked');
          navigateToDetail(Todo('', '', ''), 'Add Todo');
        },
        tooltip: 'Add Todo',
        child: Icon(Icons.add),
      ),
    );
  }

  ListView getTodoListView() {
    return ListView.builder(
      itemCount: count,
      itemBuilder: (BuildContext context, int position) {
        return Card(
          color: Colors.white,
          elevation: 2.0,
          child: ListTile(
            leading: CircleAvatar(
              backgroundColor: Colors.amber,
              child: Text(getFirstLetter(this.todoList[position].title),
                  style: TextStyle(fontWeight: FontWeight.bold)),
            ),
            title: Text(this.todoList[position].title,
                style: TextStyle(fontWeight: FontWeight.bold)),
            subtitle: Text(this.todoList[position].description),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                GestureDetector(
                  child: Icon(Icons.delete,color: Colors.red,),
                  onTap: () {
                    _delete(context, todoList[position]);
                  },
                ),
              ],
            ),
            onTap: () {
              debugPrint("ListTile Tapped");
              navigateToDetail(this.todoList[position], 'Edit Todo');
            },
          ),
        );
      },
    );
  }

  getFirstLetter(String title) {
    return title.substring(0, 2);
  }

  void _delete(BuildContext context, Todo todo) async {
    int result = await databaseHelper.deleteTodo(todo.id);
    if (result != 0) {
      _showSnackBar(context, 'Todo Deleted Successfully');
      updateListView();
    }
  }

  void _showSnackBar(BuildContext context, String message) {
    final snackBar = SnackBar(content: Text(message));
    Scaffold.of(context).showSnackBar(snackBar);
  }

  void navigateToDetail(Todo todo, String title) async {
    bool result =
        await Navigator.push(context, MaterialPageRoute(builder: (context) {
      return TodoDetail(todo, title);
    }));

    if (result == true) {
      updateListView();
    }
  }

  void updateListView() {
    final Future<Database> dbFuture = databaseHelper.initializeDatabase();
    dbFuture.then((database) {
      Future<List<Todo>> todoListFuture = databaseHelper.getTodoList();
      todoListFuture.then((todoList) {
        setState(() {
          this.todoList = todoList;
          this.count = todoList.length;
        });
      });
    });
  }

  
}

در کد فوق، یک «نمای لیست» (List View) پیاده‌سازی می‌کنیم. در این نمای لیست، ToDo-هایی که وارد شده‌اند نمایش می‌یابند. بنابراین در حال حاضر باید ToDo-ها را در پایگاه داده وارد کنیم. این کار با استفاده از کد زیر انجام می‌یابد. صفحه دیگری نیز برای افزودن ToDo-ها ایجاد می‌کنیم.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:todo_list/Models/todo.dart';
import 'package:todo_list/Utils/database_helper.dart';
import 'package:intl/intl.dart';

class TodoDetail extends StatefulWidget {

	final String appBarTitle;
	final Todo todo;

	TodoDetail(this.todo, this.appBarTitle);

	@override
  State<StatefulWidget> createState() {

    return TodoDetailState(this.todo, this.appBarTitle);
  }
}

class TodoDetailState extends State<TodoDetail> {

	DatabaseHelper helper = DatabaseHelper();

	String appBarTitle;
	Todo todo;

	TextEditingController titleController = TextEditingController();
	TextEditingController descriptionController = TextEditingController();

	TodoDetailState(this.todo, this.appBarTitle);

	@override
  Widget build(BuildContext context) {

		TextStyle textStyle = Theme.of(context).textTheme.title;

		titleController.text = todo.title;
		descriptionController.text = todo.description;

    return WillPopScope(

	    onWillPop: () {
		    moveToLastScreen();
	    },

	    child: Scaffold(
	    appBar: AppBar(
		    title: Text(appBarTitle),
		    leading: IconButton(icon: Icon(
				    Icons.arrow_back),
				    onPressed: () {
		    	    moveToLastScreen();
				    }
		    ),
	    ),

	    body: Padding(
		    padding: EdgeInsets.only(top: 15.0, left: 10.0, right: 10.0),
		    child: ListView(
			    children: <Widget>[

				    Padding(
					    padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
					    child: TextField(
						    controller: titleController,
						    style: textStyle,
						    onChanged: (value) {
						    	debugPrint('Something changed in Title Text Field');
						    	updateTitle();
						    },
						    decoration: InputDecoration(
							    labelText: 'Title',
							    labelStyle: textStyle,
							    border: OutlineInputBorder(
								    borderRadius: BorderRadius.circular(5.0)
							    )
						    ),
					    ),
				    ),

				    Padding(
					    padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
					    child: TextField(
						    controller: descriptionController,
						    style: textStyle,
						    onChanged: (value) {
							    debugPrint('Something changed in Description Text Field');
							    updateDescription();
						    },
						    decoration: InputDecoration(
								    labelText: 'Description',
								    labelStyle: textStyle,
								    border: OutlineInputBorder(
										    borderRadius: BorderRadius.circular(5.0)
								    )
						    ),
					    ),
				    ),

				    Padding(
					    padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
					    child: Row(
						    children: <Widget>[
						    	Expanded(
								    child: RaisedButton(
									    color: Theme.of(context).primaryColorDark,
									    textColor: Theme.of(context).primaryColorLight,
									    child: Text(
										    'Save',
										    textScaleFactor: 1.5,
									    ),
									    onPressed: () {
									    	setState(() {
									    	  debugPrint("Save button clicked");
									    	  _save();
									    	});
									    },
								    ),
							    ),

							    Container(width: 5.0,),

							    Expanded(
								    child: RaisedButton(
									    color: Theme.of(context).primaryColorDark,
									    textColor: Theme.of(context).primaryColorLight,
									    child: Text(
										    'Delete',
										    textScaleFactor: 1.5,
									    ),
									    onPressed: () {
										    setState(() {
											    debugPrint("Delete button clicked");
											    _delete();
										    });
									    },
								    ),
							    ),

						    ],
					    ),
				    ),


			    ],
		    ),
	    ),

    ));
  }

  void moveToLastScreen() {
		Navigator.pop(context, true);
  }

	// Update the title of todo object
  void updateTitle(){
    todo.title = titleController.text;
  }

	// Update the description of todo object
	void updateDescription() {
		todo.description = descriptionController.text;
	}

	// Save data to database
	void _save() async {

		moveToLastScreen();

		todo.date = DateFormat.yMMMd().format(DateTime.now());
		int result;
		if (todo.id != null) {  // Case 1: Update operation
			result = await helper.updateTodo(todo);
		} else { // Case 2: Insert Operation
			result = await helper.insertTodo(todo);
		}

		if (result != 0) {  // Success
			_showAlertDialog('Status', 'Todo Saved Successfully');
		} else {  // Failure
			_showAlertDialog('Status', 'Problem Saving Todo');
		}

	}


	void _delete() async {

		moveToLastScreen();

		if (todo.id == null) {
			_showAlertDialog('Status', 'No Todo was deleted');
			return;
		}

		int result = await helper.deleteTodo(todo.id);
		if (result != 0) {
			_showAlertDialog('Status', 'Todo Deleted Successfully');
		} else {
			_showAlertDialog('Status', 'Error Occured while Deleting Todo');
		}
	}

	void _showAlertDialog(String title, String message) {

		AlertDialog alertDialog = AlertDialog(
			title: Text(title),
			content: Text(message),
		);
		showDialog(
				context: context,
				builder: (_) => alertDialog
		);
	}

}

اینک کار طراحی اپلیکیشن به پایان رسیده است. ترمینال را باز کرده و پس از اتصال گوشی از طریق USB، دستور زیر را اجرا کنید:

flutter run

نتیجه

ساخت اپلیکیشن فلاتر ToDo

ساخت اپلیکیشن فلاتر ToDo

سخن پایانی

در این راهنما با شیوه استفاده از ویجت‌های فلاتر و پیاده‌سازی آن در یک اپلیکیشن ToDo آشنا شدید. علاوه بر آن با شیوه افزودن داده‌ها در پایگاه داده لوکال در فلاتر را نیز فرا گرفتید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۴ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
abeythilakeudara3

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *