Flutterで軽量なRDBMSのSQLite(sqflite)を使ってみた

2024.04.03

こんにちは、ゲームソリューション部のsoraです。
今回は、Flutterで軽量なRDBMSのSQLite(sqflite)を使ってみたことについて書いていきます。

実装した画面

ローカルのデータベースに対する各操作を実行するシンプルなアプリです。
Insert
id・name・messageを入力して、データを挿入します。 Update
idをキーとして、name・messageを更新します。 Delete
idをキーとして、データを削除します。

利用する主要なパッケージ

sqflite
sqflite | Flutter package
使用したバージョン(pubspec.yamlから抜粋):sqflite: ^2.3.2
軽量なリレーショナルデータベースマネジメントシステムであるSQLiteを使うためのパッケージ

flutter_riverpod
flutter_riverpod | Flutter package
使用したバージョン(pubspec.yamlから抜粋):flutter_riverpod: ^2.5.1
状態管理パッケージ
多くの情報が落ちている人気なパッケージのため、詳しい説明は割愛します。

コードの解説

コードは以下です。
main.dartではメインの処理、database_connectionではDB操作、chat_message.dartではテーブル内のデータ表示の状態管理をしています。

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'chat_message.dart';
import 'database_connection.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
      const ProviderScope(
          child: MyApp()
      ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'sqflite Test',
      theme: ThemeData(),
      home: const ChatPage(),
    );
  }
}

class ChatPage extends ConsumerWidget {
  const ChatPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // DB内のデータの表示用
    final chatMessageProviderValue = ref.watch(chatMessageProvider);
    // DB操作用
    final dataBaseConnectionProviderNotifier = ref.watch(dataBaseConnectionProvider.notifier);
    final _idEditController = TextEditingController();
    final _nameEditController = TextEditingController();
    final _messageEditController = TextEditingController();

    // DBの項目、idが主キー
    String id;
    String name;
    String message;

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('sqflite test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 入力フィールド
            // id
            Container(
                padding: const EdgeInsets.only(
                  left: 25,
                  right: 25,
                ),
                child: TextField(
                    keyboardType: TextInputType.text,
                    controller: _idEditController,
                    decoration: const InputDecoration(
                      labelText: 'id',
                    )
                )
            ),
            const SizedBox(height: 10),
            // name
            Container(
                padding: const EdgeInsets.only(
                  left: 25,
                  right: 25,
                ),
                child: TextField(
                    keyboardType: TextInputType.text,
                    controller: _nameEditController,
                    decoration: const InputDecoration(
                      labelText: 'name',
                    )
                )
            ),
            const SizedBox(height: 10),
            // message
            Container(
                padding: const EdgeInsets.only(
                  left: 25,
                  right: 25,
                ),
                child: TextField(
                    keyboardType: TextInputType.text,
                    controller: _messageEditController,
                    decoration: const InputDecoration(
                      labelText: 'message',
                    )
                )
            ),
            const SizedBox(height: 10),

            // ボタン
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () async{
                    id = _idEditController.text;
                    name = _nameEditController.text;
                    message = _messageEditController.text;
                    dataBaseConnectionProviderNotifier.dbInsert(id, name, message);
                  },
                  child: const Text('insert'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () async{
                    id = _idEditController.text;
                    name = _nameEditController.text;
                    message = _messageEditController.text;
                    dataBaseConnectionProviderNotifier.dbUpdate(id, name, message);
                  },
                  child: const Text('update'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                    onPressed: () async{
                      id = _idEditController.text;
                      dataBaseConnectionProviderNotifier.dbDelete(id);
                    },
                    child: const Text('delete')
                ),
              ],
            ),
            const SizedBox(height: 10),
            for(var map in chatMessageProviderValue) ...{
              Text('id = ${map["id"]}, name = ${map["name"]}, message = ${map["message"]}'),
            }
          ],
        ),
      ),
    );
  }
}

database_connection.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sqflite/sqflite.dart';
import 'chat_message.dart';

