Floor
Floorは「型安全でリアクティブ、軽量なFlutterアプリケーション向けSQLite抽象化ライブラリ」として開発された、Flutterエコシステムで人気のORMライブラリです。AndroidのRoom persistence libraryにインスパイアされ、アノテーションベースのアプローチで複雑なSQLite操作をシンプルで直感的なAPIで提供。自動マイグレーション、リアクティブストリーム、null safety対応など、モダンなDart機能を最大限活用し、Flutterアプリの堅牢なデータ永続化層を構築できます。
GitHub概要
pinchbv/floor
The typesafe, reactive, and lightweight SQLite abstraction for your Flutter applications
トピックス
スター履歴
ライブラリ
Floor
概要
Floorは「型安全でリアクティブ、軽量なFlutterアプリケーション向けSQLite抽象化ライブラリ」として開発された、Flutterエコシステムで人気のORMライブラリです。AndroidのRoom persistence libraryにインスパイアされ、アノテーションベースのアプローチで複雑なSQLite操作をシンプルで直感的なAPIで提供。自動マイグレーション、リアクティブストリーム、null safety対応など、モダンなDart機能を最大限活用し、Flutterアプリの堅牢なデータ永続化層を構築できます。
詳細
Floor 2025年版はFlutter SQLiteデータベース操作の決定版として成熟したAPIと優れた安定性を誇ります。アノテーションドリブンなコード生成により、生のSQLを排除してクリーンで保守性の高いコードを実現。Entity、DAO、Databaseの3層構成でシンプルな設計を提供し、複雑なデータモデルも直感的に管理可能です。DartのStreamAPIとの深い統合により、リアルタイムUIに必要なリアクティブデータ更新を標準サポート。軽量設計でDriftよりもシンプル、lean appに最適化されています。
主な特徴
- アノテーションベースアプローチ: 生SQLをクリーンなアノテーション駆動コードに置換
- 自動マイグレーション: アプリ進化に伴うデータベーススキーマの自動更新
- リアクティブストリーム: DartのStream APIによるリアルタイムUI対応
- 軽量設計: Driftより軽量で、シンプルなアプリに最適
- モダンDart対応: null safetyと最新Dart機能の完全サポート
- Room風API: Android開発者に馴染みやすいAPIデザイン
メリット・デメリット
メリット
- アノテーション駆動でSQLエラーをコンパイル時に検出可能
- Dartエコシステムとの深い統合とリアクティブプログラミング対応
- Android Room経験者には学習コストが低い馴染みやすいAPI
- 軽量でシンプル、Driftよりも複雑さを回避
- 自動コード生成による保守性の向上とボイラープレートコード削減
- null safety対応によるランタイムエラーの大幅削減
デメリット
- SQLとSQLiteの理解が必要で、初心者には学習ハードルあり
- 複雑なクエリでは生SQLの記述が必要な場合がある
- 生成コードに依存するため、ビルド時間の増加
- DriftやMoorと比較して高度な機能が制限的
- リレーションシップ機能が限定的で、複雑なER設計には不向き
- Web、デスクトップ対応が限定的でモバイル中心
参考ページ
書き方の例
セットアップ
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
floor: ^1.4.2
dev_dependencies:
floor_generator: ^1.4.2
build_runner: ^2.4.6
// Entity定義
import 'package:floor/floor.dart';
@entity
class Person {
@primaryKey
final int? id;
final String firstName;
final String lastName;
@ColumnInfo(name: 'custom_name')
final String? nickname;
Person(this.id, this.firstName, this.lastName, this.nickname);
}
基本的な使い方
// DAO(Data Access Object)定義
import 'package:floor/floor.dart';
@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons();
@Query('SELECT * FROM Person WHERE id = :id')
Stream<Person?> findPersonById(int id);
@Query('SELECT * FROM Person WHERE firstName LIKE :name')
Future<List<Person>> findPersonsByFirstName(String name);
@insert
Future<void> insertPerson(Person person);
@update
Future<void> updatePerson(Person person);
@delete
Future<void> deletePerson(Person person);
}
クエリ実行
// データベース定義
import 'dart:async';
import 'package:floor/floor.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
part 'app_database.g.dart'; // コード生成されるファイル
@Database(version: 1, entities: [Person])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}
// データベース初期化
class DatabaseHelper {
static late AppDatabase database;
static Future<void> initDatabase() async {
database = await $FloorAppDatabase.databaseBuilder('app_database.db').build();
}
static AppDatabase get instance => database;
}
// データベース操作の実行
class PersonRepository {
final PersonDao _personDao = DatabaseHelper.instance.personDao;
Future<List<Person>> getAllPersons() async {
return await _personDao.findAllPersons();
}
Stream<Person?> getPersonById(int id) {
return _personDao.findPersonById(id);
}
Future<void> addPerson(String firstName, String lastName, {String? nickname}) async {
final person = Person(null, firstName, lastName, nickname);
await _personDao.insertPerson(person);
}
Future<void> updatePersonNickname(Person person, String newNickname) async {
final updatedPerson = Person(person.id, person.firstName, person.lastName, newNickname);
await _personDao.updatePerson(updatedPerson);
}
Future<void> removePerson(Person person) async {
await _personDao.deletePerson(person);
}
}
データ操作
// 実際の使用例
class PersonScreen extends StatefulWidget {
@override
_PersonScreenState createState() => _PersonScreenState();
}
class _PersonScreenState extends State<PersonScreen> {
final PersonRepository _repository = PersonRepository();
List<Person> _persons = [];
@override
void initState() {
super.initState();
_loadPersons();
}
Future<void> _loadPersons() async {
final persons = await _repository.getAllPersons();
setState(() {
_persons = persons;
});
}
Future<void> _addRandomPerson() async {
await _repository.addPerson(
'John${DateTime.now().millisecond}',
'Doe',
nickname: 'JD',
);
_loadPersons(); // リスト更新
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Floor Demo')),
body: ListView.builder(
itemCount: _persons.length,
itemBuilder: (context, index) {
final person = _persons[index];
return ListTile(
title: Text('${person.firstName} ${person.lastName}'),
subtitle: Text(person.nickname ?? 'No nickname'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
await _repository.removePerson(person);
_loadPersons();
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addRandomPerson,
child: Icon(Icons.add),
),
);
}
}
設定とカスタマイズ
// 高度なEntity定義
@Entity(tableName: 'users', indices: [Index(value: ['first_name', 'last_name'])])
class User {
@PrimaryKey(autoGenerate: true)
final int? id;
@ColumnInfo(name: 'first_name')
final String firstName;
@ColumnInfo(name: 'last_name')
final String lastName;
final String email;
@ColumnInfo(name: 'created_at')
final DateTime createdAt;
@ignore
String get fullName => '$firstName $lastName';
User(this.id, this.firstName, this.lastName, this.email, this.createdAt);
}
// 複雑なクエリ例
@dao
abstract class UserDao {
@Query('SELECT * FROM users WHERE first_name LIKE :pattern OR last_name LIKE :pattern ORDER BY created_at DESC')
Future<List<User>> searchUsersByName(String pattern);
@Query('SELECT COUNT(*) FROM users WHERE created_at > :date')
Future<int?> countUsersCreatedAfter(DateTime date);
@Query('SELECT * FROM users WHERE email = :email LIMIT 1')
Future<User?> findUserByEmail(String email);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertOrUpdateUser(User user);
@transaction
Future<void> insertMultipleUsers(List<User> users) async {
for (final user in users) {
await insertOrUpdateUser(user);
}
}
}
エラーハンドリング
// エラーハンドリングとトランザクション
class UserService {
final UserDao _userDao = DatabaseHelper.instance.userDao;
Future<Result<User>> createUser(String firstName, String lastName, String email) async {
try {
// メール重複チェック
final existingUser = await _userDao.findUserByEmail(email);
if (existingUser != null) {
return Result.error('Email already exists');
}
final user = User(
null,
firstName,
lastName,
email,
DateTime.now(),
);
await _userDao.insertOrUpdateUser(user);
return Result.success(user);
} catch (e) {
return Result.error('Failed to create user: $e');
}
}
Future<Result<void>> bulkInsertUsers(List<Map<String, String>> userData) async {
try {
final users = userData.map((data) => User(
null,
data['firstName']!,
data['lastName']!,
data['email']!,
DateTime.now(),
)).toList();
await _userDao.insertMultipleUsers(users);
return Result.success(null);
} catch (e) {
return Result.error('Failed to insert users: $e');
}
}
}
// 結果クラス
sealed class Result<T> {
const Result();
factory Result.success(T data) = Success<T>;
factory Result.error(String message) = Error<T>;
}
class Success<T> extends Result<T> {
final T data;
const Success(this.data);
}
class Error<T> extends Result<T> {
final String message;
const Error(this.message);
}