Как использовать 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!


Перевод статьи Kenneth Reilly: How to use Flutter with SQLite


Поделиться статьей:


Вернуться к статьям

Комментарии

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