flutterでsqfliteを使ってローカルでデータ処理する方法

こんにちは。

flutterでローカルデータベースSQLiteを使用する事にしました。FlutterでSQLiteを使うためにsqfliteというパッケージがあり、参考サイトも沢山あるのでスムーズに行くと思ったのですが、思った以上に躓きポイントが多かったので記事に残しておきます。Flutterの開発はAndoroidStudioを使用していますので、データベースの確認方法などもご紹介します。

sqfliteパッケージのインストール

sqfliteをインストールします。またAndoroid,iosで動くアプリを目指しているのでデータベースの正しい保存場所を見つける事が出来るプラグインpath_providerもインストールします。pubspec.yamlのdependencies配下に追加して、「flutter pub get」コマンドを実行します。

dependencies:
  sqflite: ^2.0.0+4
  path_provider: ^2.0.5

データベースを作成

データベース用クラスのファイル「DBManager.dart」を用意して、まずはデータベースを作成していきます。

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

class DBManager {

  // シングルトンクラス
  DBManager._privateConstructor();
  static final DBManager instance = DBManager._privateConstructor();

  //データベースの作成
  static Database? _database;

  static Future<Database> get database async {

    if (_database != null)
      return _database!;

    Directory appDocDir = await getApplicationDocumentsDirectory();
    var path = join(appDocDir.path, 'database.db');
    var exists = await databaseExists(path);

    _database = await openDatabase(
      path,
      onCreate: _onCreate,
      version: 1,
    );

    return _database!;
  }

  static Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE IF NOT EXISTS Category (
            id INTEGER,
            title TEXT,
            subtitle TEXT,
            color INTEGER,
            icon INTEGER
          )
          ''');
    await db.execute('''
          CREATE TABLE IF NOT EXISTS Condition (
            id INTEGER,
            title TEXT,
            icon INTEGER,
            category_id INTEGER
          )
          ''');
  }

参考にしたサイトは1テーブルの作成方法しか載っていなかったので、ここでは2テーブルを作成する例を挙げました。複数テーブルを作成する場合は「await db.execute(…);」を続けて書く事で作成できます。ただし一度データベースを作成した後に、「await db.execute(…);」をしても有効にはなりませんでした。その場合は「openDatabase」の前に「deleteDatabase(path);」を入れて一度データベース自体を削除する事で対応しましたが、運用後にはこの方法は使えないので、データを退避して入れなおす必要が出てきそうです。

ところでこのデータベースは一体どこに出来るのでしょうか。AndroidStudioのエミュレータを使った場合は以下に作成されました。
/data/user/0/com.example.XXXXX/app_flutter/database.db

この場所をAndroidStudioで確認するには[表示]-[ツール・ウィンドウ]-[デバイスファイルエクスプローラー]にて確認できます。

モデルクラスの作成

先ほど、「Category」「Condition」という2つのテーブルを作成しましたので、モデルクラスもそれぞれ作成します。

import 'package:flutter/material.dart';

class Category {
  late int id;
  late String title;
  late String subtitle;
  late Color color;
  late IconData icon;

  Category(
      this.id,
      this.title,
      this.subtitle,
      this.color,
      this.icon,
      );

  Category.fromMap(Map<String, dynamic> map)
      : id = map['id'],
        title = map['title'],
        subtitle = map['subtitle'],
        color = Color(int.parse(map['color'].toString())),
        icon = IconData(int.parse(map['icon'].toString()), fontFamily: 'MaterialIcons');

  Map<String, dynamic> toMap() {
    var map = Map<String, dynamic>();
    map['id'] = id;
    map['title'] = title;
    map['subtitle'] = subtitle;
    map['color'] = color.value;
    map['icon'] = icon.codePoint;
    return map;
  }

  @override
  String toString() {
    return 'Category{id: $id, title: $title}';
  }
}

「Category」クラスにはデータベースには持てないColor、IconData型があるので、toMap(テーブルにデータを入れる時に)でintでデータを渡します。またfromMap(テーブルからデータを取り出す時に)でそれぞれColor、IconData型に変換しています。「Condition」に関してもほぼ同じです。

import 'package:flutter/material.dart';

class Condition {
  late int id;
  late String title;
  late IconData icon;
  late int category_id;

  Condition(
      this.id,
      this.title,
      this.icon,
      this.category_id,
      );

  Condition.fromMap(Map<String, dynamic> map)
      : id = map['id'],
        title = map['title'],
        icon = IconData(int.parse(map['icon'].toString()), fontFamily: 'MaterialIcons'),
        category_id = map['category_id'];

  Map<String, dynamic> toMap() {
    var map = Map<String, dynamic>();
    map['id'] = id;
    map['title'] = title;
    map['icon'] = icon.codePoint;
    map['category_id'] = category_id;
    return map;
  }

  @override
  String toString() {
    return 'Condition{id: $id, title: $title}';
  }

}

データ挿入と取得

DBManagerクラスにデータ挿入・取得用関数を用意します。データが無ければ初期データを入れるようにしています。

static Future<List<Category>> getCategories() async {

  List<Category> list= [
    Category(0,'sentiment','sentiment subtitle',Colors.redAccent,Icons.sentiment_satisfied),
    Category(1,'food','food',Colors.pinkAccent,Icons.restaurant_outlined),
    Category(2,'sports','sports',Colors.purpleAccent,Icons.sports_soccer_outlined),
    Category(3,'hardware','hardware',Colors.deepPurpleAccent,Icons.smart_toy_outlined),
    Category(4,'vehicle','vehicle',Colors.indigoAccent,Icons.directions_car_outlined),
  ];

  final Database db = await database;
  List<Map<String, dynamic>> maps = await db.query('Category');
  if (maps.isNotEmpty) {
    return maps.map((map) => Category.fromMap(map)).toList();
  } else{
    print("Category is Empty");
    for (int i = 0; i < list.length; i++) {
      await addCategory(list[i]);
    }
  }
  return list;
}

static Future<int> addCategory(Category category) async {
  final Database db = await database;
  return db.insert(
    'Category',
    category.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

データベースはAndroidStudioでは[表示]-[ツール・ウィンドウ]-[Database Inspector]にて確認できます。上記で入れたデータも確認できます。

データベースの使い方

上記を呼び出す方法を記載します。DBManagerクラスに定義したものはFutureで非同期処理にしてあるため、データベースからデータ取得した後に画面表示させるにはコツが要ります。以下にあるように「FutureBuilder」を使用して非同期処理であるデータベース取得処理を待ってから画面表示するようにします。コツは「future: _future」で_futureをinitStateで1回だけ設定するようにしてあげる必要があります。「future: initCategory()」にすると、onTapイベントの度にreloadされてしまうという謎現象に悩まされていました。この解決方法はhttps://github.com/flutter/flutter/issues/11426を参考にしています。

  Future? _future;
  List categorylist = [];

  @override
  void initState() {
    super.initState();
    _future = initCategory();
  }
  Future> initCategory() async {
    categorylist = await Future.delayed( Duration(seconds: 1),() => DBManager.getCategories()) ;
     return categorylist;
  }

・・・
    return Container(
      margin: EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10),
      width: double.infinity,
      child: FutureBuilder(
          future: _future,
          builder: (context, snapshot) {
            switch (snapshot.connectionState) {
              case ConnectionState.waiting:
                return Center(
                  child: CircularProgressIndicator(),
                );
              default:
                if (snapshot.hasError) {
                    print(snapshot.error);
                    return Text('Error: ${snapshot.error}');
                } else {
                  return SingleChildScrollView(
・・・

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)