11
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TORICOAdvent Calendar 2019

Day 24

Flutter で sqfentity を使って SQLite のレコードをアクティブレコードとして扱う

Last updated at Posted at 2019-12-24

Flutter上で大量のデータを管理・検索しようと思ったため、SQLiteを使ってみます。

生のSQL文を書くより、ORマッパーを使いたいと思ったため、アクティブレコードパターンっぽくレコードを扱える sqfentity ライブラリを使います。

SQLiteのDBファイルの作成、DDL文の発行などはすべてライブラリがやってくれますので、生SQLを一切触らずにアプリで利用できます。

Kapture 2019-12-10 at 16.47.04.gif

簡単なデモアプリのコード https://github.com/ytyng/todo-sqf-flutter

sqfentity

依存ライブラリに build_runner が入っています。このライブラリは、テーブルの定義を書いて、そこからbuild_runner でモデルコードを生成し、アプリでそれを使います。

最初は、ひと手間かかる感じが少し面倒そうだったのでコードジェネレートに少し抵抗があったのですが、生成済みのコードは機能リファレンスのように使えるため、なかなか使い勝手は良いです。

なお、DBの内容はアプリを再起動しても消えませんが、アプリをアンインストールすると消えます。

セットアップ

新規 Flutterプロジェクトの作成

空のリポジトリを作成し、その中に新たな Flutter プロジェクトを作ります。

mkdir todo-sqf-flutter
cd todo-sqf-flutter

flutter create app

作った todo-sqf-flutter ディレクトリを、Flutter プラグイン組み込み済みの Android Studio で開きます。

右上 Add Configuration

Screenshot 2019-12-10 11.56.14.png

+ → ○ more items → Flutter

Screenshot 2019-12-10 11.56.39.png

dart entry point は app/lib/main.dart

Run_Debug_Configurations.png

Dart SDK Not found になる場合は、 Fix → Flutterプラグインの場所を指定 (Dart でなくてよい)

虫マークをクリックして起動

カウントアップのデモアプリが起動する。

SQFEntity の組み込み

pubspeck.yaml に 追加

pubspeck.yaml
dependencies:
  ...

  sqfentity: ^1.2.2+10
  sqfentity_gen: ^1.2.0+8

dev_dependencies:
  ...

  build_runner: ^1.6.5
  build_verify: ^1.1.0

インストール

flutter pub get

モデルの作成

lib/models.dart を作成

lib/models.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:sqfentity/sqfentity.dart';
import 'package:sqfentity_gen/sqfentity_gen.dart';

part 'models.g.dart';


@SqfEntityBuilder(todoDbModel)
const todoDbModel = SqfEntityModel(
    modelName: 'TodoDbModel', // optional
    databaseName: 'todo.db',
    databaseTables: [todo],
    bundledDatabasePath: null
);


const todo = SqfEntityTable(
    tableName: 'todo',
    primaryKeyName: 'id',
    primaryKeyType: PrimaryKeyType.integer_auto_incremental,
    useSoftDeleting: true,
    fields: [
      SqfEntityField('title', DbType.text),
      SqfEntityField('active', DbType.bool, defaultValue: true),
    ]);

フィールドに、id (PK) と title を持つ、単純なモデルです。

(active というフィールドを作りましたがでもアプリ内では結局使いませんでした)

コードの生成

flutter packages pub run build_runner build --delete-conflicting-outputs

このようなモデルを含むコードが生成されます

