この記事は「HTC Advent Calendar 2020」の16日目の記事です。
今回は、FlutterでSQLiteを利用したデータのやりとりについて書いていきます。
簡単なメモアプリをつくってデータの永続化ができることを確認します。

前提

こちらを参考にしながら、実際に自分でやってみたという内容になります。
優しく、手取り足取り教えるチュートリアル的な記事ではないので、その辺はご理解いただければと思います。

対象者

  • Flutterを使ったことがある
  • 簡単なSQLがわかる

ざっくりとしていますが、Flutter? SQL? 何それ、美味しいの?って状態じゃなければ問題ないかと思います。

開発環境

macOS Catalina 10.15.7
Visual Studio Code 1.52.0

$  flutter --version
Flutter 1.24.0-10.2.pre • channel beta • https://github.com/flutter/flutter.git
Tools • Dart 2.12.0 (build 2.12.0-29.10.beta)

目指すゴール

作成したFlutterアプリ上で以下のようなデータのやりとりができる。
1. テーブルの作成
2. データの挿入
3. データの取得
4. データの更新
5. データの削除

※今回はあくまで「データのやりとり」についてなので、アプリのレイアウトや細かいwidgetの説明は省きます

成果物イメージ

output_demo2.gif

開発手順

SQLiteの導入

SQLite

簡単に説明すると、サーバーとしてデータベースを用意するわけではなく、アプリケーションに組み込んで利用するデータベースになります。
細かい話はぐぐってみてください。

「sqflite」パッケージの導入

プラグインを追加するために、pubspec.ymlを編集します。
sqflite:SQLiteデータベースとやりとりが出来る様になります。
path: データベースを格納するパスを扱うパッケージになります。

pubspec.yml
dependencies:
  flutter:
    sdk: flutter
  sqflite:
  path:

プラグインを追加したら、使用するためにインポートします。
dart:main.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

データモデルの定義

main.dart
class Memo {
  final int id;
  final String text;

  Memo({this.id, this.text});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'text': text,
    };
  }
}

データベース接続/テーブル作成

main.dart
class Memo {
// ... 省略
  static Future<Database> get database async {
    // openDatabase() データベースに接続
    final Future<Database> _database = openDatabase(
      // getDatabasesPath() データベースファイルを保存するパス取得
      join(await getDatabasesPath(), 'memo_database.db'),
      onCreate: (db, version) {
        return db.execute(
          // テーブルの作成
          "CREATE TABLE memo(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)",
        );
      },
      version: 1,
    );
    return _database;
  }
}

データの挿入

main.dart
class Memo {
// ... 省略
static Future<Database> get database async {
  // ... 省略
  }

  static Future<void> insertMemo(Memo memo) async {
    final Database db = await database;
    await db.insert(
      'memo',
      memo.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }
}

データの取得

main.dart
class Memo {
// ... 省略
static Future<Database> get database async {
  // ... 省略
  }
  static Future<void> insertMemo(Memo memo) async {
  // ... 省略
  }
  static Future<List<Memo>> getMemos() async {
    final Database db = await database;
    final List<Map<String, dynamic>> maps = await db.query('memo');
    return List.generate(maps.length, (i) {
      return Memo(
        id: maps[i]['id'],
        text: maps[i]['text'],
      );
    });
  }
}

データの更新

main.dart
class Memo {
// ... 省略
static Future<Database> get database async {
  // ... 省略
  }
  static Future<void> insertMemo(Memo memo) async {
  // ... 省略
  }
  static Future<List<Memo>> getMemos() async {
  // ... 省略
  }
  static Future<void> updateMemo(Memo memo) async {
    // Get a reference to the database.
    final db = await database;
    await db.update(
      'memo',
      memo.toMap(),
      where: "id = ?",
      whereArgs: [memo.id],
      conflictAlgorithm: ConflictAlgorithm.fail,
    );
  }
}

データの削除

main.dart
class Memo {
// ... 省略
static Future<Database> get database async {
  // ... 省略
  }
  static Future<void> insertMemo(Memo memo) async {
  // ... 省略
  }
  static Future<List<Memo>> getMemos() async {
  // ... 省略
  }
  static Future<void> updateMemo(Memo memo) async {
  // ... 省略
  }
  static Future<void> deleteMemo(int id) async {
    final db = await database;
    await db.delete(
      'memo',
      where: "id = ?",
      whereArgs: [id],
    );
  }
}

定義した関数の使用方法

ボタンを押したタイミングや、initState()などで実行しています。

例えばこんな感じです。

RaisedButton(
  onPressed: () async {
  Memo _memo = Memo(text: 'じゃむおじさん');
  await Memo.insertMemo(_memo);
  },
),

データの取得、更新、削除も同様にして実行することができます。

最後に

SQLiteを使って簡単にデータのやりとりができることが確認できました。
簡単に実装できるけど、Firebaseの方が多機能なので、あまり出番はないのかなと思っています。
使うとしたら、リアルタイム通信を必要としない、ローカルにデータを保存したい場合などですかね。

今回作成したソースコードはこちらに記載しておきます

main.dart
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'package:path/path.dart';

class Memo {
  final int id;
  final String text;

