Flutterでmoor(SQLite)を使う その1
こんにちは
FlutterでローカルDB(SQLite)を使ってみたいと思います。
その際に、FlutterとSQLiteの仲立ちをするmoorというモジュールを使ってみます。
LaravelのORマッパー(Eloquent)みたいに、SQLを極力書かずに簡単にローカルDBのCRUDができるみたいです。
本記事では、SQLiteとmoorの環境構築、簡単なDBの作成、画面からのCRUDをおこないます。
moorのドキュメントに沿って環境構築してみましょう
Flutterのversionは2.0;1、エディタはAndroidStudioを使用しています。
モジュールを「pubspec.yaml」に追加(赤字の部分です)
※moorのversionは4.2.0(最新)です。(2021年3月22日)
・pubspec.yaml
name: flutter_app5
description: A new Flutter application.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
provider: ^5.0.0
moor: ^4.2.0
sqlite3_flutter_libs: ^0.4.1
path_provider:
path:
dev_dependencies:
flutter_test:
sdk: flutter
moor_generator: ^4.1.0
build_runner:
flutter:
uses-material-design: true
記載が終わりましたら、右上の「Pub get」を押してinstall
■DBの作成
「lib」配下に「database」というディレクトリ、その中に「company.dart」ファイルを作成し以下を記載します。
部署テーブルと社員テーブルを作ります。
カラムの命名規則はキャメルケースを推奨しているみたいです。
・lib/database/company.dart
import 'package:moor/moor.dart';
part 'company.g.dart';
//部署テーブル
class Departments extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 2, max: 20)();
TextColumn get place => text().withLength(min: 2, max: 20)();
TextColumn get businessContent => text().nullable()();
}
//社員テーブル 別名をWorker
//部署idを外部キーに設定
@DataClassName("Worker")
class Syain extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get department =>
integer().customConstraint('REFERENCES departments(id)')();
}
@UseMoor(tables: [Departments, Syain])
class MyDatabase {}
「company.dart」内の「part」が「company.g.dart」である必要があります。
異なると「company.g.dart」が生成されません。
記載し終えたら、以下コマンドをターミナルにて打ちます
flutter packages pub run build_runner build
少し待つと、「company.g.dart」が生成されています。
このファイル内にはDB操作用のhelperクラスが記述されています。DBの煩雑な操作はこいつがやってくれます!
先ほど記載した、「company.dart」を追記します。
・lib/database/company.dart
import 'dart:io';
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'company.g.dart';
//部署テーブル
class Departments extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 2, max: 20)();
TextColumn get place => text().withLength(min: 2, max: 20)();
TextColumn get businessContent => text().nullable()();
}
//社員テーブル 別名をWorker
@DataClassName("Worker")
class Syain extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get department =>
integer().customConstraint('REFERENCES departments(id)')();
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return VmDatabase(file);
});
}
@UseMoor(tables: [Departments, Syain])
class MyDatabase extends _$MyDatabase {
// データを格納する場所をデータベースに指示する
MyDatabase() : super(_openConnection());
// スキーマバージョン
@override
int get schemaVersion => 1;
// 部署テーブル取得
Future<List> getAllDepartmentEntries() {
return select(departments).get();
}
// 部署テーブル追加
Future addDepartmentEntry(DepartmentsCompanion entry) {
return into(departments).insert(entry);
}
// 部署テーブルの更新
Future updateDepartment(int id, DepartmentsCompanion department) {
return (update(departments)..where((it) => it.id.equals(id)))
.write(department);
}
// 部署テーブルの削除
Future deleteDepartment(int id) {
return (delete(departments)..where((it) => it.id.equals(id))).go();
}
}
カラムの型等は公式ドキュメントを参照
「DepartmentsCompanion」は「company.g.dart」の中に定義されています。
「XXXXXXCompanion」はNullを許容する型となっています。
→登録時にnullableかどうかを判断できる型
■画面(View)の作成
・lib/main.dart(メインページ。部署追加画面に遷移するボタンが配置されている)
import 'package:flutter/material.dart';
import 'company/department_add_page.dart';
import 'database/company.dart';
MyDatabase database;
void main() async {
database = MyDatabase();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MainPage(),
);
}
}
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('テストFlutter'),
),
body: Center(
child: Column(children: [
ElevatedButton(
child: Text('部署一覧'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DepartmentAddPage()),
);
},
)
]),
),
),
);
}
}
・lib/company/department_model.dart(view model)
import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:flutter_app5/main.dart';
import 'package:moor/moor.dart';
class DepartmentModel extends ChangeNotifier {
String name = '';
String place = '';
List departments = [];
bool loadingFlag = false;
startLoading() {
loadingFlag = true;
notifyListeners();
}
endLoading() {
loadingFlag = false;
notifyListeners();
}
// 部署の登録を行う
Future registerDepartment() async {
if (name.isEmpty) {
throw ('名前を入力してください');
}
if (place.isEmpty) {
throw ('場所を入力してください');
}
print('部署名:$name');
print('場所:$place');
// 登録の実行
int result = await database.addDepartmentEntry(
DepartmentsCompanion(
name: Value(name),
place: Value(place),
),
);
}
// 部署の表示を行う
Future showDepartment() async {
List departmentList = await database.getAllDepartmentEntries();
this.departments = departmentList;
notifyListeners();
}
// 部署の削除を行う
Future deleteDepartment(Department department) async {
int result = await database.deleteDepartment(department.id);
}
// 部署の更新を行う
Future updateDepartment(int departmentid, Department department) async {
if (name.isEmpty) {
throw ('タイトルを入力してください');
}
if (place.isEmpty) {
throw ('コンテンツを入力してください');
}
// 登録の実行
int result = await database.updateDepartment(
departmentid,
DepartmentsCompanion(
name: Value(name),
place: Value(place),
),
);
}
}
・lib/company/department_add_page.dart(部署追加・変更画面)
import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:provider/provider.dart';
import 'department_list_page.dart';
import 'department_model.dart';
class DepartmentAddPage extends StatelessWidget {
DepartmentAddPage({this.department});
final Department department;
@override
Widget build(BuildContext context) {
final nameController = TextEditingController();
final placeController = TextEditingController();
/// 新規(false)か更新(true)か
final bool updateFlag = department != null;
if (updateFlag) {
nameController.text = department.name;
placeController.text = department.place;
}
return ChangeNotifierProvider(
create: (_) => DepartmentModel(),
child: Scaffold(
appBar: AppBar(
title: Text(updateFlag ? '部署の編集' : '部署の新規追加'),
),
body: Consumer(builder: (context, model, child) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(hintText: '部署名'),
controller: nameController,
onChanged: (text) {
model.name = text;
},
),
TextField(
controller: placeController,
decoration: InputDecoration(hintText: '場所'),
onChanged: (text) {
model.place = text;
},
),
ElevatedButton(
child: Text(updateFlag ? '更新' : '登録'),
onPressed: () async {
try {
model.startLoading();
if (updateFlag) {
///更新
await model.updateDepartment(department.id, department);
} else {
///登録
await model.registerDepartment();
}
model.endLoading();
_showMordal(context, '登録完了しました');
} catch (e) {
_showMordal(context, e.toString());
print(e);
}
},
),
ElevatedButton(
child: Text('一覧の表示'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DepartmentListPage()),
);
},
),
],
),
);
}),
),
);
}
Future _showMordal(BuildContext context, String title) {
showDialog(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
actions: [
TextButton(
child: Text('ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
・lib/company/department_list_page.dart(部署一覧画面)
import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import 'department_add_page.dart';
import 'department_model.dart';
class DepartmentListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => DepartmentModel()..showDepartment(),
child: Scaffold(
appBar: AppBar(
title: Text('部署一覧'),
),
body: Consumer(builder: (context, model, child) {
final departments = model.departments;
final listTiles = departments
.map(
(department) => ListTile(
leading: Text(department.id.toString()),
title: Text(department.name),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DepartmentAddPage(department: department),
// 縦スクロールで遷移(前画面に戻るときは×ボタンになる)
fullscreenDialog: true,
),
);
model.showDepartment();
},
),
onLongPress: () async {
await showDialog(
context: context,
// barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text('${department.name}を削除しますか'),
actions: [
TextButton(
child: Text('ok'),
onPressed: () async {
Navigator.of(context).pop();
await deleteDepartment(
context, model, department);
},
),
],
);
},
);
},
),
)
.toList();
return ListView(
children: listTiles,
);
}),
),
);
}
Future deleteDepartment(BuildContext context, DepartmentModel model,
Department department) async {
try {
await database.deleteDepartment(department.id);
await _showModal(context, '削除しました');
} catch (e) {
await _showModal(context, e.toString());
}
}
Future _showModal(BuildContext context, String title) {
showDialog(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
actions: [
TextButton(
child: Text('ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
長々と書きましたがこれでmoorを使ったDBと画面ができあがりましたので起動してみましょう。
画面一覧をポチッ
・登録
・一覧と削除(削除は項目を長押しで削除します)
・更新(部署一覧の右端にある鉛筆アイコンクリックで遷移)
単純な作りでしたので、次回は複数テーブルjoinしたり、DB例外処理を加えたり、もう少し複雑なことをおこないます。