lib/models.g.dart
// BEGIN ENTITIES
// region Todo
class Todo {
  Todo({this.id, this.title, this.active, this.isDeleted}) {
    _setDefaultValues();
  }
  Todo.withFields(this.title, this.active, this.isDeleted) {
    _setDefaultValues();
  }
  Todo.withId(this.id, this.title, this.active, this.isDeleted) {
    _setDefaultValues();
  }
  Todo.fromMap(Map<String, dynamic> o) {
    id = o['id'] as int;
    title = o['title'] as String;
    active = o['active'] != null ? o['active'] == 1 : null;
    isDeleted = o['isDeleted'] != null ? o['isDeleted'] == 1 : null;
  }
  ...

アプリに組み込む

main.dart を修正

使用前に、initializeDB をコールします。

void main() async {
  final bool isInitialized = await MyDbModel().initializeDB();
  if (isInitialized == true) {
    runApp(MyApp());
  }
}

起動します。ログは、起動時に毎回出ます

Syncing files to device iPhone 7...
flutter: init() -> modelname: null, tableName:todo
flutter: >>>>>>>>>>>>>>>>>>>>>>>>>>>> SqfEntityTableBase of [todo](id) init() successfuly
flutter: todo.db created successfully
flutter: SQFENTITIY: Table named [todo] was initialized successfuly (created table)
flutter: SQFENTITIY: The database is ready for use
flutter: SQFENTITIY: Table named [todo] was initialized successfuly (No added new columns)
flutter: SQFENTITIY: The database is ready for use

Insert

Todo(title: textEditingController.text, active: true).save();

リアクティブではないので、実施後手動でウィジェットのリビルドが必要そうです。

Update

Todo(id: widget.todoId, title: textEditingController.text,
     active: true).save();

save() メソッドは、UPSERT として機能します

Select

Todo().select().orderByDesc('id').toList()

空引数でクラスインスタンスを作ってから絞り込んでいきます。

戻り値はFuture ですので、単純にウィジェットに表示するだけなら、 FutureBuilder や StreamBuilder 使うと良さそうです。

Delete

Todo().select().id.equals(widget.todoId).delete()

スキーママイグレーション

フィールドの追加であれば、models.dart を修正し、build_runner してアプリを再起動すれば、自動的に ALTER TABLE してくれる。データは消えない。

flutter: init() -> modelname: null, tableName:todo
flutter: >>>>>>>>>>>>>>>>>>>>>>>>>>>> SqfEntityTableBase of [todo](id) init() successfuly
flutter: SQFENTITIY: alterTableQuery => [ALTER TABLE todo ADD COLUMN created datetime]
flutter: SQFENTITIY: Table named [todo] was initialized successfuly (Added new columns)
flutter: SQFENTITIY: The database is ready for use
flutter: SQFENTITIY: Table named [todo] was initialized successfuly (No added new columns)
flutter: SQFENTITIY: The database is ready for use

フィールドの型の変更は例外が出る。サポート外だと思ったほうが良いかも。アプリをアンインストールすればDBファイルが削除されるので、起動できるようになる。

[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: Exception: SQFENTITIY DATABASE INITIALIZE ERROR: The type of column [product_id(DbType.integer)] does not match the exiting column [product_id(DbType.text)]
#0      checkTableColumns (package:sqfentity/sqfentity.dart:680:9)
#1      SqfEntityModelProvider.initializeDB (package:sqfentity/sqfentity.dart:508:15)
<asynchronous suspension>
#2      SqfEntityProvider.db (package:sqfentity/sqfentity.dart:58:22)
<asynchronous suspension>
#3      SqfEntityProvider.execDataTable (package:sqfentity/sqfentity.dart:172:36)
<asynchronous suspension>
#4      SqfEntityModelProvider.initializeDB (package:sqfentity/sqfentity.dart:485:14)
<asynchronous suspension>
#5      run (package:mzv/app.dart:40:49)
<asynchronous suspension>
#6      main (package:mzv/main/local.dart:20:3)
<asynchronous suspension>
#7      _runMainZoned.<anonymous closure>.<anonymous closure> (dart:ui/hooks.dart:229:25)
#8      _rootRun (dart:async/zone.dart:1124:13)
#9      _CustomZone.run (dart<…>

その他、公式APIドキュメントに豊富な機能が紹介されています。

書いたコード

main.dart
import 'package:flutter/material.dart';

import 'models.dart';

void main() async {
  final bool isInitialized = await TodoDbModel().initializeDB();
  if (isInitialized == true) {
    runApp(MyApp());
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Todo App',
        theme: ThemeData(
          primarySwatch: Colors.green,
        ),
        home: MyScaffold(),
      );
}

class MyScaffold extends StatelessWidget {
  final todoWidgetKey = GlobalKey<_TodoState>();

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text('Todo'),
        ),
        body: TodoWidget(key: todoWidgetKey),
        floatingActionButton: Builder(builder: (BuildContext context) {
          return FloatingActionButton(
            onPressed: () async {
              await showDialog(
                  context: context,
                  builder: (BuildContext context) {
                    return EditDialog(
                      todoWidgetKey: todoWidgetKey,
                    );
                  });
            },
            child: Icon(Icons.add),
          );
        }),
      );
}

