Flutterでmoor(SQLite)を使う その1

2021年3月24日

こんにちは

FlutterでローカルDB(SQLite)を使ってみたいと思います。

その際に、FlutterとSQLiteの仲立ちをするmoorというモジュールを使ってみます。

LaravelのORマッパー(Eloquent)みたいに、SQLを極力書かずに簡単にローカルDBのCRUDができるみたいです。

本記事では、SQLiteとmoorの環境構築、簡単なDBの作成、画面からのCRUDをおこないます。

moorのドキュメントに沿って環境構築してみましょう

Flutterのversionは2.0;1、エディタはAndroidStudioを使用しています。

モジュールを「pubspec.yaml」に追加(赤字の部分です)

※moorのversionは4.2.0(最新)です。(2021年3月22日)

・pubspec.yaml

name: flutter_app5
description: A new Flutter application.

publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  provider: ^5.0.0
  moor: ^4.2.0
  sqlite3_flutter_libs: ^0.4.1
  path_provider:
  path:

dev_dependencies:
  flutter_test:
    sdk: flutter
  moor_generator: ^4.1.0
  build_runner:

flutter:
  uses-material-design: true

記載が終わりましたら、右上の「Pub get」を押してinstall

■DBの作成

「lib」配下に「database」というディレクトリ、その中に「company.dart」ファイルを作成し以下を記載します。

部署テーブルと社員テーブルを作ります。

カラムの命名規則はキャメルケースを推奨しているみたいです。

・lib/database/company.dart

import 'package:moor/moor.dart';
part 'company.g.dart';

//部署テーブル
class Departments extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 2, max: 20)();
  TextColumn get place => text().withLength(min: 2, max: 20)();
  TextColumn get businessContent => text().nullable()();
}

//社員テーブル 別名をWorker
//部署idを外部キーに設定
@DataClassName("Worker")
class Syain extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text()();
  IntColumn get department =>
      integer().customConstraint('REFERENCES departments(id)')();
}

@UseMoor(tables: [Departments, Syain])
class MyDatabase {}

「company.dart」内の「part」が「company.g.dart」である必要があります。

異なると「company.g.dart」が生成されません。

記載し終えたら、以下コマンドをターミナルにて打ちます

flutter packages pub run build_runner build

少し待つと、「company.g.dart」が生成されています。

このファイル内にはDB操作用のhelperクラスが記述されています。DBの煩雑な操作はこいつがやってくれます!

先ほど記載した、「company.dart」を追記します。

・lib/database/company.dart

import 'dart:io';

import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'company.g.dart';

//部署テーブル
class Departments extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 2, max: 20)();
  TextColumn get place => text().withLength(min: 2, max: 20)();
  TextColumn get businessContent => text().nullable()();
}

//社員テーブル 別名をWorker
@DataClassName("Worker")
class Syain extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text()();
  IntColumn get department =>
      integer().customConstraint('REFERENCES departments(id)')();
}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return VmDatabase(file);
  });
}

@UseMoor(tables: [Departments, Syain])
class MyDatabase extends _$MyDatabase {
  // データを格納する場所をデータベースに指示する
  MyDatabase() : super(_openConnection());
  // スキーマバージョン
  @override
  int get schemaVersion => 1;

  // 部署テーブル取得
  Future<List> getAllDepartmentEntries() {
    return select(departments).get();
  }

  //  部署テーブル追加
  Future addDepartmentEntry(DepartmentsCompanion entry) {
    return into(departments).insert(entry);
  }

  // 部署テーブルの更新
  Future updateDepartment(int id, DepartmentsCompanion department) {
    return (update(departments)..where((it) => it.id.equals(id)))
        .write(department);
  }

  // 部署テーブルの削除
  Future deleteDepartment(int id) {
    return (delete(departments)..where((it) => it.id.equals(id))).go();
  }
}

カラムの型等は公式ドキュメントを参照

「DepartmentsCompanion」は「company.g.dart」の中に定義されています。

「XXXXXXCompanion」はNullを許容する型となっています。

→登録時にnullableかどうかを判断できる型

■画面(View)の作成

・lib/main.dart(メインページ。部署追加画面に遷移するボタンが配置されている)

import 'package:flutter/material.dart';
import 'company/department_add_page.dart';
import 'database/company.dart';

MyDatabase database;

void main() async {
  database = MyDatabase();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MainPage(),
    );
  }
}

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('テストFlutter'),
        ),
        body: Center(
          child: Column(children: [
            ElevatedButton(
              child: Text('部署一覧'),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => DepartmentAddPage()),
                );
              },
            )
          ]),
        ),
      ),
    );
  }
}

・lib/company/department_model.dart(view model)

import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:flutter_app5/main.dart';
import 'package:moor/moor.dart';

class DepartmentModel extends ChangeNotifier {
  String name = '';
  String place = '';
  List departments = [];
  bool loadingFlag = false;

  startLoading() {
    loadingFlag = true;
    notifyListeners();
  }

  endLoading() {
    loadingFlag = false;
    notifyListeners();
  }

