はじめに
表題通り、FlutterでORMのDriftを利用していきます。
Driftとは
Dart および Flutter アプリケーション用の強力なデータベース ライブラリです。Daoクラスで定義したメソッドに関連するコードや、テーブルとそのカラムに対応するオブジェクトを自動生成します。
ソースコード
dependencies:
flutter:
sdk: flutter
drift: ^2.0.0 # Driftのライブラリ
sqlite3_flutter_libs: ^0.5.24 # sqliteのライブラリ
path_provider: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter
drift_dev: ^2.0.0 #driftの自動生成用のライブラリ
build_runner: ^2.1.5 #自動生成用のライブラリ(`.g.dart` files)
sqlite3_flutter_libs
のversionを^0.6にした場合、ビルドエラー発生。
drift_devの2.0と相性が悪いようです😅
lib/infra/repository
├── app_database.dart // データベースの設定と管理
├── app_database.g.dart // Driftのコードジェネレーターによって自動生成されたファイル
├── tables.dart // データベース内のテーブル定義
├── todo_dao.dart // DAO (Data Access Object) の定義
└── todo_dao.g.dart // Driftのコードジェネレーターによって自動生成されたファイル
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'tables.dart'; // テーブル定義をインポート
import 'todo_dao.dart'; // DAOをインポート
part 'app_database.g.dart';
@DriftDatabase(tables: [Todos], daos: [TodoDao])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
import 'package:drift/drift.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 50)();
TextColumn get content => text().named('body')();
DateTimeColumn get dueDate => dateTime().nullable()();
}
モデルからテーブルを構築するための定義となります。
import 'package:drift/drift.dart';
import 'app_database.dart';
import 'tables.dart';
part 'todo_dao.g.dart';
@DriftAccessor(tables: [Todos])
class TodoDao extends DatabaseAccessor<AppDatabase> with _$TodoDaoMixin {
TodoDao(AppDatabase db) : super(db);
Future<List<Todo>> getAllTodos() => select(todos).get();
Stream<List<Todo>> watchAllTodos() => select(todos).watch();
Future insertTodo(Insertable<Todo> todo) => into(todos).insert(todo);
Future updateTodo(Insertable<Todo> todo) => update(todos).replace(todo);
Future deleteTodo(Insertable<Todo> todo) => delete(todos).delete(todo);
}
CRUD処理をmodelの定義から記載しています。
daoを元にSQLを発行するモデルを自動生成生成します。
ここまでで準備OKです。では自動生成を実行していきます。
flutter pub run build_runner build
すると、下記2つのコードが自動生成されます。
・lib/infra/repository/app_database.g.dart
・lib/infra/repository/todo_dao.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_database.dart';
// ignore_for_file: type=lint
class $TodosTable extends Todos with TableInfo<$TodosTable, Todo> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$TodosTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _titleMeta = const VerificationMeta('title');
@override
late final GeneratedColumn<String> title = GeneratedColumn<String>(
'title', aliasedName, false,
additionalChecks:
GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50),
type: DriftSqlType.string,
requiredDuringInsert: true);
static const VerificationMeta _contentMeta =
const VerificationMeta('content');
@override
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'body', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _dueDateMeta =
const VerificationMeta('dueDate');
@override
late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(
'due_date', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
@override
List<GeneratedColumn> get $columns => [id, title, content, dueDate];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'todos';
@override
VerificationContext validateIntegrity(Insertable<Todo> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('title')) {
context.handle(
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
} else if (isInserting) {
context.missing(_titleMeta);
}
if (data.containsKey('body')) {
context.handle(_contentMeta,
content.isAcceptableOrUnknown(data['body']!, _contentMeta));
} else if (isInserting) {
context.missing(_contentMeta);
}
if (data.containsKey('due_date')) {
context.handle(_dueDateMeta,
dueDate.isAcceptableOrUnknown(data['due_date']!, _dueDateMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Todo map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return Todo(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
title: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}title'])!,
content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}body'])!,
dueDate: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}due_date']),
);
}
@override
$TodosTable createAlias(String alias) {
return $TodosTable(attachedDatabase, alias);
}
}
class Todo extends DataClass implements Insertable<Todo> {
final int id;
final String title;
final String content;
final DateTime? dueDate;
const Todo(
{required this.id,
required this.title,
required this.content,
this.dueDate});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['title'] = Variable<String>(title);
map['body'] = Variable<String>(content);
if (!nullToAbsent || dueDate != null) {
map['due_date'] = Variable<DateTime>(dueDate);
}
return map;
}
TodosCompanion toCompanion(bool nullToAbsent) {
return TodosCompanion(
id: Value(id),
title: Value(title),
content: Value(content),
dueDate: dueDate == null && nullToAbsent
? const Value.absent()
: Value(dueDate),
);
}
factory Todo.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return Todo(
id: serializer.fromJson<int>(json['id']),
title: serializer.fromJson<String>(json['title']),
content: serializer.fromJson<String>(json['content']),
dueDate: serializer.fromJson<DateTime?>(json['dueDate']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'title': serializer.toJson<String>(title),
'content': serializer.toJson<String>(content),
'dueDate': serializer.toJson<DateTime?>(dueDate),
};
}
Todo copyWith(
{int? id,
String? title,
String? content,
Value<DateTime?> dueDate = const Value.absent()}) =>
Todo(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
dueDate: dueDate.present ? dueDate.value : this.dueDate,
);
@override
String toString() {
return (StringBuffer('Todo(')
..write('id: $id, ')
..write('title: $title, ')
..write('content: $content, ')
..write('dueDate: $dueDate')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, title, content, dueDate);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Todo &&
other.id == this.id &&
other.title == this.title &&
other.content == this.content &&
other.dueDate == this.dueDate);
}
class TodosCompanion extends UpdateCompanion<Todo> {
final Value<int> id;
final Value<String> title;
final Value<String> content;
final Value<DateTime?> dueDate;
const TodosCompanion({
this.id = const Value.absent(),
this.title = const Value.absent(),
this.content = const Value.absent(),
this.dueDate = const Value.absent(),
});
TodosCompanion.insert({
this.id = const Value.absent(),
required String title,
required String content,
this.dueDate = const Value.absent(),
}) : title = Value(title),
content = Value(content);
static Insertable<Todo> custom({
Expression<int>? id,
Expression<String>? title,
Expression<String>? content,
Expression<DateTime>? dueDate,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (title != null) 'title': title,
if (content != null) 'body': content,
if (dueDate != null) 'due_date': dueDate,
});
}
TodosCompanion copyWith(
{Value<int>? id,
Value<String>? title,
Value<String>? content,
Value<DateTime?>? dueDate}) {
return TodosCompanion(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
dueDate: dueDate ?? this.dueDate,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (title.present) {
map['title'] = Variable<String>(title.value);
}
if (content.present) {
map['body'] = Variable<String>(content.value);
}
if (dueDate.present) {
map['due_date'] = Variable<DateTime>(dueDate.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TodosCompanion(')
..write('id: $id, ')
..write('title: $title, ')
..write('content: $content, ')
..write('dueDate: $dueDate')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
late final $TodosTable todos = $TodosTable(this);
late final TodoDao todoDao = TodoDao(this as AppDatabase);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [todos];
}
データベースのテーブル定義とデータアクセスロジックを一貫して扱うための基盤を提供します。アプリケーション開発者は、これらのクラスやメソッドを使うことで、SQLを直接書くことなくデータベース操作を行うことができます。
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo_dao.dart';
// ignore_for_file: type=lint
mixin _$TodoDaoMixin on DatabaseAccessor<AppDatabase> {
$TodosTable get todos => attachedDatabase.todos;
}
このmixinは、TodoDaoクラスにtodosテーブルへのアクセスを追加する役割を持っています。これにより、TodoDao内でtodosテーブルに対するクエリや操作を簡単に行えるようになります。自動生成されたコードなので、開発者はこれを直接編集する必要はなく、DAOクラス内でテーブル操作が簡潔にできるようサポートしてくれるものです。
動作検証
では実際にアプリケーションから実行できるか確認していきましょう。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'infra/repository/app_database.dart';
import 'screen/todo_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => AppDatabase(),
dispose: (_, AppDatabase db) => db.close(),
child: MaterialApp(
title: 'Drift Todo Example',
home: TodoScreen(),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../infra/repository/todo_dao.dart';
import '../infra/repository/app_database.dart';
import 'package:drift/drift.dart' as drift;
class TodoScreen extends StatefulWidget {
@override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TextEditingController _titleController = TextEditingController();
final TextEditingController _contentController = TextEditingController();
TodoDao? _todoDao;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final database = Provider.of<AppDatabase>(context);
_todoDao = database.todoDao;
}
void _insertTodo() async {
final newTodo = TodosCompanion(
title: drift.Value(_titleController.text),
content: drift.Value(_contentController.text),
);
await _todoDao?.insertTodo(newTodo);
_titleController.clear();
_contentController.clear();
setState(() {});
}
void _deleteTodo(Todo todo) async {
await _todoDao?.deleteTodo(todo);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Title'),
),
TextField(
controller: _contentController,
decoration: InputDecoration(labelText: 'Content'),
),
ElevatedButton(
onPressed: _insertTodo,
child: Text('Add Todo'),
),
],
),
),
Expanded(
child: StreamBuilder<List<Todo>>(
stream: _todoDao?.watchAllTodos(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text(todo.content),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteTodo(todo),
),
);
},
);
},
),
),
],
),
);
}
}