🍄

flutter sqflite

2024/06/29に公開

対象者

  • FlutterでSQLiteを使いたい
  • 実際は、sqfliteですが...
  • LocalDB使いたい
  • SQLの知識が少しある

ORMを使わない生のsqfliteを使っているプロジェクトに最近出会った😅
NoSQLでええやんけ!
そうもいかない😅
なので、検索して調べるがいい感じの資料がない?
メモ書きぐらい作るか📝

今回使用するライブラリ:
https://pub.dev/packages/sqflite

完成品

プロジェクトの説明

TodoListを作ってみた。しかしハマった点があった💦

Supported SQLite types
No validity check is done on values yet so please avoid non supported types https://www.sqlite.org/datatype3.html

DateTime is not a supported SQLite type. Personally I store them as int (millisSinceEpoch) or string (iso8601)

bool is not a supported SQLite type. Use INTEGER and 0 and 1 values.

More information on supported types here.

INTEGER
Dart type: int
Supported values: from -2^63 to 2^63 - 1
REAL
Dart type: num
TEXT
Dart type: String
BLOB
Dart type: Uint8List

値の有効性チェックはまだ行われていませんので、サポートされていない型は避けてください https://www.sqlite.org/datatype3.html

DateTime は SQLite でサポートされていない型です。個人的には int (millisSinceEpoch) または string (iso8601) として保存しています。

bool は SQLite でサポートされていない型です。INTEGERと0と1の値を使用してください。

えっbool型ないの?
はい、ないです😇

なので、int型にして使いましょう!

モデルを作成

idは、?をつけてる。0, 1とかにすると、同じ番号に保存するので、上書きされてします💦

モデル
class Todo {
  int? id;
  String title;
  bool done;

  Todo({this.id, required this.title, this.done = false});

  // データベースに保存するために、boolをintに変換
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'done': done ? 1 : 0, // boolをintに変換
    };
  }

  // データベースから読み込む際に、intをboolに変換
  factory Todo.fromMap(Map<String, dynamic> map) {
    return Todo(
      id: map['id'],
      title: map['title'],
      done: map['done'] == 1, // intをboolに変換
    );
  }
}

DataBase, tableを作成するクラスを作成する。今回は、追加・表示・checkboxだけ、更新・削除を行います。path.dartというモジュールを読み込む必要がある。パスを操作する必要がある。アプリ内のDBにアクセスするためですかね。
現在は標準機能として、組み込まれているみたい?
https://pub.dev/packages/path

api
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_todo/model/todo.dart';

// データベースの操作を行うクラス
class DatabaseHelper {
  // データベースのバージョン
  static const int _databaseVersion = 1;
  // データベースの名前
  static const String _databaseName = 'todo.db';

  // データベースのインスタンスを取得
  Future<Database> _getDB() async {
    return openDatabase(
      join(await getDatabasesPath(), _databaseName), // データベースのパスを指定
      version: _databaseVersion, // データベースのバージョンを指定
      onCreate: (db, version) async {
        // データベースにテーブルを作成
        await db.execute(
          'CREATE TABLE todos(id INTEGER PRIMARY KEY, title TEXT, done INTEGER)',
        );
      },
    );
  }

  // Todoをデータベースに追加
  Future<int> addTodo(Todo todo) async {
    final db = await _getDB(); // データベースのインスタンスを取得
    return db.insert(
      'todos', // テーブル名
      todo.toJson(), // TodoをMap型に変換して挿入
      conflictAlgorithm: ConflictAlgorithm.replace, // データが重複した場合は置き換える
    );
  }

  // Todoのdoneを更新
  Future<int> updateTodoDone(Todo todo) async {
    final db = await _getDB();
    return db.update(
      'todos',
      {'done': todo.done ? 1 : 0}, // doneのみを更新するためのMap
      where: 'id = ?',
      whereArgs: [todo.id],
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  // Todoを削除
  Future<int> deleteTodo(int id) async {
    final db = await _getDB();
    return db.delete(
      'todos',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // すべてのTodoを取得
  Future<List<Todo>> getTodos() async {
    final db = await _getDB();
    final List<Map<String, dynamic>> maps = await db.query('todos');
    return List.generate(maps.length, (i) {
      return Todo(
        id: maps[i]['id'],
        title: maps[i]['title'],
        done: maps[i]['done'] == 1,
      );
    });
  }
}

データを追加するボトムシートを出すボタンと、データの表示、チェックをつける機能、削除機能を実装したコードはこんな感じ。データベースを操作したあとは、画面の更新が必要なので、メソッドを実行するときは、setStateを使用します。

アプリの画面
main.dart
import 'package:flutter/material.dart';
import 'package:sqflite_todo/api/database_helper.dart';
import 'package:sqflite_todo/model/todo.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool isDone = false;
  final titleController = TextEditingController();
  // DatabaseHelperをインスタンス化
  final dbHelper = DatabaseHelper();

  
  void dispose() {
    titleController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    List<Todo> todos = [];

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurple,
        title: const Text('Todo List'),
      ),
      // Todoのリストを表示
      body: FutureBuilder<List<Todo>>(
        future: dbHelper.getTodos(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            todos = snapshot.data!;
            return ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(todos[index].title),
                  leading: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      // Todoを削除
                      dbHelper.deleteTodo(todos[index].id!);
                      setState(() {
                        todos.removeAt(index);
                      });
                    },
                  ),
                  trailing: Checkbox(
                    value: todos[index].done,
                    onChanged: (value) {
                      // Todoのdoneを更新
                      final todo = Todo(
                        id: todos[index].id,
                        title: todos[index].title,
                        done: value!,
                      );
                      dbHelper.updateTodoDone(todo);
                      setState(() {
                        todos[index].done = value;
                      });
                    },
                  ),
                );
              },
            );
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // showModalBottomSheetを使用してモーダルボトムシートを表示
          showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            builder: (context) {
              return Padding(
                padding: EdgeInsets.only(
                  bottom: MediaQuery.of(context).viewInsets.bottom,
                ),
                child: Container(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    mainAxisSize: MainAxisSize.min, // コンテンツのサイズに合わせて高さを調整
                    children: <Widget>[
                      TextField(
                        controller: titleController,
                        decoration: const InputDecoration(
                          labelText: 'Title',
                        ),
                      ),
                      const SizedBox(height: 16),
                      ElevatedButton(
                        onPressed: () async {
                          final title = titleController.text;
                          if (title.isNotEmpty) {
                            final todo = Todo(
                              title: title,
                            );
                            await dbHelper.addTodo(todo);
                            setState(() {
                              // Todoを追加
                              titleController.clear();
                            });
                            Navigator.pop(context);
                          }
                        },
                        child: const Text('Add'),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
        tooltip: 'Add',
        child: const Icon(Icons.add),
      ),
    );
  }
}

感想

bool型が使えないのが、不便でしたね💦
私は、NoSQLの方が好きなのでそっちを使いますね。Driftってライブラリもあるので、試してみると良いかもしれません。ORMを使うと、処理速度が遅くなる聞いたことありますが...

SQLでなくても良いのであれば、こちらのLocal DBをおすすめします。
https://docs.objectbox.io/advanced/custom-types

よき、Flutterライフを💙💚

Discussion