class DataBaseConnection extends AsyncNotifier<List<Map>> {
  late String path;
  late Database database;
  List<Map> listMap = [];

  @override
  Future<List<Map>> build() async {
    // getDatabasesPath():デフォルトのデータベース保存用フォルダのパスを取得
    var databasesPath = await getDatabasesPath();
    // 取得したパスから本アプリ用にて生成するDB名を指定
    path = '$databasesPath/test.db';
    // データベースを開く(pathに存在しなければ新規作成)
    database = await openDatabase(
        path,
        version: 1,
        // DBがpathに存在しなかった場合にonCreateが呼び出される
        onCreate: (Database db, int version) async {
          await db.execute(
              'CREATE TABLE Test (id TEXT PRIMARY KEY, name TEXT, message TEXT)'
          );
        });
    return [];
  }
  void dbInsert(String id, String name, String message) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // INSERT
    await database.insert(
      'Test',
      {'id': id, 'name': name, 'message': message},
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }
  void dbUpdate(String id, String name, String message) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // idをキーとしてUPDATE
    await database.update(
        'Test',
        {'name': name, 'message': message},
        where: 'id = ?',
        whereArgs: [id]
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }
  void dbDelete(String id) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // idをキーとしてDELETE
    await database.delete(
        'Test',
        where: 'id = ?',
        whereArgs: [id]
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }
}

final dataBaseConnectionProvider = AsyncNotifierProvider<DataBaseConnection, List<Map>>(() {
  return DataBaseConnection();
});

chat_message.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

class ChatMessage extends Notifier<List<Map>> {
  @override
  List<Map> build(){
    return [];
  }
  void update(List<Map> chatList) {
    state = chatList;
  }
}

final chatMessageProvider = NotifierProvider<ChatMessage, List<Map>>((){
  return ChatMessage();
});

DBの初期化

まずデフォルトのデータベース保存用のフォルダパスを取得して、本アプリに使用するDB名を指定します。
指定したパスにDBが存在しなければ、DBを作成します。
コメント記載の通り、DBがパスに存在しない場合に、onCreateが実行されます。

database_connection.dart

    // getDatabasesPath():デフォルトのデータベース保存用フォルダのパスを取得
    var databasesPath = await getDatabasesPath();
    // 取得したパスから本アプリ用にて生成するDB名を指定
    path = '$databasesPath/test.db';
    // データベースを開く(pathに存在しなければ新規作成)
    database = await openDatabase(
        path,
        version: 1,
        // DBがpathに存在しなかった場合にonCreateが呼び出される
        onCreate: (Database db, int version) async {
          await db.execute(
              'CREATE TABLE Test (id TEXT PRIMARY KEY, name TEXT, message TEXT)'
          );
        });

DB操作

次にDB操作の部分では、INSERT・UPDATE・DELETEをそれぞれメソッドとして書いています。
database.insert()database.update()database.delete()などが用意されています。
database.rawQuery()でSQL文を書いて指定することが可能です。
DB操作を行った後に、テーブル内の全データ取得をして、状態を更新しています。

database_connection.dart

  void dbInsert(String id, String name, String message) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // INSERT
    await database.insert(
      'Test',
      {'id': id, 'name': name, 'message': message},
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }
  void dbUpdate(String id, String name, String message) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // idをキーとしてUPDATE
    await database.update(
        'Test',
        {'name': name, 'message': message},
        where: 'id = ?',
        whereArgs: [id]
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }
  void dbDelete(String id) async {
    final chatMessage = ref.watch(chatMessageProvider.notifier);
    // idをキーとしてDELETE
    await database.delete(
        'Test',
        where: 'id = ?',
        whereArgs: [id]
    );
    // SELECT
    listMap = await database.rawQuery('SELECT * FROM Test');
    // 状態更新
    chatMessage.update(listMap);
  }

参考にした記事

【Flutter】ローカルデータベース(sqflite)
SQLiteでのデータ永続化

最後に

今回は、Flutterで軽量なRDBMSのSQLite(sqflite)を使ってみたことを記事にしました。
どなたかの参考になると幸いです。