こんにちは。
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; Listcategorylist = []; @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( ・・・
コメント