🦪

ローカルDBを使ったときにシングルトンを使わないとエラーになる?

に公開

同じインスタンスが生成されると起きる!

最近ローカルデータベースをよく使うのですが、driftobjectboxを使用したときに突然エラーが発生することがありました?
1ページのアプリだと起きなかったが複数のページでローカルデータベースを使うとエラーが発生した。普段は、Riverpodを使っているときは詰まることがなかったがなぜなのか気になった。

Udemyで学習していたときに、シングルトンパターンはデータベースとの接続で使われることがあると紹介されていた。同じインスタンを生成しないクラスを使う目的はなんなのか?

長くなるので折りたたむ

ローカルデータベースの実装方法:シングルトンとRiverpod

ローカルデータベースでシングルトンを使う理由

1. リソース効率

// シングルトン実装例
class TodoRepository {
  TodoRepository._(); // プライベートコンストラクタ
  static final TodoRepository instance = TodoRepository._();
  
  // データベース接続は一度だけ初期化
  late final Database _db = initDatabase();
}

2. 状態の一貫性

// 同じインスタンスを共有することで状態の一貫性を保証
void updateTodo() {
  // どこからアクセスしても同じデータベース接続を使用
  TodoRepository.instance.updateTodo(...);
}

3. メモリ管理

// 複数のデータベース接続を防ぐ
// ❌ 悪い例
class TodoPage extends StatefulWidget {
  final Database db = Database(); // 毎回新しい接続を作成
}

// ✅ 良い例
class TodoPage extends StatefulWidget {
  final repository = TodoRepository.instance; // 同じインスタンスを再利用
}

Riverpod Generatorを使用した実装

1. パッケージの追加

flutter pub add \
flutter_riverpod \
riverpod_annotation \
dev:riverpod_generator \
dev:build_runner \
dev:custom_lint \
dev:riverpod_lint

2. Providerの定義

// database_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'database_provider.g.dart';


Database database(Ref ref) {
  final db = Database();
  ref.onDispose(() => db.close());
  return db;
}


TodoRepository todoRepository(Ref ref) {
  final db = ref.watch(databaseProvider);
  return TodoRepository(db);
}

3. コード生成の実行

# 一回だけ生成
dart run build_runner build

# ファイル監視して自動生成
dart run build_runner watch

4. 生成されたコードの例

// database_provider.g.dart

class Database extends _$Database {
  
  Database build() {
    final db = Database();
    ref.onDispose(() => db.close());
    return db;
  }
}


class TodoRepository extends _$TodoRepository {
  
  TodoRepository build() {
    final db = ref.watch(databaseProvider);
    return TodoRepository(db);
  }
}

5. 依存性注入

class TodoRepository {
  final Database db;
  TodoRepository(this.db);

  Future<List<Todo>> getAllTodos() => db.query('todos');
}

6. 使用例

StreamBuilderで書いてますが、StreamNotifierで最近書くことが多いので、Riverpodに依存したコード書きすぎたくない人はStreamBuilderの方がいいかもしれません。処理が簡単に書けますね。「StreamBuilder使うところあるの?」って思う人いるかもしれませんがライブラリを使いすぎないようにしてる現場や昔から運用されているアプリには使われていますね。

class TodoList extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final repository = ref.watch(todoRepositoryProvider);
    return StreamBuilder(
      stream: repository.getAllTodos(),
      builder: // ...
    );
  }
}

シングルトンとRiverpodの比較

シングルトンの利点

  • シンプルな実装
  • グローバルなアクセス
  • メモリ効率

シングルトンの欠点

  • テストが難しい
  • 依存関係が不透明
  • 状態管理が複雑化する可能性

Riverpodの利点

  • テスタビリティ
  • 依存関係の明示的な管理
  • 状態の自動クリーンアップ
  • コード分割が容易
  • コード生成による型安全性

Riverpodの欠点

  • 初期学習コストが高い
  • ボイラープレートコードが増える
  • プロバイダーの管理が必要

参考文献とURL

シングルトンパターン全般

  • Refactoring Guru - Singleton Pattern
  • Microsoft Docs - Singleton Pattern

データベースとシングルトン

  • SQLite Database in Flutter
  • Drift Documentation

Riverpod

  • Riverpod Official Documentation
  • Flutter Riverpod 2.0 - Complete Guide

結論

シングルトンは簡単に実装できて効率的ですが、Riverpodを使用することで:

  • より柔軟なテスト
  • 明確な依存関係
  • 効率的な状態管理
  • 型安全性の向上

が可能になります。プロジェクトの規模や要件に応じて、適切なアプローチを選択することが重要です。小規模なプロジェクトではシングルトンで十分かもしれませんが、大規模なプロジェクトではRiverpodのような依存性注入フレームワークを使用することをお勧めします。

Discussion