🌏
【Flutter】Isar Databaseについて
はじめに
Flutterを用いて開発を進める中で、アプリ内で不揮発で情報を保持する場面は
必ず遭遇することかと思います。
ネイティブ言語のSwiftやKotlinにも主要なライブラリは存在しているかと思います。
その中で、Flutterを用いた際に、どのDBパッケージが良いかを確認していた中で
hive
の後継としてIsar
が登場していたことを知ったため、記述いたしました。
概要
導入
以下を実行
flutter pub add isar isar_flutter_libs
flutter pub add -d isar_generator build_runner
# Isarを開く際に指定したdirectlyのpathを指定するために使用するため追加が必要
flutter pub add path_provider
↓実行後
pubspec.yaml
dependencies:
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
path_provider: ^2.0.15
dev_dependencies:
build_runner: ^2.4.6
isar_generator: ^3.1.0+1
collection定義
user.dart
import 'package:isar/isar.dart';
part 'user.g.dart';
class User {
// 一意に識別するID(必須)
// [autoIncrement]にて、自動でIDを割り当てることが可能(独自で設定も可能)
Id id = Isar.autoIncrement;
String? name;
int? age;
}
- coleectionを定義するには
@collection
or@Collection()
アノテーションを使用します。- collectionを付与したクラスには、主キー(PK)となるフィールドを定義する必要があります。
collection定義後、build_runnerにて、.gファイルを自動生成する
flutter pub run build_runner build --delete-conflicting-outputs
生成された.gファイル
user.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.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 GetUserCollection on Isar {
IsarCollection<User> get users => this.collection();
}
const UserSchema = CollectionSchema(
name: r'User',
id: -7838171048429979076,
properties: {
r'age': PropertySchema(
id: 0,
name: r'age',
type: IsarType.long,
),
r'name': PropertySchema(
id: 1,
name: r'name',
type: IsarType.string,
)
},
estimateSize: _userEstimateSize,
serialize: _userSerialize,
deserialize: _userDeserialize,
deserializeProp: _userDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _userGetId,
getLinks: _userGetLinks,
attach: _userAttach,
version: '3.1.0+1',
);
int _userEstimateSize(
User 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 _userSerialize(
User object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeLong(offsets[0], object.age);
writer.writeString(offsets[1], object.name);
}
User _userDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = User();
object.age = reader.readLongOrNull(offsets[0]);
object.id = id;
object.name = reader.readStringOrNull(offsets[1]);
return object;
}
P _userDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readLongOrNull(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _userGetId(User object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _userGetLinks(User object) {
return [];
}
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {
object.id = id;
}
extension UserQueryWhereSort on QueryBuilder<User, User, QWhere> {
QueryBuilder<User, User, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
QueryBuilder<User, User, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
upper: id,
));
});
}
QueryBuilder<User, User, 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<User, User, QAfterWhereClause> idGreaterThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<User, User, QAfterWhereClause> idLessThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<User, User, 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 UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> ageIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'age',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> ageIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'age',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> ageEqualTo(int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'age',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> ageGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'age',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> ageLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'age',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> ageBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'age',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
));
});
}
QueryBuilder<User, User, 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<User, User, 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<User, User, 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<User, User, QAfterFilterCondition> nameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'name',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> nameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'name',
));
});
}
QueryBuilder<User, User, 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<User, User, 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<User, User, 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<User, User, 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<User, User, 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<User, User, 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<User, User, 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<User, User, 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<User, User, QAfterFilterCondition> nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'name',
value: '',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'name',
value: '',
));
});
}
}
extension UserQueryObject on QueryBuilder<User, User, QFilterCondition> {}
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {}
extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
QueryBuilder<User, User, QAfterSortBy> sortByAge() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'age', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByAgeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'age', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
QueryBuilder<User, User, QAfterSortBy> thenByAge() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'age', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByAgeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'age', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
QueryBuilder<User, User, QDistinct> distinctByAge() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'age');
});
}
QueryBuilder<User, User, QDistinct> distinctByName(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
}
extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
QueryBuilder<User, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<User, int?, QQueryOperations> ageProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'age');
});
}
QueryBuilder<User, String?, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
}
Data base定義
※Riverpodを使用して、databaseにアクセスする形としています。
database.dart
import 'package:app/user.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'database.g.dart';
(keepAlive: true)
Database database(DatabaseRef ref) => Database()..init();
class Database {
late final Isar isar;
/// 初期化処理
Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
isar = await Isar.open([UserSchema], directory: dir.path);
}
/// 追加
Future<void> put(User user) async {
await isar.writeTxn(() async => await isar.users.put(user));
}
/// 取得
Future<User?> get(Id id) async => await isar.users.get(id);
/// 複数取得
Future<List<User?>> getAll(List<Id> ids) async =>
await isar.users.getAll(ids);
/// 削除
Future<void> delete(Id id) async {
await isar.writeTxn(() => isar.users.delete(id));
}
/// 複数削除
Future<void> deleteAll(List<Id> ids) async {
await isar.writeTxn(() => isar.users.deleteAll(ids));
}
}
呼び出し側
app.dart
import 'package:app/app_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class App extends HookConsumerWidget {
const App({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final nameController = useTextEditingController();
final ageController = useTextEditingController();
final appNotifier = ref.watch(appNotifierProvider.notifier);
final state = ref.watch(appNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('DB')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('先頭の情報を取得 : 名前 - ${state.name} / 年齢 - ${state.age}'),
const SizedBox(height: 24),
_AppTextField(
text: '名前',
controller: nameController,
),
const SizedBox(height: 8),
_AppTextField(
text: '年齢',
controller: ageController,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
appNotifier.put(
nameController.value.text,
int.parse(ageController.value.text),
);
},
child: const Text('登録'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: () {
appNotifier.delete(1);
},
child: const Text('削除'),
),
],
),
],
),
),
);
}
}
class _AppTextField extends StatelessWidget {
const _AppTextField({
required this.text,
required this.controller,
});
final String text;
final TextEditingController controller;
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: text,
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(width: 1, color: Colors.blueAccent),
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(width: 1, color: Colors.blueAccent),
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
),
);
}
}
app_notifier.dart
import 'package:app/database.dart';
import 'package:app/user.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:isar/isar.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_notifier.freezed.dart';
part 'app_notifier.g.dart';
class AppState with _$AppState {
factory AppState({
(null) String? name,
(null) int? age,
}) = _AppState;
}
class AppNotifier extends _$AppNotifier {
AppNotifier();
late Database database;
AppState build() {
database = ref.watch(databaseProvider);
return AppState();
}
Future<void> put(String name, int age) async {
final user = User()
..name = name
..age = age;
database.put(user);
state = state.copyWith(name: name, age: age);
}
Future<void> delete(Id id) async {
database.delete(id);
state = state.copyWith(name: null, age: null);
}
Future<User?> get(Id id) async => await database.get(id);
}
Isar Inspector
-
Isar Inspectorのリリースノートから、
libisar_macos.dylib
をダウンロードします。
https://github.com/isar/isar/releases -
その後、アプリを再ビルドしてください。
ビルド時にDebugに以下URLが表示されるため、アクセスしましょう。
- アクセスすると以下のように、Inspectorが表示され、先ほど追加した情報が保存されていることが確認できます。
まとめ
今回は基本的な部分をピックアップしましたが
クエリを操作したり、値の状態を監視したりと、柔軟にDB操作が可能となっています。
Flutterには他にも
- Realm
- Drift
- shared_preferences
- ObjectBox
...etc
など様々なDBパッケージが存在していますので、
導入の際の参考にしていただけたら幸いです。
参考
Discussion