Dart

16. Flutter のデータベース外部パッケージSQFLiteを使ってみる

本日はFlutterでデータベース操作を学習しようと思います。
iOSとAndroidではSQLiteという軽量ライブラリがあります。SQLiteを使えばアプリ内にDB(DataBase)を作成しアプリ内にデータを保存できました。
そのSQLiteをFlutterで利用するための外部パッケージに「SQFLite」というライブラリがあります。
今回はSQFLiteを使ってデータベース操作について学習してみます。

SQFLiteのインストール

その外部パッケージのSQFLiteをインストールするためにはpubspec.yamlファイルを開き、
SQFLiteをインストールするコードを書く必要があります。

Flutter のプロジェクトファイルのpubspec.yamlを開きます。

pubspecc.yamlファイル

編集していない状態は次のコードになります。

name: practice_app
description: A new Flutter application.

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: ">=2.2.2 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

ここのdependenciesに次のコードを追加します。

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  # コードを追加
  sqflite: any

sqflite: anyがSQFLiteのインストールになります。
この状態でpackages upgradeをタップします。

下のコンソールでエラーが表示されていなければSQFLiteのインストールが完了します。

Running "flutter pub upgrade" in practice_app...   
Process finished with exit code 0

注記:

iPhoneX でのシミュレート時にcocoapod のインストールを求められたのでiOSでのデバッグの際には
エラー内容に従って

gem install cocoapods

でcocoapods をインストールする必要がありました。

SQFLite の使い方

SQFLite の使い方について解説します。

基本的には

  1. sqflite をインポートする
  2. データベースを開く
  3. データベースが開いたときの処理を書く

と通常のSQLの扱いと同じ手順になります。

  1. SQFLiteのインポート

Dartファイルに次の1行を追加します。

import 'package:sqflite/sqflite.dart';
  1. データベースを開く
Database db = await openDatabase(
        [データベースのpath], 
        version: [バージョン], 
        onCreate: [処理の内容]
      );

openDatabaseでデータベースを開くことができます。第1引数にはDBのpath、第2引数にはDBのバージョン、第3引数に処理内容という感じになります。

onCreateメソッドの中は概ね次のような処理が書かれます。

onCreate: (Database db, int version) async {
          /// 処理内容
          );
        }

サンプルコード

では、具体的にサンプルコードを書いてみます。

今回はデータベース名やテーブル名を別クラスconstants.dartとして予め作成しています。

constants.dart

class Constants {
  final String dbName = "sqflite.db";
  final int dbVersion = 1;
  final String tableName = "address";
}

main.dart

import 'package:flutter/material.dart';
import 'package:practice_app/first_screen.dart';
import 'package:practice_app/second_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: true,
      title: 'BottomNavigationBar App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: const Color(0xff2196f3),
        accentColor: const Color(0xff2196f3),
        canvasColor: const Color(0xfffafafa),
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => FirstScreen(),
        '/list': (context) => SecondScreen(),
      },
    );
  }
}

first_screen.dart

