🏖️
Isar Databaseを使ってみた
Isarとは?
Isarとは、Hiveと呼ばれているローカルDBの後継として開発されたそうです。今回もただのメモ機能ですが、作るのが意外と難しかったので、記事にしようと思いました。
公式ドキュメント通りにやっても上手くいきません!
動画も作りました
必要なパッケージを追加する
このパッケージは、ここに追加したパッケージを配置しろとルールがあるみたいで、yamlファイルを見て、必要なパッケージを追加してください。この通りにやらないとハマりました!
pubspec.yaml
name: isar_app
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.0.5 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
path_provider: ^2.0.15
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
isar_generator: ^3.1.0+1 # isar_generatorはこの位置に配置する
build_runner: ^2.4.4 # build_runnerはこの位置に配置する
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
モデルクラスを作る
今回は、公式のコードを修正して使わせていただきました。
lib/model/person.dart
import 'package:isar/isar.dart';// 1. isarパッケージをインポート
part 'person.g.dart';// ファイル名.g.dartと書く
class Person {
Id id = Isar.autoIncrement; // id = nullでも自動インクリメントされます。
String? name;
}
ファイルを自動生成するコマンドを実行する
flutter pub run build_runner build
こちらが自動生成されたファイル
PersonSchemaという定数をmain.dartで使用します。このコードを使って、Isarにアプリを接続します。
lib/model/person.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'person.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetPersonCollection on Isar {
IsarCollection<Person> get persons => this.collection();
}
const PersonSchema = CollectionSchema(
name: r'Person',
id: 7854610480646705599,
properties: {
r'name': PropertySchema(
id: 0,
name: r'name',
type: IsarType.string,
)
},
estimateSize: _personEstimateSize,
serialize: _personSerialize,
deserialize: _personDeserialize,
deserializeProp: _personDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _personGetId,
getLinks: _personGetLinks,
attach: _personAttach,
version: '3.1.0+1',
);
int _personEstimateSize(
Person object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.name;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _personSerialize(
Person object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.name);
}
Person _personDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Person();
object.id = id;
object.name = reader.readStringOrNull(offsets[0]);
return object;
}
P _personDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _personGetId(Person object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _personGetLinks(Person object) {
return [];
}
void _personAttach(IsarCollection<dynamic> col, Id id, Person object) {
object.id = id;
}
extension PersonQueryWhereSort on QueryBuilder<Person, Person, QWhere> {
QueryBuilder<Person, Person, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension PersonQueryWhere on QueryBuilder<Person, Person, QWhereClause> {
QueryBuilder<Person, Person, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
upper: id,
));
});
}
QueryBuilder<Person, Person, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Person, Person, QAfterWhereClause> idGreaterThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Person, Person, QAfterWhereClause> idLessThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Person, Person, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
));
});
}
}
extension PersonQueryFilter on QueryBuilder<Person, Person, QFilterCondition> {
QueryBuilder<Person, Person, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'name',
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'name',
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'name',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'name',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'name',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'name',
value: '',
));
});
}
QueryBuilder<Person, Person, QAfterFilterCondition> nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'name',
value: '',
));
});
}
}
extension PersonQueryObject on QueryBuilder<Person, Person, QFilterCondition> {}
extension PersonQueryLinks on QueryBuilder<Person, Person, QFilterCondition> {}
extension PersonQuerySortBy on QueryBuilder<Person, Person, QSortBy> {
QueryBuilder<Person, Person, QAfterSortBy> sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Person, Person, QAfterSortBy> sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension PersonQuerySortThenBy on QueryBuilder<Person, Person, QSortThenBy> {
QueryBuilder<Person, Person, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Person, Person, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<Person, Person, QAfterSortBy> thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Person, Person, QAfterSortBy> thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension PersonQueryWhereDistinct on QueryBuilder<Person, Person, QDistinct> {
QueryBuilder<Person, Person, QDistinct> distinctByName(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
}
extension PersonQueryProperty on QueryBuilder<Person, Person, QQueryProperty> {
QueryBuilder<Person, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<Person, String?, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
}
入力ページを作る
main.dartに入力するページとIsarに接続する設定を書いたコードを書いております。
main.dart
import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:isar_app/model/person.dart';
import 'package:isar_app/view.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
// Isarの初期化
WidgetsFlutterBinding.ensureInitialized();
// アプリのドキュメントディレクトリを取得
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
[PersonSchema],
directory: dir.path,
);
runApp(MyApp(isar: isar));
}
class MyApp extends StatelessWidget {
final Isar isar;
MyApp({required this.isar});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Memo App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AddPage(isar: isar),
);
}
}
class AddPage extends StatefulWidget {
const AddPage({Key? key, required this.isar}) : super(key: key);
final Isar isar;
State<AddPage> createState() => _AddPageState();
}
class _AddPageState extends State<AddPage> {
final nameController = TextEditingController();
void dispose() {
nameController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('メモ追加'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: nameController,
),
ElevatedButton(
onPressed: () async {
// Personクラスのインスタンスを作成
final person = Person()..name = nameController.text;
// 入力後にテキストフィールドを空にする
nameController.clear();
// widgetとは、StatefulWidgetのこと
// widget.isarで、StatefulWidgetのisarを参照できる
await widget.isar.writeTxn(() async {
await widget.isar.persons.put(person);
});
},
child: const Text('追加')),
const SizedBox(height: 20),
ElevatedButton(onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewPage(isar: widget.isar),
),
);
}, child: const Text('一覧へ'))
],
),
),
);
}
}
データの表示と削除
保存したデータの確認と削除ができるページです。
view.dart
import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:isar_app/model/person.dart';
class ViewPage extends StatefulWidget {
final Isar isar;// Isarのインスタンスを受け取るための変数
ViewPage({required this.isar});
_ViewPageState createState() => _ViewPageState();
}
class _ViewPageState extends State<ViewPage> {
List<Person> persons = []; // personsはデータベースの中身を取得するための変数
// initStateを使うと、画面が表示される前にデータベースの中身を取得できます。
void initState() {
super.initState();
loadData();
}
// データベースの中身を取得する関数
Future<void> loadData() async {
final data = await widget.isar.persons.where().findAll();
setState(() {
persons = data;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('データを表示')),
body: ListView.builder(
itemCount: persons.length,
itemBuilder: (context, index) {
final person = persons[index];
return ListTile(
title: Text(person.name ?? "値が入ってません"),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
// ここでデータベースから削除しています
await widget.isar.writeTxn(() async {
await widget.isar.persons.delete(person.id);
});
await loadData();
},
)
);
},
),
);
}
}
アプリを実行してみる
デバッグコンソールに、ブラウザでIsarのデータの中身を確認することができるリンクが表示されるので、そこからデータベースの中身を確認することができるので、Isarは便利だなと思いました。
データを追加
ブラウザでデータの中身を確認してみる
こんな感じで確認できます
アプリ側はこのようになってます
まとめ
公式のドキュメントを見ながら作ってみたのですが、中々上手くいかなかったです。ChatGPTに手伝ってもらいながら完成させました。
こちらに完成品のリンクを貼っておきます。
2023/07/09にhooks_riverpodとflutter_hooksを使用したコードも作成いたしました。
Discussion