  Memo({this.id, this.text});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'text': text,
    };
  }

  @override
  String toString() {
    return 'Memo{id: $id, text: $text}';
  }

  static Future<Database> get database async {
    final Future<Database> _database = openDatabase(
      join(await getDatabasesPath(), 'memo_database.db'),
      onCreate: (db, version) {
        return db.execute(
          "CREATE TABLE memo(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)",
        );
      },
      version: 1,
    );
    return _database;
  }

  static Future<void> insertMemo(Memo memo) async {
    final Database db = await database;
    await db.insert(
      'memo',
      memo.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  static Future<List<Memo>> getMemos() async {
    final Database db = await database;
    final List<Map<String, dynamic>> maps = await db.query('memo');
    return List.generate(maps.length, (i) {
      return Memo(
        id: maps[i]['id'],
        text: maps[i]['text'],
      );
    });
  }

  static Future<void> updateMemo(Memo memo) async {
    final db = await database;
    await db.update(
      'memo',
      memo.toMap(),
      where: "id = ?",
      whereArgs: [memo.id],
      conflictAlgorithm: ConflictAlgorithm.fail,
    );
  }

  static Future<void> deleteMemo(int id) async {
    final db = await database;
    await db.delete(
      'memo',
      where: "id = ?",
      whereArgs: [id],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo SQL',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MySqlPage(),
    );
  }
}

class MySqlPage extends StatefulWidget {
  @override
  _MySqlPageState createState() => _MySqlPageState();
}

class _MySqlPageState extends State<MySqlPage> {
  List<Memo> _memoList = [];
  final myController = TextEditingController();
  final upDateController = TextEditingController();
  var _selectedvalue;

  Future<void> initializeDemo() async {
    _memoList = await Memo.getMemos();
  }

  @override
  void dispose() {
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('メモアプリ'),
      ),
      body: Container(
        padding: EdgeInsets.all(32),
        child: FutureBuilder(
          future: initializeDemo(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              // 非同期処理未完了 = 通信中
              return Center(
                child: CircularProgressIndicator(),
              );
            }
            return ListView.builder(
              itemCount: _memoList.length,
              itemBuilder: (context, index) {
                return Card(
                  child: ListTile(
                    leading: Text(
                      'ID ${_memoList[index].id}',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    title: Text('${_memoList[index].text}'),
                    trailing: SizedBox(
                      width: 76,
                      height: 25,
                      child: RaisedButton.icon(
                        onPressed: () async {
                          await Memo.deleteMemo(_memoList[index].id);
                          final List<Memo> memos = await Memo.getMemos();
                          setState(() {
                            _memoList = memos;
                          });
                        },
                        icon: Icon(
                          Icons.delete_forever,
                          color: Colors.white,
                          size: 18,
                        ),
                        label: Text(
                          '削除',
                          style: TextStyle(fontSize: 11),
                        ),
                        color: Colors.red,
                        textColor: Colors.white,
                      ),
                    ),
                  ),
                );
              },
            );
          },
        ),
      ),
      floatingActionButton: Column(
        verticalDirection: VerticalDirection.up,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
              showDialog(
                  context: context,
                  builder: (_) => AlertDialog(
                        title: Text("新規メモ作成"),
                        content: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            Text('なんでも入力してね'),
                            TextField(controller: myController),
                            RaisedButton(
                              child: Text('保存'),
                              onPressed: () async {
                                Memo _memo = Memo(text: myController.text);
                                await Memo.insertMemo(_memo);
                                final List<Memo> memos = await Memo.getMemos();
                                setState(() {
                                  _memoList = memos;
                                  _selectedvalue = null;
                                });
                                myController.clear();
                                Navigator.pop(context);
                              },
                            ),
                          ],
                        ),
                      ));
            },
          ),
          SizedBox(height: 20),
          FloatingActionButton(
              child: Icon(Icons.update),
              backgroundColor: Colors.amberAccent,
              onPressed: () async {
                await showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return AlertDialog(
                        content: StatefulBuilder(
                          builder:
                              (BuildContext context, StateSetter setState) {
                            return Column(
                              mainAxisSize: MainAxisSize.min,
                              children: <Widget>[
                                Text('IDを選択して更新してね'),
                                Row(
                                  children: <Widget>[
                                    Flexible(
                                      flex: 1,
                                      child: DropdownButton(
                                        hint: Text("ID"),
                                        value: _selectedvalue,
                                        onChanged: (newValue) {
                                          setState(() {
                                            _selectedvalue = newValue;
                                            print(newValue);
                                          });
                                        },
                                        items: _memoList.map((entry) {
                                          return DropdownMenuItem(
                                              value: entry.id,
                                              child: Text(entry.id.toString()));
                                        }).toList(),
                                      ),
                                    ),
                                    Flexible(
                                      flex: 3,
                                      child: TextField(
                                          controller: upDateController),
                                    ),
                                  ],
                                ),
                                RaisedButton(
                                  child: Text('更新'),
                                  onPressed: () async {
                                    Memo updateMemo = Memo(
                                        id: _selectedvalue,
                                        text: upDateController.text);
                                    await Memo.updateMemo(updateMemo);
                                    final List<Memo> memos =
                                        await Memo.getMemos();
                                    super.setState(() {
                                      _memoList = memos;
                                    });
                                    upDateController.clear();
                                    Navigator.pop(context);
                                  },
                                ),
                              ],
                            );
                          },
                        ),
                      );
                    });
              }),
        ],
      ),
    );
  }
}