Floor

Floorは「型安全でリアクティブ、軽量なFlutterアプリケーション向けSQLite抽象化ライブラリ」として開発された、Flutterエコシステムで人気のORMライブラリです。AndroidのRoom persistence libraryにインスパイアされ、アノテーションベースのアプローチで複雑なSQLite操作をシンプルで直感的なAPIで提供。自動マイグレーション、リアクティブストリーム、null safety対応など、モダンなDart機能を最大限活用し、Flutterアプリの堅牢なデータ永続化層を構築できます。

ORMFlutterDartSQLiteデータベースリアクティブ

GitHub概要

pinchbv/floor

The typesafe, reactive, and lightweight SQLite abstraction for your Flutter applications

スター1,016
ウォッチ14
フォーク207
作成日:2019年1月20日
言語:Dart
ライセンス:Apache License 2.0

トピックス

dartfluttersqlite

スター履歴

pinchbv/floor Star History
データ取得日時: 2025/7/17 06:57

ライブラリ

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);
}