JINMUSOFTWARE

FlutterでSQLiteを使用する方法

Flutter

FlutterでSQLiteを使用してデータを保存する方法の紹介です。

Flutter公式にSQLiteを使用するサンプルがある

Sqfliteを使用するサンプルが掲載されています。

Beyond UI > Data & backend > Persistence > Persist data with SQLite

この公式サンプルでは、犬の名前、年齢を扱うデータモデルのようです。

出力がコマンドラインのようですので、今回はGUIで実装してみようと思います。

Sqflite

FlutterでSqliteを使用するには、Sqfliteというパッケージを使用します。

https://pub.dev/packages/sqflite

プロジェクト要求

要求

Flutterでsqliteを使用してみる。

適当にデータを保存して、次回起動時にデータが復元できるか確認して確かめる。

動作検証

  • Pixel 8 API 33 emulator
  • Pixel 7 API 30 emulator
  • Pixel 6A 実機 (Android Version 14 / API 34)

上記のシステムで動作しました。

App概要

“+”ボタンを押すと適当なデータを生成して保存する。

“-“ボタンを押すと最新のデータを1つ削除する。

次回App起動時に保存したデータを復元する。

今回は犬の名前と年齢をランダムに生成して保存してみようと思います。

Sqlite Demo
Sqlite Demo

使用するパッケージ

  • sqflite…sqliteを使用するパッケージです。
  • path…パス文字列の操作。自分が操作するより安全ですね。
  • path_provider…各プラットフォームでデータを保存可能なパスを取得できる。
  • gap…Widget間のgap用です。
  • english_words…適当な英単語を取得できます。

pubspec.yaml一部抜粋

dependencies:
    flutter:
        sdk: flutter

    cupertino_icons: ^1.0.6
    path: ^1.9.0
    path_provider: ^2.1.2
    sqflite: ^2.3.2
    gap: ^3.0.1
    english_words: ^4.0.0

ファイル

  • dog.dart …データモデルです。
  • database_helper.dart …ヘルパークラスです。
  • main.dart …メインロジックです。

データモデル

データモデルは、ワンちゃんの名前と年齢で構成されます。Flutterの中の人は犬好きなのかもしれませんね。

モデルのインスタンスは1つのレコードに対応します。

dog.dart

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({
    required this.id,
    required this.name,
    required this.age,
  });
  
  // mapに変換する用
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // insert用Mapに変換するメソッド
  // idは自動採番のため指定しません。
  Map<String, dynamic> toInsertMap() {
    return {
      'name': name,
      'age': age,
    };
  }

  // fromMap()メソッド
  // mapからDogモデルに変換する用
  static Dog fromMap(Map<String, dynamic> map) {
    return Dog(
      id: map['id'] as int,
      name: map['name'] as String,
      age: map['age'] as int,
    );
  }

  @override
  String toString() {
    return 'Dog {id: $id, name: $name, age: $age}';
  }
 
  // カードのsubtitleやbodyに表示する文字列を返すメソッド
  String toBody() {
    return 'id: $id / age: $age';
  }

  // タイトル文字列を返すメソッド
  // nameの先頭文字を大文字にして返す
  String toTitle() {
    return name[0].toUpperCase() + name.substring(1);
  }

}

フィールド

フィールドは、id, name, age の3つです。

メソッド

toInsertMap:インサート用のMapを返します。id抜きです。nameとageのMapです。idは自動採番されます。

fromMap:Mapからモデルに変換します。静的メソッドです。

toString:すべてのフィールドを文字列として返します。主にデバッグ時に使用しました。

toBody:カード本体部に表示する文字列を返します。

toTitle:カードのタイトル用の文字列。nameを返します。

メモ

フィールドにアクセスする操作はメソッドとして実装したほうがよいでしょう。(と、自分は思っている)

モデルの責務は、データ構造とインスタンスのシリアライズ、デシリアライズを定義するのが一般的のようです。

ヘルパー / プロバイダー

データベースを操作するためのクラスを定義します。

データベースヘルパーとかデータベースプロバイダーと名付けられることが多いかもしれません。

