はじめに (対象読者・この記事でわかること)

Flutter でローカルデータベースを扱う際に標準的に使用されるパッケージが sqflite です。
Flutter 2 系から導入された Null Safety に対応したコードを書くことは、ランタイムエラーの防止や IDE の補完機能を最大限に活かす上で必須となります。本記事は、以下のような読者を想定しています。

  • Flutter 入門からある程度実装経験があり、データベース操作を学びたい方
  • 既存プロジェクトを Null Safety に移行したいが、sqflite の扱いに不安がある方
  • テストを書きやすい安全なコードベースを構築したい開発者

この記事を読むことで、sqflite の Null Safety 対応方法、型定義のベストプラクティス、マイグレーション手順 が具体的に理解でき、実際にアプリに組み込んで動作させることができるようになります。また、開発中に遭遇しやすい典型的なエラーとその対処法も併せて紹介します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Flutter の基本的なウィジェット構成と State 管理(StatefulWidget の概念)
  • Dart の Null Safety の基本概念(?!required など)
  • SQLite の基本的なテーブル設計と CRUD の概念

Sqflite の Null Safety 対応概要

sqflite は Dart の非同期 API として提供され、Future<T> 系の戻り値が中心です。Null Safety が有効になると、データベースから取得したレコードやクエリ結果の型が nullable になるケースが増えます。具体的には次の点に注意が必要です。

  1. Database インスタンスの取得
    openDatabaseFuture<Database> を返しますが、エラー時に null が返るわけではありません。したがって、await で取得した結果は non‑null とみなせますが、エラー処理は try/catch で行う必要があります。

  2. クエリ結果の型
    db.query()Future<List<Map<String, dynamic>>> を返します。Map の各キーはカラム名、値は dynamic ですが、SQLite のカラム自体が NULL を許容する場合、Map の値は null になる可能性があります。したがって、取得したデータをモデルクラスへ変換する際は null 判定デフォルト値 の設定が必須です。

  3. パラメータバインディング
    ? プレースホルダに null を渡すことは可能ですが、型安全に扱うために Object? を明示的に受け取るメソッドシグネチャを使用します。Dart の null 安全機能を利用して、パラメータが null の場合は NULL として SQL に埋め込まれます。

  4. バージョン管理とマイグレーション
    アプリのアップデート時にデータベーススキーマを変更する場合、onUpgrade コールバックで nullable カラムの追加既存カラムの制約変更 を行うことがあります。Null Safety では、マイグレーション時のデータ整合性チェックを忘れずに実装しましょう。

以上のポイントを抑えておくと、Null Safety によるコンパイルエラーを最小限に抑えつつ、堅牢なデータベース層を構築できます。

実装手順と注意点

以下では、実際に Flutter プロジェクトに sqflitepath_provider を導入し、Null Safety に対応したデータベース操作を行うまでの手順を示します。

ステップ 1: パッケージの導入と設定

pubspec.yaml に必要なパッケージを追加します。

Yaml
dependencies: flutter: sdk: flutter sqflite: ^2.2.8+1 # Null Safety 対応版 path_provider: ^2.1.1

flutter pub get を実行して依存関係を取得したら、IDE が自動的に Null Safety を有効にした状態でコード補完を行います。

ディレクトリ構成の例

lib/
 ├─ data/
 │   ├─ database_helper.dart   # DB 初期化・CRUD を集約
 │   └─ models/
 │        └─ note.dart        # データモデル(Null Safety 対応)
 └─ main.dart

ステップ 2: データベースヘルパークラスの実装

database_helper.dart にシングルトンパターンで DB インスタンスを管理します。

