💬
driftを使ってToDoアプリを作ってみる
初記事です。
ずっとFlutterを勉強してるのですが
成果が分かりずらいので記事書きやってみたいと思います。
参考記事
今回はdriftです。
driftはスマホ内でデータベースを保存できるツールです。
アプリの動き
今回使用するパッケージ
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
drift: ^1.5.0
path: ^1.8.0
path_provider: ^2.0.9
sqlite3_flutter_libs: ^0.5.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
build_runner: ^2.1.11
drift_dev: ^1.5.2
まずは大枠の作成
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
home: DriftHome(),
);
}
}
class DriftHome extends StatefulWidget {
const DriftHome({Key? key}) : super(key: key);
State<DriftHome> createState() => _DriftHomeState();
}
class _DriftHomeState extends State<DriftHome> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TODO-DRIFT'),
backgroundColor: Colors.cyan,
),
body: SafeArea(
child: Column(
children: [
TextButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'ここにタイトルを入力',
style: TextStyle(color: Colors.black, fontSize: 20),
),
],
),
onPressed: () {}, //ワンタッチ処理※これを書かないとエラー
onLongPress: () {}, //長押し処理
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
icon: Icon(Icons.notes),
foregroundColor: Colors.black, //文字色
backgroundColor: Colors.amber, //背景色
label: Text('Add ToDo Page'),
onPressed: () {},
),
);
}
}
add_page.dart
import 'package:flutter/material.dart';
class AddPage extends StatefulWidget {
const AddPage({Key? key}) : super(key: key);
State<AddPage> createState() => _AddPageState();
}
class _AddPageState extends State<AddPage> {
final TextEditingController textCon =TextEditingController();
final TextEditingController alphaCon =TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('AddPage'),
),
body: Column(
children: [
TextFormField(
controller: textCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列左'
),
),
TextFormField(
controller: alphaCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列右'
),
),
],
),
);
}
}
今回は編集画面を追加画面を同じにするので
コードを複製します。(多分もっといい方法があるはず...)
一部、修正
edit_page.dart
import 'package:flutter/material.dart';
class EditPage extends StatefulWidget {
const EditPage({Key? key}) : super(key: key);
State<EditPage> createState() => _EditPageState();
}
class _EditPageState extends State<EditPage> {
final TextEditingController textCon =TextEditingController();
final TextEditingController alphaCon =TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('EditPage'),
backgroundColor: Colors.greenAccent,
),
body: Column(
children: [
TextFormField(
controller: textCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列左'
),
),
TextFormField(
controller: alphaCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列右'
),
),
],
),
);
}
}
これで画面構成の大枠は完成です。
Picture
![]
続いてdrift部分
todo_try_db.dart
import 'package:drift/drift.dart';
// ファイル名+g.dartの形式で記載
part 'todo_try_db.g.dart';
//データベースのテーブルを定義
//クラス名をTodoTry-->テーブル名がTodoTryになる
//Column=列でid(int),textLeft(String),textRight(String)を持っています。
//autoIncrement()を設定すると、データ追加時に自動生成される
class TodoTry extends Table{
IntColumn get id=>integer().autoIncrement()();
TextColumn get textLeft=>text()();
TextColumn get textRight=>text()();
}
//データベースクラスの定義
//生成処理やデータの追加処理などを記載
//@DriftDatabase(tables:[TodoTry])でデータベースとテーブルの紐づけができる
(tables:[TodoTry])
class MyDB extends _$MyDB{}
コードの自動生成
flutter pub run build_runner build
再生成するときは
flutter pub run build_runner build --delete-conflicting-outputs
ターミナル文
[INFO] Generating build script...
[INFO] Generating build script completed, took 416ms
[INFO] Precompiling build script......
[INFO] Precompiling build script... completed, took 7.6s
[INFO] Initializing inputs
[INFO] Building new asset graph...
[INFO] Building new asset graph completed, took 985ms
[INFO] Checking for unexpected pre-existing outputs....
[INFO] Checking for unexpected pre-existing outputs. completed, took 2ms
[INFO] Running build...
[INFO] Generating SDK summary...
[INFO] 3.5s elapsed, 0/5 actions completed.
[INFO] Generating SDK summary completed, took 3.5s
[INFO] 4.5s elapsed, 0/5 actions completed.
[INFO] 5.6s elapsed, 0/5 actions completed.
[INFO] 6.6s elapsed, 0/5 actions completed.
[INFO] 7.7s elapsed, 0/5 actions completed.
[INFO] 13.5s elapsed, 0/5 actions completed.
[INFO] Running build completed, took 14.3s
[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 36ms
[INFO] Succeeded after 14.4s with 2 outputs (11 actions)
todo_try_db.g.dart(自動生成コード)
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo_try_db.dart';
// **************************************************************************
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class TodoTryData extends DataClass implements Insertable<TodoTryData> {
final int id;
final String textLeft;
final String textRight;
TodoTryData(
{required this.id, required this.textLeft, required this.textRight});
factory TodoTryData.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoTryData(
id: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
textLeft: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}text_left'])!,
textRight: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}text_right'])!,
);
}
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['text_left'] = Variable<String>(textLeft);
map['text_right'] = Variable<String>(textRight);
return map;
}
TodoTryCompanion toCompanion(bool nullToAbsent) {
return TodoTryCompanion(
id: Value(id),
textLeft: Value(textLeft),
textRight: Value(textRight),
);
}
factory TodoTryData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoTryData(
id: serializer.fromJson<int>(json['id']),
textLeft: serializer.fromJson<String>(json['textLeft']),
textRight: serializer.fromJson<String>(json['textRight']),
);
}
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'textLeft': serializer.toJson<String>(textLeft),
'textRight': serializer.toJson<String>(textRight),
};
}
TodoTryData copyWith({int? id, String? textLeft, String? textRight}) =>
TodoTryData(
id: id ?? this.id,
textLeft: textLeft ?? this.textLeft,
textRight: textRight ?? this.textRight,
);
String toString() {
return (StringBuffer('TodoTryData(')
..write('id: $id, ')
..write('textLeft: $textLeft, ')
..write('textRight: $textRight')
..write(')'))
.toString();
}
int get hashCode => Object.hash(id, textLeft, textRight);
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoTryData &&
other.id == this.id &&
other.textLeft == this.textLeft &&
other.textRight == this.textRight);
}
class TodoTryCompanion extends UpdateCompanion<TodoTryData> {
final Value<int> id;
final Value<String> textLeft;
final Value<String> textRight;
const TodoTryCompanion({
this.id = const Value.absent(),
this.textLeft = const Value.absent(),
this.textRight = const Value.absent(),
});
TodoTryCompanion.insert({
this.id = const Value.absent(),
required String textLeft,
required String textRight,
}) : textLeft = Value(textLeft),
textRight = Value(textRight);
static Insertable<TodoTryData> custom({
Expression<int>? id,
Expression<String>? textLeft,
Expression<String>? textRight,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (textLeft != null) 'text_left': textLeft,
if (textRight != null) 'text_right': textRight,
});
}
TodoTryCompanion copyWith(
{Value<int>? id, Value<String>? textLeft, Value<String>? textRight}) {
return TodoTryCompanion(
id: id ?? this.id,
textLeft: textLeft ?? this.textLeft,
textRight: textRight ?? this.textRight,
);
}
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (textLeft.present) {
map['text_left'] = Variable<String>(textLeft.value);
}
if (textRight.present) {
map['text_right'] = Variable<String>(textRight.value);
}
return map;
}
String toString() {
return (StringBuffer('TodoTryCompanion(')
..write('id: $id, ')
..write('textLeft: $textLeft, ')
..write('textRight: $textRight')
..write(')'))
.toString();
}
}
class $TodoTryTable extends TodoTry with TableInfo<$TodoTryTable, TodoTryData> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
$TodoTryTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _textLeftMeta = const VerificationMeta('textLeft');
late final GeneratedColumn<String?> textLeft = GeneratedColumn<String?>(
'text_left', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _textRightMeta = const VerificationMeta('textRight');
late final GeneratedColumn<String?> textRight = GeneratedColumn<String?>(
'text_right', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
List<GeneratedColumn> get $columns => [id, textLeft, textRight];
String get aliasedName => _alias ?? 'todo_try';
String get actualTableName => 'todo_try';
VerificationContext validateIntegrity(Insertable<TodoTryData> 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('text_left')) {
context.handle(_textLeftMeta,
textLeft.isAcceptableOrUnknown(data['text_left']!, _textLeftMeta));
} else if (isInserting) {
context.missing(_textLeftMeta);
}
if (data.containsKey('text_right')) {
context.handle(_textRightMeta,
textRight.isAcceptableOrUnknown(data['text_right']!, _textRightMeta));
} else if (isInserting) {
context.missing(_textRightMeta);
}
return context;
}
Set<GeneratedColumn> get $primaryKey => {id};
TodoTryData map(Map<String, dynamic> data, {String? tablePrefix}) {
return TodoTryData.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
$TodoTryTable createAlias(String alias) {
return $TodoTryTable(attachedDatabase, alias);
}
}
abstract class _$MyDB extends GeneratedDatabase {
_$MyDB(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
late final $TodoTryTable todoTry = $TodoTryTable(this);
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
List<DatabaseSchemaEntity> get allSchemaEntities => [todoTry];
}
データベースの生成
todo_try_db.dart
import 'package:drift/drift.dart';
//下記のパッケージを追加
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p_dev;
part 'todo_try_db.g.dart';
class TodoTry extends Table{
IntColumn get id=>integer().autoIncrement()();
TextColumn get textLeft=>text()();
TextColumn get textRight=>text()();
}
(tables:[TodoTry])
class MyDB extends _$MyDB{
//データベースのインスタンス生成と同時にデータベースとの接続処理を行う
MyDB() : super(_openConnection());
//データベースのバージョン指定
int get schemaVersion=>1;
}
//データベースの保存位置を取得する設定
LazyDatabase _openConnection(){
return LazyDatabase(()async{
final dbFolder=await getApplicationDocumentsDirectory();
final file=File(p_dev.join(dbFolder.path,'db.sqlite'));
return NativeDatabase(file);
});
}
データベースの生成を行うためにmain.dartのmain関数の中で、runApp()の前に行う。
main.dart
void main() {
final database=MyDB();
runApp(const MyApp());
}
データの表示
todo_try_db.dart
//データベースクラスの定義
//生成処理やデータの追加処理などを記載
//@DriftDatabase(tables:[TodoTry])でデータベースとテーブルの紐づけができる
(tables:[TodoTry])
class MyDB extends _$MyDB{
//データベースのインスタンス生成と同時にデータベースとの接続処理を行う
MyDB() : super(_openConnection());
//データベースのバージョン指定
int get schemaVersion=>1;
//Streamでデータ取得する,selectでデータ選択,getでデータ取得
Stream<List<TodoTryData>> watchEnries(){
return (select(todoTry)).watch();
}
//Futureを用いてデータを取得
//selectでデータ選択,getでデータ取得
//データの監視を続けるためにStreamを用いて,main.dartにStreamBuilderを追加
Future<List<TodoTryData>> get allTodoEntries =>select(todoTry).get();
}
databaseの受け渡し&StreamBuilderの追加
main.dart
import 'package:drift_220605/add_page.dart';
import 'package:drift_220605/todo_try_db.dart';
import 'package:flutter/material.dart';
void main() {
final database = MyDB();
runApp(MyApp(db: database));
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.db,
}) : super(key: key);
final MyDB db;
Widget build(BuildContext context) {
return MaterialApp(
home: DriftHome(db: db),
);
}
}
class DriftHome extends StatefulWidget {
const DriftHome({
Key? key,
required this.db,
}) : super(key: key);
final MyDB db;
State<DriftHome> createState() => _DriftHomeState();
}
class _DriftHomeState extends State<DriftHome> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TODO-DRIFT'),
backgroundColor: Colors.cyan,
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: StreamBuilder(
stream: widget.db.watchEnries(),
builder: (BuildContext context,
AsyncSnapshot<List<TodoTryData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 3),
borderRadius: BorderRadius.circular(10),
),
child: TextButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
snapshot.data![index].textLeft,
style: TextStyle(
color: Colors.black, fontSize: 20),
),
Text(
snapshot.data![index].textRight,
style: TextStyle(
color: Colors.black, fontSize: 20),
),
],
),
onLongPress: () async {},
onPressed: () async {},
),
),
);
});
},
),
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
icon: Icon(Icons.notes),
foregroundColor: Colors.black,
//文字色
backgroundColor: Colors.amber,
//背景色
label: Text('Add ToDo Page'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddPage(),
),
);
},
),
);
}
}
database.watchEntriesでデータの取得を行う
本当は状態管理した方がいいけど今回はパス
todo_try_db.dart
//データベースのバージョン指定
int get schemaVersion => 1;
Stream<List<TodoTryData>> watchEnries() {
return (select(todoTry)).watch();
}
Future<List<TodoTryData>> get allTodoEntries => select(todoTry).get();
//追加処理
//into...データ追加するテーブルの指定
//insert...データクラスであるTodoTryCompanionを追加
//TodoTryCompanion...データの追加や後進に有用なデータクラス
//このデータクラスを使うことにより、idを指定せずにデータを追加したいときなど、一部のデータだけを追加できる
Future<int> addTodoTry(String textLeft, String textRight) {
return into(todoTry).insert(TodoTryCompanion(
textLeft: Value(textLeft), textRight: Value(textRight)));
}
main.dart
floatingActionButton: FloatingActionButton.extended(
icon: Icon(Icons.notes),
foregroundColor: Colors.black,
//文字色
backgroundColor: Colors.amber,
//背景色
label: Text('Add ToDo Page'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
//追加
builder: (context) => AddPage(db: widget.db,),
),
);
},
),
add_page.dart
import 'package:drift_220605/main.dart';
import 'package:drift_220605/todo_try_db.dart';
import 'package:flutter/material.dart';
class AddPage extends StatefulWidget {
//追加
const AddPage({Key? key,required this.db}) : super(key: key);
final MyDB db;
State<AddPage> createState() => _AddPageState();
}
class _AddPageState extends State<AddPage> {
final TextEditingController textCon =TextEditingController();
final TextEditingController alphaCon =TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.greenAccent,
title: Text('AddPage'),
),
body: Column(
children: [
TextFormField(
controller: textCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列左'
),
),
TextFormField(
controller: alphaCon,
decoration: InputDecoration(
hintText: '文字列を入力',
labelText: '文字列右'
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: Icon(Icons.plus_one),
foregroundColor: Colors.black,
//文字色
backgroundColor: Colors.greenAccent,
//背景色
label: Text('Add ToDo Page'),
//追加
//空欄の場合はボタンを押せないようにする
onPressed: () async{
if(textCon.text!=""){
if(alphaCon.text!=""){
await widget.db.addTodoTry(textCon.text, alphaCon.text);
Navigator.push(
context,
MaterialPageRoute(
//追加
builder: (context) => DriftHome(db: widget.db,),
),
);
}
}
},
),
);
}
}
表示を変更
main.dart
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Center(
child: Text(
snapshot.data![index].textLeft,
style: TextStyle(
color: Colors.black, fontSize: 20),
),
),
),
Expanded(
child: Center(
child: Text(
snapshot.data![index].textRight,
style: TextStyle(
color: Colors.black, fontSize: 20),
),
),
),
],
),
画像!
データの更新
main.dart
//編集処理
onPressed: () async {
final todolist = await widget.db.allTodoEntries;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditPage(
db: widget.db,
editTextL: snapshot
.data![index].textLeft,
editTextR: snapshot
.data![index].textRight,
index: index,
)));
},
edit_page.dart
import 'package:drift_220605/main.dart';
import 'package:drift_220605/todo_try_db.dart';
import 'package:flutter/material.dart';
class EditPage extends StatefulWidget {
const EditPage({Key? key,
required this.db,
required this.editTextL,
required this.editTextR,
required this.index})
: super(key: key);
final MyDB db;
final String? editTextL;
final String? editTextR;
final int? index;
State<EditPage> createState() => _EditPageState();
}
class _EditPageState extends State<EditPage> {
// final TextEditingController textCon = TextEditingController();
// final TextEditingController alphaCon = TextEditingController();
void initState() {
super.initState();
final textCon = TextEditingController(text: widget.editTextL);
final alphaCon = TextEditingController(text: widget.editTextR);
}
Widget build(BuildContext context) {
final TextEditingController textCon = TextEditingController();
final TextEditingController alphaCon = TextEditingController();
textCon.text = widget.editTextL ?? "";
alphaCon.text = widget.editTextR ?? "";
return Scaffold(
appBar: AppBar(
title: Text('EditPage'),
backgroundColor: Colors.greenAccent,
),
body: Column(
children: [
TextFormField(
controller: textCon,
decoration: InputDecoration(
hintText: '文字列を入力', labelText: '文字列左'),
),
TextFormField(
controller: alphaCon,
decoration: InputDecoration(
hintText: '文字列を入力', labelText: '文字列右'),
),
],
),
floatingActionButton: StreamBuilder(
stream: widget.db.watchEnries(),
builder: (BuildContext context,
AsyncSnapshot<List<TodoTryData>> snapshot) {
return FloatingActionButton.extended(
icon: Icon(Icons.plus_one),
label: Text("Edit ToDo"),
backgroundColor: (textCon.text == "") ? Colors.black : Colors
.red,
onPressed: () async {
if (textCon.text != null) {
// await widget.database.addTodo(textCon.text);
await widget.db.updateTodoTry(
snapshot.data![widget.index!], textCon.text,
alphaCon.text);
Navigator.push(context, MaterialPageRoute(
builder: (context) => DriftHome(db: widget.db,)));
}
}
);
}
),
);
}
}
todo_try_db.dart.dart
//更新
Future<int> updateTodoTry(TodoTryData todoTryData,String textLeft, String textRight){
return (update(todoTry)..where((tbl) => tbl.id.equals(todoTryData.id)))
.write(TodoTryCompanion(
textLeft: Value(textLeft),
textRight: Value(textRight),
));
}
GIF
削除
main.dart
onLongPress: () async {
final list =
await widget.db.allTodoEntries;
if (list.isNotEmpty) {
await widget.db.deleteTodoTry(list[index]);
}
},
todo_try_db.dart
//編集の下に
//削除
Future<void> deleteTodoTry(TodoTryData todolistData){
return (delete(todoTry)..where((tbl) => tbl.id.equals(todolistData.id))).go();
}
GIF
これで一連の流れは完成です。
今回はAoiさんの記事を参考にしました。
ありがとうございました!
【Flutter】 Drift の基本的な使い方解説 #Flutter大学
@Aoi_Umigishiより
Discussion