今回は、”DatabaseHelper”というクラスを定義しました。

このヘルパーは以下の機能を有します。

  • シングルトンパターンでインスタンス管理
  • データベースの作成
  • レコードの取得
  • レコードの挿入
  • レコードの削除

今回はUpdate(更新)は使いません。

シングルトンパターン

このヘルパークラスのインスタンス管理にシングルトンパターンを採用しています。

シングルトンパターンはインスタンスを1つだけ生成し保持します。

プロパティ経由でアクセスして利用します。

database_helper.dart

import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
import 'dog.dart';

class DatabaseHelper {
  static const _databaseName = "my_database2.db";
  static const _databaseTableName = "my_table";
  static const _databaseVersion = 1;

  // シングルトン採用しています
  // プロパティ介してアクセスされます
  static DatabaseHelper? _instance;
  DatabaseHelper._();
  static DatabaseHelper get instance => _instance ??= DatabaseHelper._();

  // getterアクセスを利用するので、newでインスタンスを生成しませんが、バカ除け程度にfactoryを置いています
  factory DatabaseHelper() => instance;
  

  // データベースのインスタンス生成
  Database? _database;
  Future<Database> get database async => _database ??= await _initDatabase();

  // DB作成1
  _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
    );
  }
  
  // DB作成2
  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $_databaseTableName (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            age INTEGER NOT NULL
          )
          ''');
  }
  

  // 全レコードを取得
  Future<List<Dog>> dogs() async {
    // ~~~
  }

  // レコードを1行追加
  Future<void> addDog(Dog dog) async {
    // ~~~
  }

  // 最新行を1つ削除
  Future<void> deleteDog() async {
    // ~~
  }

}

利用方法

メソッドへのアクセスは、dogs = await DatabaseHelper.instance.dogs(); のようにinstanceプロパティ経由でインスタンスにアクセスします。

main側では、このインスタンスを変数に格納することなく、毎回、DatabaseHelper.instance.~ のようにクラス名からアクセスします。

データベースの作成

プロパティdatabaseにアクセスするとデータベースが作成されます。

getApplicationDocumentsDirectory() を使用して、各プラットフォームで使用できるローカルパスを取得しています。

join()でパスの結合を行っています。

_onCreate()でDBを作成していますが、DBファイルが無い場合のみ作成します。

各メソッド

database_helper.dart

class DatabaseHelper {

  // ~~~省略
  
  // データベースのインスタンス
  Database? _database;
  Future<Database> get database async => _database ??= await _initDatabase();

  // ~~~省略
  
  // 全レコードを取得
  Future<List<Dog>> dogs() async {
    try {
      final db = await database;

      // MapのListを取得
      final List<Map<String, dynamic>> maps =
          await db.query(_databaseTableName);

      // この関数の返却型が示すとおり、DogモデルのListを返します
      return List.generate(maps.length, (i) {
        return Dog.fromMap(maps[i]);
      });
    } catch (e) {
      throw Exception('Recordの取得に失敗しました');
    }
  }


  // レコードを1行追加
  Future<void> addDog(Dog dog) async {
    try {

      final db = await database;

      await db.insert(
        _databaseTableName,
        dog.toInsertMap(),
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
    } catch (e) {
      throw Exception('dataのinsertに失敗しました');
    }
  }


  // 最新行を1つ削除
  Future<void> deleteDog() async {
    final db = await database;
    await db.rawDelete('''
      DELETE FROM $_databaseTableName
        WHERE id = (SELECT MAX(id) FROM $_databaseTableName)
        ''');
  }

}

dogs()は、レコードを全て取得します。返り値は、DogモデルのListです。

addDog()は、レコードを追加します。引数にDogモデルを受け取っています。

deleteDog()は、最新のレコードを1つ削除します。

main.dart

メインロジックです。

ボタンを押すと、DB操作を行い、画面を更新します。

では、コードです。

main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'database_helper.dart';
import 'dog.dart';
import 'dart:math'; // Random

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'sqflite demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'sqflite demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  // ランダムなdogを生成する
  Dog createRandomDog() {
    var name = WordPair.random().asLowerCase;
    var age = Random().nextInt(10);
    return Dog(id: 0, name: name, age: age);
  }

  // 状態変数です。Dogモデルのリストですね。
  late Future<List<Dog>> _dogs;

  @override
  void initState() {
    super.initState();
    _dogs = DatabaseHelper.instance.dogs();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      //
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              //
              // Title.
              const Padding(
                padding: EdgeInsets.all(18.0),
                child: Text('LIST OF DOGS', style: TextStyle(fontSize: 26)),
              ),
              //
              // Body.
              FutureBuilder<List<Dog>>(
                future: _dogs,
                builder:
                    (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    // loading...
                    // return Container(); // これでもいいけどね
                    return const CircularProgressIndicator();
                  } else if (snapshot.hasError) {
                    // Error! Why?
                    return Text('Error: ${snapshot.error}');
                  } else {
                    // Yes! Got Data!
                    return DogList(dogs: snapshot.data!);
                  }
                },
              )
            ],
          ),
        ),
      ),
      //
      // Action Button
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          //
          // Insert Random Dog
          FloatingActionButton(
              onPressed: () async {
                try {
                  await DatabaseHelper.instance.addDog(createRandomDog());
                  setState(() {
                    _dogs = DatabaseHelper.instance.dogs();
                  });
                } catch (e) {
                  debugPrint('データの追加に失敗しました: $e');
                }
              },
              tooltip: 'データを追加する',
              child: const Icon(Icons.add)),
          //
          const Gap(8),
          //
          // Delete Dog
          FloatingActionButton(
              onPressed: () async {
                try {
                  await DatabaseHelper.instance.deleteLatestDog();
                  setState(() {
                    _dogs = DatabaseHelper.instance.dogs();
                  });
                } catch (e) {
                  debugPrint('データの取得に失敗しました: $e');
                }
              },
              tooltip: '最新データを1件削除する',
              child: const Icon(Icons.remove)),
          //
        ],
      ),
    );
  }
}

/// Dogレコードで構成されたListViewのWidgetを返します
/// 固定型のListViewで構成されます。なので親コンテナでスクロールできると思います
class DogList extends StatelessWidget {
  final List<Dog> dogs;

  const DogList({
    super.key,
    required this.dogs,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      reverse: true,
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      itemCount: dogs.length,
      itemBuilder: (BuildContext context, int index) {
        return Card(
          child: ListTile(
            title: Text(dogs[index].toTitle()),
            subtitle: Text(dogs[index].toBody()),
          ),
        );
      },
    );
  }
}

Dog createRandomDog()

適当な文字列とランダムな数値を生成し、Dogインスタンスとして返します。

late Future<List<Dog>> _dogs;

状態変数です。

DogモデルのListを保持します。

initState()で初期化しています。

FutureBuilder<List<Dog>>()

future: _dogs,

「future:」 に置く引数のFutureは前もって取得しておきます。

FutureBuilder内でFutureを作成しないのがベターな使用方法です。そうでなければ、リビルドする度にfutureを取得してしまいます。

FutureBuilder Take2 – YouTube

FutureBuilder<T> class – Flutter Document

FloatingActionButtonなど

“+”追加ボタンを押すと適当なDogモデルを作成して、それを追加します。

await DatabaseHelper.instance.addDog(createRandomDog());

テーブルの変更を画面に更新するため、setState(() {});を実行しています。

setState()内では、_dogs = DatabaseHelper.instance.dogs(); Futureを再取得しています。DogモデルのListですね。

class DogList extends StatelessWidget

DogモデルのListを受け取り、ListViewで返すWidgetです。

shrinkWrap、physics を設定することで、親Widgetでスクロールが可能になっています。

結果

  • Pixel 8 API 33 emulator
  • Pixel 7 API 30 emulator
  • Pixel 6A 実機 (Android Version 14 / API 34)

上記のシステムで動作しました。

Flutter Sqflite Demo

GitHubにも置いておきます。

https://github.com/someiyoshino/flutter_sqflite_sample1

関連情報

関連記事