この記事は「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アプリ上で以下のようなデータのやりとりができる。
- テーブルの作成
- データの挿入
- データの取得
- データの更新
- データの削除
※今回はあくまで「データのやりとり」についてなので、アプリのレイアウトや細かいwidgetの説明は省きます
成果物イメージ
開発手順
SQLiteの導入
SQLite
簡単に説明すると、サーバーとしてデータベースを用意するわけではなく、アプリケーションに組み込んで利用するデータベースになります。
細かい話はぐぐってみてください。
「sqflite」パッケージの導入
プラグインを追加するために、pubspec.yml
を編集します。
sqflite
:SQLiteデータベースとやりとりが出来る様になります。
path
: データベースを格納するパスを扱うパッケージになります。
dependencies:
flutter:
sdk: flutter
sqflite:
path:
プラグインを追加したら、使用するためにインポートします。
import 'package:sqflite/sqflite.dart';
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,
};
}
}
データベース接続/テーブル作成
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;
}
}
データの挿入
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,
);
}
}
データの取得
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'],
);
});
}
}
データの更新
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,
);
}
}
データの削除
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の方が多機能なので、あまり出番はないのかなと思っています。
使うとしたら、リアルタイム通信を必要としない、ローカルにデータを保存したい場合などですかね。
今回作成したソースコードはこちらに記載しておきます
<details><summary>ソースコード</summary><div>
```dart: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);
},
),
],
);
},
),
);
});
}),
],
),
);
}
}
```
</div></details>