import 'package:flutter/material.dart';
import 'package:practice_app/constants.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class FirstScreen extends StatefulWidget {
  @override
  _FirstScreenState createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final phoneController = TextEditingController();

  final TextStyle style1 = TextStyle(
    fontSize: 30.0,
    color: Colors.black
  );
  final TextStyle style2 = TextStyle(
    fontSize: 30.0,
    color: Colors.black
  );


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Input'),
      ),
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Align(
              alignment: Alignment.centerLeft,
              child: Text('名前:', style: style2,),
            ),
            TextField(
              controller: nameController,
              style: style1,
            ),
            Align(
              alignment: Alignment.centerLeft,
              child: Text('Email:', style: style2,),
            ),
            TextField(
              controller: emailController,
              style: style1,
            ),
            Align(
              alignment: Alignment.centerLeft,
              child: Text('電話番号:', style: style2,),
            ),
            TextField(
              controller: phoneController,
              style: style1,
            )
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: 0,
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            title: Text('追加'),
            icon: Icon(Icons.home),
          ),
          BottomNavigationBarItem(
            title: Text('一覧'),
            icon: Icon(Icons.list)
          ),
        ],
        onTap: (int index) {
          if (index == 1) {
            Navigator.pushNamed(context, '/list');
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.save),
        onPressed: () {
          _saveData();
          showDialog(
              context: context,
              builder: (BuildContext context) => AlertDialog(
                title: Text("保存しました"),
                content: Text('データベースに保存できました'),
              )
          );
        },
      ),
    );
  }

  /// データを保存する
  void _saveData() async {
    /// データベースのパスを取得
    String dbFilePath = await getDatabasesPath();
    String path = join(dbFilePath, Constants().dbName);

    /// 保存するデータの用意
    String name = nameController.text;
    String email = emailController.text;
    String phone = phoneController.text;

    /// SQL文
    String query = 'INSERT INTO ${Constants().tableName}(name, mail, tel) VALUES("$name", "$email", "$phone")';

    Database db = await openDatabase(path, version: Constants().dbVersion, onCreate: (Database db, int version) async {
      await db.execute(
        "CREATE TABLE IF NOT EXISTS ${Constants().tableName} (id INTEGER PRIMARY KEY, name TEXT, mail TEXT, tel TEXT)"
      );
    });

    /// SQL 実行
    await db.transaction((txn) async {
      int id = await txn.rawInsert(query);
      print("保存成功 id: $id");
    });

    /// ウィジェットの更新
    setState(() {
      nameController.text = "";
      emailController.text = "";
      phoneController.text = "";
    });
  }
}

second_screen.dart

import 'package:flutter/material.dart';
import 'package:practice_app/constants.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class SecondScreen extends StatefulWidget {
  @override
  _SecondScreenState createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
  List<Widget> _items = <Widget>[];

  @override
  void initState() {
    super.initState();
    getItems();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('一覧'),
      ),
      body: ListView(
        children: _items,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: 1,
        items: <BottomNavigationBarItem> [
          BottomNavigationBarItem(
            title: Text("追加"),
            icon: Icon(Icons.home)
          ),
          BottomNavigationBarItem(
            title: Text('一覧'),
            icon: Icon(Icons.list)
          )
        ],
        onTap: (int index) {
          if (index == 0) {
            Navigator.pop(context);
          }
        },
      ),
    );
  }

  /// 保存したデータを取り出す
  void getItems() async {
    /// データベースのパスを取得
    List<Widget> list = <Widget>[];
    String dbFilePath = await getDatabasesPath();
    String path = join (dbFilePath, Constants().dbName);

    /// テーブルがなければ作成する
    Database db = await openDatabase(
        path,
        version: Constants().dbVersion,
        onCreate: (Database db, int version) async {
          await db.execute("CREATE TABLE IF NOT EXISTS ${Constants().tableName} (id INTEGER PRIMARY KEY, name TEXT, mail TEXT, tel TEXT)"
          );
        });

    /// SQLの実行
    List<Map> result = await db.rawQuery('SELECT * FROM ${Constants().tableName}');

    /// データの取り出し
    for (Map item in result) {
      list.add(ListTile(
        title: Text(item['name']),
        subtitle: Text(item['mail'] + ' ' + item['tel']),
      ));
    }

    /// ウィジェットの更新
    setState(() {
      _items = list;
    });
  }
}

これでビルドすると次のような2つの画面が表示されます。
下のタブバーには「追加」と「一覧」タブを表示しています。

初期表示の画面 入力後
f:id:qed805:20200201200808p:plain
初期表示の画面、追加画面
f:id:qed805:20200201200828p:plain
入力後
データの保存 保存したデータの一覧画面
f:id:qed805:20200201200846p:plain
データの保存
f:id:qed805:20200201200903p:plain
保存したデータの一覧画面

これでちゃんとデータベースに保存して、表示できることが確認できました。

通常のアプリ開発ではあまりキャッシュを使うことがありませんので用途は限られますが
使い方を理解しておいたほうが後々便利ですね。
だいたいのアプリ開発では結局サーバー連携のためにAPIからデータを取得することが多いですけど。

これでSQFLiteの使い方からライブラリのインストールまで学習できましたのでいい勉強になったと思います。

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。