class TodoWidget extends StatefulWidget {
  const TodoWidget({
    Key key,
  }) : super(key: key);

  @override
  _TodoState createState() => _TodoState();
}

class _TodoState extends State<TodoWidget> {
  void update() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: Todo().select().orderByDesc('id').toList(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.hasData) {
            return ListView(
              children: snapshot.data.map<Widget>((Todo item) {
                return GestureDetector(
                  onTap: () async {
                    await showDialog(
                        context: context,
                        builder: (BuildContext context) {
                          return EditDialog(
                            title: item.title,
                            todoId: item.id,
                            todoWidgetKey: widget.key,
                          );
                        });
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      border: Border(
                          bottom:
                              BorderSide(width: 1.0, color: Colors.black26)),
                    ),
                    padding: EdgeInsets.all(16),
                    child: Text(item.title),
                  ),
                );
              }).toList(),
            );
          } else {
            return Text('wait');
          }
        });
  }
}

class EditDialog extends StatefulWidget {
  final int todoId;
  final String title;
  final GlobalKey<_TodoState> todoWidgetKey;
  final List<Widget> children;

  const EditDialog({
    Key key,
    this.todoId,
    this.title,
    this.todoWidgetKey,
    this.children,
  }) : super(key: key);

  @override
  _EditDialogState createState() => _EditDialogState();
}

class _EditDialogState extends State<EditDialog> {
  final textEditingController = TextEditingController();

  @override
  initState() {
    super.initState();
    if (widget.title != null) {
      textEditingController.text = widget.title;
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
        content: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
      TextField(
        controller: textEditingController,
        autofocus: widget.todoId == null,
        decoration: InputDecoration(
          labelText: 'Task?',
        ),
      ),
      ...widget.todoId == null
          ? buttonsForCreate(context)
          : buttonsForUpdate(context)
    ]));
  }

  List<Widget> buttonsForCreate(BuildContext context) => <Widget>[
        buildButton(
            context: context,
            labelText: '登録',
            onPressed: () {
              if (textEditingController.text.length > 0) {
                Todo(title: textEditingController.text, active: true).save();
                widget.todoWidgetKey.currentState.update();
              }
              Navigator.of(context).pop();
            })
      ];

  List<Widget> buttonsForUpdate(BuildContext context) => <Widget>[
        buildButton(
            context: context,
            labelText: '更新',
            onPressed: () {
              if (textEditingController.text.length > 0) {
                Todo(
                        id: widget.todoId,
                        title: textEditingController.text,
                        active: true)
                    .save();
                widget.todoWidgetKey.currentState.update();
              }
              Navigator.of(context).pop();
            }),
        buildButton(
            context: context,
            labelText: '消去',
            buttonColor: Colors.red,
            onPressed: () {
              Todo().select().id.equals(widget.todoId).delete();
              widget.todoWidgetKey.currentState.update();
              Navigator.of(context).pop();
            })
      ];
}

Widget buildButton(
    {BuildContext context,
    String labelText,
    VoidCallback onPressed,
    Color buttonColor}) {
  return Padding(
    padding: const EdgeInsets.only(top: 10),
    child: ButtonTheme(
      minWidth: double.infinity,
      height: 50,
      child: RaisedButton(
        color: buttonColor ?? Theme.of(context).primaryColor,
        onPressed: onPressed,
        child: Text(labelText, style: TextStyle(color: Colors.white)),
      ),
    ),
  );
}
11
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?