Flutterでsqfliteを使用する。シングルトンパターンを使わない方法バージョン
 
                                以前、Flutterでsqfliteを使用する方法を記事にしました。実装方法はシングルトンパターンを採用していました。
今回は、シングルトンパターンを使用しない方法の紹介です。
目次
作成物
名前と年齢を適当に生成してdatabaseに保存します。
次回起動時にデータが復元されます。

sqfliteとは
Flutterでsqliteを使用できるパッケージです。

Flutter公式でも説明がありますね。

sqfliteを使う
3つのファイルを作成します
- データモデルクラス – dog_model.dart
- ヘルパークラス – dog_provider.dart
- ページ – dog_page.dart
データモデルクラス
なんらかのデータ構造に着目する場合、モデルを作成したほうがいいかもしれません。
jsonファイルやデータベースの読み書きなどがある場合、モデルの作成を考えた方がいいでしょう。
今回のデータモデルは、テーブルのレコードに対応します。
ヘルパークラス
sqfliteの操作をラッパーします。適当なメソッドとして提供します。
ページ
ヘルパークラスを利用してページを構成します。
データモデル
dog_model.dart
/// dog model
/// --- 備考
/// idは自動採番です。Insertするときは不要(null)です。
/// Insertするときは、toInsertMap()を使います。これはidを含まないMapを返します。
/// コンストラクタではidは必須ではありません。
class DogModel {
  final int? id;
  final String name;
  final int age;
  DogModel({this.id, required this.name, required this.age});
  factory DogModel.fromMap(Map<String, dynamic> map) {
    return DogModel(id: map['id'], name: map['name'], age: map['age']);
  }
  Map<String, dynamic> toMap() {
    return {'id': id, 'name': name, 'age': age};
  }
  /// Insert用Map変換
  Map<String, dynamic> toInsertMap() {
    return {'name': name, 'age': age};
  }
  @override
  String toString() {
    return 'DogModel{id: $id, name: $name, age: $age}';
  }
}近頃は、「GitHub Copilot」が、コメントを適当に打つと残りのコードを生成してくれます。
idとnameとageだけの簡単なフィールド。
id不要のtoInsertMap()を用意。
ヘルパー
dog_provider.dart
私はProviderと名付けています。一般的でないかも?
今回はシングルトンパターンを使用していません。ChatGPT等にsqfliteのコードを書かせるとシングルトンパターンで回答してくることが多かったので今回は使わないパターンを書いてみました。
/// sqfliteを使ったデータモデルの永続化プロバイダ
/// Dogデータ専用
/// --- memo
/// シングルトンを使用しないパターンです。
library;
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'dog_model.dart';
class DogProvider {
  /// DBのインスタンス保存用
  late final Database _db;
  // コンストラクタ。内部用、名前付きコンストラクタ
  DogProvider._internal(this._db);
  // static factory
  static Future<DogProvider> create() async {
    String path = join(await getDatabasesPath(), 'dog.db');
    Database db = await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE dogs(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)',
        );
      },
    );
    return DogProvider._internal(db);
  }
  /// データ挿入用
  /// idは無視されます
  Future<int> insertDog(DogModel dog) async {
    return await _db.insert('dogs', dog.toInsertMap());
  }
  /// 全件取得
  Future<List<DogModel>> getDogs() async {
    final List<Map<String, dynamic>> maps = await _db.query('dogs');
    return List.generate(maps.length, (i) {
      return DogModel.fromMap(maps[i]);
    });
  }
  /// idを指定して削除
  Future<void> deleteDog(int id) async {
    await _db.delete('dogs', where: 'id = ?', whereArgs: [id]);
  }
  /// 全件削除
  Future<void> deleteAllDogs() async {
    await _db.delete('dogs');
  }
  /// 最新のレコードを1行削除する。
  Future<void> deleteLatestDogSubQuery() async {
    // サブクエリでidの最大値を取得して削除する方法
    await _db.rawDelete(
      'DELETE FROM dogs WHERE id = (SELECT MAX(id) FROM dogs)',
    );
  }
}
名前付きコンストラクタでインスタンス生成します。
インスタンス変数「_db」はFuture型ではありません。
ページ
dog_page.dart
/// dog_providerを使用するサンプル
library;
import 'package:flutter/material.dart';
import 'package:dog1/dog/dog_provider.dart';
import 'package:dog1/dog/dog_model.dart';
import 'package:english_words/english_words.dart';
import 'dart:math'; // Random
class DogPage extends StatefulWidget {
  const DogPage({super.key});
  @override
  State<DogPage> createState() => _DogPageState();
}
class _DogPageState extends State<DogPage> {
  // 変数
  late DogProvider _dogProvider;
  List<DogModel> _dogs = [];
  @override
  void initState() {
    super.initState();
    _initProvider();
  }
  /// 初期化処理
  _initProvider() async {
    _dogProvider = await DogProvider.create();
    _dogs = await _dogProvider.getDogs();
    setState(() {});
  }
  /// insert用
  _insertDog() async {
    final newDog = DogModel(
      name: WordPair.random().asLowerCase,
      age: Random().nextInt(10),
    );
    await _dogProvider.insertDog(newDog);
    _dogs = await _dogProvider.getDogs();
    setState(() {});
  }
  /// 全削除用
  _deleteAllDogs() async {
    await _dogProvider.deleteAllDogs();
    _dogs = await _dogProvider.getDogs();
    setState(() {});
  }
  /// 最新のレコードを1行削除
  _deleteLatestDog() async {
    await _dogProvider.deleteLatestDogSubQuery();
    _dogs = await _dogProvider.getDogs();
    setState(() {});
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dog Page')),
      body: Column(
        children: [
          Wrap(
            spacing: 10,
            children: [
              // 追加ボタン
              ElevatedButton(
                onPressed: () {
                  _insertDog();
                },
                child: const Text('Add Dog'),
              ),
              // 最新削除ボタン
              ElevatedButton(
                onPressed: () {
                  _deleteLatestDog();
                },
                child: const Text('Delete Latest Dog'),
              ),
              // 全削除ボタン
              ElevatedButton(
                onPressed: () {
                  _deleteAllDogs();
                },
                child: const Text('Delete All Dogs'),
              ),
            ],
          ),
          Expanded(child: DogList(dogs: _dogs)),
        ],
      ),
    );
  }
}
class DogList extends StatelessWidget {
  const DogList({super.key, required this.dogs});
  final List<DogModel> dogs;
  @override
  Widget build(BuildContext context) {
    // ListView.builderはitemCountが0の場合でもListを返すようだ。
    return ListView.builder(
      itemCount: dogs.length,
      itemBuilder: (context, index) {
        final dog = dogs[index];
        return ListTile(
          title: Text(dog.name),
          subtitle: Text('ID: ${dog.id} Age: ${dog.age}'),
        );
      },
    );
  }
}
データは「_dogs」に格納されています。List<DogModel>型です。
ListView.builderで展開しています。
表示前に、空Listをチェックしてもいいですね。
動作確認
追加ボタンを押すと1行追加され、削除ボタンを押すと1行削除されます。
再起動してもデータは残っていることを確認できます。

sqfliteの使い方(前回)
前回の記事ではシングルトンパターンを使用したものでした。現在もChat系AIに質問するとこのパターンで実装する例がよく出力されます。
今回は対象のページを開くと毎回インスタンスを生成する方式です。
ページを開くと毎回インスタンスが生成されますが問題はありませんでした。

