Введение
SQLite появилась в 2000 году и с тех пор стала одним из самых популярных решений для встраивания баз данных в локальные приложения. Давайте в демонстрационном проекте создадим очень простое приложение для управления задачами, которое может создавать, обновлять и удалять элементы из базового интерфейса.
Если у вас ещё нет Flutter, скачать можно на странице установки. Исходный код, который мы используем, доступен здесь на GitHub.
Конфигурация проекта
Итак, что нам нужно для использования SQLite в приложении на Flutter? Во-первых, включить пакет sqflite
внутри проекта в pubspec.yaml
вот таким образом:
name: flutter_sqlite_demo
description: проект-образец, демонстрирующий использование Flutter с SQLite
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
sqflite: ^1.2.0
path_provider: ^1.6.0
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
Мы указали здесь версию пакета sqflite
1.2.0
или новее и path_provider
версии не старше 1.6.0
, а проект упростили для лучшего понимания и облегчения работы с ним.
Создаём простую модель
Для хранения данных нам понадобится модель. Простой класс модели данных позволяет применять необходимые методы для преобразования приемлемого для SQLite формата данных в объект, который может быть использован в приложении. Абстрактный класс Model будет служить базовым классом для моделей данных. Этот файл находится в lib/models/model.dart:
abstract class Model {
int id;
static fromMap() {}
toMap() {}
}
Класс Model очень прост и удобен для определения свойств/методов (таких как приведённый выше id
), которые можно ожидать от моделей данных. Это позволяет создавать одну или несколько конкретных моделей данных, соответствующих такому базовому шаблону проектирования. В примере с нашим приложением для управления задачами класс модели конкретного элемента создаётся в lib/models/todo-item.dart:
import 'package:flutter_sqlite_demo/models/model.dart';
class TodoItem extends Model {
static String table = 'todo_items';
int id;
String task;
bool complete;
TodoItem({ this.id, this.task, this.complete });
Map<String, dynamic> toMap() {
Map<String, dynamic> map = {
'task': task,
'complete': complete
};
if (id != null) { map['id'] = id; }
return map;
}
static TodoItem fromMap(Map<String, dynamic> map) {
return TodoItem(
id: map['id'],
task: map['task'],
complete: map['complete'] == 1
);
}
}
Этот класс TodoItem содержит свойства для task
и complete
и простой конструктор для создания нового элемента, а для преобразования между экземплярами TodoItem и объектами карты, используемыми базой данных, определены методы toMap
и fromMap
. Заметим, что id
добавляется в карту, только будучи значением не null
.
Класс базы данных
Ради удобства и простоты сопровождения основные методы обработки базы данных помещаем в lib/services/db.dart (так лучше, чем разбрасывать логику обработки данных по всему приложению):
import 'dart:async';
import 'package:flutter_sqlite_demo/models/model.dart';
import 'package:sqflite/sqflite.dart';
abstract class DB {
static Database _db;
static int get _version => 1;
static Future<void> init() async {
if (_db != null) { return; }
try {
String _path = await getDatabasesPath() + 'example';
_db = await openDatabase(_path, version: _version, onCreate: onCreate);
}
catch(ex) {
print(ex);
}
}
static void onCreate(Database db, int version) async =>
await db.execute('CREATE TABLE todo_items (id INTEGER PRIMARY KEY NOT NULL, task STRING, complete BOOLEAN)');
static Future<List<Map<String, dynamic>>> query(String table) async => _db.query(table);
static Future<int> insert(String table, Model model) async =>
await _db.insert(table, model.toMap());
static Future<int> update(String table, Model model) async =>
await _db.update(table, model.toMap(), where: 'id = ?', whereArgs: [model.id]);
static Future<int> delete(String table, Model model) async =>
await _db.delete(table, where: 'id = ?', whereArgs: [model.id]);
}
Этот класс абстрактный: из него нельзя создавать объекты, да и нужна всего одна его копия в памяти. В свойстве _db
у него есть ссылка на базу данных SQLite. Номер версии базы данных захардкоден значением 1
, хотя в более сложных приложениях версию базы данных можно использовать для миграции схем базы данных вверх или вниз в версии, благодаря чему можно развёртывать новые функции без необходимости стирать базу данных и начинать всё с нуля.
Внутри метода init
создаётся экземпляр базы данных SQLite с именем базы данных example
специально для нашего проекта. Если база данных с именем example
ещё не существует, автоматически вызывается onCreate
. Именно здесь находятся запросы на создание структуры таблицы. В нашем случае создаётся таблица todo_items
с первичным ключом для id
и полями, которые соответствуют свойствам в классе TodoItem
.
Метод query
, равно как и методы insert
, update
и delete
, определяется для выполнения стандартных операций в базе данных. Благодаря им, у нас есть простые абстракции и возможность поместить логику обработки данных в этот класс, что может быть очень полезным при рефакторинге и сопровождении кода в приложении. Если бы их не было, нам пришлось бы, например, искать и заменять кучу строк в разных файлах или устранять странные ошибки после внесения простых изменений.
Главный файл приложения
И последнее, но не менее важное: логика приложения и пользовательский интерфейс у нас находятся в lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_sqlite_demo/models/todo-item.dart';
import 'package:flutter_sqlite_demo/services/db.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await DB.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData( primarySwatch: Colors.indigo ),
home: MyHomePage(title: 'Flutter SQLite Demo App'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _task;
List<TodoItem> _tasks = [];
TextStyle _style = TextStyle(color: Colors.white, fontSize: 24);
List<Widget> get _items => _tasks.map((item) => format(item)).toList();
Widget format(TodoItem item) {
return Dismissible(
key: Key(item.id.toString()),
child: Padding(
padding: EdgeInsets.fromLTRB(12, 6, 12, 4),
child: FlatButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(item.task, style: _style),
Icon(item.complete == true ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: Colors.white)
]
),
onPressed: () => _toggle(item),
)
),
onDismissed: (DismissDirection direction) => _delete(item),
);
}
void _toggle(TodoItem item) async {
item.complete = !item.complete;
dynamic result = await DB.update(TodoItem.table, item);
print(result);
refresh();
}
void _delete(TodoItem item) async {
DB.delete(TodoItem.table, item);
refresh();
}
void _save() async {
Navigator.of(context).pop();
TodoItem item = TodoItem(
task: _task,
complete: false
);
await DB.insert(TodoItem.table, item);
setState(() => _task = '' );
refresh();
}
void _create(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Create New Task"),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop()
),
FlatButton(
child: Text('Save'),
onPressed: () => _save()
)
],
content: TextField(
autofocus: true,
decoration: InputDecoration(labelText: 'Task Name', hintText: 'e.g. pick up bread'),
onChanged: (value) { _task = value; },
),
);
}
);
}
@override
void initState() {
refresh();
super.initState();
}
void refresh() async {
List<Map<String, dynamic>> _results = await DB.query(TodoItem.table);
_tasks = _results.map((item) => TodoItem.fromMap(item)).toList();
setState(() { });
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar( title: Text(widget.title) ),
body: Center(
child: ListView( children: _items )
),
floatingActionButton: FloatingActionButton(
onPressed: () { _create(context); },
tooltip: 'New TODO',
child: Icon(Icons.library_add),
)
);
}
}
Это стандартный файл, определяющий внешний вид и поведение любого приложения с Flutter. Во время инициализации строка WidgetsFlutterBinding.ensureInitialized()
обеспечит корректную инициализацию приложения Flutter, тогда как база данных инициализируется с помощью await DB.init()
.
Когда приложение запускается и виджет MyHomePage
визуализируется, вызов refresh()
извлекает список задач из таблицы todo_items
и выполняет его отображение на список List
объектов TodoItem
. Они отображаются в главном ListView
через средство доступа _items
, которое принимает список List
объектов TodoItem
и форматирует его как список виджетов, содержащий текстовый элемент задач и индикатор завершённости этого элемента.
Задачи добавляются нажатием на плавающую круглую кнопку и введением названия задачи. При нажатии на кнопку Save
вновь созданный элемент списка задач будет добавлен в базу данных с помощью DB.insert
. Нажав на задачу в списке, можно переключаться между состояниями завершена /не завершена: здесь используем булеву переменную complete
и сохраняем изменённый объект в базе данных с помощью DB.update
. Проведя пальцем по задаче в горизонтальном направлении, можно удалить элемент задач: используем для этого метод DB.delete
. Всякий раз, когда в список вносятся изменения, вызов refresh()
с последующим setState()
обеспечивает правильность обновления списка.
Заключение
SQLite представляет удобный способ локального хранения данных в приложении. В примере проекта было показано, как выполнять основные операции управления данными для создания, добавления, изменения, обновления и удаления простых записей в базе данных SQLite. Ещё больше о плагине sqflite
и функциях, которые он поддерживает, можно узнать здесь.
Спасибо за внимание и удачи в вашем следующем проекте на Flutter!
Читайте также:
Перевод статьи Kenneth Reilly: How to use Flutter with SQLite