Как использовать Flutter с SQLite



Книга Как использовать Flutter с SQLite

Введение


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!



691   0  

Comments

    Ничего не найдено.