select_pageを修正しています。
修正箇所についてはこちらをごらんください
簡単な動画撮りました。
こちらからご覧ください
select_pageをListView化しました。
修正箇所についてはこちらをご覧ください
全件表示ページをつけて、ビデオを撮りました。
修正箇所についてはこちらをごらんください
サンプルアプリを公開する
これはFlutterを使って、MySQL8のためのフロントサイドをつくる、サンプルアプリです。サンプルアプリですから、シンプルで読みやすく、改造しやすいこと、を主眼に構成しています。ですから、「もっとこうなってればよいのに」というところがたくさん見つかるでしょう。ですが、それはぜひ、皆さんご自身で改良してください。
なぜFlutterか
私がFlutterしか知らないから、といってしまえばそれまでですが、Flutterはマルチプラットフォームのスーパーツールです。一つの開発作業で、iOS、Androidの両モバイルはもとより、macOS、Web、Windows、Linux対応のアプリが同時に(ちょっと工夫が要るけど)、できあがります。この時代、これを使わない手があるでしょうか?
なぜMySQL8か
そんな便利なFlutterを使っているDeveloperにとっては、データベースも手軽であることが大事。例えばFirebaseのfirestoreを使えば、サーバーがどうしたとか、考えずに開発できます。
でも、逆の視点から見たらどうでしょうか。MySQLは、長い歴史を誇る、「世界で最も使われているリレーショナル・データベース」です。MySQL5.7のサポート終了を前に、最新版8への移行が急ピッチで行われているところです。
そういうMySQLユーザーから見て、Flutterにはどんな価値があるでしょうか。長い歴史、つまり、スマホ以前、タブレット以前のフロントサイドを持ったサービスである可能性がとても高いのです。そのユーザーの皆さんが今、もっと手軽なフロントサイドを模索するとしたら、Flutterは最適な選択になると思います。
なぜmysql_clientか
これは、dartで書かれたパッケージで、MySQL8とFlutterを繋ぎます。現在、mysql1というパッケージもあり、これについては記事やサンプルアプリが複数ありますけれども、残念ながらMySQL8でエラーが多いという問題があります。(鋭意改良中ということです)。そこで今回は、mysql_clientを採用します。まだまだ開発途上の、あまり知られていないパッケージですが、MySQL8でちゃんと動きますから。
このサンプルアプリの使い方
Flutterの開発環境があり、MySQL8に接続可能な方であれば、こちらからcloneしていただければ、すぐに動きます。今回はiOS、Android、macOSのみで設定していますが、そこは皆さんの用途に応じて変更が可能です。
では、fileを1枚ずつ見ていきましょう。
main.dart
Flutterはdartという言語で書きます。今回のmain.dartは、「ここから始まる」という宣言以外にとくだん用途はありません。
import 'package:flutter/material.dart';
import 'top_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter MySQL8 Demo',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: const TopPage(),
);
}
}
pubspec.yaml
プロジェクトの下のほうに、pubspec.yamlというfileがあります。ここで、今回使用するmysql_clientを呼んでいます。cloneで使う場合は既に記載されていますので、右上からPub getしてください。もしmysql_clientのversionが上がっていたら、修正してPub getをクリックしてください。
format.dart
今回、入力データや選択項目を書き込むために、たくさんのTextFormFieldを使います。これを簡単にするため、Fieldの設定を独立させています。ここでFieldの大きさや色などを一括管理することができます。
import 'package:flutter/material.dart';
class Format extends StatelessWidget {
final String hintText;
final ValueChanged<String> onChanged;
const Format({
required this.hintText,
required this.onChanged,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(
fontSize: 12,
color: Colors.black54),
fillColor: Colors.grey,
filled: true,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(
color: Colors.grey,
width: 2.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Colors.grey,
width: 1.0,
),
),
),
onChanged: onChanged,
);
}
}
top_page.dart
今回、各ファイルをシンプルで読みやすくするため、機能ごとにpageを分けています。このpageは、各機能ごとのページに遷移するための目次に相当します。現状ではpageからpageへは、画面左上の戻るボタンで目次に戻っていただく必要があります。ちょっとめんどうくさいので改善予定です。
各ボタンの設定も、TextFormFieldのように独立させればよかったな、と今ちょっと思っています。近日中に改訂するかもしれません。
import 'package:flutter/material.dart';
import 'delete_page.dart';
import 'insert_page.dart';
import 'select_page.dart';
import 'update_page.dart';
class TopPage extends StatelessWidget {
const TopPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 200),
const Padding(
padding: EdgeInsets.all(30.0),
child: Text(
'Flutter MySQL8 Sample',
style: TextStyle(
color: Colors.brown,
fontSize: 24,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const InsertPage(title: 'Flutter MySQL Insert Sample'),
),
);
},
child: const Text(
"Insert Page",
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UpdatePage(title: 'Flutter MySQL Update Sample'),
),
);
},
child: const Text(
"Update Page",
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SelectPage(title: 'Flutter MySQL Select Sample'),
),
);
},
child: const Text(
"Select Page",
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeletePage(title: 'Flutter MySQL Delete Sample'),
),
);
},
child: const Text(
"Delete Page",
),
),
),
],
)
)
);
}
}
insert_page.dart
このサンプルは、既にデータベースとテーブルがある、という前提で作っています。ですから、最初の作業は既存のテーブルにデータを追加する、です。 テーブル名がtimelineになっていますので、適宜書き換えてください。
class _InsertPageState extends State<InsertPage> {
var newYear = '';
var newName = '';
var newCountry = '';
年表のテーブルで、年、事件名、それが起こった国という三要素を必須項目とする、という想定です。varという形で、「新しく入力する文字列」を定義しています。ここには、file下方のTextFormFieldから取得された文字列が入ることになります。ですから、ここをお好みのcolumn名に変えていただければ、汎用的に使えます。
final conn = await MySQLConnection.createConnection(
host: "127.0.0.1", //when you use simulator
//host: "10.0.2.2", when you use emulator
//host: "localhost"
port: 3306,
userName: "root",
password: "myPassword", // you need to replace with your password
databaseName: "myDatabase", // you need to replace with your db name
);
この部分でデータベースに接続します。今回は、各機能ごとのpageを単独でも使えるように、接続関数も各ページについています。ここにご自分のデータベース名やパスワードを設定して利用してください。
このサンプルの肝
一言でいえば、以下の部分の" "内に、正しいSQL文を書く、というのが、このサンプルの肝になります。どの機能に対しても同じことです。
このpageではINSERT文を書いています。idについては、autoincrement設定がしてある、という前提で、null、つまり指定せず、になっています。もし特定のIDを設定したい場合は、idに対しても他の項目に倣って入力Formを作ってください。
var res = await conn.execute(
"INSERT INTO timeline (id, year, name, country) VALUES (:id, :year, :name, :country)",
{
"id": null, //if you set it as auto-increment
"year": newYear,
"name": newName,
"country": newCountry,
},
);
各フィールドにデータを記入後、一番下のボタンを押すと、関数が走る設定になっています。残念ながら、無事入力されたかどうかは画面上では判断できません。Androidstudioのコンソールには、「一行追加」を意味するflutter:1が表示されます。workbenchやSequelAceを使って、成果を確認してみてください。
全体像は以下のとおりです。
import 'package:flutter/material.dart';
import 'package:mysql_client/mysql_client.dart';
import 'domain/format.dart';
class InsertPage extends StatefulWidget {
const InsertPage({Key? key,required this.title}) : super(key: key);
final String title;
@override
State<InsertPage> createState() => _InsertPageState();
}
class _InsertPageState extends State<InsertPage> {
var newYear = '';
var newName = '';
var newCountry = '';
Future<void> _insert() async {
print("Connecting to mysql server...");
// create connection
final conn = await MySQLConnection.createConnection(
host: "127.0.0.1", //when you use simulator
//host: "10.0.2.2", when you use emulator
//host: "localhost"
port: 3306,
userName: "root",
password: "myPassword", // you need to replace with your password
databaseName: "myDatabase", // you need to replace with your db name
);
await conn.connect();
print("Connected");
// insert some rows
var res = await conn.execute(
"INSERT INTO timeline (id, year, name, country) VALUES (:id, :year, :name, :country)",
{
"id": null, //if you set it auto increment
"year": newYear,
"name": newName,
"country": newCountry,
},
);
print(res.affectedRows);
// close all connections
await conn.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "year",
onChanged: (text) {
newYear = text;
},
)
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "name",
onChanged: (text) {
newName = text;
},
)
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "country",
onChanged: (text) {
newCountry = text;
},
)
),
const Text(
'Push button to insert',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _insert,
tooltip: 'insert',
child: const Icon(Icons.add),
),
);
}
}
update_page.dart
既に入力されているデータを修正したり、項目を追加したりするpageです。前回同様、接続設定をしてください。
var targetId = ''; // id to update
var targetTerm = ''; // column name to update
var newTerm = ''; // new data to replace
- targetId 修正する行のidをFieldで指定します。
- targetTerm 修正するcolumn名をFieldで取得します。
- newTerm 書き換えたり、新たに追加する文字列をFieldで取得します。
var res = await conn.execute(
"UPDATE timeline SET $targetTerm = :$targetTerm WHERE id = $targetId",
{
targetTerm: newTerm,
},
);
ここがUPDATE文です。Fieldで取得したtargetTermでcolumnを、targetIdでWHEREの条件となるidを取得しているのがわかります。そして指定された部分がnewTermに置き換わります。
Fieldに入力後、ボタンを押すと関数が走ること、変更行数1がコンソールに表示されることは、前ページと同じです。
全文は以下のとおりです。
import 'package:flutter/material.dart';
import 'package:mysql_client/mysql_client.dart';
import 'domain/format.dart';
class UpdatePage extends StatefulWidget {
const UpdatePage({Key? key,required this.title}) : super(key: key);
final String title;
@override
State<UpdatePage> createState() => _UpdatePageState();
}
class _UpdatePageState extends State<UpdatePage> {
var res = '';
var targetId = ''; // id to update
var targetTerm = ''; // column name to update
var newTerm = ''; // new data to replace
Future<void> _update() async {
print("Connecting to mysql server...");
// create connection
final conn = await MySQLConnection.createConnection(
host: "127.0.0.1", //when you use simulator
//host: "10.0.2.2", when you use emulator
//host: "localhost"
port: 3306,
userName: "root",
password: "myPassword", // you need to replace with your password
databaseName: "myDatabase", // you need to replace with your db name
);
await conn.connect();
print("Connected");
// update some rows
var res = await conn.execute(
"UPDATE timeline SET $targetTerm = :$targetTerm WHERE id = $targetId",
{
targetTerm: newTerm,
},
);
print(res.affectedRows);
// close all connections
await conn.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "id",
onChanged: (text) {
targetId = text;
},
)
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "term",
onChanged: (text) {
targetTerm = text;
},
)
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "new term",
onChanged: (text) {
newTerm = text;
},
)
),
const Text(
'Push button to update',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _update,
tooltip: 'update',
child: const Icon(Icons.add),
),
);
}
}
select_page.dart
このpageはまだ改良の余地が多いので、順次更新予定です。
現在、指定されたcolumnに対して一つの条件を設定する、ということしかできていません。
また、SELECT文で複数の項目が見つかっても、「最後の一件」しか表示されません。ここも、ListView化して、複数表示をめざしています。
MySQLの特性である柔軟な検索を堪能するために、大いに変更していきたいpageです。
接続設定は他pageと同じです。
var targetCountry = ''; // WHERE term
var selectedId = '';
var selectedYear = '';
var selectedName = '';
var selectedCountry = '';
現状はcountryというcolumnでのみ絞り込める形です。したがって、targetCountryをFieldから取得することで、さまざまな国の中から、その国の事件だけを取得する形になります。 selectedの各項目は、検索結果表示用です。このサンプルでは、idは表示していませんが、今後の展開上、idも取得しておく方が便利かなと思います。
var result = await conn.execute("SELECT * FROM timeline WHERE country = '$targetCountry'");
これがSELECT文です。これに対して、以下の結果を取得して、表示しています。データは配列として取得されるので、配列番号で表示内容を決めています。ちなみに(0)はid番号です。
for (final row in result.rows) {
setState((){
selectedYear = row.colAt(1)!;
selectedName = row.colAt(2)!;
selectedCountry = row.colAt(3)!;
});
Fieldに入力後ボタンを押すと関数が走り、取得データの最後の一件が表示されます。Androidstudioのconsoleには、取得できたデータの総数が表示されますので、その情報を元にListView化を行うのが今後の課題です。
以下が全文です。
import 'package:flutter/material.dart';
import 'package:mysql_client/mysql_client.dart';
import 'domain/format.dart';
class SelectPage extends StatefulWidget {
const SelectPage({Key? key,required this.title}) : super(key: key);
final String title;
@override
State<SelectPage> createState() => _SelectPageState();
}
class _SelectPageState extends State<SelectPage> {
//今回は国別に絞り込んでいる。ここの自由度も上げたい。
var targetCountry = ''; // WHERE term
var selectedId = '';
var selectedYear = '';
var selectedName = '';
var selectedCountry = '';
Future<void> _select() async {
print("Connecting to mysql server...");
// create connection
final conn = await MySQLConnection.createConnection(
host: "127.0.0.1", //when you use simulator
//host: "10.0.2.2", when you use emulator
//host: "localhost"
port: 3306,
userName: "root",
password: "myPassword", // you need to replace with your password
databaseName: "myDatabase", // you need to replace with your db name
);
await conn.connect();
print("Connected");
// make query
var result = await conn.execute("SELECT * FROM timeline WHERE country = '$targetCountry'");
// print some result data
print(result.numOfColumns);
print(result.numOfRows);
//print(result.lastInsertID);
//print(result.affectedRows);
// print query result
for (final row in result.rows) {
setState((){
selectedYear = row.colAt(1)!;
selectedName = row.colAt(2)!;
selectedCountry = row.colAt(3)!;
});
// print(row.colAt(0));
// print(row.colByName("title"));
// print all rows as Map<String, String>
//print(row.assoc());
//as Map<String, dynamic>
print(row.typedAssoc());
}
// close all connections
await conn.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "country",
onChanged: (text) {
targetCountry = text;
},
)
),
const Text(
'Push button to move:',
),
Text(
style: Theme.of(context).textTheme.headline4,
selectedYear,
),
Text(
style: Theme.of(context).textTheme.headline4,
selectedName,
),
Text(
style: Theme.of(context).textTheme.headline4,
selectedCountry,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _select,
tooltip: 'select',
child: const Icon(Icons.add),
),
);
}
}
delete_page.dart
最後のpageです。deleteについては、データを喪失するリスクがありますし、ユーザー権限にも関わってくるので、扱いの難しいところかも知れませんが、今回はサンプルですので、作っておきます。なくてよければ、このpageは捨ててしまってかまいません。
接続設定は他pageと同じです。
targetIdを入力して、その1行を消すというDELETE文になります。
var res = await conn.execute(
"DELETE FROM timeline WHERE id = $targetId",
);
consoleに変更のあった行数1 が、表示されます。
全文は以下のとおりです。
import 'package:flutter/material.dart';
import 'package:mysql_client/mysql_client.dart';
import 'domain/format.dart';
class DeletePage extends StatefulWidget {
const DeletePage({Key? key,required this.title}) : super(key: key);
final String title;
@override
State<DeletePage> createState() => _DeletePageState();
}
class _DeletePageState extends State<DeletePage> {
var res = '';
var targetId = '';
Future<void> _delete() async {
print("Connecting to mysql server...");
// create connection
final conn = await MySQLConnection.createConnection(
host: "127.0.0.1", //when you use simulator
//host: "10.0.2.2", when you use emulator
//host: "localhost"
port: 3306,
userName: "root",
password: "myPassword", // you need to replace with your password
databaseName: "myDatabase", // you need to replace with your db name
);
await conn.connect();
print("Connected");
// update some rows
var res = await conn.execute(
"DELETE FROM timeline WHERE id = $targetId",
);
print(res.affectedRows);
// close all connections
await conn.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(30.0),
child: Format(
hintText: "id",
onChanged: (text) {
targetId = text;
},
)
),
const Text(
'Push button to delete',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _delete,
tooltip: 'delete',
child: const Icon(Icons.add),
),
);
}
}
長々お付き合いいただき、ありがとうございました。
改善点についてPRをお送りいただけましたらとても光栄です。このサンプルと記事がどなたかのお役に立てるとよいなと思っています。