Dart
import 'dart:async'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; class DatabaseHelper { // シングルトンインスタンス static final DatabaseHelper _instance = DatabaseHelper._internal(); factory DatabaseHelper() => _instance; DatabaseHelper._internal(); static Database? _database; // データベース取得(初回は初期化) Future<Database> get database async { if (_database != null) return _database!; // null であれば初期化 _database = await _initDB(); return _database!; } // DB の初期化処理 Future<Database> _initDB() async { final docsDir = await getApplicationDocumentsDirectory(); final path = join(docsDir.path, 'notes.db'); // バージョンは 1 から開始 return await openDatabase( path, version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade, ); } // テーブル作成(NULL 許容カラムを明示的に定義) FutureOr<void> _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL ) '''); } // バージョンアップ時のマイグレーション例 FutureOr<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { // 例: is_favorite カラムを追加(NULL 許容 + デフォルト false) await db.execute('ALTER TABLE notes ADD COLUMN is_favorite INTEGER'); } } // CRUD メソッド(例: INSERT) Future<int> insertNote(Note note) async { final db = await database; return await db.insert('notes', note.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); } // SELECT(全件取得)※ null 判定を入れる Future<List<Note>> getAllNotes() async { final db = await database; final List<Map<String, dynamic>> maps = await db.query('notes'); return List.generate(maps.length, (i) { return Note.fromMap(maps[i]); }); } // UPDATE・DELETE も同様に実装 }

ポイント解説

  • _databaseDatabase? として宣言し、await database 時に nonnull にキャスト (!) しています。これは openDatabase が必ず Database を返すため安全です。
  • onCreatecontent TEXT のように NULL 許容カラムを明示的に指定することで、後続の Map<String, dynamic>null が来ても型エラーになりません。
  • onUpgradeバージョンアップ時のスキーマ変更 を集中管理できるので、将来的な Null Safety 変更(例: カラムに NOT NULL 制約を付与)にも対応しやすくなります。

ステップ 3: データモデルの実装(Null Safety 対応)

note.dart にモデルクラスを作成します。ここでは contentisFavorite を nullable にしています。

Dart
class Note { final int? id; // 自動増分なので insert 時は null final String title; // NOT NULL final String? content; // NULL 許容 final DateTime createdAt; // NOT NULL final bool? isFavorite; // SQLite では INTEGER (0/1) で保存 Note({ this.id, required this.title, this.content, required this.createdAt, this.isFavorite, }); // Map へ変換(INSERT 時に使用) Map<String, dynamic> toMap() { return { 'id': id, 'title': title, 'content': content, 'created_at': createdAt.millisecondsSinceEpoch, // bool を整数に変換。null の場合は null のまま。 'is_favorite': isFavorite == null ? null : (isFavorite! ? 1 : 0), }; } // Map からインスタンス生成(SELECT 時に使用) factory Note.fromMap(Map<String, dynamic> map) { return Note( id: map['id'] as int?, title: map['title'] as String, content: map['content'] as String?, // nullable createdAt: DateTime.fromMillisecondsSinceEpoch( map['created_at'] as int), isFavorite: map['is_favorite'] == null ? null : (map['is_favorite'] as int) == 1, ); } }

重要なポイント

  • int? id は自動採番なので、insert 時は null で問題ありません。
  • contentString? であることで、ユーザーがメモの本文を省略した場合でも型エラーが起きません。
  • isFavoritebool? とし、SQLite では INTEGER (0/1) に変換しています。null のまま保存すれば「未設定」状態を表現できます。
  • fromMapmap['is_favorite']null の場合に null を返す処理は、Null Safety の典型的なパターンです。

ステップ 4: UI からの呼び出し例

main.dart で簡単な UI とデータベース操作を結びつけます。

Dart
import 'package:flutter/material.dart'; import 'data/database_helper.dart'; import 'data/models/note.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Sqflite Null Safety Demo', theme: ThemeData(primarySwatch: Colors.indigo), home: const NoteListPage(), ); } } class NoteListPage extends StatefulWidget { const NoteListPage({super.key}); @override State<NoteListPage> createState() => _NoteListPageState(); } class _NoteListPageState extends State<NoteListPage> { final DatabaseHelper _dbHelper = DatabaseHelper(); List<Note> _notes = []; @override void initState() { super.initState(); _loadNotes(); } Future<void> _loadNotes() async { final notes = await _dbHelper.getAllNotes(); setState(() { _notes = notes; }); } Future<void> _addSampleNote() async { final note = Note( title: 'サンプルメモ', content: null, // null でも OK createdAt: DateTime.now(), ); await _dbHelper.insertNote(note); await _loadNotes(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('メモ一覧')), body: ListView.builder( itemCount: _notes.length, itemBuilder: (context, index) { final note = _notes[index]; return ListTile( title: Text(note.title), subtitle: Text(note.content ?? '(本文なし)'), trailing: Icon( note.isFavorite == true ? Icons.star : Icons.star_border, ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: _addSampleNote, child: const Icon(Icons.add), ), ); } }

実装上の注意点

  • note.content ?? '(本文なし)' のように null 合体演算子 (??) を活用して UI 側でデフォルト表示を行うと、Null Safety の恩恵が最大化します。
  • note.isFavorite == true の比較は bool?bool の混在を防ぐ安全な書き方です。
  • await _loadNotes() の前に setState で UI を更新するタイミングに注意し、非同期処理完了後に UI が正しく描画されるようにしています。

ハマった点やエラー解決

1. Map<String, dynamic> の取得時に null が原因で型エラー

症状Note.fromMapmap['content'] as String とキャストしたところ、null が入っていると type 'Null' is not a subtype of type 'String' が発生。

解決策:キャスト対象を String? に変更し、as String? と明示的に nullable にする。また、モデル側でも String? と宣言し、?? でデフォルトを設定。

2. bool を SQLite の INTEGER に変換した際の null ハンドリング

症状isFavoritenull のときに note.isFavorite! と書くと Null check operator used on a null value がスロー。

解決策isFavorite の使用箇所すべてで note.isFavorite == true のように null 安全比較に置き換え、bool? のまま扱うか、note.isFavorite ?? false でデフォルトを設定。

3. データベースバージョンアップ時に onUpgrade が呼ばれない

症状:スキーマ変更を ALTER TABLE で記述したにもかかわらず、既存デバイスでテーブルが更新されなかった。

解決策openDatabaseversion パラメータを 必ず 1 以上に上げる 必要がある。デバッグ時はアプリをアンインストールして DB を初期化するか、onDowngrade: onDatabaseDowngradeDelete を付与して強制的に再作成させた。

まとめ

本記事では、Flutter の sqflite パッケージを Null Safety に完全対応させる方法 を、以下の流れで解説しました。

  • パッケージ導入とプロジェクト構成の基本
  • Singleton パターンで DB インスタンスを管理し、openDatabaseonCreateonUpgrade の Null Safety 対応ポイント
  • データモデル (Note) を nullable フィールドと共に実装し、toMapfromMap 変換で安全に型変換
  • UI 側でのデータ取得・表示例と、??== true などの Null 安全演算子の実践的活用
  • 実装中によくある null 受け取りエラーやバージョンアップ時のマイグレーション問題の対処法

これにより、ランタイムエラーのリスクを低減し、IDE の型チェックを最大限に活かしたモダンな SQLite 操作 が実現できます。今後は、SQL のトランザクション管理やストリームを用いたリアクティブ更新、そして moor(Drift)などの上位ラッパーへの移行も視野に入れた記事を執筆予定です。

参考資料