  // 部署の登録を行う
  Future registerDepartment() async {
    if (name.isEmpty) {
      throw ('名前を入力してください');
    }

    if (place.isEmpty) {
      throw ('場所を入力してください');
    }

    print('部署名:$name');
    print('場所:$place');

    // 登録の実行
    int result = await database.addDepartmentEntry(
      DepartmentsCompanion(
        name: Value(name),
        place: Value(place),
      ),
    );
  }

//  部署の表示を行う
  Future showDepartment() async {
    List departmentList = await database.getAllDepartmentEntries();
    this.departments = departmentList;
    notifyListeners();
  }

  //  部署の削除を行う
  Future deleteDepartment(Department department) async {
    int result = await database.deleteDepartment(department.id);
  }

  //  部署の更新を行う
  Future updateDepartment(int departmentid, Department department) async {
    if (name.isEmpty) {
      throw ('タイトルを入力してください');
    }

    if (place.isEmpty) {
      throw ('コンテンツを入力してください');
    }

    // 登録の実行
    int result = await database.updateDepartment(
      departmentid,
      DepartmentsCompanion(
        name: Value(name),
        place: Value(place),
      ),
    );
  }
}

・lib/company/department_add_page.dart(部署追加・変更画面)

import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:provider/provider.dart';

import 'department_list_page.dart';
import 'department_model.dart';

class DepartmentAddPage extends StatelessWidget {
  DepartmentAddPage({this.department});
  final Department department;

  @override
  Widget build(BuildContext context) {
    final nameController = TextEditingController();
    final placeController = TextEditingController();

    /// 新規(false)か更新(true)か
    final bool updateFlag = department != null;

    if (updateFlag) {
      nameController.text = department.name;
      placeController.text = department.place;
    }

    return ChangeNotifierProvider(
      create: (_) => DepartmentModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text(updateFlag ? '部署の編集' : '部署の新規追加'),
        ),
        body: Consumer(builder: (context, model, child) {
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                TextField(
                  decoration: InputDecoration(hintText: '部署名'),
                  controller: nameController,
                  onChanged: (text) {
                    model.name = text;
                  },
                ),
                TextField(
                  controller: placeController,
                  decoration: InputDecoration(hintText: '場所'),
                  onChanged: (text) {
                    model.place = text;
                  },
                ),
                ElevatedButton(
                  child: Text(updateFlag ? '更新' : '登録'),
                  onPressed: () async {
                    try {
                      model.startLoading();

                      if (updateFlag) {
                        ///更新
                        await model.updateDepartment(department.id, department);
                      } else {
                        ///登録
                        await model.registerDepartment();
                      }
                      model.endLoading();

                      _showMordal(context, '登録完了しました');
                    } catch (e) {
                      _showMordal(context, e.toString());
                      print(e);
                    }
                  },
                ),
                ElevatedButton(
                  child: Text('一覧の表示'),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => DepartmentListPage()),
                    );
                  },
                ),
              ],
            ),
          );
        }),
      ),
    );
  }

  Future _showMordal(BuildContext context, String title) {
    showDialog(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          actions: [
            TextButton(
              child: Text('ok'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

・lib/company/department_list_page.dart(部署一覧画面)

import 'package:flutter/material.dart';
import 'package:flutter_app5/database/company.dart';
import 'package:provider/provider.dart';

import '../main.dart';
import 'department_add_page.dart';
import 'department_model.dart';

class DepartmentListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => DepartmentModel()..showDepartment(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('部署一覧'),
        ),
        body: Consumer(builder: (context, model, child) {
          final departments = model.departments;
          final listTiles = departments
              .map(
                (department) => ListTile(
                  leading: Text(department.id.toString()),
                  title: Text(department.name),
                  trailing: IconButton(
                    icon: Icon(Icons.edit),
                    onPressed: () async {
                      await Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) =>
                              DepartmentAddPage(department: department),
                          // 縦スクロールで遷移(前画面に戻るときは×ボタンになる)
                          fullscreenDialog: true,
                        ),
                      );
                      model.showDepartment();
                    },
                  ),
                  onLongPress: () async {
                    await showDialog(
                      context: context,
                      // barrierDismissible: false, // user must tap button!
                      builder: (BuildContext context) {
                        return AlertDialog(
                          title: Text('${department.name}を削除しますか'),
                          actions: [
                            TextButton(
                              child: Text('ok'),
                              onPressed: () async {
                                Navigator.of(context).pop();
                                await deleteDepartment(
                                    context, model, department);
                              },
                            ),
                          ],
                        );
                      },
                    );
                  },
                ),
              )
              .toList();
          return ListView(
            children: listTiles,
          );
        }),
      ),
    );
  }

  Future deleteDepartment(BuildContext context, DepartmentModel model,
      Department department) async {
    try {
      await database.deleteDepartment(department.id);
      await _showModal(context, '削除しました');
    } catch (e) {
      await _showModal(context, e.toString());
    }
  }

  Future _showModal(BuildContext context, String title) {
    showDialog(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          actions: [
            TextButton(
              child: Text('ok'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

長々と書きましたがこれでmoorを使ったDBと画面ができあがりましたので起動してみましょう。

画面一覧をポチッ

・登録

・一覧と削除(削除は項目を長押しで削除します)

・更新(部署一覧の右端にある鉛筆アイコンクリックで遷移)

単純な作りでしたので、次回は複数テーブルjoinしたり、DB例外処理を加えたり、もう少し複雑なことをおこないます。